From 75a44fb30a241cf6f09d3fc3a7356fcf45627008 Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Sat, 11 Aug 2018 19:13:52 +0530 Subject: [PATCH 01/68] Added HTTPS request to get version data. Added APK flaor for github and fdroid. --- app/build.gradle | 17 +++ .../java/org/schabi/newpipe/MainActivity.java | 111 ++++++++++++++++++ build.gradle | 2 +- 3 files changed, 129 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a5ff67bee..7264c0ab9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,12 +14,14 @@ android { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } + buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } + debug { multiDexEnabled true @@ -34,10 +36,25 @@ android { // but continue the build even when errors are found: abortOnError false } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + flavorDimensions "apkSource" + productFlavors { + github { + dimension "apkSource" + applicationIdSuffix ".github" + + } + + fdroid { + dimension "apkSource" + applicationIdSuffix ".fdroid" + } + } } ext { diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 0ce6772bb..4dba91706 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -24,6 +24,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -47,6 +48,8 @@ import android.widget.Button; import android.widget.ImageButton; import android.widget.TextView; +import org.json.JSONException; +import org.json.JSONObject; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.fragments.BackPressable; @@ -61,6 +64,13 @@ import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); @@ -80,6 +90,10 @@ public class MainActivity extends AppCompatActivity { ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); + if (BuildConfig.FLAVOR.equals("github")) { + new versionCheckTask().execute(); + } + super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); @@ -409,4 +423,101 @@ public class MainActivity extends AppCompatActivity { NavigationHelper.gotoMainFragment(getSupportFragmentManager()); } } + + /** + * AsyncTask to check if there is a newer version of the 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 download link. + */ + private static class versionCheckTask extends AsyncTask { + + String newPipeApiUrl = "https://api.myjson.com/bins/19gx44"; + int timeoutPeriod = 10000; + + @Override + protected String doInBackground(Void... voids) { + + String output; + + HttpURLConnection connection = null; + + try { + + URL url = new URL(newPipeApiUrl); + + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(timeoutPeriod); + connection.setReadTimeout(timeoutPeriod); + connection.setRequestProperty("Content-length", "0"); + connection.setUseCaches(false); + connection.setAllowUserInteraction(false); + connection.connect(); + + int responseStatus = connection.getResponseCode(); + + switch (responseStatus) { + + case 200: + case 201: + BufferedReader bufferedReader + = new BufferedReader( + new InputStreamReader(connection.getInputStream())); + + StringBuilder stringBuilder = new StringBuilder(); + + String line; + + while ((line = bufferedReader.readLine()) != null) { + stringBuilder.append(line + "\n"); + } + + bufferedReader.close(); + output = stringBuilder.toString(); + + return output; + } + } catch (MalformedURLException ex) { + ex.printStackTrace(); + } catch (IOException ex) { + ex.printStackTrace(); + } finally { + if (connection != null) { + try { + connection.disconnect(); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } + + return null; + } + + @Override + protected void onPostExecute(String output) { + + if (output != null) { + + Log.i("output---", output); + + try { + JSONObject mainObject = new JSONObject(output); + JSONObject flavoursObject = mainObject.getJSONObject("flavors"); + JSONObject githubObject = flavoursObject.getJSONObject("github"); + JSONObject githubStableObject = githubObject.getJSONObject("stable"); + + String version = githubStableObject.getString("version"); + // String versionCode = githubStableObject.getString("version_code"); + String apkLocationUrl = githubStableObject.getString("apk"); + + Log.i("jsonConvert---", version + " " + apkLocationUrl); + } catch (JSONException ex) { + ex.printStackTrace(); + } + } + } + } + + } diff --git a/build.gradle b/build.gradle index a45c00aef..20c8a0dfc 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.2' + classpath 'com.android.tools.build:gradle:3.1.4' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From 5e2aa51627846afbf86fd8c34e94c4b09628cd5b Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Sat, 11 Aug 2018 19:36:23 +0530 Subject: [PATCH 02/68] Moved the asynctask to its own class. --- .../schabi/newpipe/FetchAppVersionTask.java | 118 ++++++++++++++++++ .../java/org/schabi/newpipe/MainActivity.java | 109 +--------------- 2 files changed, 119 insertions(+), 108 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java diff --git a/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java new file mode 100644 index 000000000..2f98fe1ad --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java @@ -0,0 +1,118 @@ +package org.schabi.newpipe; + +import android.os.AsyncTask; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +/** + * AsyncTask to check if there is a newer version of the 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 download link. + */ +public class FetchAppVersionTask extends AsyncTask { + + private String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json"; + private int timeoutPeriod = 10000; + + @Override + protected String doInBackground(Void... voids) { + + String output; + + HttpURLConnection connection = null; + + try { + + URL url = new URL(newPipeApiUrl); + + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(timeoutPeriod); + connection.setReadTimeout(timeoutPeriod); + connection.setRequestProperty("Content-length", "0"); + connection.setUseCaches(false); + connection.setAllowUserInteraction(false); + connection.connect(); + + int responseStatus = connection.getResponseCode(); + + switch (responseStatus) { + + case 200: + case 201: + BufferedReader bufferedReader + = new BufferedReader( + new InputStreamReader(connection.getInputStream())); + + StringBuilder stringBuilder = new StringBuilder(); + + String line; + + while ((line = bufferedReader.readLine()) != null) { + stringBuilder.append(line + "\n"); + } + + bufferedReader.close(); + output = stringBuilder.toString(); + + return output; + } + } catch (MalformedURLException ex) { + ex.printStackTrace(); + } catch (IOException ex) { + ex.printStackTrace(); + } finally { + if (connection != null) { + try { + connection.disconnect(); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } + + return null; + } + + @Override + protected void onPostExecute(String output) { + + if (output != null) { + + Log.i("output---", output); + + try { + JSONObject mainObject = new JSONObject(output); + 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); + + } catch (JSONException ex) { + ex.printStackTrace(); + } + } + } + + private void compareAppVersionAndShowNotification(String versionName, String apkLocationUrl) { + + if (!BuildConfig.VERSION_NAME.equals(versionName)) { + + + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 4dba91706..1554dddc0 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -24,7 +24,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -48,8 +47,6 @@ import android.widget.Button; import android.widget.ImageButton; import android.widget.TextView; -import org.json.JSONException; -import org.json.JSONObject; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.fragments.BackPressable; @@ -64,13 +61,6 @@ import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; - public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); @@ -91,7 +81,7 @@ public class MainActivity extends AppCompatActivity { ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); if (BuildConfig.FLAVOR.equals("github")) { - new versionCheckTask().execute(); + new FetchAppVersionTask().execute(); } super.onCreate(savedInstanceState); @@ -423,101 +413,4 @@ public class MainActivity extends AppCompatActivity { NavigationHelper.gotoMainFragment(getSupportFragmentManager()); } } - - /** - * AsyncTask to check if there is a newer version of the 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 download link. - */ - private static class versionCheckTask extends AsyncTask { - - String newPipeApiUrl = "https://api.myjson.com/bins/19gx44"; - int timeoutPeriod = 10000; - - @Override - protected String doInBackground(Void... voids) { - - String output; - - HttpURLConnection connection = null; - - try { - - URL url = new URL(newPipeApiUrl); - - connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - connection.setConnectTimeout(timeoutPeriod); - connection.setReadTimeout(timeoutPeriod); - connection.setRequestProperty("Content-length", "0"); - connection.setUseCaches(false); - connection.setAllowUserInteraction(false); - connection.connect(); - - int responseStatus = connection.getResponseCode(); - - switch (responseStatus) { - - case 200: - case 201: - BufferedReader bufferedReader - = new BufferedReader( - new InputStreamReader(connection.getInputStream())); - - StringBuilder stringBuilder = new StringBuilder(); - - String line; - - while ((line = bufferedReader.readLine()) != null) { - stringBuilder.append(line + "\n"); - } - - bufferedReader.close(); - output = stringBuilder.toString(); - - return output; - } - } catch (MalformedURLException ex) { - ex.printStackTrace(); - } catch (IOException ex) { - ex.printStackTrace(); - } finally { - if (connection != null) { - try { - connection.disconnect(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - } - - return null; - } - - @Override - protected void onPostExecute(String output) { - - if (output != null) { - - Log.i("output---", output); - - try { - JSONObject mainObject = new JSONObject(output); - JSONObject flavoursObject = mainObject.getJSONObject("flavors"); - JSONObject githubObject = flavoursObject.getJSONObject("github"); - JSONObject githubStableObject = githubObject.getJSONObject("stable"); - - String version = githubStableObject.getString("version"); - // String versionCode = githubStableObject.getString("version_code"); - String apkLocationUrl = githubStableObject.getString("apk"); - - Log.i("jsonConvert---", version + " " + apkLocationUrl); - } catch (JSONException ex) { - ex.printStackTrace(); - } - } - } - } - - } From f85e19c75defdabdc391cd792d73c6d14ef65852 Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Sun, 12 Aug 2018 15:01:50 +0530 Subject: [PATCH 03/68] Added notification channel and code to show notification. --- app/src/main/java/org/schabi/newpipe/App.java | 23 ++++++++++++ .../schabi/newpipe/FetchAppVersionTask.java | 37 +++++++++++++++++-- app/src/main/res/values/strings.xml | 8 ++++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index f436a26b8..77c6b6cb0 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -63,6 +63,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 context; @SuppressWarnings("unchecked") private static final Class[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class}; @@ -85,6 +86,8 @@ public class App extends Application { } refWatcher = installLeakCanary(); + context = this; + // Initialize settings first because others inits can use its values SettingsActivity.initSettings(this); @@ -202,6 +205,22 @@ public class App extends Application { NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mNotificationManager.createNotificationChannel(mChannel); + + // Set up notification channel for app update + 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 @@ -217,4 +236,8 @@ public class App extends Application { protected boolean isDisposedRxExceptionsReported() { return false; } + + public static App getContext() { + return context; + } } diff --git a/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java index 2f98fe1ad..75da96096 100644 --- a/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java @@ -1,7 +1,12 @@ package org.schabi.newpipe; +import android.app.Application; +import android.app.PendingIntent; +import android.content.Intent; +import android.net.Uri; import android.os.AsyncTask; -import android.util.Log; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; import org.json.JSONException; import org.json.JSONObject; @@ -88,8 +93,6 @@ public class FetchAppVersionTask extends AsyncTask { if (output != null) { - Log.i("output---", output); - try { JSONObject mainObject = new JSONObject(output); JSONObject flavoursObject = mainObject.getJSONObject("flavors"); @@ -108,11 +111,37 @@ public class FetchAppVersionTask extends AsyncTask { } } + /** + * Method to compare + * @param versionName + * @param apkLocationUrl + */ private void compareAppVersionAndShowNotification(String versionName, String apkLocationUrl) { - if (!BuildConfig.VERSION_NAME.equals(versionName)) { + int NOTIFICATION_ID = 2000; + if (!BuildConfig.VERSION_NAME.equals(versionName.replace("v", ""))) { + Application app = App.getContext(); + + 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_triangle_white) + .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); + + // notificationId is a unique int for each notification that you must define + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5ee80536f..6ad772494 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -144,6 +144,10 @@ NewPipe Notification Notifications for NewPipe Background and Popup Players + newpipeAppUpdate + App Update Notification + Notifications for new NewPipe version + [Unknown] Toggle Orientation @@ -504,4 +508,8 @@ 144p + + NewPipe Update Available + Tap to download + From 06f20c66f8fc3f66cd255f13d5f9ab1e9b1ec384 Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Sun, 12 Aug 2018 16:35:53 +0530 Subject: [PATCH 04/68] Moved the new version check to the application class. --- app/src/main/java/org/schabi/newpipe/App.java | 5 +++++ .../main/java/org/schabi/newpipe/FetchAppVersionTask.java | 1 + app/src/main/java/org/schabi/newpipe/MainActivity.java | 4 ---- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 77c6b6cb0..0a218d061 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -99,6 +99,11 @@ public class App extends Application { ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50)); configureRxJavaErrorHandler(); + + // Check for new version + if (BuildConfig.FLAVOR.equals("github")) { + new FetchAppVersionTask().execute(); + } } protected Downloader getDownloader() { diff --git a/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java index 75da96096..d56e07127 100644 --- a/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java @@ -124,6 +124,7 @@ public class FetchAppVersionTask extends AsyncTask { Application app = App.getContext(); + // 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); diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 1554dddc0..0ce6772bb 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -80,10 +80,6 @@ public class MainActivity extends AppCompatActivity { ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); - if (BuildConfig.FLAVOR.equals("github")) { - new FetchAppVersionTask().execute(); - } - super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); From 930c971035f425e3d692f0c1debb295513aa4b15 Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Sun, 12 Aug 2018 16:41:21 +0530 Subject: [PATCH 05/68] Added version check in the pop-up player --- .../java/org/schabi/newpipe/player/PopupVideoPlayer.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 8107345a1..a4c91cd30 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -58,6 +58,7 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.FetchAppVersionTask; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; @@ -663,6 +664,11 @@ public final class PopupVideoPlayer extends Service { lockManager.acquireWifiAndCpu(); hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + + // Check for new version + if (BuildConfig.FLAVOR.equals("github")) { + new FetchAppVersionTask().execute(); + } } @Override From 12b93d66377b57e4bbfc7a1cd1556a7f21ab676f Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Sun, 12 Aug 2018 17:18:46 +0530 Subject: [PATCH 06/68] Added new icon for update notification. --- .../org/schabi/newpipe/FetchAppVersionTask.java | 2 +- .../main/res/drawable-hdpi/ic_newpipe_update.png | Bin 0 -> 680 bytes .../main/res/drawable-mdpi/ic_newpipe_update.png | Bin 0 -> 401 bytes .../res/drawable-xhdpi/ic_newpipe_update.png | Bin 0 -> 734 bytes .../res/drawable-xxhdpi/ic_newpipe_update.png | Bin 0 -> 1370 bytes .../res/drawable-xxxhdpi/ic_newpipe_update.png | Bin 0 -> 2177 bytes 6 files changed, 1 insertion(+), 1 deletion(-) create mode 100755 app/src/main/res/drawable-hdpi/ic_newpipe_update.png create mode 100755 app/src/main/res/drawable-mdpi/ic_newpipe_update.png create mode 100755 app/src/main/res/drawable-xhdpi/ic_newpipe_update.png create mode 100755 app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png diff --git a/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java index d56e07127..3130dc655 100644 --- a/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java @@ -131,7 +131,7 @@ public class FetchAppVersionTask extends AsyncTask { NotificationCompat.Builder notificationBuilder = new NotificationCompat .Builder(app, app.getString(R.string.app_update_notification_channel_id)) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setSmallIcon(R.drawable.ic_newpipe_update) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentIntent(pendingIntent) .setAutoCancel(true) diff --git a/app/src/main/res/drawable-hdpi/ic_newpipe_update.png b/app/src/main/res/drawable-hdpi/ic_newpipe_update.png new file mode 100755 index 0000000000000000000000000000000000000000..cbf336a1f173c751889af7f1a0164edb045c82d9 GIT binary patch literal 680 zcmV;Z0$2TsP)l zsW^T3lkhj-|}{4v?F6wr|V;0X?)u@-=mx`#!z1O)v!j};hS z3qUZ4r#Mp8G?oD*w&_DRHmXK7W&m~LR|aT5E~@l3kpZ+0d+|W1xd)u6nSkB0w?BX}pr%MvCbT z=2QZxBhT;o)P;rd(=IgNqy}-k5OS~kJzqNxN(#Eg;SWGirt2XDcifU+DM5&+ptrddo086 zlzk6QBz#s0Gg4AHVT74FQlR>j&lKGDzXpVgTqvX_9diKt3c?fs$_Y7d&G4zicKN5s zKlq&IJv_pUa)A1TmQb__Vg}C1UrG7&9LqCdQb6Ca2Q!uU*9@i!hdHaK<%EO0vXqz+ zCJW`+T#`fn7;U(Mm-vWJLYsIjd{`E~2&*O3qeZB$)x!7Mq)`W{g#G~=P}v~1B^9y& O0000*Ius2C3<`nbKafxXrl|oO9tSv} zRSlvv0UFo`v9rnaa%Z!5+2wO{&NufxbLO5Ym7YuJ##B22y{KRZZ`%ogJFMYNy8ty^ zVj3^n1*qu=?^_MH&p;~JL{F;#^EghNY6hK81q_GyXZ*>5Tw`toA literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_newpipe_update.png b/app/src/main/res/drawable-xhdpi/ic_newpipe_update.png new file mode 100755 index 0000000000000000000000000000000000000000..5ee02aaa9359524b5a83988ddd54724dbef58854 GIT binary patch literal 734 zcmV<40wMj0P)dQcBIjU}J@%q+}^2AyQI|5*sTkQfd~Ig^j#rDP(~( zE0nj!TbIFnEY7_;-R?Pao_o)|=T7-9PtWsze&5@3&-tI{luG~UKrjp4MdxT3tidq0 zVRF%Vf~g3MiMWsD=qfr_W@-fBH_l;B(b+OlN1#d>n=rlTjH#puRC$NRMdwT;gFuy= zMQ5!`fWR1jVkagQo%I zXXO{}VOggL?7|dm#m4}V@3?@T!UbaEdT<)w0z}?mH>MUY5F6iz8yL+fF@`6Vb@Fc# zLj_{j;6^{1f;TAn^y=<<%rMD^RJ?jUiDho|Tta(-8u(TB}8EM#ytz zNHgyqc?wkObzvWV1W5eGevLfM^A?EJosMe(5-@@lg$rO3F3EnYRT(=Vv{lbrKy-T@ z2?!2nHF~s@mjG7cMM|wCUSml^;qwp>9gJNS_NT4o3%0aXK(x;f;X}&&5Wb_oEduD* z*mfC@x0ul~0rcXss2yk(7h5T?E@f;Yb=JI{GW@-euC)UNVBJI)xX^BaRH+68?zBsw zjEka|`C5)-SbHWO*O~jU)dGJ+ZS%s)^>+#L52mc!jb{n-uC!9%3kGVg-%FSeC+n;^ z4bM~NWItaD6zEN;aV#YaoT?c=oHDkoRkzWU8ncc6I(affc(|K6d|m>TI%mcSM${^# zF^WZb2#n&mfRi=RpYmx2i8PJ|g-;N;j-|ncqFVEfzeSQ5Y-G*2z%Vvw^7IUID?dV3 z%GlG)f~&Asj$i9V?+TmoASL)$oEN(1+YJ_oPPY%>u;|ETy-xpp`=2`e1=-pLm1y2b QXaE2J07*qoM6N<$f|apLeEMjH*Ix2@BF%WW^mYM$C>?(*8E}Eo0<20-v7Sedwjm*j_c5& zLy=$%Mi(7NX~9)^0Ow#>(eacH+X_FNc( zAlM^jyIU}>=r&}^5CzpZfKM^0=yqgALP0flh{x_M3@^G(sgjZ)Vp*^OcVTkT?MjUl z1rf`FKKz8s{1QsGiy;__U(iIwW!sL@WzF!y?DpA~PpJ zH4furoL+RhTc!;`AK)D^rB@P2>c=KLgc-_yTF{1|r!fh)V|@ZaH4bAf7URUCJq$`a zf~wyifs=5bI5$etn=0tVyXY?3MMK&ZG~l=M@gDvXJt7G`ctkvQql$3RkR*btzn_3x zu?G7SN;-tkafh;Of|ClWD`zV16sM3$s9=}a1Yc9QsI&ytl{Z?9DM=qWIEdvKo3)2# z4N6r|wak%NAjYR&$p&)Rh`TXO-ut^O1l5&21viVwB`N#GB6D#T9-1{GGeOnzhl}y) zVX-VoLcbWFR%5n27MWQKs;g%h=He@{EJ*s;{UJ8NQ!^Enm!P`(Cg7&3sHBqm99fNL zGu!1YsIJ~Kunb!hN`jqYpEy6w7;AwRBB-wZ8F*h@P$unBaIe@3rSsSoEvOnpm&gT3 zLr_27Kv!D972SiTSLOVef}jIRKpjchcP0L54z%v&ly8RM-Bb`m`KuPOy zMVemfgU@m=Hbxw(txH)-BevnvV?vPF&CkQL;-oN*>AMxS;KEW1!enuyx>0ohyxD{~ zr4u9$%C5urVw}mV*EHfSWmyM_$7v2$iG^Aj_#F$%B50Oa?4>z7t;VAbK5JY2eMKxM zk|)JWE~s1Fwx-F#LF&4vjWV`kdPh)muI~tH&MO^3&Dqcq)SMk<7gP~98O_Q+fX!lI zS^Hc*NL@D$_};IY&z}3sDyWvVu@={g_t6Gpm&g^G&}Q6IutJs`(G2lC2v`{{g{?asu5u-%UX@xaq5wr~-i6qT` z<@VsI78}%u2QhJwy1Lqs153*ws0&x(89v45VTn&iO-TzVvj0;zZYqnS z+M6yAW8UldTI3+^6`31-V#B*0FN+syx-;uFWkoCnCW@@HGjT4?6SuT;MJ~|^MaS8p cLx-062ZvJ(o>WhTC;$Ke07*qoM6N<$f`%81KmY&$ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png b/app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png new file mode 100755 index 0000000000000000000000000000000000000000..1f44c1aaf28c6773c1125ab7c6a0692c7c76b81c GIT binary patch literal 2177 zcmV-{2!8j8P)asI5TDR=Q1R=Hnl~`!dgQrt{{#d+vSj&HW~m`RC=H-}!y- z`JHpVzu!6cpo1z@s8FFog$nHr9D-AEM%lKEOW-hEgiTn3#bw(xu7KIN82hmk_hU6? zmTlX(Kpg@77|4j&hF@Ss+4glkh7eH00QO)DevDUPdfB#iB}O2i9?#--T!o`Bp=_JG z3Jnkdeb|Z(Sc&5>xoq3J0u2&SLq8tCI-HH?m2H2=V~hk4^TTf3h|6)BYJO-QO%PCz zZMXrK;02gec7Hk!EfP?}{rC>vf}_jsQ%4~u0^m8^g)if^m{E4$Is!QoP{R)V3LnNI zOf0*PZH)v35WDFu_&LtUT-E$AJQ5L5!(I`VzJg_Ve%bwPJ0vE6Shn`!4txZQRrA9j z3Lv0{n#i{f7h;}jekhcHdThn#@fsXiwhu~0NdydF7w*KTvCw$kO_xMKJ$7R~mSev0 z7M9w809o5lh}^^JLiQdX%U10{Kn;B&H@#YH;(By@{z3Bu>=NOlFGm7u=)*?5Q-q~y zx|Imc6L1TDhljCGOssih2YxDY4~OfcybI!l<1F>xFjM`R-38-u5 zV6m28hCfQPfvbspkKk*ffULbYa0()zuD$7E;{2!x8!6B)Lf#)lZuL3Ed z#QciP-VM@?fL<{IJ}as-#`g#iLf(r+sry=?dZrNbcH^%i(X&LzCF6Sz5Y9hW$O!j| zU2+OMjh~59m}89hRgh3kPZi6;yT!&SZ`9=LTf{;1d5V^=wHW~-ojFguiPwpZQmToz zU(_D05u32$a|hal0I|9`5^oUlKuX8gV<2nmYP?d>d1WONARK>)SZ`k{j=c6M#`!~W zoj7&?0_=!0aHY{IX~-sJK$z)2sbluGOiTK^LE90yjbexr~(KOj-MuKIzNbqnrN>KT!|w(ngAgm%oa(Hb)tx> zQ}7I4rxT?JgKzy}agoRF{QOcV*5M_cNWd(-N7QbnIyX=RdW9mIW)w(Az+7=fK~c`X zWR!j`Y2n#F!Tj8vpBYvWFgmv4wA`Ja8CDT6I{NWmDGGj71T=~@SWpo#GEx)pYn)OM zFfvjTa37Xb1dNQ-1U!Lrx|M(dt&4G~3D|=-bsGV-WXNwv`CUD35PH&iL;empK^l}p z>i^y*cDjvVAC`3s0R!Sv@LNTF&sljy`}fMG?@OIqn*)5Kf<%{q`tv z-^5t(kSLs*JI4Ob6mR}moKI9xzq&T!fw>zda1!9Ao@p@qPvGRDkT9|;V+{0{>_**!f$)TO^VIn0elk2jndCUh4b%dvYjh% zY>Vz5*059r94=z~avbjfH86_8SE4?Cr<*Ei`PVkE3KWF&~Z#7f-O!n1G3q842}V#nL1FzaBvTWdwP4~lR!KYQj8 z!ufZ$@XS6De$wQIwGROkM5x->g0mPL2`3BZ-`e8yHsMUED(18a0hujRgzmmaYtqC& zL|I8IZ$7d6{fv|!rO@0rWcJ~!n3KSs8RD>Z%FCV_#ckq9RJsG*#zsJ|7?RhDn*E;9 zTWHFr(?uvsaR{SPJSZJ1nd-> zmoJGz+CuJE5CvVI5*ksFBoZ67=0HTv@lOefVyQB2T@(u82Mk_#*S4RY5+`Xc76RYS?4=ni5jDr}GAL=6 z_p<50Z^{dOXNCCa^foba_ll8G3#GNc{*0)-nbWu9&*FnZ`uRH5NH^t}c4L00000NkvXXu0mjf DUcmjm literal 0 HcmV?d00001 From af42e32ae6ef75cc1ae143694f79640ddf45e6de Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Sun, 12 Aug 2018 18:34:20 +0530 Subject: [PATCH 07/68] Code refactored and added comments. --- app/src/main/java/org/schabi/newpipe/App.java | 4 +-- ...sk.java => CheckForNewAppVersionTask.java} | 34 +++++++++++++------ .../newpipe/player/PopupVideoPlayer.java | 6 ++-- 3 files changed, 26 insertions(+), 18 deletions(-) rename app/src/main/java/org/schabi/newpipe/{FetchAppVersionTask.java => CheckForNewAppVersionTask.java} (80%) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 0a218d061..fe64d95fb 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -101,9 +101,7 @@ public class App extends Application { configureRxJavaErrorHandler(); // Check for new version - if (BuildConfig.FLAVOR.equals("github")) { - new FetchAppVersionTask().execute(); - } + new CheckForNewAppVersionTask().execute(); } protected Downloader getDownloader() { diff --git a/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java similarity index 80% rename from app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java rename to app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 3130dc655..2be4712a7 100644 --- a/app/src/main/java/org/schabi/newpipe/FetchAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -19,20 +19,29 @@ import java.net.MalformedURLException; import java.net.URL; /** - * AsyncTask to check if there is a newer version of the github apk available or not. + * 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 download link. + * the notification, the user will be directed to the download link. */ -public class FetchAppVersionTask extends AsyncTask { +public class CheckForNewAppVersionTask extends AsyncTask { private String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json"; private int timeoutPeriod = 10000; + @Override + protected void onPreExecute() { + // Continue with version check only if the build variant is of type "github". + if (!BuildConfig.FLAVOR.equals("github")) { + this.cancel(true); + } + } + @Override protected String doInBackground(Void... voids) { - String output; + // Make a network request to get latest NewPipe data. + String response; HttpURLConnection connection = null; try { @@ -63,13 +72,14 @@ public class FetchAppVersionTask extends AsyncTask { String line; while ((line = bufferedReader.readLine()) != null) { - stringBuilder.append(line + "\n"); + stringBuilder.append(line); + stringBuilder.append("\n"); } bufferedReader.close(); - output = stringBuilder.toString(); + response = stringBuilder.toString(); - return output; + return response; } } catch (MalformedURLException ex) { ex.printStackTrace(); @@ -89,12 +99,13 @@ public class FetchAppVersionTask extends AsyncTask { } @Override - protected void onPostExecute(String output) { + protected void onPostExecute(String response) { - if (output != null) { + // Parse the json from the response. + if (response != null) { try { - JSONObject mainObject = new JSONObject(output); + JSONObject mainObject = new JSONObject(response); JSONObject flavoursObject = mainObject.getJSONObject("flavors"); JSONObject githubObject = flavoursObject.getJSONObject("github"); JSONObject githubStableObject = githubObject.getJSONObject("stable"); @@ -112,7 +123,8 @@ public class FetchAppVersionTask extends AsyncTask { } /** - * Method to compare + * 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 */ diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index a4c91cd30..2c233f219 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -58,7 +58,7 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; import org.schabi.newpipe.BuildConfig; -import org.schabi.newpipe.FetchAppVersionTask; +import org.schabi.newpipe.CheckForNewAppVersionTask; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; @@ -666,9 +666,7 @@ public final class PopupVideoPlayer extends Service { hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); // Check for new version - if (BuildConfig.FLAVOR.equals("github")) { - new FetchAppVersionTask().execute(); - } + new CheckForNewAppVersionTask().execute(); } @Override From 2a18eacf622e71226aa7216183ab1096ff288153 Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Sun, 12 Aug 2018 20:57:30 +0530 Subject: [PATCH 08/68] More refactoring. --- .../org/schabi/newpipe/CheckForNewAppVersionTask.java | 11 ++++------- app/src/main/res/values/strings.xml | 4 ++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 2be4712a7..134fb3406 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -25,13 +25,15 @@ import java.net.URL; */ public class CheckForNewAppVersionTask extends AsyncTask { + private Application app = App.getContext(); + private String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json"; private int timeoutPeriod = 10000; @Override protected void onPreExecute() { // Continue with version check only if the build variant is of type "github". - if (!BuildConfig.FLAVOR.equals("github")) { + if (!BuildConfig.FLAVOR.equals(app.getString(R.string.app_flavor_github))) { this.cancel(true); } } @@ -63,8 +65,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { case 200: case 201: - BufferedReader bufferedReader - = new BufferedReader( + BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(connection.getInputStream())); StringBuilder stringBuilder = new StringBuilder(); @@ -134,8 +135,6 @@ public class CheckForNewAppVersionTask extends AsyncTask { if (!BuildConfig.VERSION_NAME.equals(versionName.replace("v", ""))) { - Application app = App.getContext(); - // A pending intent to open the apk location url in the browser. Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl)); PendingIntent pendingIntent @@ -152,8 +151,6 @@ public class CheckForNewAppVersionTask extends AsyncTask { + " " + versionName); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(app); - - // notificationId is a unique int for each notification that you must define notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ad772494..f83324845 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -512,4 +512,8 @@ NewPipe Update Available Tap to download + + github + fdroid + From e7abeb5ad91afb700f0d914ea1906a5c05f18b89 Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Fri, 17 Aug 2018 00:53:42 +0530 Subject: [PATCH 09/68] Added version code check. --- app/src/main/java/org/schabi/newpipe/App.java | 30 +++++++++++-------- .../newpipe/CheckForNewAppVersionTask.java | 10 ++++--- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index fe64d95fb..a9002fda2 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -209,21 +209,25 @@ public class App extends Application { NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mNotificationManager.createNotificationChannel(mChannel); - // Set up notification channel for app update - 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); + // Set up notification channel for app update only if it's a github apk. - NotificationChannel appUpdateChannel - = new NotificationChannel(appUpdateId, appUpdateName, importance); - appUpdateChannel.setDescription(appUpdateDescription); + if (!BuildConfig.FLAVOR.equals(getString(R.string.app_flavor_github))) { - NotificationManager appUpdateNotificationManager - = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - appUpdateNotificationManager.createNotificationChannel(appUpdateChannel); + 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 diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 134fb3406..b6b269bb5 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -112,10 +112,10 @@ public class CheckForNewAppVersionTask extends AsyncTask { JSONObject githubStableObject = githubObject.getJSONObject("stable"); String versionName = githubStableObject.getString("version"); - // String versionCode = githubStableObject.getString("version_code"); + String versionCode = githubStableObject.getString("version_code"); String apkLocationUrl = githubStableObject.getString("apk"); - compareAppVersionAndShowNotification(versionName, apkLocationUrl); + compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode); } catch (JSONException ex) { ex.printStackTrace(); @@ -129,11 +129,13 @@ public class CheckForNewAppVersionTask extends AsyncTask { * @param versionName * @param apkLocationUrl */ - private void compareAppVersionAndShowNotification(String versionName, String apkLocationUrl) { + private void compareAppVersionAndShowNotification(String versionName, + String apkLocationUrl, + String versionCode) { int NOTIFICATION_ID = 2000; - if (!BuildConfig.VERSION_NAME.equals(versionName.replace("v", ""))) { + 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)); From 04e974b3264db7001bd03511bdfa32a326ef12d0 Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Fri, 17 Aug 2018 01:11:51 +0530 Subject: [PATCH 10/68] Bug fix. --- app/src/main/java/org/schabi/newpipe/App.java | 3 +-- .../java/org/schabi/newpipe/CheckForNewAppVersionTask.java | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index a9002fda2..c9ebc976c 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -210,8 +210,7 @@ public class App extends Application { mNotificationManager.createNotificationChannel(mChannel); // Set up notification channel for app update only if it's a github apk. - - if (!BuildConfig.FLAVOR.equals(getString(R.string.app_flavor_github))) { + if (BuildConfig.FLAVOR.equals(getString(R.string.app_flavor_github))) { final String appUpdateId = getString(R.string.app_update_notification_channel_id); diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index b6b269bb5..b53ef1794 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -7,6 +7,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; +import android.util.Log; import org.json.JSONException; import org.json.JSONObject; @@ -34,7 +35,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { protected void onPreExecute() { // Continue with version check only if the build variant is of type "github". if (!BuildConfig.FLAVOR.equals(app.getString(R.string.app_flavor_github))) { - this.cancel(true); + // this.cancel(true); } } @@ -102,6 +103,8 @@ public class CheckForNewAppVersionTask extends AsyncTask { @Override protected void onPostExecute(String response) { + Log.i("Response--", response); + // Parse the json from the response. if (response != null) { From 910c10f55428305b671b609a219a7f746ef32757 Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Fri, 17 Aug 2018 01:46:33 +0530 Subject: [PATCH 11/68] Removed debug code --- .../main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index b53ef1794..5794b0d0f 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -35,7 +35,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { protected void onPreExecute() { // Continue with version check only if the build variant is of type "github". if (!BuildConfig.FLAVOR.equals(app.getString(R.string.app_flavor_github))) { - // this.cancel(true); + this.cancel(true); } } From 17197ad670e96feea6c45b0d7d62f9de56d94032 Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Sat, 15 Sep 2018 12:51:39 +0530 Subject: [PATCH 12/68] Pull request changes begins here. --- .../java/org/schabi/newpipe/CheckForNewAppVersionTask.java | 4 ++-- .../main/java/org/schabi/newpipe/player/PopupVideoPlayer.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 5794b0d0f..628704b23 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -44,6 +44,8 @@ public class CheckForNewAppVersionTask extends AsyncTask { // Make a network request to get latest NewPipe data. + Log.i("Update---", "checking"); + String response; HttpURLConnection connection = null; @@ -103,8 +105,6 @@ public class CheckForNewAppVersionTask extends AsyncTask { @Override protected void onPostExecute(String response) { - Log.i("Response--", response); - // Parse the json from the response. if (response != null) { diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 2c233f219..f9892ecb5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -666,7 +666,7 @@ public final class PopupVideoPlayer extends Service { hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); // Check for new version - new CheckForNewAppVersionTask().execute(); + //new CheckForNewAppVersionTask().execute(); } @Override From 6417bd91ef339e246f2413ae791367bb2b21fd1c Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Sat, 15 Sep 2018 14:08:32 +0530 Subject: [PATCH 13/68] Pull request changes v1. --- .../java/org/schabi/newpipe/CheckForNewAppVersionTask.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 628704b23..4b75c202c 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -7,7 +7,6 @@ import android.net.Uri; import android.os.AsyncTask; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; -import android.util.Log; import org.json.JSONException; import org.json.JSONObject; @@ -16,7 +15,6 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; -import java.net.MalformedURLException; import java.net.URL; /** @@ -44,8 +42,6 @@ public class CheckForNewAppVersionTask extends AsyncTask { // Make a network request to get latest NewPipe data. - Log.i("Update---", "checking"); - String response; HttpURLConnection connection = null; @@ -85,8 +81,6 @@ public class CheckForNewAppVersionTask extends AsyncTask { return response; } - } catch (MalformedURLException ex) { - ex.printStackTrace(); } catch (IOException ex) { ex.printStackTrace(); } finally { From 7124d9bca50ab5fbc7965d069f9f41edf3dc73ea Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Sat, 15 Sep 2018 20:51:17 +0530 Subject: [PATCH 14/68] Removed flvor checks. Added update settings under main settings. --- app/build.gradle | 14 -------- app/src/main/java/org/schabi/newpipe/App.java | 29 +++++++-------- .../newpipe/CheckForNewAppVersionTask.java | 18 ++++++++-- .../settings/UpdateSettingsFragment.java | 35 +++++++++++++++++++ .../res/drawable/ic_settings_update_black.xml | 9 +++++ .../res/drawable/ic_settings_update_white.xml | 5 +++ app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/settings_keys.xml | 3 ++ app/src/main/res/values/strings.xml | 9 ++--- app/src/main/res/values/styles.xml | 2 ++ app/src/main/res/xml/main_settings.xml | 5 +++ app/src/main/res/xml/update_settings.xml | 13 +++++++ 12 files changed, 107 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java create mode 100644 app/src/main/res/drawable/ic_settings_update_black.xml create mode 100644 app/src/main/res/drawable/ic_settings_update_white.xml create mode 100644 app/src/main/res/xml/update_settings.xml diff --git a/app/build.gradle b/app/build.gradle index b5a106a6d..2fd175fbe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,20 +41,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - - flavorDimensions "apkSource" - productFlavors { - github { - dimension "apkSource" - applicationIdSuffix ".github" - - } - - fdroid { - dimension "apkSource" - applicationIdSuffix ".fdroid" - } - } } ext { diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index c9ebc976c..bc824699e 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -209,24 +209,21 @@ public class App extends Application { NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mNotificationManager.createNotificationChannel(mChannel); - // Set up notification channel for app update only if it's a github apk. - if (BuildConfig.FLAVOR.equals(getString(R.string.app_flavor_github))) { + // Set up notification channel for app update. + 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); - 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); - NotificationChannel appUpdateChannel - = new NotificationChannel(appUpdateId, appUpdateName, importance); - appUpdateChannel.setDescription(appUpdateDescription); - - NotificationManager appUpdateNotificationManager - = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - appUpdateNotificationManager.createNotificationChannel(appUpdateChannel); - } + NotificationManager appUpdateNotificationManager + = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + appUpdateNotificationManager.createNotificationChannel(appUpdateChannel); } @Nullable diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 4b75c202c..9694850d7 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -3,10 +3,13 @@ package org.schabi.newpipe; import android.app.Application; import android.app.PendingIntent; import android.content.Intent; +import android.content.SharedPreferences; 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 android.util.Log; import org.json.JSONException; import org.json.JSONObject; @@ -29,12 +32,23 @@ public class CheckForNewAppVersionTask extends AsyncTask { private String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json"; private int timeoutPeriod = 10000; + private SharedPreferences mPrefs; + @Override protected void onPreExecute() { - // Continue with version check only if the build variant is of type "github". - if (!BuildConfig.FLAVOR.equals(app.getString(R.string.app_flavor_github))) { + + mPrefs = PreferenceManager.getDefaultSharedPreferences(app); + + // Check if user has enabled/ disabled update checking. + if (mPrefs.getBoolean(app.getString(R.string.update_app_key), true)) { + + // Go ahead with further checks. + Log.i("pref---", "true"); + } else { + Log.i("pref---", "false"); this.cancel(true); } + } @Override diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java new file mode 100644 index 000000000..f6aab7cda --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -0,0 +1,35 @@ +package org.schabi.newpipe.settings; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.preference.Preference; + +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 + = new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + + defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), + (boolean) newValue).apply(); + + return true; + } + }; +} diff --git a/app/src/main/res/drawable/ic_settings_update_black.xml b/app/src/main/res/drawable/ic_settings_update_black.xml new file mode 100644 index 000000000..261c31217 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_update_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_update_white.xml b/app/src/main/res/drawable/ic_settings_update_white.xml new file mode 100644 index 000000000..0feb270af --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_update_white.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 7b879fb4c..75c39c490 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -38,6 +38,7 @@ + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 9b39fec26..b7ffb101d 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -201,6 +201,9 @@ @string/always_ask_open_action_key + + update_app_key + af diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ab55b6f6c..29fa4dc03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,6 +109,7 @@ Appearance Other Debug + Updates Playing in background Playing in popup mode Queued on background player @@ -515,6 +516,10 @@ 144p + + Updates + Show a notification to prompt app update when a new version is available + Minimize on application switch Action when switching to other application from main video player — %s @@ -526,8 +531,4 @@ NewPipe Update Available Tap to download - - github - fdroid - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 87e19cede..8eae8343d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -54,6 +54,7 @@ @drawable/ic_add_black_24dp @drawable/ic_settings_backup_restore_black_24dp @drawable/ic_blank_page_black_24dp + @drawable/ic_settings_update_black @color/light_separator_color @color/light_contrast_background_color @@ -114,6 +115,7 @@ @drawable/ic_add_white_24dp @drawable/ic_settings_backup_restore_white_24dp @drawable/ic_blank_page_white_24dp + @drawable/ic_settings_update_white @color/dark_separator_color @color/dark_contrast_background_color diff --git a/app/src/main/res/xml/main_settings.xml b/app/src/main/res/xml/main_settings.xml index 4196a98d3..3773460d6 100644 --- a/app/src/main/res/xml/main_settings.xml +++ b/app/src/main/res/xml/main_settings.xml @@ -29,6 +29,11 @@ android:icon="?attr/language" android:title="@string/content"/> + + + + + + + From e2341363d4b35e28181aa09f9f3d2993004b659f Mon Sep 17 00:00:00 2001 From: krtkush Date: Sun, 14 Oct 2018 19:16:28 +0530 Subject: [PATCH 15/68] Added check for SHA1 key. --- .../newpipe/CheckForNewAppVersionTask.java | 91 +++++++++++++++++-- 1 file changed, 82 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 9694850d7..fd94980c9 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -4,6 +4,9 @@ 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; @@ -15,10 +18,18 @@ import org.json.JSONException; import org.json.JSONObject; 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; /** * AsyncTask to check if there is a newer version of the NewPipe github apk available or not. @@ -29,6 +40,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { private Application app = App.getContext(); + private 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 String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json"; private int timeoutPeriod = 10000; @@ -39,23 +51,18 @@ public class CheckForNewAppVersionTask extends AsyncTask { mPrefs = PreferenceManager.getDefaultSharedPreferences(app); - // Check if user has enabled/ disabled update checking. - if (mPrefs.getBoolean(app.getString(R.string.update_app_key), true)) { - - // Go ahead with further checks. - Log.i("pref---", "true"); - } else { - Log.i("pref---", "false"); + // 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) + || !getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1)) { this.cancel(true); } - } @Override protected String doInBackground(Void... voids) { // Make a network request to get latest NewPipe data. - String response; HttpURLConnection connection = null; @@ -167,4 +174,70 @@ public class CheckForNewAppVersionTask extends AsyncTask { 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 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 e) { + e.printStackTrace(); + } + + Signature[] signatures = packageInfo.signatures; + byte[] cert = signatures[0].toByteArray(); + InputStream input = new ByteArrayInputStream(cert); + CertificateFactory cf = null; + + try { + cf = CertificateFactory.getInstance("X509"); + } catch (CertificateException e) { + e.printStackTrace(); + } + + X509Certificate c = null; + + try { + c = (X509Certificate) cf.generateCertificate(input); + } catch (CertificateException e) { + e.printStackTrace(); + } + + String hexString = null; + + try { + MessageDigest md = MessageDigest.getInstance("SHA1"); + byte[] publicKey = md.digest(c.getEncoded()); + hexString = byte2HexFormatted(publicKey); + } catch (NoSuchAlgorithmException e1) { + e1.printStackTrace(); + } catch (CertificateEncodingException e) { + e.printStackTrace(); + } + + 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(); + } } From 8ef702fa075529feee099e3f496ea8aea4f67520 Mon Sep 17 00:00:00 2001 From: krtkush Date: Thu, 18 Oct 2018 22:59:33 +0530 Subject: [PATCH 16/68] Removed updates options from settings in case of non github apk. --- .../gen/org/schabi/newpipe/BuildConfig.java | 8 ++++++++ .../debug/gen/org/schabi/newpipe/Manifest.java | 7 +++++++ app/src/debug/gen/org/schabi/newpipe/R.java | 7 +++++++ .../gen/org/schabi/newpipe/BuildConfig.java | 8 ++++++++ .../main/gen/org/schabi/newpipe/Manifest.java | 7 +++++++ app/src/main/gen/org/schabi/newpipe/R.java | 7 +++++++ .../newpipe/CheckForNewAppVersionTask.java | 18 +++++++++++------- .../newpipe/settings/MainSettingsFragment.java | 8 ++++++++ .../settings/UpdateSettingsFragment.java | 1 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 2 +- app/src/main/res/xml/main_settings.xml | 3 ++- 12 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 app/src/debug/gen/org/schabi/newpipe/BuildConfig.java create mode 100644 app/src/debug/gen/org/schabi/newpipe/Manifest.java create mode 100644 app/src/debug/gen/org/schabi/newpipe/R.java create mode 100644 app/src/main/gen/org/schabi/newpipe/BuildConfig.java create mode 100644 app/src/main/gen/org/schabi/newpipe/Manifest.java create mode 100644 app/src/main/gen/org/schabi/newpipe/R.java diff --git a/app/src/debug/gen/org/schabi/newpipe/BuildConfig.java b/app/src/debug/gen/org/schabi/newpipe/BuildConfig.java new file mode 100644 index 000000000..575afb9cb --- /dev/null +++ b/app/src/debug/gen/org/schabi/newpipe/BuildConfig.java @@ -0,0 +1,8 @@ +/*___Generated_by_IDEA___*/ + +package org.schabi.newpipe; + +/* This stub is only used by the IDE. It is NOT the BuildConfig class actually packed into the APK */ +public final class BuildConfig { + public final static boolean DEBUG = Boolean.parseBoolean(null); +} \ No newline at end of file diff --git a/app/src/debug/gen/org/schabi/newpipe/Manifest.java b/app/src/debug/gen/org/schabi/newpipe/Manifest.java new file mode 100644 index 000000000..10e45a45a --- /dev/null +++ b/app/src/debug/gen/org/schabi/newpipe/Manifest.java @@ -0,0 +1,7 @@ +/*___Generated_by_IDEA___*/ + +package org.schabi.newpipe; + +/* This stub is only used by the IDE. It is NOT the Manifest class actually packed into the APK */ +public final class Manifest { +} \ No newline at end of file diff --git a/app/src/debug/gen/org/schabi/newpipe/R.java b/app/src/debug/gen/org/schabi/newpipe/R.java new file mode 100644 index 000000000..088bdfb0d --- /dev/null +++ b/app/src/debug/gen/org/schabi/newpipe/R.java @@ -0,0 +1,7 @@ +/*___Generated_by_IDEA___*/ + +package org.schabi.newpipe; + +/* This stub is only used by the IDE. It is NOT the R class actually packed into the APK */ +public final class R { +} \ No newline at end of file diff --git a/app/src/main/gen/org/schabi/newpipe/BuildConfig.java b/app/src/main/gen/org/schabi/newpipe/BuildConfig.java new file mode 100644 index 000000000..575afb9cb --- /dev/null +++ b/app/src/main/gen/org/schabi/newpipe/BuildConfig.java @@ -0,0 +1,8 @@ +/*___Generated_by_IDEA___*/ + +package org.schabi.newpipe; + +/* This stub is only used by the IDE. It is NOT the BuildConfig class actually packed into the APK */ +public final class BuildConfig { + public final static boolean DEBUG = Boolean.parseBoolean(null); +} \ No newline at end of file diff --git a/app/src/main/gen/org/schabi/newpipe/Manifest.java b/app/src/main/gen/org/schabi/newpipe/Manifest.java new file mode 100644 index 000000000..10e45a45a --- /dev/null +++ b/app/src/main/gen/org/schabi/newpipe/Manifest.java @@ -0,0 +1,7 @@ +/*___Generated_by_IDEA___*/ + +package org.schabi.newpipe; + +/* This stub is only used by the IDE. It is NOT the Manifest class actually packed into the APK */ +public final class Manifest { +} \ No newline at end of file diff --git a/app/src/main/gen/org/schabi/newpipe/R.java b/app/src/main/gen/org/schabi/newpipe/R.java new file mode 100644 index 000000000..088bdfb0d --- /dev/null +++ b/app/src/main/gen/org/schabi/newpipe/R.java @@ -0,0 +1,7 @@ +/*___Generated_by_IDEA___*/ + +package org.schabi.newpipe; + +/* This stub is only used by the IDE. It is NOT the R class actually packed into the APK */ +public final class R { +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index fd94980c9..9ab0f5d36 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -38,11 +38,10 @@ import java.security.cert.X509Certificate; */ public class CheckForNewAppVersionTask extends AsyncTask { - private Application app = App.getContext(); - - private 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 String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json"; - private int timeoutPeriod = 10000; + private static final Application app = App.getContext(); + 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 = 10000; private SharedPreferences mPrefs; @@ -54,7 +53,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { // 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) - || !getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1)) { + || !isGithubApk()) { this.cancel(true); } } @@ -179,7 +178,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { * Method to get the apk's SHA1 key. * https://stackoverflow.com/questions/9293019/get-certificate-fingerprint-from-android-app#22506133 */ - private String getCertificateSHA1Fingerprint() { + private static String getCertificateSHA1Fingerprint() { PackageManager pm = app.getPackageManager(); String packageName = app.getPackageName(); @@ -240,4 +239,9 @@ public class CheckForNewAppVersionTask extends AsyncTask { } return str.toString(); } + + public static boolean isGithubApk() { + + return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1); + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java index 5e07e2b12..43270926e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -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); diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java index f6aab7cda..157f52a0e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -4,6 +4,7 @@ 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 { diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index e282a3bf5..9c6ae92b0 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -205,6 +205,7 @@ update_app_key + update_pref_screen_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f8c7932a7..f7e461829 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -529,7 +529,7 @@ Switch View - NewPipe Update Available + NewPipe Update Available! Tap to download diff --git a/app/src/main/res/xml/main_settings.xml b/app/src/main/res/xml/main_settings.xml index 3773460d6..74450f77b 100644 --- a/app/src/main/res/xml/main_settings.xml +++ b/app/src/main/res/xml/main_settings.xml @@ -32,7 +32,8 @@ + android:title="@string/settings_category_updates_title" + android:key="update_pref_screen_key"/> Date: Thu, 18 Oct 2018 23:02:19 +0530 Subject: [PATCH 17/68] Delete BuildConfig.java --- app/src/debug/gen/org/schabi/newpipe/BuildConfig.java | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 app/src/debug/gen/org/schabi/newpipe/BuildConfig.java diff --git a/app/src/debug/gen/org/schabi/newpipe/BuildConfig.java b/app/src/debug/gen/org/schabi/newpipe/BuildConfig.java deleted file mode 100644 index 575afb9cb..000000000 --- a/app/src/debug/gen/org/schabi/newpipe/BuildConfig.java +++ /dev/null @@ -1,8 +0,0 @@ -/*___Generated_by_IDEA___*/ - -package org.schabi.newpipe; - -/* This stub is only used by the IDE. It is NOT the BuildConfig class actually packed into the APK */ -public final class BuildConfig { - public final static boolean DEBUG = Boolean.parseBoolean(null); -} \ No newline at end of file From 2d5bc3ada85d2fb0151bfe32b6a6fef9d08c0df8 Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Thu, 18 Oct 2018 23:02:30 +0530 Subject: [PATCH 18/68] Delete Manifest.java --- app/src/debug/gen/org/schabi/newpipe/Manifest.java | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 app/src/debug/gen/org/schabi/newpipe/Manifest.java diff --git a/app/src/debug/gen/org/schabi/newpipe/Manifest.java b/app/src/debug/gen/org/schabi/newpipe/Manifest.java deleted file mode 100644 index 10e45a45a..000000000 --- a/app/src/debug/gen/org/schabi/newpipe/Manifest.java +++ /dev/null @@ -1,7 +0,0 @@ -/*___Generated_by_IDEA___*/ - -package org.schabi.newpipe; - -/* This stub is only used by the IDE. It is NOT the Manifest class actually packed into the APK */ -public final class Manifest { -} \ No newline at end of file From d1a9033525250071a6263f6c4caf3f55ebb285cc Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Thu, 18 Oct 2018 23:02:38 +0530 Subject: [PATCH 19/68] Delete R.java --- app/src/debug/gen/org/schabi/newpipe/R.java | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 app/src/debug/gen/org/schabi/newpipe/R.java diff --git a/app/src/debug/gen/org/schabi/newpipe/R.java b/app/src/debug/gen/org/schabi/newpipe/R.java deleted file mode 100644 index 088bdfb0d..000000000 --- a/app/src/debug/gen/org/schabi/newpipe/R.java +++ /dev/null @@ -1,7 +0,0 @@ -/*___Generated_by_IDEA___*/ - -package org.schabi.newpipe; - -/* This stub is only used by the IDE. It is NOT the R class actually packed into the APK */ -public final class R { -} \ No newline at end of file From ec28e972bbf756c98fc95cf1080a3be465d68bcb Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Thu, 18 Oct 2018 23:02:59 +0530 Subject: [PATCH 20/68] Delete BuildConfig.java --- app/src/main/gen/org/schabi/newpipe/BuildConfig.java | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 app/src/main/gen/org/schabi/newpipe/BuildConfig.java diff --git a/app/src/main/gen/org/schabi/newpipe/BuildConfig.java b/app/src/main/gen/org/schabi/newpipe/BuildConfig.java deleted file mode 100644 index 575afb9cb..000000000 --- a/app/src/main/gen/org/schabi/newpipe/BuildConfig.java +++ /dev/null @@ -1,8 +0,0 @@ -/*___Generated_by_IDEA___*/ - -package org.schabi.newpipe; - -/* This stub is only used by the IDE. It is NOT the BuildConfig class actually packed into the APK */ -public final class BuildConfig { - public final static boolean DEBUG = Boolean.parseBoolean(null); -} \ No newline at end of file From 6ef25eb861d0dcf9cf659c29e041b7ec2dd7a05a Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Thu, 18 Oct 2018 23:03:21 +0530 Subject: [PATCH 21/68] Delete Manifest.java --- app/src/main/gen/org/schabi/newpipe/Manifest.java | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 app/src/main/gen/org/schabi/newpipe/Manifest.java diff --git a/app/src/main/gen/org/schabi/newpipe/Manifest.java b/app/src/main/gen/org/schabi/newpipe/Manifest.java deleted file mode 100644 index 10e45a45a..000000000 --- a/app/src/main/gen/org/schabi/newpipe/Manifest.java +++ /dev/null @@ -1,7 +0,0 @@ -/*___Generated_by_IDEA___*/ - -package org.schabi.newpipe; - -/* This stub is only used by the IDE. It is NOT the Manifest class actually packed into the APK */ -public final class Manifest { -} \ No newline at end of file From 3c6d27b5044520d4877eaef028d6a7fe729a021e Mon Sep 17 00:00:00 2001 From: Kartikey Kushwaha Date: Thu, 18 Oct 2018 23:04:07 +0530 Subject: [PATCH 22/68] Delete R.java --- app/src/main/gen/org/schabi/newpipe/R.java | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 app/src/main/gen/org/schabi/newpipe/R.java diff --git a/app/src/main/gen/org/schabi/newpipe/R.java b/app/src/main/gen/org/schabi/newpipe/R.java deleted file mode 100644 index 088bdfb0d..000000000 --- a/app/src/main/gen/org/schabi/newpipe/R.java +++ /dev/null @@ -1,7 +0,0 @@ -/*___Generated_by_IDEA___*/ - -package org.schabi.newpipe; - -/* This stub is only used by the IDE. It is NOT the R class actually packed into the APK */ -public final class R { -} \ No newline at end of file From fda9b591295ef057043c985820e7c4707d61adcc Mon Sep 17 00:00:00 2001 From: Tobias Groza Date: Mon, 22 Oct 2018 23:08:46 +0530 Subject: [PATCH 23/68] Code review changes. Co-Authored-By: krtkush --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7e461829..f4910388f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -518,7 +518,7 @@ Updates - Show a notification to prompt app update when a new version is available + Show a notification to prompt app update when a new version is available Minimize on application switch From 96dac0f9791e47b72fb25f19cd57a49cd1f444a1 Mon Sep 17 00:00:00 2001 From: krtkush Date: Mon, 22 Oct 2018 23:12:25 +0530 Subject: [PATCH 24/68] Code review suggested changes. --- .../org/schabi/newpipe/CheckForNewAppVersionTask.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 9ab0f5d36..e122a5688 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -194,17 +194,12 @@ public class CheckForNewAppVersionTask extends AsyncTask { Signature[] signatures = packageInfo.signatures; byte[] cert = signatures[0].toByteArray(); InputStream input = new ByteArrayInputStream(cert); + CertificateFactory cf = null; - - try { - cf = CertificateFactory.getInstance("X509"); - } catch (CertificateException e) { - e.printStackTrace(); - } - X509Certificate c = null; try { + cf = CertificateFactory.getInstance("X509"); c = (X509Certificate) cf.generateCertificate(input); } catch (CertificateException e) { e.printStackTrace(); From 5825843f68c0d44b0c65fbdc2c838d0c6a01e5f2 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Sun, 23 Sep 2018 15:12:23 -0300 Subject: [PATCH 25/68] main commit Post-processing infrastructure * remove interfaces with one implementation * fix download resources with unknow length * marquee style for ProgressDrawable * "view details" option in mission context menu * notification for finished downloads * postprocessing infrastructure: sub-missions, circular file, layers for layers of abstractions for Java IO streams * Mp4 muxing (only DASH brand) * WebM muxing * Captions downloading * alert dialog for overwrite existing downloads finished or not. Misc changes * delete SQLiteDownloadDataSource.java * delete DownloadMissionSQLiteHelper.java * implement Localization from #114 Misc fixes (this branch) * restore old mission listeners variables. Prevents registered listeners get de-referenced on low-end devices * DownloadManagerService.checkForRunningMission() now return false if the mission has a error. * use Intent.FLAG_ACTIVITY_NEW_TASK when launching an activity from gigaget threads (apparently it is required in old versions of android) More changes * proper error handling "infrastructure" * queue instead of multiple downloads * move serialized pending downloads (.giga files) to app data * stop downloads when swicthing to mobile network (never works, see 2nd point) * save the thread count for next downloads * a lot of incoherences fixed * delete DownloadManagerTest.java (too many changes to keep this file updated) --- .../java/org/schabi/newpipe/Downloader.java | 9 +- .../download/DeleteDownloadManager.java | 33 +- .../newpipe/download/DownloadActivity.java | 48 +- .../newpipe/download/DownloadDialog.java | 258 +++++- .../fragments/detail/VideoDetailFragment.java | 19 +- .../newpipe/player/helper/PlayerHelper.java | 10 +- .../resolver/VideoPlaybackResolver.java | 6 +- .../newpipe/util/StreamItemAdapter.java | 46 +- .../giga/get/DownloadInitializer.java | 158 ++++ .../us/shandian/giga/get/DownloadManager.java | 53 -- .../us/shandian/giga/get/DownloadMission.java | 638 ++++++++++----- .../shandian/giga/get/DownloadRunnable.java | 143 ++-- .../giga/get/DownloadRunnableFallback.java | 133 +-- .../us/shandian/giga/get/FinishedMission.java | 16 + .../java/us/shandian/giga/get/Mission.java | 66 ++ .../giga/get/sqlite/DownloadDataSource.java | 73 ++ ...Helper.java => DownloadMissionHelper.java} | 67 +- .../get/sqlite/SQLiteDownloadDataSource.java | 79 -- .../giga/postprocessing/Mp4DashMuxer.java | 31 + .../giga/postprocessing/Postprocessing.java | 149 ++++ .../giga/postprocessing/TestAlgo.java | 54 ++ .../giga/postprocessing/TttmlConverter.java | 49 ++ .../giga/postprocessing/WebMMuxer.java | 44 + .../io/ChunkFileInputStream.java | 153 ++++ .../giga/postprocessing/io/CircularFile.java | 345 ++++++++ .../giga/postprocessing/io/FileStream.java | 126 +++ .../postprocessing/io/SharpInputStream.java | 59 ++ .../giga/service/DownloadManager.java | 670 ++++++++++++++++ .../giga/service/DownloadManagerService.java | 359 ++++++--- .../giga/ui/adapter/MissionAdapter.java | 759 ++++++++++++------ .../us/shandian/giga/ui/common/Deleter.java | 169 ++++ .../giga/ui/common/ProgressDrawable.java | 94 ++- .../giga/ui/fragment/MissionsFragment.java | 160 ++-- .../java/us/shandian/giga/util/Utility.java | 117 ++- app/src/main/res/drawable-hdpi/grid.png | Bin 3039 -> 3341 bytes app/src/main/res/drawable-hdpi/list.png | Bin 3129 -> 3156 bytes app/src/main/res/drawable-xhdpi/subtitle.png | Bin 0 -> 3733 bytes app/src/main/res/layout/download_dialog.xml | 7 + app/src/main/res/layout/mission_item.xml | 18 +- app/src/main/res/layout/missions.xml | 17 +- app/src/main/res/layout/missions_header.xml | 30 + app/src/main/res/menu/download_menu.xml | 28 +- app/src/main/res/menu/mission.xml | 79 +- app/src/main/res/values-es/strings.xml | 52 +- app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/settings_keys.xml | 6 + app/src/main/res/values/strings.xml | 53 +- app/src/main/res/xml/download_settings.xml | 13 + 48 files changed, 4379 insertions(+), 1119 deletions(-) create mode 100644 app/src/main/java/us/shandian/giga/get/DownloadInitializer.java delete mode 100644 app/src/main/java/us/shandian/giga/get/DownloadManager.java create mode 100644 app/src/main/java/us/shandian/giga/get/FinishedMission.java create mode 100644 app/src/main/java/us/shandian/giga/get/Mission.java create mode 100644 app/src/main/java/us/shandian/giga/get/sqlite/DownloadDataSource.java rename app/src/main/java/us/shandian/giga/get/sqlite/{DownloadMissionSQLiteHelper.java => DownloadMissionHelper.java} (63%) delete mode 100644 app/src/main/java/us/shandian/giga/get/sqlite/SQLiteDownloadDataSource.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java create mode 100644 app/src/main/java/us/shandian/giga/service/DownloadManager.java create mode 100644 app/src/main/java/us/shandian/giga/ui/common/Deleter.java create mode 100644 app/src/main/res/drawable-xhdpi/subtitle.png create mode 100644 app/src/main/res/layout/missions_header.xml diff --git a/app/src/main/java/org/schabi/newpipe/Downloader.java b/app/src/main/java/org/schabi/newpipe/Downloader.java index 62c7d1671..177f1f624 100644 --- a/app/src/main/java/org/schabi/newpipe/Downloader.java +++ b/app/src/main/java/org/schabi/newpipe/Downloader.java @@ -89,7 +89,8 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { .build(); response = client.newCall(request).execute(); - return Long.parseLong(response.header("Content-Length")); + String contentLength = response.header("Content-Length"); + return contentLength == null ? -1 : Long.parseLong(contentLength); } catch (NumberFormatException e) { throw new IOException("Invalid content length", e); } finally { @@ -104,13 +105,13 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { * but set the HTTP header field "Accept-Language" to the supplied string. * * @param siteUrl the URL of the text file to return the contents of - * @param localization the language and country (usually a 2-character code) to set + * @param localisation the language and country (usually a 2-character code) to set * @return the contents of the specified text file */ @Override - public String download(String siteUrl, Localization localization) throws IOException, ReCaptchaException { + public String download(String siteUrl, Localization localisation) throws IOException, ReCaptchaException { Map requestProperties = new HashMap<>(); - requestProperties.put("Accept-Language", localization.getLanguage()); + requestProperties.put("Accept-Language", localisation.getLanguage()); return download(siteUrl, requestProperties); } diff --git a/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java b/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java index 5a2d4a486..2f539e343 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java +++ b/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java @@ -28,14 +28,14 @@ public class DeleteDownloadManager { private static final String KEY_STATE = "delete_manager_state"; - private final View mView; - private final HashSet mPendingMap; - private final List mDisposableList; + private View mView; + private ArrayList mPendingMap; + private List mDisposableList; private DownloadManager mDownloadManager; private final PublishSubject publishSubject = PublishSubject.create(); DeleteDownloadManager(Activity activity) { - mPendingMap = new HashSet<>(); + mPendingMap = new ArrayList<>(); mDisposableList = new ArrayList<>(); mView = activity.findViewById(android.R.id.content); } @@ -45,11 +45,11 @@ public class DeleteDownloadManager { } public boolean contains(@NonNull DownloadMission mission) { - return mPendingMap.contains(mission.url); + return mPendingMap.contains(mission.timestamp); } public void add(@NonNull DownloadMission mission) { - mPendingMap.add(mission.url); + mPendingMap.add(mission.timestamp); if (mPendingMap.size() == 1) { showUndoDeleteSnackbar(mission); @@ -67,9 +67,10 @@ public class DeleteDownloadManager { public void restoreState(@Nullable Bundle savedInstanceState) { if (savedInstanceState == null) return; - List list = savedInstanceState.getStringArrayList(KEY_STATE); + long[] list = savedInstanceState.getLongArray(KEY_STATE); if (list != null) { - mPendingMap.addAll(list); + mPendingMap.ensureCapacity(mPendingMap.size() + list.length); + for (long timestamp : list) mPendingMap.add(timestamp); } } @@ -80,17 +81,20 @@ public class DeleteDownloadManager { disposable.dispose(); } - outState.putStringArrayList(KEY_STATE, new ArrayList<>(mPendingMap)); + long[] list = new long[mPendingMap.size()]; + for (int i = 0; i < mPendingMap.size(); i++) list[i] = mPendingMap.get(i); + + outState.putLongArray(KEY_STATE, list); } private void showUndoDeleteSnackbar() { if (mPendingMap.size() < 1) return; - String url = mPendingMap.iterator().next(); + long timestamp = mPendingMap.iterator().next(); for (int i = 0; i < mDownloadManager.getCount(); i++) { DownloadMission mission = mDownloadManager.getMission(i); - if (url.equals(mission.url)) { + if (timestamp == mission.timestamp) { showUndoDeleteSnackbar(mission); break; } @@ -106,7 +110,7 @@ public class DeleteDownloadManager { mDisposableList.add(disposable); snackbar.setAction(R.string.undo, v -> { - mPendingMap.remove(mission.url); + mPendingMap.remove(mission.timestamp); publishSubject.onNext(mission); disposable.dispose(); snackbar.dismiss(); @@ -115,12 +119,13 @@ public class DeleteDownloadManager { snackbar.addCallback(new BaseTransientBottomBar.BaseCallback() { @Override public void onDismissed(Snackbar transientBottomBar, int event) { + // TODO: disposable.isDisposed() is always true. fix this if (!disposable.isDisposed()) { Completable.fromAction(() -> deletePending(mission)) .subscribeOn(Schedulers.io()) .subscribe(); } - mPendingMap.remove(mission.url); + mPendingMap.remove(mission.timestamp); snackbar.removeCallback(this); mDisposableList.remove(disposable); showUndoDeleteSnackbar(); @@ -149,7 +154,7 @@ public class DeleteDownloadManager { private void deletePending(@NonNull DownloadMission mission) { for (int i = 0; i < mDownloadManager.getCount(); i++) { - if (mission.url.equals(mDownloadManager.getMission(i).url)) { + if (mission.timestamp == mDownloadManager.getMission(i).timestamp) { mDownloadManager.deleteMission(i); break; } diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index 4a2c85149..29940f802 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -15,19 +15,16 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.util.ThemeHelper; -import io.reactivex.Completable; -import io.reactivex.schedulers.Schedulers; import us.shandian.giga.service.DownloadManagerService; -import us.shandian.giga.ui.fragment.AllMissionsFragment; import us.shandian.giga.ui.fragment.MissionsFragment; public class DownloadActivity extends AppCompatActivity { private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag"; - private DeleteDownloadManager mDeleteDownloadManager; @Override protected void onCreate(Bundle savedInstanceState) { + // Service Intent i = new Intent(); i.setClass(this, DownloadManagerService.class); @@ -47,32 +44,17 @@ public class DownloadActivity extends AppCompatActivity { actionBar.setDisplayShowTitleEnabled(true); } - mDeleteDownloadManager = new DeleteDownloadManager(this); - mDeleteDownloadManager.restoreState(savedInstanceState); - - MissionsFragment fragment = (MissionsFragment) getFragmentManager().findFragmentByTag(MISSIONS_FRAGMENT_TAG); - if (fragment != null) { - fragment.setDeleteManager(mDeleteDownloadManager); - } else { - getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - updateFragments(); - getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this); - } - }); - } - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - mDeleteDownloadManager.saveState(outState); - super.onSaveInstanceState(outState); + getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + updateFragments(); + getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + }); } private void updateFragments() { - MissionsFragment fragment = new AllMissionsFragment(); - fragment.setDeleteManager(mDeleteDownloadManager); + MissionsFragment fragment = new MissionsFragment(); getFragmentManager().beginTransaction() .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG) @@ -99,7 +81,6 @@ public class DownloadActivity extends AppCompatActivity { case R.id.action_settings: { Intent intent = new Intent(this, SettingsActivity.class); startActivity(intent); - deletePending(); return true; } default: @@ -108,14 +89,7 @@ public class DownloadActivity extends AppCompatActivity { } @Override - public void onBackPressed() { - super.onBackPressed(); - deletePending(); - } - - private void deletePending() { - Completable.fromAction(mDeleteDownloadManager::deletePending) - .subscribeOn(Schedulers.io()) - .subscribe(); + public void onRestoreInstanceState(Bundle inState){ + super.onRestoreInstanceState(inState); } } diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 9bbda6032..aab6da1a4 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -1,11 +1,14 @@ package org.schabi.newpipe.download; import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; +import android.preference.PreferenceManager; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.LayoutInflater; @@ -22,10 +25,14 @@ import android.widget.Toast; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.extractor.utils.Localization; import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.util.FilenameUtils; import org.schabi.newpipe.util.ListHelper; @@ -36,24 +43,36 @@ import org.schabi.newpipe.util.ThemeHelper; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import icepick.Icepick; import icepick.State; import io.reactivex.disposables.CompositeDisposable; +import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.service.DownloadManagerService; public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { private static final String TAG = "DialogFragment"; private static final boolean DEBUG = MainActivity.DEBUG; - @State protected StreamInfo currentInfo; - @State protected StreamSizeWrapper wrappedAudioStreams = StreamSizeWrapper.empty(); - @State protected StreamSizeWrapper wrappedVideoStreams = StreamSizeWrapper.empty(); - @State protected int selectedVideoIndex = 0; - @State protected int selectedAudioIndex = 0; + @State + protected StreamInfo currentInfo; + @State + protected StreamSizeWrapper wrappedAudioStreams = StreamSizeWrapper.empty(); + @State + protected StreamSizeWrapper wrappedVideoStreams = StreamSizeWrapper.empty(); + @State + protected StreamSizeWrapper wrappedSubtitleStreams = StreamSizeWrapper.empty(); + @State + protected int selectedVideoIndex = 0; + @State + protected int selectedAudioIndex = 0; + @State + protected int selectedSubtitleIndex = 0; private StreamItemAdapter audioStreamsAdapter; private StreamItemAdapter videoStreamsAdapter; + private StreamItemAdapter subtitleStreamsAdapter; private final CompositeDisposable disposables = new CompositeDisposable(); @@ -63,6 +82,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private TextView threadsCountTextView; private SeekBar threadsSeekBar; + private SharedPreferences prefs; + public static DownloadDialog newInstance(StreamInfo info) { DownloadDialog dialog = new DownloadDialog(); dialog.setInfo(info); @@ -78,6 +99,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck instance.setVideoStreams(streamsList); instance.setSelectedVideoStream(selectedStreamIndex); instance.setAudioStreams(info.getAudioStreams()); + instance.setSubtitleStreams(info.getSubtitles()); + return instance; } @@ -86,7 +109,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } public void setAudioStreams(List audioStreams) { - setAudioStreams(new StreamSizeWrapper<>(audioStreams)); + setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext())); } public void setAudioStreams(StreamSizeWrapper wrappedAudioStreams) { @@ -94,13 +117,21 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } public void setVideoStreams(List videoStreams) { - setVideoStreams(new StreamSizeWrapper<>(videoStreams)); + setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext())); } public void setVideoStreams(StreamSizeWrapper wrappedVideoStreams) { this.wrappedVideoStreams = wrappedVideoStreams; } + public void setSubtitleStreams(List subtitleStreams) { + setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext())); + } + + public void setSubtitleStreams(StreamSizeWrapper wrappedSubtitleStreams) { + this.wrappedSubtitleStreams = wrappedSubtitleStreams; + } + public void setSelectedVideoStream(int selectedVideoIndex) { this.selectedVideoIndex = selectedVideoIndex; } @@ -109,6 +140,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck this.selectedAudioIndex = selectedAudioIndex; } + public void setSelectedSubtitleStream(int selectedSubtitleIndex) { + this.selectedSubtitleIndex = selectedSubtitleIndex; + } + /*////////////////////////////////////////////////////////////////////////// // LifeCycle //////////////////////////////////////////////////////////////////////////*/ @@ -116,7 +151,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + if (DEBUG) + Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { getDialog().dismiss(); return; @@ -127,11 +163,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, true); this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams); + this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); + if (DEBUG) + Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); return inflater.inflate(R.layout.download_dialog, container); } @@ -142,6 +180,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName())); selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams()); + selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll()); + streamsSpinner = view.findViewById(R.id.quality_spinner); streamsSpinner.setOnItemSelectedListener(this); @@ -154,14 +194,18 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck initToolbar(view.findViewById(R.id.toolbar)); setupDownloadOptions(); - int def = 3; - threadsCountTextView.setText(String.valueOf(def)); - threadsSeekBar.setProgress(def - 1); + prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + + int threads = prefs.getInt(getString(R.string.default_download_threads), 3); + threadsCountTextView.setText(String.valueOf(threads)); + threadsSeekBar.setProgress(threads - 1); threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) { - threadsCountTextView.setText(String.valueOf(progress + 1)); + progress++; + prefs.edit().putInt(getString(R.string.default_download_threads), progress).apply(); + threadsCountTextView.setText(String.valueOf(progress)); } @Override @@ -189,6 +233,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck setupAudioSpinner(); } })); + disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> { + if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { + setupSubtitleSpinner(); + } + })); } @Override @@ -216,7 +265,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck toolbar.setOnMenuItemClickListener(item -> { if (item.getItemId() == R.id.okay) { - downloadSelected(); + prepareSelectedDownload(); return true; } return false; @@ -239,13 +288,24 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck setRadioButtonsState(true); } + private void setupSubtitleSpinner() { + if (getContext() == null) return; + + streamsSpinner.setAdapter(subtitleStreamsAdapter); + streamsSpinner.setSelection(selectedSubtitleIndex); + setRadioButtonsState(true); + } + /*////////////////////////////////////////////////////////////////////////// // Radio group Video&Audio options - Listener //////////////////////////////////////////////////////////////////////////*/ @Override public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) { - if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]"); + if (DEBUG) + Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]"); + boolean flag = true; + switch (checkedId) { case R.id.audio_button: setupAudioSpinner(); @@ -253,7 +313,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck case R.id.video_button: setupVideoSpinner(); break; + case R.id.subtitle_button: + setupSubtitleSpinner(); + flag = false; + break; } + + threadsSeekBar.setEnabled(flag); } /*////////////////////////////////////////////////////////////////////////// @@ -262,7 +328,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (DEBUG) Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); + if (DEBUG) + Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); switch (radioVideoAudioGroup.getCheckedRadioButtonId()) { case R.id.audio_button: selectedAudioIndex = position; @@ -270,6 +337,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck case R.id.video_button: selectedVideoIndex = position; break; + case R.id.subtitle_button: + selectedSubtitleIndex = position; + break; } } @@ -286,11 +356,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button); final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button); + final RadioButton subtitleButton = radioVideoAudioGroup.findViewById(R.id.subtitle_button); final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0; final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; + final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0; audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE); videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE); + subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE); if (isVideoStreamsAvailable) { videoButton.setChecked(true); @@ -298,6 +371,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } else if (isAudioStreamsAvailable) { audioButton.setChecked(true); setupAudioSpinner(); + } else if (isSubtitleStreamsAvailable) { + subtitleButton.setChecked(true); + setupSubtitleSpinner(); } else { Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show(); getDialog().dismiss(); @@ -307,28 +383,144 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private void setRadioButtonsState(boolean enabled) { radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled); radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled); + radioVideoAudioGroup.findViewById(R.id.subtitle_button).setEnabled(enabled); } - private void downloadSelected() { - Stream stream; - String location; - - String fileName = nameEditText.getText().toString().trim(); - if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName()); - - boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button; - if (isAudio) { - stream = audioStreamsAdapter.getItem(selectedAudioIndex); - location = NewPipeSettings.getAudioDownloadPath(getContext()); - } else { - stream = videoStreamsAdapter.getItem(selectedVideoIndex); - location = NewPipeSettings.getVideoDownloadPath(getContext()); + private int getSubtitleIndexBy(List streams) { + Localization loc = NewPipe.getLocalization(); + for (int j = 0; j < 2; j++) { + for (int i = 0; i < streams.size(); i++) { + Locale streamLocale = streams.get(i).getLocale(); + if (streamLocale.getLanguage().equals(loc.getLanguage())) { + if (j > 0 || streamLocale.getCountry().equals(loc.getCountry())) { + return i; + } + } + } } - String url = stream.getUrl(); - fileName += "." + stream.getFormat().getSuffix(); + return 0; + } + + private void prepareSelectedDownload() { + final Context context = getContext(); + Stream stream; + String location; + char kind; + + String fileName = nameEditText.getText().toString().trim(); + if (fileName.isEmpty()) + fileName = FilenameUtils.createFilename(context, currentInfo.getName()); + + switch (radioVideoAudioGroup.getCheckedRadioButtonId()) { + case R.id.audio_button: + stream = audioStreamsAdapter.getItem(selectedAudioIndex); + location = NewPipeSettings.getAudioDownloadPath(context); + kind = 'a'; + break; + case R.id.video_button: + stream = videoStreamsAdapter.getItem(selectedVideoIndex); + location = NewPipeSettings.getVideoDownloadPath(context); + kind = 'v'; + break; + case R.id.subtitle_button: + stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); + location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video go together + kind = 's'; + break; + default: + return; + } + + int threads; + + if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { + threads = 1;// use unique thread for subtitles due small file size + fileName += ".srt";// final subtitle format + } else { + threads = threadsSeekBar.getProgress() + 1; + fileName += "." + stream.getFormat().getSuffix(); + } + + 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) + .setMessage(finished ? R.string.overwrite_warning : R.string.download_already_running) + .setPositiveButton( + finished ? R.string.overwrite : R.string.generate_unique_name, + (dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads) + ) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> { + dialog.cancel(); + }) + .create() + .show(); + } else { + downloadSelected(context, stream, location, finalFileName, kind, threads); + } + }); + } + + private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) { + String[] urls; + String psName = null; + String[] psArgs = null; + String secondaryStream = null; + + if (selectedStream instanceof VideoStream) { + VideoStream videoStream = (VideoStream) selectedStream; + if (videoStream.isVideoOnly() && videoStream.getFormat() != MediaFormat.v3GPP) { + boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4; + + for (AudioStream audio : audioStreamsAdapter.getAll()) { + if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) { + secondaryStream = audio.getUrl(); + break; + } + } + + if (secondaryStream == null) { + // retry, but this time in reverse order + List audioStreams = audioStreamsAdapter.getAll(); + for (int i = audioStreams.size() - 1; i >= 0; i--) { + AudioStream audio = audioStreams.get(i); + if (audio.getFormat() == (m4v ? MediaFormat.MP3 : MediaFormat.OPUS)) { + secondaryStream = audio.getUrl(); + break; + } + } + } + + if (secondaryStream == null) { + Log.w(TAG, "No audio stream candidates for video format " + videoStream.getFormat().name()); + psName = null; + psArgs = null; + } else { + psName = m4v ? Postprocessing.ALGORITHM_MP4_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; + psArgs = null; + } + } + } else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) { + psName = Postprocessing.ALGORITHM_TTML_CONVERTER; + psArgs = new String[]{ + selectedStream.getFormat().getSuffix(), + "false",//ignore empty frames + "false",// detect youtube duplicateLines + }; + } + + if (secondaryStream == null) { + urls = new String[]{selectedStream.getUrl()}; + } else { + urls = new String[]{selectedStream.getUrl(), secondaryStream}; + } + + DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs); - DownloadManagerService.startMission(getContext(), url, location, fileName, isAudio, threadsSeekBar.getProgress() + 1); getDialog().dismiss(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 9ab40e81c..ea5300a2e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -63,6 +63,7 @@ import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; @@ -371,14 +372,14 @@ public class VideoDetailFragment Log.w(TAG, "Can't open channel because we got no channel URL"); } else { try { - NavigationHelper.openChannelFragment( - getFragmentManager(), - currentInfo.getServiceId(), - currentInfo.getUploaderUrl(), - currentInfo.getUploaderName()); + NavigationHelper.openChannelFragment( + getFragmentManager(), + currentInfo.getServiceId(), + currentInfo.getUploaderUrl(), + currentInfo.getUploaderName()); } catch (Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); - } + } } break; case R.id.detail_thumbnail_root_layout: @@ -571,9 +572,6 @@ public class VideoDetailFragment .show(getFragmentManager(), TAG); } break; - case 3: - shareUrl(item.getName(), item.getUrl()); - break; default: break; } @@ -745,7 +743,7 @@ public class VideoDetailFragment sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false); selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams); - final StreamItemAdapter streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams), isExternalPlayerEnabled); + final StreamItemAdapter streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled); spinnerToolbar.setAdapter(streamsAdapter); spinnerToolbar.setSelection(selectedVideoStreamIndex); spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @@ -1276,6 +1274,7 @@ public class VideoDetailFragment downloadDialog.setVideoStreams(sortedVideoStreams); downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); + downloadDialog.setSubtitleStreams(currentInfo.getSubtitles()); downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); } catch (Exception e) { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 16dffc3de..5da262781 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -19,11 +19,11 @@ import com.google.android.exoplayer2.util.MimeTypes; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.Subtitles; +import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.SubtitlesFormat; +import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; @@ -87,7 +87,7 @@ public class PlayerHelper { return pitchFormatter.format(pitch); } - public static String mimeTypesOf(final SubtitlesFormat format) { + public static String subtitleMimeTypesOf(final MediaFormat format) { switch (format) { case VTT: return MimeTypes.TEXT_VTT; case TTML: return MimeTypes.APPLICATION_TTML; @@ -97,8 +97,8 @@ public class PlayerHelper { @NonNull public static String captionLanguageOf(@NonNull final Context context, - @NonNull final Subtitles subtitles) { - final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale()); + @NonNull final SubtitlesStream subtitles) { + final String displayName = subtitles.getDisplayLanguageName(); return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : ""); } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java index 8f91f4886..84eeedead 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -10,10 +10,10 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.Subtitles; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.util.ListHelper; @@ -93,8 +93,8 @@ public class VideoPlaybackResolver implements PlaybackResolver { // Below are auxiliary media sources // Create subtitle sources - for (final Subtitles subtitle : info.getSubtitles()) { - final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); + for (final SubtitlesStream subtitle : info.getSubtitles()) { + final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat()); if (mimeType == null) continue; final Format textFormat = Format.createTextSampleFormat(null, mimeType, diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java index e100a447b..5ee04ef76 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -13,6 +13,7 @@ import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Stream; +import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; import java.io.Serializable; @@ -94,12 +95,25 @@ public class StreamItemAdapter extends BaseAdapter { if (!showIconNoAudio) { woSoundIconVisibility = View.GONE; } else if (((VideoStream) stream).isVideoOnly()) { - woSoundIconVisibility = View.VISIBLE; + switch (stream.getFormat()) { + case WEBM:// fully supported + case MPEG_4:// ¿is DASH MPEG-4? + woSoundIconVisibility = View.INVISIBLE; + break; + default: + woSoundIconVisibility = View.VISIBLE; + break; + } } else if (isDropdownItem) { woSoundIconVisibility = View.INVISIBLE; } } else if (stream instanceof AudioStream) { qualityString = ((AudioStream) stream).getAverageBitrate() + "kbps"; + } else if (stream instanceof SubtitlesStream) { + qualityString = ((SubtitlesStream) stream).getDisplayLanguageName(); + if (((SubtitlesStream) stream).isAutoGenerated()) { + qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")"; + } } else { qualityString = stream.getFormat().getSuffix(); } @@ -111,7 +125,12 @@ public class StreamItemAdapter extends BaseAdapter { sizeView.setVisibility(View.GONE); } - formatNameView.setText(stream.getFormat().getName()); + if (stream instanceof SubtitlesStream) { + formatNameView.setText(((SubtitlesStream) stream).getLanguageTag()); + } else { + formatNameView.setText(stream.getFormat().getName()); + } + qualityView.setText(qualityString); woSoundIconView.setVisibility(woSoundIconVisibility); @@ -122,15 +141,17 @@ public class StreamItemAdapter extends BaseAdapter { * A wrapper class that includes a way of storing the stream sizes. */ public static class StreamSizeWrapper implements Serializable { - private static final StreamSizeWrapper EMPTY = new StreamSizeWrapper<>(Collections.emptyList()); + private static final StreamSizeWrapper EMPTY = new StreamSizeWrapper<>(Collections.emptyList(), null); private final List streamsList; - private final long[] streamSizes; + private long[] streamSizes; + private final String unknownSize; - public StreamSizeWrapper(List streamsList) { + public StreamSizeWrapper(List streamsList, Context context) { this.streamsList = streamsList; this.streamSizes = new long[streamsList.size()]; + this.unknownSize = context == null ? "--.-" : context.getString(R.string.unknown_content); - for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -1; + for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -2; } /** @@ -143,7 +164,7 @@ public class StreamItemAdapter extends BaseAdapter { final Callable fetchAndSet = () -> { boolean hasChanged = false; for (X stream : streamsWrapper.getStreamsList()) { - if (streamsWrapper.getSizeInBytes(stream) > 0) { + if (streamsWrapper.getSizeInBytes(stream) > -2) { continue; } @@ -173,11 +194,18 @@ public class StreamItemAdapter extends BaseAdapter { } public String getFormattedSize(int streamIndex) { - return Utility.formatBytes(getSizeInBytes(streamIndex)); + return formatSize(getSizeInBytes(streamIndex)); } public String getFormattedSize(T stream) { - return Utility.formatBytes(getSizeInBytes(stream)); + return formatSize(getSizeInBytes(stream)); + } + + private String formatSize(long size) { + if (size > -1) { + return Utility.formatBytes(size); + } + return unknownSize; } public void setSize(int streamIndex, long sizeInBytes) { diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java new file mode 100644 index 000000000..190bac285 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -0,0 +1,158 @@ +package us.shandian.giga.get; + +import android.support.annotation.NonNull; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.net.HttpURLConnection; +import java.nio.channels.ClosedByInterruptException; + +import us.shandian.giga.util.Utility; + +import static org.schabi.newpipe.BuildConfig.DEBUG; + +public class DownloadInitializer implements Runnable { + private final static String TAG = "DownloadInitializer"; + final static int mId = 0; + + private DownloadMission mMission; + + DownloadInitializer(@NonNull DownloadMission mission) { + mMission = mission; + } + + @Override + public void run() { + if (mMission.current > 0) mMission.resetState(); + + int retryCount = 0; + while (true) { + try { + mMission.currentThreadCount = mMission.threadCount; + + HttpURLConnection conn = mMission.openConnection(mId, -1, -1); + if (!mMission.running || Thread.interrupted()) return; + + mMission.length = conn.getContentLength(); + if (mMission.length == 0) { + mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null); + return; + } + + // check for dynamic generated content + if (mMission.length == -1 && conn.getResponseCode() == 200) { + mMission.blocks = 0; + mMission.length = 0; + mMission.fallback = true; + mMission.unknownLength = true; + mMission.currentThreadCount = 1; + + if (DEBUG) { + Log.d(TAG, "falling back (unknown length)"); + } + } else { + // Open again + conn = mMission.openConnection(mId, mMission.length - 10, mMission.length); + + int code = conn.getResponseCode(); + if (!mMission.running || Thread.interrupted()) return; + + if (code == 206) { + if (mMission.currentThreadCount > 1) { + mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE; + + if (mMission.currentThreadCount > mMission.blocks) { + mMission.currentThreadCount = (int) mMission.blocks; + } + if (mMission.currentThreadCount <= 0) { + mMission.currentThreadCount = 1; + } + if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) { + mMission.blocks++; + } + } else { + // if one thread is solicited don't calculate blocks, is useless + mMission.blocks = 0; + mMission.fallback = true; + mMission.unknownLength = false; + } + + if (DEBUG) { + Log.d(TAG, "http response code = " + code); + } + } else { + // Fallback to single thread + mMission.blocks = 0; + mMission.fallback = true; + mMission.unknownLength = false; + mMission.currentThreadCount = 1; + + if (DEBUG) { + Log.d(TAG, "falling back due http response code = " + code); + } + } + } + + for (long i = 0; i < mMission.currentThreadCount; i++) { + mMission.threadBlockPositions.add(i); + mMission.threadBytePositions.add(0); + } + + File file; + if (mMission.current == 0) { + file = new File(mMission.location); + if (!Utility.mkdir(file, true)) { + mMission.notifyError(DownloadMission.ERROR_PATH_CREATION, null); + return; + } + + file = new File(file, mMission.name); + + // if the name is used by "something", delete it + if (file.exists() && !file.isFile() && !file.delete()) { + mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null); + return; + } + + if (!file.exists() && !file.createNewFile()) { + mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null); + return; + } + } else { + file = new File(mMission.location, mMission.name); + } + + RandomAccessFile af = new RandomAccessFile(file, "rw"); + af.setLength(mMission.offsets[mMission.current] + mMission.length); + af.seek(mMission.offsets[mMission.current]); + af.close(); + + if (Thread.interrupted()) return; + + mMission.running = false; + break; + } catch (Exception e) { + if (e instanceof ClosedByInterruptException) { + return; + } else if (e instanceof IOException && e.getMessage().contains("Permission denied")) { + mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e); + return; + } + + if (retryCount++ > mMission.maxRetry) { + Log.e(TAG, "initializer failed", e); + mMission.running = false; + mMission.notifyError(e); + return; + } + + //try again + Log.e(TAG, "initializer failed, retrying", e); + } + } + + mMission.start(); + } +} diff --git a/app/src/main/java/us/shandian/giga/get/DownloadManager.java b/app/src/main/java/us/shandian/giga/get/DownloadManager.java deleted file mode 100644 index 45beb5563..000000000 --- a/app/src/main/java/us/shandian/giga/get/DownloadManager.java +++ /dev/null @@ -1,53 +0,0 @@ -package us.shandian.giga.get; - -public interface DownloadManager { - int BLOCK_SIZE = 512 * 1024; - - /** - * 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(); - -} 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 79c4baf05..73df11ecb 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -1,102 +1,165 @@ package us.shandian.giga.get; import android.os.Handler; -import android.os.Looper; +import android.os.Message; import android.util.Log; import java.io.File; -import java.io.ObjectInputStream; -import java.io.Serializable; -import java.lang.ref.WeakReference; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.ConnectException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; -import java.util.Map; +import javax.net.ssl.SSLException; + +import us.shandian.giga.postprocessing.Postprocessing; +import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; -public class DownloadMission implements Serializable { - private static final long serialVersionUID = 0L; +public class DownloadMission extends Mission { + private static final long serialVersionUID = 3L;// last bump: 16 october 2018 - private static final String TAG = DownloadMission.class.getSimpleName(); + static final int BUFFER_SIZE = 64 * 1024; + final static int BLOCK_SIZE = 512 * 1024; - public interface MissionListener { - HashMap handlerStore = new HashMap<>(); + private static final String TAG = "DownloadMission"; - 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 static final int ERROR_NOTHING = -1; + public static final int ERROR_PATH_CREATION = 1000; + public static final int ERROR_FILE_CREATION = 1001; + public static final int ERROR_UNKNOWN_EXCEPTION = 1002; + public static final int ERROR_PERMISSION_DENIED = 1003; + 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_HTTP_NO_CONTENT = 204; + public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206; /** - * The filename + * The urls of the file to download */ - public String name; + public String[] urls; /** - * The url of the file to download + * Number of blocks the size of {@link DownloadMission#BLOCK_SIZE} */ - 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; + long blocks = -1; /** * Number of bytes downloaded */ public long done; + + /** + * Indicates a file generated dynamically on the web server + */ + public boolean unknownLength; + + /** + * offset in the file where the data should be written + */ + public long[] offsets; + + /** + * The post-processing algorithm arguments + */ + public String[] postprocessingArgs; + + /** + * The post-processing algorithm name + */ + public String postprocessingName; + + /** + * Indicates if the post-processing algorithm is actually running, used to detect corrupt downloads + */ + public boolean postprocessingRunning; + + /** + * Indicate if the post-processing algorithm works on the same file + */ + public boolean postprocessingThis; + + /** + * The current resource to download {@code urls[current]} + */ + public int current; + + /** + * Metadata where the mission state is saved + */ + public File metadata; + + /** + * maximum attempts + */ + public int maxRetry; + public int threadCount = 3; - public int finishCount; - private final List threadPositions = new ArrayList<>(); - public final Map blockState = new HashMap<>(); - public boolean running; - public boolean finished; - public boolean fallback; - public int errCode = -1; - public long timestamp; + boolean fallback; + private int finishCount; + public transient boolean running; + public transient boolean enqueued = true; + public int errCode = ERROR_NOTHING; + + public transient Exception errObject = null; public transient boolean recovered; - - private transient ArrayList> mListeners = new ArrayList<>(); + public transient Handler mHandler; private transient boolean mWritingToFile; - private static final int NO_IDENTIFIER = -1; + @SuppressWarnings("UseSparseArrays")// LongSparseArray is not serializable + private final HashMap blockState = new HashMap<>(); + final List threadBlockPositions = new ArrayList<>(); + final List threadBytePositions = new ArrayList<>(); + + private transient boolean deleted; + int currentThreadCount; + private transient Thread[] threads = null; + private transient Thread init = null; + + + protected DownloadMission() { - public DownloadMission() { } - public DownloadMission(String name, String url, String location) { + public DownloadMission(String url, String name, String location, char kind) { + this(new String[]{url}, name, location, kind, null, null); + } + + public DownloadMission(String[] urls, String name, String location, char kind, String postprocessingName, String[] postprocessingArgs) { 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 (urls == null) throw new NullPointerException("urls is null"); + if (urls.length < 1) throw new IllegalArgumentException("urls is empty"); if (location == null) throw new NullPointerException("location is null"); if (location.isEmpty()) throw new IllegalArgumentException("location is empty"); - this.url = url; + this.urls = urls; this.name = name; this.location = location; - } + this.kind = kind; + this.offsets = new long[urls.length]; + if (postprocessingName != null) { + Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null); + this.postprocessingThis = algorithm.worksOnSameFile; + this.offsets[0] = algorithm.recommendedReserve; + this.postprocessingName = postprocessingName; + this.postprocessingArgs = postprocessingArgs; + } else { + if (DEBUG && urls.length > 1) { + Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?"); + } + } + } private void checkBlock(long block) { if (block < 0 || block >= blocks) { @@ -110,12 +173,12 @@ public class DownloadMission implements Serializable { * @param block the block identifier * @return true if the block is reserved and false if otherwise */ - public boolean isBlockPreserved(long block) { + boolean isBlockPreserved(long block) { checkBlock(block); return blockState.containsKey(block) ? blockState.get(block) : false; } - public void preserveBlock(long block) { + void preserveBlock(long block) { checkBlock(block); synchronized (blockState) { blockState.put(block, true); @@ -123,125 +186,192 @@ public class DownloadMission implements Serializable { } /** - * Set the download position of the file + * Set the block of the file * * @param threadId the identifier of the thread - * @param position the download position of the thread + * @param position the block of the thread */ - public void setPosition(int threadId, long position) { - threadPositions.set(threadId, position); + void setBlockPosition(int threadId, long position) { + threadBlockPositions.set(threadId, position); } /** - * Get the position of a thread + * Get the block of a file * * @param threadId the identifier of the thread - * @return the position for the thread + * @return the block for the thread */ - public long getPosition(int threadId) { - return threadPositions.get(threadId); + long getBlockPosition(int threadId) { + return threadBlockPositions.get(threadId); } - public synchronized void notifyProgress(long deltaLen) { + /** + * Save the position of the desired thread + * + * @param threadId the identifier of the thread + * @param position the relative position in bytes or zero + */ + void setThreadBytePosition(int threadId, int position) { + threadBytePositions.set(threadId, position); + } + + /** + * Get position inside of the block, where thread will be resumed + * + * @param threadId the identifier of the thread + * @return the relative position in bytes or zero + */ + int getBlockBytePosition(int threadId) { + return threadBytePositions.get(threadId); + } + + /** + * Open connection + * + * @param threadId id of the calling thread, used only for debug + * @param rangeStart range start + * @param rangeEnd range end + * @return a {@link java.net.URLConnection URLConnection} linking to the URL. + * @throws IOException if an I/O exception occurs. + * @throws HttpError if the the http response is not satisfiable + */ + HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException, HttpError { + URL url = new URL(urls[current]); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setInstanceFollowRedirects(true); + + if (rangeStart >= 0) { + String req = "bytes=" + rangeStart + "-"; + if (rangeEnd > 0) req += rangeEnd; + + conn.setRequestProperty("Range", req); + if (DEBUG) { + Log.d(TAG, threadId + ":" + conn.getRequestProperty("Range")); + Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode()); + } + } + + int statusCode = conn.getResponseCode(); + switch (statusCode) { + case 204: + case 205: + case 207: + throw new HttpError(conn.getResponseCode()); + default: + if (statusCode < 200 || statusCode > 299) { + throw new HttpError(statusCode); + } + } + + return conn; + } + + private void notify(int what) { + Message m = new Message(); + m.what = what; + m.obj = this; + + mHandler.sendMessage(m); + } + + synchronized void notifyProgress(long deltaLen) { if (!running) return; if (recovered) { recovered = false; } + if (unknownLength) { + length += deltaLen;// Update length before proceeding + } + done += deltaLen; if (done > length) { done = length; } - if (done != length) { - writeThisToFile(); + if (done != length && !deleted && !mWritingToFile) { + mWritingToFile = true; + runAsync(-2, this::writeThisToFile); } - for (WeakReference ref : mListeners) { - final MissionListener listener = ref.get(); - if (listener != null) { - MissionListener.handlerStore.get(listener).post(new Runnable() { - @Override - public void run() { - listener.onProgressUpdate(DownloadMission.this, done, length); - } - }); - } + notify(DownloadManagerService.MESSAGE_PROGRESS); + } + + synchronized void notifyError(Exception err) { + Log.e(TAG, "notifyError()", err); + + if (err instanceof FileNotFoundException) { + notifyError(ERROR_FILE_CREATION, null); + } else if (err instanceof SSLException) { + notifyError(ERROR_SSL_EXCEPTION, null); + } else if (err instanceof HttpError) { + notifyError(((HttpError) err).statusCode, null); + } else if (err instanceof ConnectException) { + notifyError(ERROR_CONNECT_HOST, null); + } else if (err instanceof UnknownHostException) { + notifyError(ERROR_UNKNOWN_HOST, null); + } else { + notifyError(ERROR_UNKNOWN_EXCEPTION, err); } } - /** - * Called by a download thread when it finished. - */ - public synchronized void notifyFinished() { - if (errCode > 0) return; + synchronized void notifyError(int code, Exception err) { + Log.e(TAG, "notifyError() code = " + code, err); + + errCode = code; + errObject = err; + + pause(); + + notify(DownloadManagerService.MESSAGE_ERROR); + } + + synchronized void notifyFinished() { + if (errCode > ERROR_NOTHING) return; finishCount++; - if (finishCount == threadCount) { - onFinish(); + if (finishCount == currentThreadCount) { + if ((current + 1) < urls.length) { + // prepare next sub-mission + long current_offset = offsets[current++]; + offsets[current] = current_offset + length; + initializer(); + return; + } + + current++; + unknownLength = false; + + if (!doPostprocessing()) return; + + if (errCode > ERROR_NOTHING) return; + if (DEBUG) { + Log.d(TAG, "onFinish"); + } + running = false; + deleteThisFromFile(); + + notify(DownloadManagerService.MESSAGE_FINISHED); } } - /** - * Called when all parts are downloaded - */ - private void onFinish() { - if (errCode > 0) return; - + private void notifyPostProcessing(boolean processing) { if (DEBUG) { - Log.d(TAG, "onFinish"); + Log.d(TAG, (processing ? "enter" : "exit") + " postprocessing on " + location + File.separator + name); } - running = false; - finished = true; - - deleteThisFromFile(); - - for (WeakReference ref : mListeners) { - final MissionListener listener = ref.get(); - if (listener != null) { - MissionListener.handlerStore.get(listener).post(new Runnable() { - @Override - public void run() { - listener.onFinish(DownloadMission.this); - } - }); + synchronized (blockState) { + if (!processing) { + postprocessingName = null; + postprocessingArgs = null; } - } - } - public synchronized void notifyError(int err) { - errCode = err; - - writeThisToFile(); - - for (WeakReference ref : mListeners) { - final MissionListener listener = ref.get(); - MissionListener.handlerStore.get(listener).post(new Runnable() { - @Override - public void run() { - listener.onError(DownloadMission.this, errCode); - } - }); - } - } - - public synchronized void addListener(MissionListener listener) { - Handler handler = new Handler(Looper.getMainLooper()); - MissionListener.handlerStore.put(listener, handler); - mListeners.add(new WeakReference<>(listener)); - } - - public synchronized void removeListener(MissionListener listener) { - for (Iterator> iterator = mListeners.iterator(); - iterator.hasNext(); ) { - WeakReference weakRef = iterator.next(); - if (listener != null && listener == weakRef.get()) { - iterator.remove(); - } + // don't return without fully write the current state + postprocessingRunning = processing; + Utility.writeToFile(metadata, DownloadMission.this); } } @@ -249,92 +379,206 @@ public class DownloadMission implements Serializable { * Start downloading with multiple threads. */ public void start() { - if (!running && !finished) { - running = true; + if (running || current >= urls.length) return; + enqueued = false; + running = true; + errCode = ERROR_NOTHING; - if (!fallback) { - for (int i = 0; i < threadCount; i++) { - if (threadPositions.size() <= i && !recovered) { - threadPositions.add((long) i); - } - new Thread(new DownloadRunnable(this, i)).start(); - } - } else { - // In fallback mode, resuming is not supported. - threadCount = 1; + if (blocks < 0) { + initializer(); + return; + } + + init = null; + + if (threads == null) { + threads = new Thread[currentThreadCount]; + } + + if (fallback) { + if (unknownLength) { done = 0; - blocks = 0; - new Thread(new DownloadRunnableFallback(this)).start(); + length = 0; + } + + threads[0] = runAsync(1, new DownloadRunnableFallback(this)); + } else { + for (int i = 0; i < currentThreadCount; i++) { + threads[i] = runAsync(i + 1, new DownloadRunnable(this, i)); } } } - public void pause() { - if (running) { - running = false; - recovered = true; + /** + * Pause the mission, does not affect the blocks that are being downloaded. + */ + public synchronized void pause() { + if (!running) return; - // TODO: Notify & Write state to info file - // if (err) + running = false; + recovered = true; + enqueued = false; + + if (init != null && init != Thread.currentThread() && init.isAlive()) { + init.interrupt(); + + try { + init.join(); + } catch (InterruptedException e) { + // nothing to do + } + + resetState(); + return; } + + if (DEBUG && blocks < 1) { + Log.w(TAG, "pausing a download that can not be resumed."); + } + + if (threads == null || Thread.interrupted()) { + writeThisToFile(); + return; + } + + // wait for all threads are suspended before save the state + runAsync(-1, () -> { + try { + for (Thread thread : threads) { + if (thread == Thread.currentThread()) continue; + + if (thread.isAlive()) { + thread.interrupt(); + thread.join(); + } + } + } catch (Exception e) { + // nothing to do + } finally { + writeThisToFile(); + } + }); } /** * Removes the file and the meta file */ - public void delete() { - deleteThisFromFile(); - new File(location, name).delete(); + @Override + public boolean delete() { + deleted = true; + boolean res = deleteThisFromFile(); + if (!super.delete()) res = false; + return res; + } + + void resetState() { + done = 0; + blocks = -1; + errCode = ERROR_NOTHING; + fallback = false; + unknownLength = false; + finishCount = 0; + threadBlockPositions.clear(); + threadBytePositions.clear(); + blockState.clear(); + threads = null; + + Utility.writeToFile(metadata, DownloadMission.this); + } + + private void initializer() { + init = runAsync(DownloadInitializer.mId, new DownloadInitializer(this)); + } /** * Write this {@link DownloadMission} to the meta file asynchronously * if no thread is already running. */ - public void writeThisToFile() { - if (!mWritingToFile) { - mWritingToFile = true; - new Thread() { - @Override - public void run() { - doWriteThisToFile(); - mWritingToFile = false; - } - }.start(); - } - } - - /** - * Write this {@link DownloadMission} to the meta file. - */ - private void doWriteThisToFile() { + private void writeThisToFile() { synchronized (blockState) { - Utility.writeToFile(getMetaFilename(), this); + if (deleted) return; + Utility.writeToFile(metadata, DownloadMission.this); + } + mWritingToFile = false; + } + + public boolean isFinished() { + return current >= urls.length && postprocessingName == null; + } + + private boolean doPostprocessing() { + if (postprocessingName == null) return true; + + try { + notifyPostProcessing(true); + notifyProgress(0); + + Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name); + + Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, this); + algorithm.run(); + } catch (Exception err) { + StringBuilder args = new StringBuilder(" "); + if (postprocessingArgs != null) { + for (String arg : postprocessingArgs) { + args.append(", "); + args.append(arg); + } + args.delete(0, 1); + } + Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err); + + notifyError(ERROR_POSTPROCESSING_FAILED, err); + return false; + } finally { + notifyPostProcessing(false); + } + + if (errCode != ERROR_NOTHING) notify(DownloadManagerService.MESSAGE_ERROR); + + return errCode == ERROR_NOTHING; + } + + private boolean deleteThisFromFile() { + synchronized (blockState) { + return metadata.delete(); } } - private void readObject(ObjectInputStream inputStream) - throws java.io.IOException, ClassNotFoundException - { - inputStream.defaultReadObject(); - mListeners = new ArrayList<>(); - } - - private void deleteThisFromFile() { - new File(getMetaFilename()).delete(); - } - /** - * Get the path of the meta file + * run a method in a new thread * - * @return the path to the meta file + * @param id id of new thread (used for debugging only) + * @param who the object whose {@code run} method is invoked when this thread is started + * @return the created thread */ - private String getMetaFilename() { - return location + "/" + name + ".giga"; + private Thread runAsync(int id, Runnable who) { + // known thread ids: + // -2: state saving by notifyProgress() method + // -1: wait for saving the state by pause() method + // 0: initializer + // >=1: any download thread + + Thread thread = new Thread(who); + if (DEBUG) { + thread.setName(String.format("[%s] id = %s filename = %s", TAG, id, name)); + } + thread.start(); + + return thread; } - public File getDownloadedFile() { - return new File(location, name); - } + static class HttpError extends Exception { + int statusCode; + HttpError(int statusCode) { + this.statusCode = statusCode; + } + + @Override + public String getMessage() { + return "Http status code" + String.valueOf(statusCode); + } + } } 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 6ad8626c3..ad2fa7113 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -2,9 +2,12 @@ package us.shandian.giga.get; import android.util.Log; +import java.io.BufferedInputStream; +import java.io.FileNotFoundException; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.channels.ClosedByInterruptException; import static org.schabi.newpipe.BuildConfig.DEBUG; @@ -18,7 +21,7 @@ public class DownloadRunnable implements Runnable { private final DownloadMission mMission; private final int mId; - public DownloadRunnable(DownloadMission mission, int id) { + DownloadRunnable(DownloadMission mission, int id) { if (mission == null) throw new NullPointerException("mission is null"); mMission = mission; mId = id; @@ -27,14 +30,25 @@ public class DownloadRunnable implements Runnable { @Override public void run() { boolean retry = mMission.recovered; - long position = mMission.getPosition(mId); + long blockPosition = mMission.getBlockPosition(mId); + int retryCount = 0; if (DEBUG) { - Log.d(TAG, mId + ":default pos " + position); + Log.d(TAG, mId + ":default pos " + blockPosition); Log.d(TAG, mId + ":recovered: " + mMission.recovered); } - while (mMission.errCode == -1 && mMission.running && position < mMission.blocks) { + BufferedInputStream ipt = null; + RandomAccessFile f; + + try { + f = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); + } catch (FileNotFoundException e) { + mMission.notifyError(e);// this never should happen + return; + } + + while (mMission.errCode == DownloadMission.ERROR_NOTHING && mMission.running && blockPosition < mMission.blocks) { if (Thread.currentThread().isInterrupted()) { mMission.pause(); @@ -42,57 +56,47 @@ public class DownloadRunnable implements Runnable { } if (DEBUG && retry) { - Log.d(TAG, mId + ":retry is true. Resuming at " + position); + Log.d(TAG, mId + ":retry is true. Resuming at " + blockPosition); } // Wait for an unblocked position - while (!retry && position < mMission.blocks && mMission.isBlockPreserved(position)) { + while (!retry && blockPosition < mMission.blocks && mMission.isBlockPreserved(blockPosition)) { if (DEBUG) { - Log.d(TAG, mId + ":position " + position + " preserved, passing"); + Log.d(TAG, mId + ":position " + blockPosition + " preserved, passing"); } - position++; + blockPosition++; } retry = false; - if (position >= mMission.blocks) { + if (blockPosition >= mMission.blocks) { break; } if (DEBUG) { - Log.d(TAG, mId + ":preserving position " + position); + Log.d(TAG, mId + ":preserving position " + blockPosition); } - mMission.preserveBlock(position); - mMission.setPosition(mId, position); + mMission.preserveBlock(blockPosition); + mMission.setBlockPosition(mId, blockPosition); - long start = position * DownloadManager.BLOCK_SIZE; - long end = start + DownloadManager.BLOCK_SIZE - 1; + long start = (blockPosition * DownloadMission.BLOCK_SIZE) + mMission.getBlockBytePosition(mId); + long end = start + DownloadMission.BLOCK_SIZE - 1; if (end >= mMission.length) { end = mMission.length - 1; } - HttpURLConnection conn = null; - int total = 0; try { - URL url = new URL(mMission.url); - conn = (HttpURLConnection) url.openConnection(); - conn.setRequestProperty("Range", "bytes=" + start + "-" + end); + HttpURLConnection conn = mMission.openConnection(mId, start, end); - if (DEBUG) { - Log.d(TAG, mId + ":" + conn.getRequestProperty("Range")); - Log.d(TAG, mId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode()); - } - - // A server may be ignoring the range request + // The server may be ignoring the range request if (conn.getResponseCode() != 206) { - mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED; - notifyError(); + mMission.notifyError(new DownloadMission.HttpError(conn.getResponseCode())); if (DEBUG) { Log.e(TAG, mId + ":Unsupported " + conn.getResponseCode()); @@ -101,76 +105,67 @@ public class DownloadRunnable implements Runnable { break; } - RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw"); - f.seek(start); - java.io.InputStream ipt = conn.getInputStream(); - byte[] buf = new byte[64*1024]; + f.seek(mMission.offsets[mMission.current] + start); - while (start < end && mMission.running) { - int len = ipt.read(buf, 0, buf.length); + ipt = new BufferedInputStream(conn.getInputStream()); + byte[] buf = new byte[DownloadMission.BUFFER_SIZE]; + int len; - if (len == -1) { - break; - } else { - start += len; - total += len; - f.write(buf, 0, len); - notifyProgress(len); - } + while (start < end && mMission.running && (len = ipt.read(buf, 0, buf.length)) != -1) { + f.write(buf, 0, len); + start += len; + total += len; + mMission.notifyProgress(len); } if (DEBUG && mMission.running) { - Log.d(TAG, mId + ":position " + position + " finished, total length " + total); + Log.d(TAG, mId + ":position " + blockPosition + " finished, total length " + total); } - f.close(); - ipt.close(); - - // TODO We should save progress for each thread + // if the download is paused, save progress for this thread + if (!mMission.running) { + mMission.setThreadBytePosition(mId, total); + break; + } } catch (Exception e) { - // TODO Retry count limit & notify error - retry = true; + mMission.setThreadBytePosition(mId, total); - notifyProgress(-total); + if (e instanceof ClosedByInterruptException) break; + + if (retryCount++ > mMission.maxRetry) { + mMission.notifyError(e); + break; + } if (DEBUG) { - Log.d(TAG, mId + ":position " + position + " retrying", e); + Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e); } } } + try { + f.close(); + } catch (Exception err) { + // ¿ejected media storage? ¿file deleted? ¿storage ran out of space? + } + + try { + if (ipt != null) ipt.close(); + } catch (Exception err) { + // nothing to do + } + if (DEBUG) { - Log.d(TAG, "thread " + mId + " exited main loop"); + Log.d(TAG, "thread " + mId + " exited from main download loop"); } - - if (mMission.errCode == -1 && mMission.running) { + if (mMission.errCode == DownloadMission.ERROR_NOTHING && mMission.running) { if (DEBUG) { Log.d(TAG, "no error has happened, notifying"); } - notifyFinished(); + mMission.notifyFinished(); } - if (DEBUG && !mMission.running) { Log.d(TAG, "The mission has been paused. Passing."); } } - - private void notifyProgress(final long len) { - synchronized (mMission) { - mMission.notifyProgress(len); - } - } - - private void notifyError() { - synchronized (mMission) { - mMission.notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED); - mMission.pause(); - } - } - - private void notifyFinished() { - synchronized (mMission) { - mMission.notifyFinished(); - } - } } 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 f24139910..a7c48c170 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -1,74 +1,109 @@ package us.shandian.giga.get; +import android.support.annotation.NonNull; +import android.util.Log; + import java.io.BufferedInputStream; +import java.io.IOException; import java.io.RandomAccessFile; import java.net.HttpURLConnection; -import java.net.URL; +import java.nio.channels.ClosedByInterruptException; + + +import static org.schabi.newpipe.BuildConfig.DEBUG; // Single-threaded fallback mode public class DownloadRunnableFallback implements Runnable { - private final DownloadMission mMission; - //private int mId; + private static final String TAG = "DownloadRunnableFallbac"; - public DownloadRunnableFallback(DownloadMission mission) { - if (mission == null) throw new NullPointerException("mission is null"); - //mId = id; + private final DownloadMission mMission; + private int retryCount = 0; + + private BufferedInputStream ipt; + private RandomAccessFile f; + + DownloadRunnableFallback(@NonNull DownloadMission mission) { mMission = mission; + ipt = null; + f = null; + } + + private void dispose() { + try { + if (ipt != null) ipt.close(); + } catch (IOException e) { + // nothing to do + } + + try { + if (f != null) f.close(); + } catch (IOException e) { + // ¿ejected media storage? ¿file deleted? ¿storage ran out of space? + } } @Override public void run() { - try { - URL url = new URL(mMission.url); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + boolean done; - if (conn.getResponseCode() != 200 && conn.getResponseCode() != 206) { - notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED); - } else { - RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw"); - f.seek(0); - BufferedInputStream ipt = new BufferedInputStream(conn.getInputStream()); - byte[] buf = new byte[512]; - int len = 0; + int start = 0; - while ((len = ipt.read(buf, 0, 512)) != -1 && mMission.running) { - f.write(buf, 0, len); - notifyProgress(len); - - if (Thread.interrupted()) { - break; - } - - } - - f.close(); - ipt.close(); + if (!mMission.unknownLength) { + start = mMission.getBlockBytePosition(0); + if (DEBUG && start > 0) { + Log.i(TAG, "Resuming a single-thread download at " + start); } + } + + try { + int rangeStart = (mMission.unknownLength || start < 1) ? -1 : start; + HttpURLConnection conn = mMission.openConnection(1, rangeStart, -1); + + // secondary check for the file length + if (!mMission.unknownLength) mMission.unknownLength = conn.getContentLength() == -1; + + f = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); + f.seek(mMission.offsets[mMission.current] + start); + + ipt = new BufferedInputStream(conn.getInputStream()); + + byte[] buf = new byte[DownloadMission.BUFFER_SIZE]; + int len = 0; + + while (mMission.running && (len = ipt.read(buf, 0, buf.length)) != -1) { + f.write(buf, 0, len); + start += len; + + mMission.notifyProgress(len); + + if (Thread.interrupted()) break; + } + + // if thread goes interrupted check if the last part is written. This avoid re-download the whole file + done = len == -1; } catch (Exception e) { - notifyError(DownloadMission.ERROR_UNKNOWN); + dispose(); + + // save position + mMission.setThreadBytePosition(0, start); + + if (e instanceof ClosedByInterruptException) return; + + if (retryCount++ > mMission.maxRetry) { + mMission.notifyError(e); + return; + } + + run();// try again + return; } - if (mMission.errCode == -1 && mMission.running) { - notifyFinished(); - } - } + dispose(); - private void notifyProgress(final long len) { - synchronized (mMission) { - mMission.notifyProgress(len); - } - } - - private void notifyError(final int err) { - synchronized (mMission) { - mMission.notifyError(err); - mMission.pause(); - } - } - - private void notifyFinished() { - synchronized (mMission) { + if (done) { mMission.notifyFinished(); + } else { + mMission.setThreadBytePosition(0, start); } } } diff --git a/app/src/main/java/us/shandian/giga/get/FinishedMission.java b/app/src/main/java/us/shandian/giga/get/FinishedMission.java new file mode 100644 index 000000000..b7d6908a5 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/get/FinishedMission.java @@ -0,0 +1,16 @@ +package us.shandian.giga.get; + +public class FinishedMission extends Mission { + + public FinishedMission() { + } + + public FinishedMission(DownloadMission mission) { + source = mission.source; + length = mission.length;// ¿or mission.done? + timestamp = mission.timestamp; + name = mission.name; + location = mission.location; + kind = mission.kind; + } +} diff --git a/app/src/main/java/us/shandian/giga/get/Mission.java b/app/src/main/java/us/shandian/giga/get/Mission.java new file mode 100644 index 000000000..ec2ddaa26 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/get/Mission.java @@ -0,0 +1,66 @@ +package us.shandian.giga.get; + +import java.io.File; +import java.io.Serializable; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +public abstract class Mission implements Serializable { + private static final long serialVersionUID = 0L;// last bump: 5 october 2018 + + /** + * Source url of the resource + */ + public String source; + + /** + * Length of the current resource + */ + public long length; + + /** + * creation timestamp (and maybe unique identifier) + */ + public long timestamp; + + /** + * The filename + */ + public String name; + + /** + * The directory to store the download + */ + public String location; + + /** + * pre-defined content type + */ + public char kind; + + /** + * get the target file on the storage + * + * @return File object + */ + public File getDownloadedFile() { + return new File(location, name); + } + + public boolean delete() { + deleted = true; + return getDownloadedFile().delete(); + } + + /** + * Indicate if this mission is deleted whatever is stored + */ + public transient boolean deleted = false; + + @Override + public String toString() { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(timestamp); + return "[" + calendar.getTime().toString() + "] " + location + File.separator + name; + } +} diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/DownloadDataSource.java b/app/src/main/java/us/shandian/giga/get/sqlite/DownloadDataSource.java new file mode 100644 index 000000000..4b4d5d733 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/get/sqlite/DownloadDataSource.java @@ -0,0 +1,73 @@ +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.DownloadMission; +import us.shandian.giga.get.FinishedMission; +import us.shandian.giga.get.Mission; + +import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_LOCATION; +import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_NAME; +import static us.shandian.giga.get.sqlite.DownloadMissionHelper.MISSIONS_TABLE_NAME; + +public class DownloadDataSource { + + private static final String TAG = "DownloadDataSource"; + private final DownloadMissionHelper downloadMissionHelper; + + public DownloadDataSource(Context context) { + downloadMissionHelper = new DownloadMissionHelper(context); + } + + public ArrayList loadFinishedMissions() { + SQLiteDatabase database = downloadMissionHelper.getReadableDatabase(); + Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null, + null, null, null, DownloadMissionHelper.KEY_TIMESTAMP); + + int count = cursor.getCount(); + if (count == 0) return new ArrayList<>(1); + + ArrayList result = new ArrayList<>(count); + while (cursor.moveToNext()) { + result.add(DownloadMissionHelper.getMissionFromCursor(cursor)); + } + + return result; + } + + public void addMission(DownloadMission downloadMission) { + if (downloadMission == null) throw new NullPointerException("downloadMission is null"); + SQLiteDatabase database = downloadMissionHelper.getWritableDatabase(); + ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission); + database.insert(MISSIONS_TABLE_NAME, null, values); + } + + public void deleteMission(Mission downloadMission) { + if (downloadMission == null) throw new NullPointerException("downloadMission is null"); + SQLiteDatabase database = downloadMissionHelper.getWritableDatabase(); + database.delete(MISSIONS_TABLE_NAME, + KEY_LOCATION + " = ? AND " + + KEY_NAME + " = ?", + new String[]{downloadMission.location, downloadMission.name}); + } + + public void updateMission(DownloadMission downloadMission) { + if (downloadMission == null) throw new NullPointerException("downloadMission is null"); + SQLiteDatabase database = downloadMissionHelper.getWritableDatabase(); + ContentValues values = DownloadMissionHelper.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); + } + } +} diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionSQLiteHelper.java b/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionHelper.java similarity index 63% rename from app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionSQLiteHelper.java rename to app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionHelper.java index d5a83551b..6dadc98c8 100644 --- a/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionSQLiteHelper.java +++ b/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionHelper.java @@ -7,19 +7,19 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import us.shandian.giga.get.DownloadMission; +import us.shandian.giga.get.FinishedMission; /** - * SqliteHelper to store {@link us.shandian.giga.get.DownloadMission} + * SQLiteHelper to store finished {@link us.shandian.giga.get.DownloadMission}'s */ -public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper { - - +public class DownloadMissionHelper 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; + private static final int DATABASE_VERSION = 3; + /** * The table name of download missions */ @@ -30,9 +30,9 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper { */ static final String KEY_LOCATION = "location"; /** - * The key to the url of a mission + * The key to the urls of a mission */ - static final String KEY_URL = "url"; + static final String KEY_SOURCE_URL = "url"; /** * The key to the name of a mission */ @@ -45,6 +45,8 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper { static final String KEY_TIMESTAMP = "timestamp"; + static final String KEY_KIND = "kind"; + /** * The statement to create the table */ @@ -52,16 +54,28 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper { "CREATE TABLE " + MISSIONS_TABLE_NAME + " (" + KEY_LOCATION + " TEXT NOT NULL, " + KEY_NAME + " TEXT NOT NULL, " + - KEY_URL + " TEXT NOT NULL, " + + KEY_SOURCE_URL + " TEXT NOT NULL, " + KEY_DONE + " INTEGER NOT NULL, " + KEY_TIMESTAMP + " INTEGER NOT NULL, " + + KEY_KIND + " TEXT NOT NULL, " + " UNIQUE(" + KEY_LOCATION + ", " + KEY_NAME + "));"; - - DownloadMissionSQLiteHelper(Context context) { + public DownloadMissionHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(MISSIONS_CREATE_TABLE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion == 2) { + db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME + " ADD COLUMN " + KEY_KIND + " TEXT;"); + } + } + /** * Returns all values of the download mission as ContentValues. * @@ -70,34 +84,29 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper { */ public static ContentValues getValuesOfMission(DownloadMission downloadMission) { ContentValues values = new ContentValues(); - values.put(KEY_URL, downloadMission.url); + values.put(KEY_SOURCE_URL, downloadMission.source); values.put(KEY_LOCATION, downloadMission.location); values.put(KEY_NAME, downloadMission.name); values.put(KEY_DONE, downloadMission.done); values.put(KEY_TIMESTAMP, downloadMission.timestamp); + values.put(KEY_KIND, String.valueOf(downloadMission.kind)); 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) { + public static FinishedMission 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)); + + String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND)); + if (kind == null || kind.isEmpty()) kind = "?"; + + FinishedMission mission = new FinishedMission(); + mission.name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME)); + mission.location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION)); + mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE_URL));; + mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE)); mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP)); - mission.finished = true; + mission.kind = kind.charAt(0); + 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 deleted file mode 100644 index e7b4caeb8..000000000 --- a/app/src/main/java/us/shandian/giga/get/sqlite/SQLiteDownloadDataSource.java +++ /dev/null @@ -1,79 +0,0 @@ -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/postprocessing/Mp4DashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java new file mode 100644 index 000000000..738135253 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java @@ -0,0 +1,31 @@ +package us.shandian.giga.postprocessing; + +import org.schabi.newpipe.extractor.utils.Mp4DashWriter; +import org.schabi.newpipe.extractor.utils.io.SharpStream; + +import java.io.IOException; + +import us.shandian.giga.get.DownloadMission; + +/** + * @author kapodamy + */ +class Mp4DashMuxer extends Postprocessing { + + Mp4DashMuxer(DownloadMission mission) { + super(mission); + recommendedReserve = 2048 * 1024;// 2 MiB + worksOnSameFile = true; + } + + @Override + int process(SharpStream out, SharpStream... sources) throws IOException { + Mp4DashWriter muxer = new Mp4DashWriter(sources); + muxer.parseSources(); + muxer.selectTracks(0, 0); + muxer.build(out); + + return OK_RESULT; + } + +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java new file mode 100644 index 000000000..811ec70d7 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -0,0 +1,149 @@ +package us.shandian.giga.postprocessing; + +import android.os.Message; + +import org.schabi.newpipe.extractor.utils.io.SharpStream; + +import java.io.File; +import java.io.IOException; + +import us.shandian.giga.get.DownloadMission; +import us.shandian.giga.postprocessing.io.ChunkFileInputStream; +import us.shandian.giga.postprocessing.io.CircularFile; +import us.shandian.giga.service.DownloadManagerService; + +public abstract class Postprocessing { + + static final byte OK_RESULT = DownloadMission.ERROR_NOTHING; + + public static final String ALGORITHM_TTML_CONVERTER = "ttml"; + public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D"; + 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); + case ALGORITHM_MP4_DASH_MUXER: + return new Mp4DashMuxer(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: + throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); + } + } + + /** + * Get a boolean value that indicate if the given algorithm work on the same + * file + */ + public boolean worksOnSameFile; + + /** + * Get the recommended space to reserve for the given algorithm. The amount + * is in bytes + */ + public int recommendedReserve; + + protected DownloadMission mission; + + Postprocessing(DownloadMission mission) { + this.mission = mission; + } + + public void run() throws IOException { + File file = mission.getDownloadedFile(); + CircularFile out = null; + 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 -1; + }; + + out = new CircularFile(file, 0, this::progressReport, checker); + + mission.done = 0; + 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(); + } + } + } + + /** + * Abstract method to execute the pos-processing algorithm + * + * @param out output stream + * @param sources files to be processed + * @return a error code, 0 means the operation was successful + * @throws IOException if an I/O error occurs. + */ + abstract int process(SharpStream out, SharpStream... sources) throws IOException; + + String getArgumentAt(int index, String defaultValue) { + if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) { + return defaultValue; + } + + return mission.postprocessingArgs[index]; + } + + private void progressReport(long done) { + mission.done = done; + if (mission.length < mission.done) mission.length = mission.done; + + Message m = new Message(); + m.what = DownloadManagerService.MESSAGE_PROGRESS; + m.obj = mission; + + mission.mHandler.sendMessage(m); + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java b/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java new file mode 100644 index 000000000..996f02d97 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java @@ -0,0 +1,54 @@ +package us.shandian.giga.postprocessing; + +import android.util.Log; + +import org.schabi.newpipe.extractor.utils.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; + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java b/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java new file mode 100644 index 000000000..d05440d70 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java @@ -0,0 +1,49 @@ +package us.shandian.giga.postprocessing; + +import org.schabi.newpipe.extractor.utils.io.SharpStream; +import org.schabi.newpipe.extractor.utils.SubtitleConverter; + +import java.io.IOException; + +import us.shandian.giga.get.DownloadMission; +import us.shandian.giga.postprocessing.io.SharpInputStream; +/** + * @author kapodamy + */ +class TttmlConverter extends Postprocessing { + + TttmlConverter(DownloadMission mission) { + super(mission); + recommendedReserve = 0;// due how XmlPullParser works, the xml is fully loaded on the ram + worksOnSameFile = true; + } + + @Override + int process(SharpStream out, SharpStream... sources) throws IOException { + // check if the subtitle is already in srt and copy, this should never happen + String format = getArgumentAt(0, null); + + if (format == null || format.equals("ttml")) { + SubtitleConverter ttmlDumper = new SubtitleConverter(); + + int res = ttmlDumper.dumpTTML( + sources[0], + out, + getArgumentAt(1, "true").equals("true"), + getArgumentAt(2, "true").equals("true") + ); + + return res == 0 ? OK_RESULT : res; + } else if (format.equals("srt")) { + byte[] buffer = new byte[8 * 1024]; + int read; + while ((read = sources[0].read(buffer)) > 0) { + out.write(buffer, 0, read); + } + return OK_RESULT; + } + + throw new UnsupportedOperationException("Can't convert this subtitle, unimplemented format: " + format); + } + +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java new file mode 100644 index 000000000..d73fdc3b7 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java @@ -0,0 +1,44 @@ +package us.shandian.giga.postprocessing; + +import org.schabi.newpipe.extractor.utils.WebMReader.TrackKind; +import org.schabi.newpipe.extractor.utils.WebMReader.WebMTrack; +import org.schabi.newpipe.extractor.utils.WebMWriter; +import org.schabi.newpipe.extractor.utils.io.SharpStream; + +import java.io.IOException; + +import us.shandian.giga.get.DownloadMission; + +/** + * @author kapodamy + */ +class WebMMuxer extends Postprocessing { + + WebMMuxer(DownloadMission mission) { + super(mission); + recommendedReserve = (1024 + 512) * 1024;// 1.50 MiB + worksOnSameFile = true; + } + + @Override + int process(SharpStream out, SharpStream... sources) throws IOException { + WebMWriter muxer = new WebMWriter(sources); + muxer.parseSources(); + + // youtube uses a webm with a fake video track that acts as a "cover image" + WebMTrack[] tracks = muxer.getTracksFromSource(1); + int audioTrackIndex = 0; + for (int i = 0; i < tracks.length; i++) { + if (tracks[i].kind == TrackKind.Audio) { + audioTrackIndex = i; + break; + } + } + + muxer.selectTracks(0, audioTrackIndex); + muxer.build(out); + + return OK_RESULT; + } + +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java new file mode 100644 index 000000000..f3e3ccdda --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java @@ -0,0 +1,153 @@ +package us.shandian.giga.postprocessing.io; + +import org.schabi.newpipe.extractor.utils.io.SharpStream; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + +public class ChunkFileInputStream extends SharpStream { + + private RandomAccessFile source; + private final long offset; + private final long length; + private long position; + + public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException { + source = new RandomAccessFile(file, mode); + offset = start; + length = end - start; + position = 0; + + if (length < 1) { + source.close(); + throw new IOException("The chunk is empty or invalid"); + } + if (source.length() < end) { + try { + throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length())); + } finally { + source.close(); + } + } + + source.seek(offset); + } + + /** + * Get absolute position on file + * + * @return the position + */ + public long getFilePointer() { + return offset + position; + } + + @Override + public int read() throws IOException { + if ((position + 1) > length) { + return 0; + } + + int res = source.read(); + if (res >= 0) { + position++; + } + + return res; + } + + @Override + public int read(byte b[]) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + if ((position + len) > length) { + len = (int) (length - position); + } + if (len == 0) { + return 0; + } + + int res = source.read(b, off, len); + position += res; + + return res; + } + + @Override + public long skip(long pos) throws IOException { + pos = Math.min(pos + position, length); + + if (pos == 0) { + return 0; + } + + source.seek(offset + pos); + + long oldPos = position; + position = pos; + + return pos - oldPos; + } + + @Override + public int available() { + return (int) (length - position); + } + + @SuppressWarnings("EmptyCatchBlock") + @Override + public void dispose() { + try { + source.close(); + } catch (IOException err) { + } finally { + source = null; + } + } + + @Override + public boolean isDisposed() { + return source == null; + } + + @Override + public void rewind() throws IOException { + position = 0; + source.seek(offset); + } + + @Override + public boolean canRewind() { + return true; + } + + @Override + public boolean canRead() { + return true; + } + + @Override + public boolean canWrite() { + return false; + } + + @Override + public void write(byte value) { + } + + @Override + public void write(byte[] buffer) { + } + + @Override + public void write(byte[] buffer, int offset, int count) { + } + + @Override + public void flush() { + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java new file mode 100644 index 000000000..3d4f2931f --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java @@ -0,0 +1,345 @@ +package us.shandian.giga.postprocessing.io; + +import org.schabi.newpipe.extractor.utils.io.SharpStream; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; + +public class CircularFile extends SharpStream { + + private final static int AUX_BUFFER_SIZE = 1024 * 1024;// 1 MiB + private final static int AUX2_BUFFER_SIZE = 256 * 1024;// 256 KiB + private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB + + private RandomAccessFile out; + private long position; + private long maxLengthKnown = -1; + + private ArrayList auxiliaryBuffers; + private OffsetChecker callback; + private ManagedBuffer queue; + private long startOffset; + private ProgressReport onProgress; + private long reportPosition; + + public CircularFile(File file, long offset, ProgressReport progressReport, OffsetChecker checker) throws IOException { + if (checker == null) { + throw new NullPointerException("checker is null"); + } + + try { + queue = new ManagedBuffer(QUEUE_BUFFER_SIZE); + out = new RandomAccessFile(file, "rw"); + out.seek(offset); + position = offset; + } catch (IOException err) { + try { + if (out != null) { + out.close(); + } + } catch (IOException e) { + // nothing to do + } + throw err; + } + + auxiliaryBuffers = new ArrayList<>(1); + callback = checker; + startOffset = offset; + reportPosition = offset; + onProgress = progressReport; + + } + + /** + * Close the file without flushing any buffer + */ + @Override + public void dispose() { + try { + auxiliaryBuffers = null; + if (out != null) { + out.close(); + out = null; + } + } catch (IOException err) { + // nothing to do + } + } + + /** + * Flush any buffer and close the output file. Use this method if the + * operation is successful + * + * @return the final length of the file + * @throws IOException if an I/O error occurs + */ + public long finalizeFile() throws IOException { + flushEverything(); + + if (maxLengthKnown > -1) { + position = maxLengthKnown; + } + if (position < out.length()) { + out.setLength(position); + } + + dispose(); + + return position; + } + + @Override + public void write(byte b) throws IOException { + write(new byte[]{b}, 0, 1); + } + + @Override + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + if (len == 0) { + return; + } + + long end = callback.check(); + int available; + + if (end == -1) { + available = Integer.MAX_VALUE; + } else { + if (end < startOffset) { + throw new IOException("The reported offset is invalid. reported offset is " + String.valueOf(end)); + } + available = (int) (end - position); + } + + while (available > 0 && auxiliaryBuffers.size() > 0) { + ManagedBuffer aux = auxiliaryBuffers.get(0); + + if ((queue.size + aux.size) > available) { + available = 0;// wait for next check + break; + } + + writeQueue(aux.buffer, 0, aux.size); + available -= aux.size; + aux.dereference(); + auxiliaryBuffers.remove(0); + } + + if (available > (len + queue.size)) { + writeQueue(b, off, len); + } else { + int i = auxiliaryBuffers.size() - 1; + while (len > 0) { + if (i < 0) { + // allocate a new auxiliary buffer + auxiliaryBuffers.add(new ManagedBuffer(AUX_BUFFER_SIZE)); + i++; + } + + ManagedBuffer aux = auxiliaryBuffers.get(i); + available = aux.available(); + + if (available < 1) { + // secondary auxiliary buffer + available = len; + aux = new ManagedBuffer(Math.max(len, AUX2_BUFFER_SIZE)); + auxiliaryBuffers.add(aux); + i++; + } else { + available = Math.min(len, available); + } + + aux.write(b, off, available); + + len -= available; + if (len < 1) { + break; + } + off += available; + } + } + } + + private void writeOutside(byte buffer[], int offset, int length) throws IOException { + out.write(buffer, offset, length); + position += length; + + if (onProgress != null && position > reportPosition) { + reportPosition = position + AUX2_BUFFER_SIZE;// notify every 256 KiB (approx) + onProgress.report(position); + } + } + + private void writeQueue(byte[] buffer, int offset, int length) throws IOException { + while (length > 0) { + if (queue.available() < length) { + flushQueue(); + + if (length >= queue.buffer.length) { + writeOutside(buffer, offset, length); + return; + } + } + + int size = Math.min(queue.available(), length); + queue.write(buffer, offset, size); + + offset += size; + length -= size; + } + } + + private void flushQueue() throws IOException { + writeOutside(queue.buffer, 0, queue.size); + queue.size = 0; + } + + private void flushEverything() throws IOException { + flushQueue(); + + if (auxiliaryBuffers.size() > 0) { + for (ManagedBuffer aux : auxiliaryBuffers) { + writeOutside(aux.buffer, 0, aux.size); + aux.dereference(); + } + auxiliaryBuffers.clear(); + } + } + + /** + * Flush any buffer directly to the file. Warning: use this method ONLY if + * all read dependencies are disposed + * + * @throws IOException if the dependencies are not disposed + */ + @Override + public void flush() throws IOException { + if (callback.check() != -1) { + throw new IOException("All read dependencies of this file must be disposed first"); + } + flushEverything(); + + // Save the current file length in case the method {@code rewind()} is called + if (position > maxLengthKnown) { + maxLengthKnown = position; + } + } + + @Override + public void rewind() throws IOException { + flush(); + out.seek(startOffset); + + if (onProgress != null) onProgress.report(-position); + + position = startOffset; + reportPosition = startOffset; + + } + + @Override + public long skip(long amount) throws IOException { + flush(); + position += amount; + + out.seek(position); + + return amount; + } + + @Override + public boolean isDisposed() { + return out == null; + } + + @Override + public boolean canRewind() { + return true; + } + + @Override + public boolean canWrite() { + return true; + } + + // + @Override + public boolean canRead() { + return false; + } + + @Override + public int read() { + throw new UnsupportedOperationException("write-only"); + } + + @Override + public int read(byte[] buffer) { + throw new UnsupportedOperationException("write-only"); + } + + @Override + public int read(byte[] buffer, int offset, int count) { + throw new UnsupportedOperationException("write-only"); + } + + @Override + public int available() { + throw new UnsupportedOperationException("write-only"); + } +// + + public interface OffsetChecker { + + /** + * Checks the amount of available space ahead + * + * @return absolute offset in the file where no more data SHOULD NOT be + * written. If the value is -1 the whole file will be used + */ + long check(); + } + + public interface ProgressReport { + + void report(long progress); + } + + class ManagedBuffer { + + byte[] buffer; + int size; + + ManagedBuffer(int length) { + buffer = new byte[length]; + } + + void dereference() { + buffer = null; + size = 0; + } + + protected int available() { + return buffer.length - size; + } + + private void write(byte[] b, int off, int len) { + System.arraycopy(b, off, buffer, size, len); + size += len; + } + + @Override + public String toString() { + return "holding: " + String.valueOf(size) + " length: " + String.valueOf(buffer.length) + " available: " + String.valueOf(available()); + } + + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java new file mode 100644 index 000000000..dd3f8c697 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java @@ -0,0 +1,126 @@ +package us.shandian.giga.postprocessing.io; + +import org.schabi.newpipe.extractor.utils.io.SharpStream; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; + +/** + * @author kapodamy + */ +public class FileStream extends SharpStream { + + public enum Mode { + Read, + ReadWrite + } + + public RandomAccessFile source; + private final Mode mode; + + public FileStream(String path, Mode mode) throws IOException { + String flags; + + if (mode == Mode.Read) { + flags = "r"; + } else { + flags = "rw"; + } + + this.mode = mode; + source = new RandomAccessFile(path, flags); + } + + @Override + public int read() throws IOException { + return source.read(); + } + + @Override + public int read(byte b[]) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + return source.read(b, off, len); + } + + @Override + public long skip(long pos) throws IOException { + FileChannel fc = source.getChannel(); + fc.position(fc.position() + pos); + return pos; + } + + @Override + public int available() { + try { + return (int) (source.length() - source.getFilePointer()); + } catch (IOException ex) { + return 0; + } + } + + @SuppressWarnings("EmptyCatchBlock") + @Override + public void dispose() { + try { + source.close(); + } catch (IOException err) { + + } finally { + source = null; + } + } + + @Override + public boolean isDisposed() { + return source == null; + } + + @Override + public void rewind() throws IOException { + source.getChannel().position(0); + } + + @Override + public boolean canRewind() { + return true; + } + + @Override + public boolean canRead() { + return mode == Mode.Read || mode == Mode.ReadWrite; + } + + @Override + public boolean canWrite() { + return mode == Mode.ReadWrite; + } + + @Override + public void write(byte value) throws IOException { + source.write(value); + } + + @Override + public void write(byte[] buffer) throws IOException { + source.write(buffer); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + source.write(buffer, offset, count); + } + + @Override + public void flush() { + } + + @Override + public void setLength(long length) throws IOException { + source.setLength(length); + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java new file mode 100644 index 000000000..831afbfc2 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java @@ -0,0 +1,59 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package us.shandian.giga.postprocessing.io; + +import android.support.annotation.NonNull; + +import org.schabi.newpipe.extractor.utils.io.SharpStream; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Wrapper for the classic {@link java.io.InputStream} + * @author kapodamy + */ +public class SharpInputStream extends InputStream { + + private final SharpStream base; + + public SharpInputStream(SharpStream base) throws IOException { + if (!base.canRead()) { + throw new IOException("The provided stream is not readable"); + } + this.base = base; + } + + @Override + public int read() throws IOException { + return base.read(); + } + + @Override + public int read(@NonNull byte[] bytes) throws IOException { + return base.read(bytes); + } + + @Override + public int read(@NonNull byte[] bytes, int i, int i1) throws IOException { + return base.read(bytes, i, i1); + } + + @Override + public long skip(long l) throws IOException { + return base.skip(l); + } + + @Override + public int available() { + return base.available(); + } + + @Override + public void close() { + base.dispose(); + } +} diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java new file mode 100644 index 000000000..97a0da523 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -0,0 +1,670 @@ +package us.shandian.giga.service; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.util.DiffUtil; +import android.util.Log; +import android.widget.Toast; + +import org.schabi.newpipe.R; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; + +import us.shandian.giga.get.DownloadMission; +import us.shandian.giga.get.FinishedMission; +import us.shandian.giga.get.Mission; +import us.shandian.giga.get.sqlite.DownloadDataSource; +import us.shandian.giga.util.Utility; + +import static org.schabi.newpipe.BuildConfig.DEBUG; + +public class DownloadManager { + private static final String TAG = DownloadManager.class.getSimpleName(); + + enum NetworkState {Unavailable, WifiOperating, MobileOperating, OtherOperating} + + public final static int SPECIAL_NOTHING = 0; + public final static int SPECIAL_PENDING = 1; + public final static int SPECIAL_FINISHED = 2; + + private final DownloadDataSource mDownloadDataSource; + + private final ArrayList mMissionsPending = new ArrayList<>(); + private final ArrayList mMissionsFinished; + + private final Handler mHandler; + private final File mPendingMissionsDir; + + private NetworkState mLastNetworkStatus = NetworkState.Unavailable; + + private SharedPreferences mPrefs; + private String mPrefMaxRetry; + private String mPrefCrossNetwork; + + /** + * Create a new instance + * + * @param context Context for the data source for finished downloads + * @param handler Thread required for Messaging + */ + DownloadManager(@NonNull Context context, Handler handler) { + if (DEBUG) { + Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode())); + } + + mDownloadDataSource = new DownloadDataSource(context); + mHandler = handler; + mMissionsFinished = loadFinishedMissions(); + mPendingMissionsDir = getPendingDir(context); + mPrefs = PreferenceManager.getDefaultSharedPreferences(context); + mPrefMaxRetry = context.getString(R.string.downloads_max_retry); + mPrefCrossNetwork = context.getString(R.string.cross_network_downloads); + + if (!Utility.mkdir(mPendingMissionsDir, false)) { + throw new RuntimeException("failed to create pending_downloads in data directory"); + } + + loadPendingMissions(); + } + + private static File getPendingDir(@NonNull Context context) { + //File dir = new File(ContextCompat.getDataDir(context), "pending_downloads"); + File dir = context.getExternalFilesDir("pending_downloads"); + + if (dir == null) { + // One of the following paths are not accessible ¿unmounted internal memory? + // /storage/emulated/0/Android/data/org.schabi.newpipe[.debug]/pending_downloads + // /sdcard/Android/data/org.schabi.newpipe[.debug]/pending_downloads + Log.w(TAG, "path to pending downloads are not accessible"); + } + + return dir; + } + + /** + * Loads finished missions from the data source + */ + private ArrayList loadFinishedMissions() { + ArrayList finishedMissions = mDownloadDataSource.loadFinishedMissions(); + + // missions always is stored by creation order, simply reverse the list + ArrayList result = new ArrayList<>(finishedMissions.size()); + for (int i = finishedMissions.size() - 1; i >= 0; i--) { + FinishedMission mission = finishedMissions.get(i); + File file = mission.getDownloadedFile(); + + if (!file.isFile()) { + if (DEBUG) { + Log.d(TAG, "downloaded file removed: " + file.getAbsolutePath()); + } + mDownloadDataSource.deleteMission(mission); + continue; + } + + result.add(mission); + } + + return result; + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private void loadPendingMissions() { + File[] subs = mPendingMissionsDir.listFiles(); + + if (subs == null) { + Log.e(TAG, "listFiles() returned null"); + return; + } + if (subs.length < 1) { + return; + } + if (DEBUG) { + Log.d(TAG, "Loading pending downloads from directory: " + mPendingMissionsDir.getAbsolutePath()); + } + + for (File sub : subs) { + if (sub.isFile()) { + DownloadMission mis = Utility.readFromFile(sub); + + if (mis == null) { + sub.delete(); + } else { + if (mis.isFinished()) { + sub.delete(); + continue; + } + + 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()); + } + exists = true; + mis.postprocessingRunning = false; + mis.errCode = DownloadMission.ERROR_POSTPROCESSING_FAILED; + mis.errObject = new RuntimeException("post-processing stopped unexpectedly"); + } + + if (exists && !dl.isFile()) { + // probably a folder, this should never happens + if (!sub.delete()) { + Log.w(TAG, "Unable to delete serialized file: " + sub.getPath()); + } + continue; + } + + if (!exists) { + // downloaded file deleted, reset mission state + DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs); + m.timestamp = mis.timestamp; + m.threadCount = mis.threadCount; + m.source = mis.source; + m.maxRetry = mis.maxRetry; + mis = m; + } + + mis.running = false; + mis.recovered = exists; + mis.metadata = sub; + mis.mHandler = mHandler; + + mMissionsPending.add(mis); + } + } + } + + if (mMissionsPending.size() > 1) { + Collections.sort(mMissionsPending, (mission1, mission2) -> Long.compare(mission1.timestamp, mission2.timestamp)); + } + } + + /** + * Start a new download mission + * + * @param urls the list of urls to download + * @param location the location + * @param name the name of the file to create + * @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined) + * @param threads the number of threads maximal used to download chunks of the file. + * @param postprocessingName the name of the required post-processing algorithm, or {@code null} to ignore. + * @param source source url of the resource + * @param postProcessingArgs the arguments for the post-processing algorithm. + */ + void startMission(String[] urls, String location, String name, char kind, int threads, String source, + String postprocessingName, String[] postProcessingArgs) { + synchronized (this) { + // check for existing pending download + DownloadMission pendingMission = getPendingMission(location, name); + if (pendingMission != null) { + // generate unique filename (?) + 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); + } + } else { + // check for existing finished download + int index = getFinishedMissionIndex(location, name); + if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index)); + } + + DownloadMission mission = new DownloadMission(urls, name, location, kind, postprocessingName, postProcessingArgs); + mission.timestamp = System.currentTimeMillis(); + mission.threadCount = threads; + mission.source = source; + mission.mHandler = mHandler; + mission.maxRetry = mPrefs.getInt(mPrefMaxRetry, 3); + + while (true) { + mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp)); + if (!mission.metadata.isFile() && !mission.metadata.exists()) { + try { + if (!mission.metadata.createNewFile()) + throw new RuntimeException("Cant create download metadata file"); + } catch (IOException e) { + throw new RuntimeException(e); + } + break; + } + mission.timestamp = System.currentTimeMillis(); + } + + mMissionsPending.add(mission); + + // Before starting, save the state in case the internet connection is not available + Utility.writeToFile(mission.metadata, mission); + + if (canDownloadInCurrentNetwork() && (getRunningMissionsCount() < 1)) { + mission.start(); + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING); + } + } + } + + + public void resumeMission(DownloadMission mission) { + if (!mission.running) { + mission.start(); + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING); + } + } + + public void pauseMission(DownloadMission mission) { + if (mission.running) { + mission.pause(); + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); + } + } + + public void deleteMission(Mission mission) { + synchronized (this) { + if (mission instanceof DownloadMission) { + mMissionsPending.remove(mission); + } else if (mission instanceof FinishedMission) { + mMissionsFinished.remove(mission); + mDownloadDataSource.deleteMission(mission); + } + + mission.delete(); + } + } + + + /** + * Get a pending mission by its location and name + * + * @param location the location + * @param name the name + * @return the mission or null if no such mission exists + */ + @Nullable + private DownloadMission getPendingMission(String location, String name) { + for (DownloadMission mission : mMissionsPending) { + if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) { + return mission; + } + } + return null; + } + + /** + * Get a finished mission by its location and name + * + * @param location the location + * @param name the name + * @return the mission index or -1 if no such mission exists + */ + private int getFinishedMissionIndex(String location, String name) { + for (int i = 0; i < mMissionsFinished.size(); i++) { + FinishedMission mission = mMissionsFinished.get(i); + if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) { + return i; + } + } + + return -1; + } + + public Mission getAnyMission(String location, String name) { + synchronized (this) { + Mission mission = getPendingMission(location, name); + if (mission != null) return mission; + + int idx = getFinishedMissionIndex(location, name); + if (idx >= 0) return mMissionsFinished.get(idx); + } + + return null; + } + + int getRunningMissionsCount() { + int count = 0; + synchronized (this) { + for (DownloadMission mission : mMissionsPending) { + if (mission.running && mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED && !mission.isFinished()) + count++; + } + } + + return count; + } + + void pauseAllMissions() { + synchronized (this) { + for (DownloadMission mission : mMissionsPending) mission.pause(); + } + } + + + /** + * 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((dir, name1) -> name1.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; + } + + /** + * Set a pending download as finished + * + * @param mission the desired mission + * @return true if exits pending missions running, otherwise, false + */ + boolean setFinished(DownloadMission mission) { + synchronized (this) { + int i = mMissionsPending.indexOf(mission); + mMissionsPending.remove(i); + + mMissionsFinished.add(0, new FinishedMission(mission)); + mDownloadDataSource.addMission(mission); + + if (mMissionsPending.size() < 1) return false; + + i = getRunningMissionsCount(); + if (i > 0) return true; + + // before returning, check the queue + if (!canDownloadInCurrentNetwork()) return false; + + for (DownloadMission mission1 : mMissionsPending) { + if (!mission1.running && mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED && mission1.enqueued) { + resumeMission(mMissionsPending.get(i)); + return true; + } + } + + return false; + } + } + + public MissionIterator getIterator() { + return new MissionIterator(); + } + + /** + * Forget all finished downloads, but, doesn't delete any file + */ + public void forgetFinishedDownloads() { + synchronized (this) { + for (FinishedMission mission : mMissionsFinished) { + mDownloadDataSource.deleteMission(mission); + } + mMissionsFinished.clear(); + } + } + + private boolean canDownloadInCurrentNetwork() { + if (mLastNetworkStatus == NetworkState.Unavailable) return false; + return !(mPrefs.getBoolean(mPrefCrossNetwork, false) && mLastNetworkStatus == NetworkState.MobileOperating); + } + + void handleConnectivityChange(NetworkState currentStatus) { + if (currentStatus == mLastNetworkStatus) return; + + mLastNetworkStatus = currentStatus; + boolean pauseOnMobile = mPrefs.getBoolean(mPrefCrossNetwork, false); + + if (currentStatus == NetworkState.Unavailable) { + return; + } else if (currentStatus != NetworkState.MobileOperating || !pauseOnMobile) { + return; + } + + boolean flag = false; + synchronized (this) { + for (DownloadMission mission : mMissionsPending) { + if (mission.running && mission.isFinished() && !mission.postprocessingRunning) { + flag = true; + mission.pause(); + } + } + } + + if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); + } + + /** + * Fast check for pending downloads. If exists, the user will be notified + * TODO: call this method in somewhere + * + * @param context the application context + */ + public static void notifyUserPendingDownloads(Context context) { + int pending = getPendingDir(context).list().length; + if (pending < 1) return; + + Toast.makeText(context, context.getString( + R.string.msg_pending_downloads, + String.valueOf(pending) + ), Toast.LENGTH_LONG).show(); + } + + void checkForRunningMission(String location, String name, DownloadManagerService.DMChecker check) { + boolean listed; + boolean finished = false; + + synchronized (this) { + DownloadMission mission = getPendingMission(location, name); + if (mission != null) { + listed = true; + } else { + listed = getFinishedMissionIndex(location, name) >= 0; + finished = listed; + } + } + + check.callback(listed, finished); + } + + public class MissionIterator extends DiffUtil.Callback { + final Object FINISHED = new Object(); + final Object PENDING = new Object(); + + ArrayList snapshot; + ArrayList current; + ArrayList hidden; + + private MissionIterator() { + hidden = new ArrayList<>(2); + current = null; + snapshot = getSpecialItems(); + } + + private ArrayList getSpecialItems() { + synchronized (DownloadManager.this) { + ArrayList pending = new ArrayList<>(mMissionsPending); + ArrayList finished = new ArrayList<>(mMissionsFinished); + ArrayList remove = new ArrayList<>(hidden); + + // hide missions (if required) + Iterator iterator = remove.iterator(); + while (iterator.hasNext()) { + Mission mission = iterator.next(); + if (pending.remove(mission) || finished.remove(mission)) iterator.remove(); + } + + int fakeTotal = pending.size(); + if (fakeTotal > 0) fakeTotal++; + + fakeTotal += finished.size(); + if (finished.size() > 0) fakeTotal++; + + ArrayList list = new ArrayList<>(fakeTotal); + if (pending.size() > 0) { + list.add(PENDING); + list.addAll(pending); + } + if (finished.size() > 0) { + list.add(FINISHED); + list.addAll(finished); + } + + + return list; + } + } + + public MissionItem getItem(int position) { + Object object = snapshot.get(position); + + if (object == PENDING) return new MissionItem(SPECIAL_PENDING); + if (object == FINISHED) return new MissionItem(SPECIAL_FINISHED); + + return new MissionItem(SPECIAL_NOTHING, (Mission) object); + } + + public int getSpecialAtItem(int position) { + Object object = snapshot.get(position); + + if (object == PENDING) return SPECIAL_PENDING; + if (object == FINISHED) return SPECIAL_FINISHED; + + return SPECIAL_NOTHING; + } + + public MissionItem getItemUnsafe(int position) { + synchronized (DownloadManager.this) { + int count = mMissionsPending.size(); + int count2 = mMissionsFinished.size(); + + if (count > 0) { + position--; + if (position == -1) + return new MissionItem(SPECIAL_PENDING); + else if (position < count) + return new MissionItem(SPECIAL_NOTHING, mMissionsPending.get(position)); + else if (position == count && count2 > 0) + return new MissionItem(SPECIAL_FINISHED); + else + position -= count; + } else { + if (count2 > 0 && position == 0) { + return new MissionItem(SPECIAL_FINISHED); + } + } + + position--; + + if (count2 < 1) { + throw new RuntimeException( + String.format("Out of range. pending_count=%s finished_count=%s position=%s", count, count2, position) + ); + } + + return new MissionItem(SPECIAL_NOTHING, mMissionsFinished.get(position)); + } + } + + + public void start() { + current = getSpecialItems(); + } + + public void end() { + snapshot = current; + current = null; + } + + public void hide(Mission mission) { + hidden.add(mission); + } + + public void unHide(Mission mission) { + hidden.remove(mission); + } + + + @Override + public int getOldListSize() { + return snapshot.size(); + } + + @Override + public int getNewListSize() { + return current.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return snapshot.get(oldItemPosition) == current.get(newItemPosition); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return areItemsTheSame(oldItemPosition, newItemPosition); + } + } + + public class MissionItem { + public int special; + public Mission mission; + + MissionItem(int s, Mission m) { + special = s; + mission = m; + } + + MissionItem(int s) { + this(s, null); + } + } + +} 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 ff410a79a..797fb1c1d 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -2,17 +2,25 @@ package us.shandian.giga.service; import android.Manifest; import android.app.Notification; +import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.net.Uri; import android.os.Binder; +import android.os.Build; import android.os.Handler; -import android.os.HandlerThread; import android.os.IBinder; +import android.os.Looper; import android.os.Message; import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.content.PermissionChecker; @@ -21,48 +29,61 @@ import android.widget.Toast; import org.schabi.newpipe.R; import org.schabi.newpipe.download.DownloadActivity; -import org.schabi.newpipe.settings.NewPipeSettings; +import java.io.File; import java.util.ArrayList; +import java.util.Iterator; -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 us.shandian.giga.service.DownloadManager.NetworkState; +import static org.schabi.newpipe.BuildConfig.APPLICATION_ID; import static org.schabi.newpipe.BuildConfig.DEBUG; 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; + public static final int MESSAGE_RUNNING = 1; + public static final int MESSAGE_PAUSED = 2; + public static final int MESSAGE_FINISHED = 3; + public static final int MESSAGE_PROGRESS = 4; + public static final int MESSAGE_ERROR = 5; + + private static final int FOREGROUND_NOTIFICATION_ID = 1000; + private static final int DOWNLOADS_NOTIFICATION_ID = 1001; + + private static final String EXTRA_URLS = "DownloadManagerService.extra.urls"; 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_KIND = "DownloadManagerService.extra.kind"; private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; + private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName"; + private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; + private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; + private static final String ACTION_RESET_DOWNLOAD_COUNT = APPLICATION_ID + ".reset_download_count"; private DMBinder mBinder; private DownloadManager mManager; private Notification mNotification; private Handler mHandler; - private long mLastTimeStamp = System.currentTimeMillis(); - private DownloadDataSource mDataSource; + private int downloadDoneCount = 0; + private Builder downloadDoneNotification = null; + private StringBuilder downloadDoneList = null; + NotificationManager notificationManager = null; + private boolean mForeground = false; + private final ArrayList mEchoObservers = new ArrayList<>(1); + private BroadcastReceiver mNetworkStateListener; - private final 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)); + /** + * notify media scanner on downloaded media file ... + * + * @param file the downloaded file + */ + private void notifyMediaScanner(File file) { + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); } @Override @@ -74,19 +95,14 @@ public class DownloadManagerService extends Service { } mBinder = new DMBinder(); - if (mDataSource == null) { - mDataSource = new SQLiteDownloadDataSource(this); - } - if (mManager == null) { - ArrayList paths = new ArrayList<>(2); - paths.add(NewPipeSettings.getVideoDownloadPath(this)); - paths.add(NewPipeSettings.getAudioDownloadPath(this)); - mManager = new DownloadManagerImpl(paths, mDataSource, this); - if (DEBUG) { - Log.d(TAG, "mManager == null"); - Log.d(TAG, "Download directory: " + paths); + mHandler = new Handler(Looper.myLooper()) { + @Override + public void handleMessage(Message msg) { + DownloadManagerService.this.handleMessage(msg); } - } + }; + + mManager = new DownloadManager(this, mHandler); Intent openDownloadListIntent = new Intent(this, DownloadActivity.class) .setAction(Intent.ACTION_MAIN); @@ -105,56 +121,49 @@ public class DownloadManagerService extends Service { .setContentText(getString(R.string.msg_running_detail)); mNotification = builder.build(); + notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - HandlerThread thread = new HandlerThread("ServiceMessenger"); - thread.start(); - - mHandler = new Handler(thread.getLooper()) { + mNetworkStateListener = new BroadcastReceiver() { @Override - public void handleMessage(Message msg) { - 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; - } + public void onReceive(Context context, Intent intent) { + if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) { + handleConnectivityChange(null); + return; } + handleConnectivityChange(intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO)); } }; - - } - - 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)); - } - }); + registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (DEBUG) { + if (intent == null) { + Log.d(TAG, "Restarting"); + return START_NOT_STICKY; + } 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); + if (action != null) { + if (action.equals(Intent.ACTION_RUN)) { + String[] urls = intent.getStringArrayExtra(EXTRA_URLS); + String name = intent.getStringExtra(EXTRA_NAME); + String location = intent.getStringExtra(EXTRA_LOCATION); + int threads = intent.getIntExtra(EXTRA_THREADS, 1); + char kind = intent.getCharExtra(EXTRA_KIND, '?'); + String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); + String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); + String source = intent.getStringExtra(EXTRA_SOURCE); + + mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs)); + + } else if (downloadDoneNotification != null && action.equals(ACTION_RESET_DOWNLOAD_COUNT)) { + downloadDoneCount = 0; + downloadDoneList.setLength(0); + } } return START_NOT_STICKY; } @@ -167,11 +176,17 @@ public class DownloadManagerService extends Service { Log.d(TAG, "Destroying"); } - for (int i = 0; i < mManager.getCount(); i++) { - mManager.pauseMission(i); + stopForeground(true); + + if (notificationManager != null && downloadDoneNotification != null) { + downloadDoneNotification.setDeleteIntent(null);// prevent NewPipe running when is killed, cleared from recent, etc + notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); } - stopForeground(true); + unregisterReceiver(mNetworkStateListener); + + mManager.pauseAllMissions(); + } @Override @@ -192,53 +207,171 @@ public class DownloadManagerService extends Service { return mBinder; } - private void postUpdateMessage() { - mHandler.sendEmptyMessage(UPDATE_MESSAGE); - } + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_FINISHED: + DownloadMission mission = (DownloadMission) msg.obj; + notifyMediaScanner(mission.getDownloadedFile()); + notifyFinishedDownload(mission.name); + updateForegroundState(mManager.setFinished(mission)); + break; + case MESSAGE_RUNNING: + case MESSAGE_PROGRESS: + updateForegroundState(true); + break; + case MESSAGE_PAUSED: + case MESSAGE_ERROR: + updateForegroundState(mManager.getRunningMissionsCount() > 0); + break; + } - private void updateState(int runningCount) { - if (runningCount == 0) { - stopForeground(true); - } else { - startForeground(NOTIFICATION_ID, mNotification); + + synchronized (mEchoObservers) { + Iterator iterator = mEchoObservers.iterator(); + while (iterator.hasNext()) { + Handler handler = iterator.next(); + if (handler.getLooper().getThread().isAlive()) { + Message echo = new Message(); + echo.what = msg.what; + echo.obj = msg.obj; + handler.sendMessage(echo); + } else { + iterator.remove();// ¿missing call to removeMissionEventListener()? + } + } } } - public static void startMission(Context context, String url, String location, String name, boolean isAudio, int threads) { + private void handleConnectivityChange(NetworkInfo info) { + NetworkState status; + + if (info == null) { + status = NetworkState.Unavailable; + Log.i(TAG, "actual connectivity status is unavailable"); + } else if (!info.isAvailable() || !info.isConnected()) { + status = NetworkState.Unavailable; + Log.i(TAG, "actual connectivity status is not available and not connected"); + } else { + int type = info.getType(); + if (type == ConnectivityManager.TYPE_MOBILE || type == ConnectivityManager.TYPE_MOBILE_DUN) { + status = NetworkState.MobileOperating; + } else if (type == ConnectivityManager.TYPE_WIFI) { + status = NetworkState.WifiOperating; + } else if (type == ConnectivityManager.TYPE_WIMAX || + type == ConnectivityManager.TYPE_ETHERNET || + type == ConnectivityManager.TYPE_BLUETOOTH) { + status = NetworkState.OtherOperating; + } else { + status = NetworkState.Unavailable; + } + Log.i(TAG, "actual connectivity status is " + status.name()); + } + + if (mManager == null) return;// avoid race-conditions while the service is starting + mManager.handleConnectivityChange(status); + } + + public void updateForegroundState(boolean state) { + if (state == mForeground) return; + + if (state) { + startForeground(FOREGROUND_NOTIFICATION_ID, mNotification); + } else { + stopForeground(true); + } + + mForeground = state; + } + + public static void startMission(Context context, String urls[], String location, String name, + char kind, int threads, String source, String postprocessingName, + String[] postprocessingArgs) { Intent intent = new Intent(context, DownloadManagerService.class); intent.setAction(Intent.ACTION_RUN); - intent.setData(Uri.parse(url)); + intent.putExtra(EXTRA_URLS, urls); intent.putExtra(EXTRA_NAME, name); intent.putExtra(EXTRA_LOCATION, location); - intent.putExtra(EXTRA_IS_AUDIO, isAudio); + intent.putExtra(EXTRA_KIND, kind); intent.putExtra(EXTRA_THREADS, threads); + intent.putExtra(EXTRA_SOURCE, source); + intent.putExtra(EXTRA_POSTPROCESSING_NAME, postprocessingName); + intent.putExtra(EXTRA_POSTPROCESSING_ARGS, postprocessingArgs); context.startService(intent); } + public static void checkForRunningMission(Context context, String location, String name, DMChecker check) { + Intent intent = new Intent(); + intent.setClass(context, DownloadManagerService.class); + context.bindService(intent, new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName cname, IBinder service) { + try { + ((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, check); + } catch (Exception err) { + Log.w(TAG, "checkForRunningMission() callback is defective", err); + } - private 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; + // TODO: find a efficient way to unbind the service. This destroy the service due idle, but is started again when the user start a download. + context.unbindService(this); } - } - @Override - public void onFinish(DownloadMission downloadMission) { - postUpdateMessage(); - notifyMediaScanner(downloadMission); - } - - @Override - public void onError(DownloadMission downloadMission, int errCode) { - postUpdateMessage(); - } + @Override + public void onServiceDisconnected(ComponentName name) { + } + }, Context.BIND_AUTO_CREATE); } + public void notifyFinishedDownload(String name) { + if (notificationManager == null) { + return; + } + + if (downloadDoneNotification == null) { + downloadDoneList = new StringBuilder(name.length()); + + Bitmap icon = BitmapFactory.decodeResource(this.getResources(), android.R.drawable.stat_sys_download_done); + downloadDoneNotification = new Builder(this, getString(R.string.notification_channel_id)) + .setAutoCancel(true) + .setLargeIcon(icon) + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .setDeleteIntent(PendingIntent.getService(this, (int) System.currentTimeMillis(), + new Intent(this, DownloadManagerService.class) + .setAction(ACTION_RESET_DOWNLOAD_COUNT) + , PendingIntent.FLAG_UPDATE_CURRENT)) + .setContentIntent(mNotification.contentIntent); + } + + if (downloadDoneCount < 1) { + downloadDoneList.append(name); + + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + downloadDoneNotification.setContentTitle(getString(R.string.app_name)); + downloadDoneNotification.setContentText(getString(R.string.download_finished, name)); + } else { + downloadDoneNotification.setContentTitle(getString(R.string.download_finished, name)); + downloadDoneNotification.setContentText(null); + } + } else { + downloadDoneList.append(", "); + downloadDoneList.append(name); + + downloadDoneNotification.setContentTitle(getString(R.string.download_finished_more, String.valueOf(downloadDoneCount + 1))); + downloadDoneNotification.setContentText(downloadDoneList.toString()); + } + + notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); + downloadDoneCount++; + } + + private void manageObservers(Handler handler, boolean add) { + synchronized (mEchoObservers) { + if (add) { + mEchoObservers.add(handler); + } else { + mEchoObservers.remove(handler); + } + } + } // Wrapper of DownloadManager public class DMBinder extends Binder { @@ -246,14 +379,24 @@ public class DownloadManagerService extends Service { return mManager; } - public void onMissionAdded(DownloadMission mission) { - mission.addListener(missionListener); - postUpdateMessage(); + public void addMissionEventListener(Handler handler) { + manageObservers(handler, true); } - public void onMissionRemoved(DownloadMission mission) { - mission.removeListener(missionListener); - postUpdateMessage(); + public void removeMissionEventListener(Handler handler) { + manageObservers(handler, false); + } + + public void resetFinishedDownloadCount() { + if (notificationManager == null || downloadDoneNotification == null) return; + notificationManager.cancel(DOWNLOADS_NOTIFICATION_ID); + downloadDoneList.setLength(0); + downloadDoneCount = 0; } } + + public interface DMChecker { + void callback(boolean listed, boolean finished); + } + } 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 d5555c2be..c40c215b8 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 @@ -1,5 +1,6 @@ package us.shandian.giga.ui.adapter; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; @@ -7,12 +8,20 @@ import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.FileProvider; import android.support.v4.view.ViewCompat; +import android.support.v7.app.AlertDialog; +import android.support.v7.util.DiffUtil; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; import android.util.Log; +import android.util.SparseArray; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -24,28 +33,28 @@ import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; +import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; -import org.schabi.newpipe.download.DeleteDownloadManager; +import org.schabi.newpipe.util.NavigationHelper; import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import java.util.Locale; -import java.util.Map; -import us.shandian.giga.get.DownloadManager; import us.shandian.giga.get.DownloadMission; +import us.shandian.giga.get.FinishedMission; +import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; +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_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<>(); +public class MissionAdapter extends RecyclerView.Adapter { + private static final SparseArray ALGORITHMS = new SparseArray<>(); private static final String TAG = "MissionAdapter"; static { @@ -53,109 +62,131 @@ public class MissionAdapter extends RecyclerView.Adapter mItemList; - private DownloadManagerService.DMBinder mBinder; + private Deleter mDeleter; private int mLayout; + private DownloadManager.MissionIterator mIterator; + private Handler mHandler; + private ArrayList mPendingDownloadsItems = new ArrayList<>(); + private MenuItem mClear; + private View mEmptyMessage; - public MissionAdapter(Activity context, DownloadManagerService.DMBinder binder, DownloadManager downloadManager, DeleteDownloadManager deleteDownloadManager, boolean isLinear) { + public MissionAdapter(Context context, DownloadManager downloadManager, MenuItem clearButton, View emptyMessage) { mContext = context; mDownloadManager = downloadManager; - mDeleteDownloadManager = deleteDownloadManager; - mBinder = binder; + mDeleter = null; mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item; + mLayout = R.layout.mission_item; - mItemList = new ArrayList<>(); - updateItemList(); + mHandler = new Handler(Looper.myLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case DownloadManagerService.MESSAGE_PROGRESS: + case DownloadManagerService.MESSAGE_ERROR: + case DownloadManagerService.MESSAGE_FINISHED: + onServiceMessage(msg); + } + } + }; + + mClear = clearButton; + mEmptyMessage = emptyMessage; + + mIterator = downloadManager.getIterator(); + + checkEmptyMessageVisibility(); } - public void updateItemList() { - mItemList.clear(); + @Override + @NonNull + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case DownloadManager.SPECIAL_PENDING: + case DownloadManager.SPECIAL_FINISHED: + return new ViewHolderHeader(mInflater.inflate(R.layout.missions_header, parent, false)); + } - for (int i = 0; i < mDownloadManager.getCount(); i++) { - DownloadMission mission = mDownloadManager.getMission(i); - if (!mDeleteDownloadManager.contains(mission)) { - mItemList.add(mDownloadManager.getMission(i)); + return new ViewHolderItem(mInflater.inflate(mLayout, parent, false)); + } + + @Override + public void onViewRecycled(@NonNull ViewHolder view) { + super.onViewRecycled(view); + + if (view instanceof ViewHolderHeader) return; + ViewHolderItem h = (ViewHolderItem) view; + + if (h.item.mission instanceof DownloadMission) mPendingDownloadsItems.remove(h); + + h.popupMenu.dismiss(); + h.item = null; + h.lastTimeStamp = -1; + h.lastDone = -1; + h.lastCurrent = -1; + h.state = 0; + } + + @Override + @SuppressLint("SetTextI18n") + public void onBindViewHolder(@NonNull ViewHolder view, @SuppressLint("RecyclerView") int pos) { + DownloadManager.MissionItem item = mIterator.getItem(pos); + + if (view instanceof ViewHolderHeader) { + if (item.special == DownloadManager.SPECIAL_NOTHING) return; + int str; + if (item.special == DownloadManager.SPECIAL_PENDING) { + str = R.string.missions_header_pending; + } else { + str = R.string.missions_header_finished; + mClear.setVisible(true); } + + ((ViewHolderHeader) view).header.setText(str); + return; + } + + ViewHolderItem h = (ViewHolderItem) view; + h.item = item; + + Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.name); + + h.icon.setImageResource(Utility.getIconForFileType(type)); + h.name.setText(item.mission.name); + h.size.setText(Utility.formatBytes(item.mission.length)); + + h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type)); + + if (h.item.mission instanceof DownloadMission) { + DownloadMission mission = (DownloadMission) item.mission; + h.progress.setMarquee(mission.done < 1); + updateProgress(h); + h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause); + mPendingDownloadsItems.add(h); + } else { + h.progress.setMarquee(false); + h.status.setText("100%"); + h.progress.setProgress(1f); } } - @Override - public MissionAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - final ViewHolder h = new ViewHolder(mInflater.inflate(mLayout, parent, false)); - - h.menu.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - buildPopup(h); - } - }); - - /*h.itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - showDetail(h); - } - });*/ - - return h; - } - - @Override - public void onViewRecycled(MissionAdapter.ViewHolder h) { - super.onViewRecycled(h); - h.mission.removeListener(h.observer); - h.mission = null; - h.observer = null; - h.progress = null; - h.position = -1; - h.lastTimeStamp = -1; - h.lastDone = -1; - h.colorId = 0; - } - - @Override - public void onBindViewHolder(MissionAdapter.ViewHolder h, int pos) { - DownloadMission ms = mItemList.get(pos); - h.mission = ms; - h.position = pos; - - Utility.FileType type = Utility.getFileType(ms.name); - - h.icon.setImageResource(Utility.getIconForFileType(type)); - h.name.setText(ms.name); - h.size.setText(Utility.formatBytes(ms.length)); - - h.progress = new ProgressDrawable(mContext, Utility.getBackgroundForFileType(type), Utility.getForegroundForFileType(type)); - ViewCompat.setBackground(h.bkg, h.progress); - - h.observer = new MissionObserver(this, h); - ms.addListener(h.observer); - - updateProgress(h); - } - @Override public int getItemCount() { - return mItemList.size(); + return mIterator.getOldListSize(); } @Override - public long getItemId(int position) { - return position; + public int getItemViewType(int position) { + return mIterator.getSpecialAtItem(position); } - private void updateProgress(ViewHolder h) { - updateProgress(h, false); - } + private void updateProgress(ViewHolderItem h) { + if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return; - private void updateProgress(ViewHolder h, boolean finished) { - if (h.mission == null) return; + DownloadMission mission = (DownloadMission) h.item.mission; long now = System.currentTimeMillis(); @@ -164,130 +195,110 @@ public class MissionAdapter extends RecyclerView.Adapter 1000 || finished) { - if (h.mission.errCode > 0) { - h.status.setText(R.string.msg_error); + if (hasError || deltaTime == 0 || deltaTime > 1000) { + // on error hide marquee or show if condition (mission.done < 1 || mission.unknownLength) is true + h.progress.setMarquee(!hasError && (mission.done < 1 || mission.unknownLength)); + + float progress; + if (mission.unknownLength) { + progress = Float.NaN; + h.progress.setProgress(0f); } else { - float progress = (float) h.mission.done / h.mission.length; - h.status.setText(String.format(Locale.US, "%.2f%%", progress * 100)); + progress = (float) mission.done / mission.length; + if (mission.urls.length > 1 && mission.current < mission.urls.length) { + progress = (progress / mission.urls.length) + ((float) mission.current / mission.urls.length); + } + } + + if (hasError) { + if (Float.isNaN(progress) || Float.isInfinite(progress)) h.progress.setProgress(1f); + h.status.setText(R.string.msg_error); + } else if (Float.isNaN(progress) || Float.isInfinite(progress)) { + h.status.setText("--.-%"); + } else { + h.status.setText(String.format("%.2f%%", progress * 100)); h.progress.setProgress(progress); } } + long length = mission.offsets[mission.current < mission.offsets.length ? mission.current : (mission.offsets.length - 1)]; + length += mission.length; + + int state = 0; + if (!mission.isFinished()) { + if (!mission.running) { + state = mission.enqueued ? 1 : 2; + } else if (mission.postprocessingRunning) { + state = 3; + } + } + + if (state != 0) { + if (h.state != state) { + String statusStr; + h.state = state; + + switch (state) { + case 1: + statusStr = mContext.getString(R.string.queued); + break; + case 2: + statusStr = mContext.getString(R.string.paused); + break; + case 3: + statusStr = mContext.getString(R.string.post_processing); + break; + default: + statusStr = "?"; + break; + } + + h.size.setText(Utility.formatBytes(length).concat(" (").concat(statusStr).concat(")")); + } else if (deltaTime > 1000 && deltaDone > 0) { + h.lastTimeStamp = now; + h.lastDone = mission.done; + } + + return; + } + if (deltaTime > 1000 && deltaDone > 0) { float speed = (float) deltaDone / deltaTime; String speedStr = Utility.formatSpeed(speed * 1000); - String sizeStr = Utility.formatBytes(h.mission.length); + String sizeStr = Utility.formatBytes(length); - h.size.setText(sizeStr + " " + speedStr); + h.size.setText(sizeStr.concat(" ").concat(speedStr)); h.lastTimeStamp = now; - h.lastDone = h.mission.done; + h.lastDone = mission.done; } } + private boolean viewWithFileProvider(@NonNull File file) { + if (!file.exists()) return true; - private void buildPopup(final ViewHolder h) { - PopupMenu popup = new PopupMenu(mContext, h.menu); - popup.inflate(R.menu.mission); + String ext = Utility.getFileExt(file.getName()); + if (ext == null) return false; - Menu menu = popup.getMenu(); - MenuItem start = menu.findItem(R.id.start); - MenuItem pause = menu.findItem(R.id.pause); - MenuItem view = menu.findItem(R.id.view); - MenuItem delete = menu.findItem(R.id.delete); - MenuItem checksum = menu.findItem(R.id.checksum); + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); + Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); - // Set to false first - start.setVisible(false); - pause.setVisible(false); - view.setVisible(false); - delete.setVisible(false); - checksum.setVisible(false); - - if (!h.mission.finished) { - if (!h.mission.running) { - if (h.mission.errCode == -1) { - start.setVisible(true); - } - - delete.setVisible(true); - } else { - pause.setVisible(true); - } - } else { - view.setVisible(true); - delete.setVisible(true); - checksum.setVisible(true); - } - - popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - int id = item.getItemId(); - switch (id) { - case R.id.start: - mDownloadManager.resumeMission(h.position); - mBinder.onMissionAdded(mItemList.get(h.position)); - return true; - case R.id.pause: - mDownloadManager.pauseMission(h.position); - mBinder.onMissionRemoved(mItemList.get(h.position)); - h.lastTimeStamp = -1; - h.lastDone = -1; - return true; - case R.id.view: - File f = new File(h.mission.location, h.mission.name); - String ext = Utility.getFileExt(h.mission.name); - - Log.d(TAG, "Viewing file: " + f.getAbsolutePath() + " ext: " + ext); - - if (ext == null) { - Log.w(TAG, "Can't view file because it has no extension: " + - h.mission.name); - return false; - } - - String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); - Log.v(TAG, "Mime: " + mime + " package: " + mContext.getApplicationContext().getPackageName() + ".provider"); - if (f.exists()) { - viewFileWithFileProvider(f, mime); - } else { - Log.w(TAG, "File doesn't exist"); - } - - return true; - case R.id.delete: - mDeleteDownloadManager.add(h.mission); - updateItemList(); - notifyDataSetChanged(); - return true; - case R.id.md5: - case R.id.sha1: - DownloadMission mission = mItemList.get(h.position); - new ChecksumTask(mContext).execute(mission.location + "/" + mission.name, ALGORITHMS.get(id)); - return true; - default: - return false; - } - } - }); - - popup.show(); - } - - private void viewFileWithFileProvider(File file, String mimetype) { - String ourPackage = mContext.getApplicationContext().getPackageName(); - Uri uri = FileProvider.getUriForFile(mContext, ourPackage + ".provider", file); + Uri uri = FileProvider.getUriForFile(mContext, BuildConfig.APPLICATION_ID + ".provider", file); Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); - intent.setDataAndType(uri, mimetype); + 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); @@ -300,75 +311,338 @@ public class MissionAdapter extends RecyclerView.Adapter= 100 && mission.errCode < 600) { + str.append("HTTP"); + } else if (mission.errObject == null) { + str.append("(not_decelerated_error_code)"); + } + break; } - @Override - public void onProgressUpdate(DownloadMission downloadMission, long done, long total) { - mAdapter.updateProgress(mHolder); + if (mission.errObject != null) { + str.append("\n\n"); + str.append(mission.errObject.toString()); } - @Override - public void onFinish(DownloadMission downloadMission) { - //mAdapter.mManager.deleteMission(mHolder.position); - // TODO Notification - //mAdapter.notifyDataSetChanged(); - if (mHolder.mission != null) { - mHolder.size.setText(Utility.formatBytes(mHolder.mission.length)); - mAdapter.updateProgress(mHolder, true); + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setTitle(mission.name) + .setMessage(str) + .setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel()) + .create() + .show(); + } + + public void clearFinishedDownloads() { + mDownloadManager.forgetFinishedDownloads(); + applyChanges(); + mClear.setVisible(false); + } + + private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) { + int id = option.getItemId(); + DownloadMission mission = h.item.mission instanceof DownloadMission ? (DownloadMission) h.item.mission : null; + + if (mission != null) { + switch (id) { + case R.id.start: + h.state = -1; + h.size.setText(Utility.formatBytes(mission.length)); + mDownloadManager.resumeMission(mission); + return true; + case R.id.pause: + h.state = -1; + mDownloadManager.pauseMission(mission); + notifyItemChanged(h.getAdapterPosition()); + h.lastTimeStamp = -1; + h.lastDone = -1; + return true; + case R.id.error_message_view: + showError(mission); + return true; + case R.id.queue: + h.queue.setChecked(!h.queue.isChecked()); + mission.enqueued = h.queue.isChecked(); + updateProgress(h); + return true; } } - @Override - public void onError(DownloadMission downloadMission, int errCode) { - mAdapter.updateProgress(mHolder); + switch (id) { + case R.id.open: + return viewWithFileProvider(h.item.mission.getDownloadedFile()); + case R.id.delete: + if (mDeleter == null) { + mDownloadManager.deleteMission(h.item.mission); + } else { + mDeleter.append(h.item.mission); + } + applyChanges(); + return true; + case R.id.md5: + case R.id.sha1: + new ChecksumTask(mContext).execute(h.item.mission.getDownloadedFile().getAbsolutePath(), ALGORITHMS.get(id)); + return true; + case R.id.source: + /*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source)); + mContext.startActivity(intent);*/ + try { + Intent intent = NavigationHelper.getIntentByLink(mContext, h.item.mission.source); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + } catch (Exception e) { + Log.w(TAG, "Selected item has a invalid source", e); + } + return true; + default: + return false; } - } - private static class ChecksumTask extends AsyncTask { - ProgressDialog prog; - final WeakReference weakReference; + public void applyChanges() { + mIterator.start(); + DiffUtil.calculateDiff(mIterator, true).dispatchUpdatesTo(this); + mIterator.end(); - ChecksumTask(@NonNull Activity activity) { - weakReference = new WeakReference<>(activity); + checkEmptyMessageVisibility(); + + if (mIterator.getOldListSize() > 0) { + int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1); + mClear.setVisible(lastItemType == DownloadManager.SPECIAL_FINISHED); + } + } + + public void forceUpdate() { + mIterator.start(); + mIterator.end(); + + notifyDataSetChanged(); + } + + public void setLinear(boolean isLinear) { + mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item; + } + + private void checkEmptyMessageVisibility() { + int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE; + if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag); + } + + + public void deleterDispose(Bundle bundle) { + if (mDeleter != null) mDeleter.dispose(bundle); + } + + public void deleterLoad(Bundle bundle, View view) { + if (mDeleter == null) + mDeleter = new Deleter(bundle, view, mContext, this, mDownloadManager, mIterator, mHandler); + } + + public void deleterResume() { + if (mDeleter != null) mDeleter.resume(); + } + + + class ViewHolderItem extends RecyclerView.ViewHolder { + DownloadManager.MissionItem item; + + TextView status; + ImageView icon; + TextView name; + TextView size; + ProgressDrawable progress; + + PopupMenu popupMenu; + MenuItem start; + MenuItem pause; + MenuItem open; + MenuItem queue; + MenuItem showError; + MenuItem delete; + MenuItem source; + MenuItem checksum; + + long lastTimeStamp = -1; + long lastDone = -1; + int lastCurrent = -1; + int state = 0; + + ViewHolderItem(View view) { + super(view); + + progress = new ProgressDrawable(); + ViewCompat.setBackground(itemView.findViewById(R.id.item_bkg), progress); + + status = itemView.findViewById(R.id.item_status); + name = itemView.findViewById(R.id.item_name); + icon = itemView.findViewById(R.id.item_icon); + size = itemView.findViewById(R.id.item_size); + + name.setSelected(true); + + ImageView button = itemView.findViewById(R.id.item_more); + popupMenu = buildPopup(button); + button.setOnClickListener(v -> showPopupMenu()); + + Menu menu = popupMenu.getMenu(); + start = menu.findItem(R.id.start); + pause = menu.findItem(R.id.pause); + open = menu.findItem(R.id.open); + queue = menu.findItem(R.id.queue); + showError = menu.findItem(R.id.error_message_view); + delete = menu.findItem(R.id.delete); + source = menu.findItem(R.id.source); + checksum = menu.findItem(R.id.checksum); + + //h.itemView.setOnClickListener(v -> showDetail(h)); + } + + private void showPopupMenu() { + start.setVisible(false); + pause.setVisible(false); + open.setVisible(false); + queue.setVisible(false); + showError.setVisible(false); + delete.setVisible(false); + source.setVisible(false); + checksum.setVisible(false); + + DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null; + + if (mission != null) { + if (!mission.postprocessingRunning) { + if (mission.running) { + pause.setVisible(true); + } else { + if (mission.errCode != DownloadMission.ERROR_NOTHING) { + showError.setVisible(true); + } + + queue.setChecked(mission.enqueued); + + start.setVisible(mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED); + delete.setVisible(true); + queue.setVisible(true); + } + } + } else { + open.setVisible(true); + delete.setVisible(true); + checksum.setVisible(true); + } + + if (item.mission.source != null && !item.mission.source.isEmpty()) { + source.setVisible(true); + } + + popupMenu.show(); + } + + private PopupMenu buildPopup(final View button) { + PopupMenu popup = new PopupMenu(mContext, button); + popup.inflate(R.menu.mission); + popup.setOnMenuItemClickListener(option -> handlePopupItem(this, option)); + + return popup; + } + } + + class ViewHolderHeader extends RecyclerView.ViewHolder { + TextView header; + + ViewHolderHeader(View view) { + super(view); + header = itemView.findViewById(R.id.item_name); + } + } + + + static class ChecksumTask extends AsyncTask { + ProgressDialog progressDialog; + WeakReference weakReference; + + ChecksumTask(@NonNull Context context) { + weakReference = new WeakReference<>((Activity) context); } @Override @@ -378,10 +652,10 @@ public class MissionAdapter extends RecyclerView.Adapter items; + private boolean running = true; + + private Context mContext; + private MissionAdapter mAdapter; + private DownloadManager mDownloadManager; + private MissionIterator mIterator; + private Handler mHandler; + private View mView; + + private final Runnable rShow; + private final Runnable rNext; + private final Runnable rCommit; + + public Deleter(Bundle b, View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) { + mView = v; + mContext = c; + mAdapter = a; + mDownloadManager = d; + mIterator = i; + mHandler = h; + + // use variables to know the reference of the lambdas + rShow = this::show; + rNext = this::next; + rCommit = this::commit; + + items = new ArrayList<>(2); + + if (b != null) { + String[] names = b.getStringArray(BUNDLE_NAMES); + String[] locations = b.getStringArray(BUNDLE_LOCATIONS); + + if (names == null || locations == null) return; + if (names.length < 1 || locations.length < 1) return; + if (names.length != locations.length) return; + + items.ensureCapacity(names.length); + + for (int j = 0; j < locations.length; j++) { + Mission mission = mDownloadManager.getAnyMission(locations[j], names[j]); + if (mission == null) continue; + + items.add(mission); + mIterator.hide(mission); + } + + if (items.size() > 0) resume(); + } + } + + public void append(Mission item) { + mIterator.hide(item); + items.add(0, item); + + show(); + } + + private void forget() { + mIterator.unHide(items.remove(0)); + mAdapter.applyChanges(); + + show(); + } + + private void show() { + if (items.size() < 1) return; + + pause(); + running = true; + + mHandler.postDelayed(rNext, DELAY); + } + + private void next() { + if (items.size() < 1) return; + + String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).name); + + snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); + snackbar.setAction(R.string.undo, s -> forget()); + snackbar.setActionTextColor(Color.YELLOW); + snackbar.show(); + + mHandler.postDelayed(rCommit, TIMEOUT); + } + + private void commit() { + if (items.size() < 1) return; + + while (items.size() > 0) { + Mission mission = items.remove(0); + if (mission.deleted) continue; + + mIterator.unHide(mission); + mDownloadManager.deleteMission(mission); + break; + } + + if (items.size() < 1) { + pause(); + return; + } + + show(); + } + + private void pause() { + running = false; + mHandler.removeCallbacks(rNext); + mHandler.removeCallbacks(rShow); + mHandler.removeCallbacks(rCommit); + if (snackbar != null) snackbar.dismiss(); + } + + public void resume() { + if (running) return; + mHandler.postDelayed(rShow, (int) (DELAY * 1.5f));// 150% of the delay + } + + public void dispose(Bundle bundle) { + if (items.size() < 1) return; + + pause(); + + if (bundle == null) { + for (Mission mission : items) mDownloadManager.deleteMission(mission); + items = null; + return; + } + + String[] names = new String[items.size()]; + String[] locations = new String[items.size()]; + + for (int i = 0; i < items.size(); i++) { + Mission mission = items.get(i); + names[i] = mission.name; + locations[i] = mission.location; + } + + bundle.putStringArray(BUNDLE_NAMES, names); + bundle.putStringArray(BUNDLE_LOCATIONS, locations); + } +} \ No newline at end of file diff --git a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java index 955ce4c65..6ecc843a4 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java +++ b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java @@ -1,25 +1,36 @@ -package us.shandian.giga.ui.common; +package us.shandian.giga.ui.common;// TODO: ¡git it! -import android.content.Context; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; +import android.graphics.Path; import android.graphics.PixelFormat; +import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.support.annotation.ColorRes; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.ColorInt; import android.support.annotation.NonNull; -import android.support.v4.content.ContextCompat; public class ProgressDrawable extends Drawable { - private float mProgress; - private final int mBackgroundColor; - private final int mForegroundColor; + private static final int MARQUEE_INTERVAL = 150; - public ProgressDrawable(Context context, @ColorRes int background, @ColorRes int foreground) { - this(ContextCompat.getColor(context, background), ContextCompat.getColor(context, foreground)); + private float mProgress; + private int mBackgroundColor, mForegroundColor; + private Handler mMarqueeHandler; + private float mMarqueeProgress; + private Path mMarqueeLine; + private int mMarqueeSize; + private long mMarqueeNext; + + public ProgressDrawable() { + mMarqueeLine = null;// marquee disabled + mMarqueeProgress = 0f; + mMarqueeSize = 0; + mMarqueeNext = 0; } - public ProgressDrawable(int background, int foreground) { + public void setColors(@ColorInt int background, @ColorInt int foreground) { mBackgroundColor = background; mForegroundColor = foreground; } @@ -29,10 +40,20 @@ public class ProgressDrawable extends Drawable { invalidateSelf(); } + public void setMarquee(boolean marquee) { + if (marquee == (mMarqueeLine != null)) { + return; + } + mMarqueeLine = marquee ? new Path() : null; + mMarqueeHandler = marquee ? new Handler(Looper.getMainLooper()) : null; + mMarqueeSize = 0; + mMarqueeNext = 0; + } + @Override public void draw(@NonNull Canvas canvas) { - int width = canvas.getWidth(); - int height = canvas.getHeight(); + int width = getBounds().width(); + int height = getBounds().height(); Paint paint = new Paint(); @@ -40,6 +61,42 @@ public class ProgressDrawable extends Drawable { canvas.drawRect(0, 0, width, height, paint); paint.setColor(mForegroundColor); + + if (mMarqueeLine != null) { + if (mMarqueeSize < 1) setupMarquee(width, height); + + int size = mMarqueeSize; + Paint paint2 = new Paint(); + paint2.setColor(mForegroundColor); + paint2.setStrokeWidth(size); + paint2.setStyle(Paint.Style.STROKE); + + size *= 2; + + if (mMarqueeProgress >= size) { + mMarqueeProgress = 1; + } else { + mMarqueeProgress++; + } + + // render marquee + width += size * 2; + Path marquee = new Path(); + for (float i = -size; i < width; i += size) { + marquee.addPath(mMarqueeLine, i + mMarqueeProgress, 0); + } + marquee.close(); + + canvas.drawPath(marquee, paint2);// draw marquee + + if (System.currentTimeMillis() >= mMarqueeNext) { + // program next update + mMarqueeNext = System.currentTimeMillis() + MARQUEE_INTERVAL; + mMarqueeHandler.postDelayed(this::invalidateSelf, MARQUEE_INTERVAL); + } + return; + } + canvas.drawRect(0, 0, (int) (mProgress * width), height, paint); } @@ -58,4 +115,17 @@ public class ProgressDrawable extends Drawable { return PixelFormat.OPAQUE; } + @Override + public void onBoundsChange(Rect rect) { + if (mMarqueeLine != null) setupMarquee(rect.width(), rect.height()); + } + + private void setupMarquee(int width, int height) { + mMarqueeSize = (int) ((width * 10f) / 100f);// the size is 10% of the width + + mMarqueeLine.rewind(); + mMarqueeLine.moveTo(-mMarqueeSize, -mMarqueeSize); + mMarqueeLine.lineTo(-mMarqueeSize * 4, height + mMarqueeSize); + mMarqueeLine.close(); + } } diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 5241415b2..c3a60f6d0 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -10,8 +10,6 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -23,39 +21,47 @@ import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.R; -import org.schabi.newpipe.download.DeleteDownloadManager; -import io.reactivex.disposables.Disposable; -import us.shandian.giga.get.DownloadManager; +import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; +import us.shandian.giga.service.DownloadManagerService.DMBinder; import us.shandian.giga.ui.adapter.MissionAdapter; -public abstract class MissionsFragment extends Fragment { - private DownloadManager mDownloadManager; - private DownloadManagerService.DMBinder mBinder; +public class MissionsFragment extends Fragment { + + private static final int SPAN_SIZE = 2; private SharedPreferences mPrefs; private boolean mLinear; private MenuItem mSwitch; + private MenuItem mClear; private RecyclerView mList; + private View mEmpty; private MissionAdapter mAdapter; private GridLayoutManager mGridManager; private LinearLayoutManager mLinearManager; private Context mActivity; - private DeleteDownloadManager mDeleteDownloadManager; - private Disposable mDeleteDisposable; + + private DMBinder mBinder; + private Bundle mBundle; + private boolean mForceUpdate; private final ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder binder) { mBinder = (DownloadManagerService.DMBinder) binder; - mDownloadManager = setupDownloadManager(mBinder); - if (mDeleteDownloadManager != null) { - mDeleteDownloadManager.setDownloadManager(mDownloadManager); - updateList(); - } + mBinder.resetFinishedDownloadCount(); + + mAdapter = new MissionAdapter(mActivity, mBinder.getDownloadManager(), mClear, mEmpty); + mAdapter.deleterLoad(mBundle, getView()); + + mBundle = null; + + mBinder.addMissionEventListener(mAdapter.getMessenger()); + + updateList(); } @Override @@ -66,14 +72,6 @@ public abstract class MissionsFragment extends Fragment { }; - public void setDeleteManager(@NonNull DeleteDownloadManager deleteDownloadManager) { - mDeleteDownloadManager = deleteDownloadManager; - if (mDownloadManager != null) { - mDeleteDownloadManager.setDownloadManager(mDownloadManager); - updateList(); - } - } - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.missions, container, false); @@ -81,24 +79,47 @@ public abstract class MissionsFragment extends Fragment { mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); mLinear = mPrefs.getBoolean("linear", false); + mActivity = getActivity(); + mBundle = savedInstanceState; + // Bind the service - Intent i = new Intent(); - i.setClass(getActivity(), DownloadManagerService.class); - getActivity().bindService(i, mConnection, Context.BIND_AUTO_CREATE); + mActivity.bindService(new Intent(mActivity, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE); // Views + mEmpty = v.findViewById(R.id.list_empty_view); mList = v.findViewById(R.id.mission_recycler); // Init - mGridManager = new GridLayoutManager(getActivity(), 2); + mGridManager = new GridLayoutManager(getActivity(), SPAN_SIZE); + mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + switch (mAdapter.getItemViewType(position)) { + case DownloadManager.SPECIAL_PENDING: + case DownloadManager.SPECIAL_FINISHED: + return SPAN_SIZE; + default: + return 1; + } + } + }); + mLinearManager = new LinearLayoutManager(getActivity()); - mList.setLayoutManager(mGridManager); setHasOptionsMenu(true); return v; } + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (menu != null) { + mSwitch = menu.findItem(R.id.switch_mode); + mClear = menu.findItem(R.id.clear_list); + } + } + /** * Added in API level 23. */ @@ -108,7 +129,7 @@ public abstract class MissionsFragment extends Fragment { // Bug: in api< 23 this is never called // so mActivity=null - // so app crashes with nullpointer exception + // so app crashes with null-pointer exception mActivity = activity; } @@ -119,71 +140,78 @@ public abstract class MissionsFragment extends Fragment { @Override public void onAttach(Activity activity) { super.onAttach(activity); - mActivity = activity; } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - if (mDeleteDownloadManager != null) { - mDeleteDisposable = mDeleteDownloadManager.getUndoObservable().subscribe(mission -> { - if (mAdapter != null) { - mAdapter.updateItemList(); - mAdapter.notifyDataSetChanged(); - } - }); + public void onDestroy() { + super.onDestroy(); + if (mBinder == null || mAdapter == null) return; + + mBinder.removeMissionEventListener(mAdapter.getMessenger()); + mActivity.unbindService(mConnection); + mAdapter.deleterDispose(null); + + mBinder = null; + mAdapter = null; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mAdapter != null) { + mAdapter.deleterDispose(outState); + mForceUpdate = true; + mBinder.removeMissionEventListener(mAdapter.getMessenger()); } } @Override - public void onDestroyView() { - super.onDestroyView(); - getActivity().unbindService(mConnection); - if (mDeleteDisposable != null) { - mDeleteDisposable.dispose(); - } - } + public void onResume() { + super.onResume(); + if (mAdapter != null) { + mAdapter.deleterResume(); - @Override - public void onPrepareOptionsMenu(Menu menu) { - mSwitch = menu.findItem(R.id.switch_mode); - super.onPrepareOptionsMenu(menu); + if (mForceUpdate) { + mForceUpdate = false; + mAdapter.forceUpdate(); + } + + mBinder.addMissionEventListener(mAdapter.getMessenger()); + } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.switch_mode: - mLinear = !mLinear; - updateList(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - public void notifyChange() { - mAdapter.notifyDataSetChanged(); + mLinear = !mLinear; + updateList(); + return true; + case R.id.clear_list: + mAdapter.clearFinishedDownloads(); + return true; + default: + return super.onOptionsItemSelected(item); + } } private void updateList() { - mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mDownloadManager, mDeleteDownloadManager, mLinear); - if (mLinear) { mList.setLayoutManager(mLinearManager); } else { mList.setLayoutManager(mGridManager); } + mList.setAdapter(null); + mAdapter.notifyDataSetChanged(); + mAdapter.setLinear(mLinear); mList.setAdapter(mAdapter); if (mSwitch != null) { mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list); + mSwitch.setTitle(mLinear ? R.string.grid : R.string.list); + mPrefs.edit().putBoolean("linear", mLinear).apply(); } - - mPrefs.edit().putBoolean("linear", mLinear).apply(); } - - protected abstract DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder); } diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index 163ac2b14..ac690be10 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -3,10 +3,11 @@ package us.shandian.giga.util; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.support.annotation.ColorRes; +import android.support.annotation.ColorInt; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; import android.widget.Toast; import org.schabi.newpipe.R; @@ -21,12 +22,14 @@ import java.io.ObjectOutputStream; import java.io.Serializable; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Locale; public class Utility { public enum FileType { VIDEO, MUSIC, + SUBTITLE, UNKNOWN } @@ -54,41 +57,32 @@ public class Utility { } } - public static void writeToFile(@NonNull String fileName, @NonNull Serializable serializable) { - ObjectOutputStream objectOutputStream = null; + public static void writeToFile(@NonNull File file, @NonNull Serializable serializable) { - try { - objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(fileName))); + try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) { objectOutputStream.writeObject(serializable); } catch (Exception e) { //nothing to do - } finally { - if(objectOutputStream != null) { - try { - objectOutputStream.close(); - } catch (Exception e) { - //nothing to do - } - } } + //nothing to do } @Nullable @SuppressWarnings("unchecked") - public static T readFromFile(String file) { - T object = null; + public static T readFromFile(File file) { + T object; ObjectInputStream objectInputStream = null; try { objectInputStream = new ObjectInputStream(new FileInputStream(file)); object = (T) objectInputStream.readObject(); } catch (Exception e) { - //nothing to do + object = null; } - if(objectInputStream != null){ + if (objectInputStream != null) { try { - objectInputStream .close(); + objectInputStream.close(); } catch (Exception e) { //nothing to do } @@ -119,39 +113,68 @@ public class Utility { } } - public static FileType getFileType(String file) { - if (file.endsWith(".mp3") || file.endsWith(".wav") || file.endsWith(".flac") || file.endsWith(".m4a")) { + public static FileType getFileType(char kind, String file) { + switch (kind) { + case 'v': + return FileType.VIDEO; + case 'a': + return FileType.MUSIC; + case 's': + return FileType.SUBTITLE; + //default '?': + } + + if (file.endsWith(".srt") || file.endsWith(".vtt") || file.endsWith(".ssa")) { + return FileType.SUBTITLE; + } else if (file.endsWith(".mp3") || file.endsWith(".wav") || file.endsWith(".flac") || file.endsWith(".m4a") || file.endsWith(".opus")) { return FileType.MUSIC; } else if (file.endsWith(".mp4") || file.endsWith(".mpeg") || file.endsWith(".rm") || file.endsWith(".rmvb") || file.endsWith(".flv") || file.endsWith(".webp") || file.endsWith(".webm")) { return FileType.VIDEO; - } else { - return FileType.UNKNOWN; } + + return FileType.UNKNOWN; } - @ColorRes - public static int getBackgroundForFileType(FileType type) { + @ColorInt + public static int getBackgroundForFileType(Context ctx, FileType type) { + int colorRes; switch (type) { case MUSIC: - return R.color.audio_left_to_load_color; + colorRes = R.color.audio_left_to_load_color; + break; case VIDEO: - return R.color.video_left_to_load_color; + colorRes = R.color.video_left_to_load_color; + break; + case SUBTITLE: + colorRes = R.color.subtitle_left_to_load_color; + break; default: - return R.color.gray; + colorRes = R.color.gray; } + + return ContextCompat.getColor(ctx, colorRes); } - @ColorRes - public static int getForegroundForFileType(FileType type) { + @ColorInt + public static int getForegroundForFileType(Context ctx, FileType type) { + int colorRes; switch (type) { case MUSIC: - return R.color.audio_already_load_color; + colorRes = R.color.audio_already_load_color; + break; case VIDEO: - return R.color.video_already_load_color; + colorRes = R.color.video_already_load_color; + break; + case SUBTITLE: + colorRes = R.color.subtitle_already_load_color; + break; default: - return R.color.gray; + colorRes = R.color.gray; + break; } + + return ContextCompat.getColor(ctx, colorRes); } @DrawableRes @@ -161,6 +184,8 @@ public class Utility { return R.drawable.music; case VIDEO: return R.drawable.video; + case SUBTITLE: + return R.drawable.subtitle; default: return R.drawable.video; } @@ -168,12 +193,18 @@ public class Utility { public static void copyToClipboard(Context context, String str) { ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + + if (cm == null) { + Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_LONG).show(); + return; + } + cm.setPrimaryClip(ClipData.newPlainText("text", str)); Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show(); } public static String checksum(String path, String algorithm) { - MessageDigest md = null; + MessageDigest md; try { md = MessageDigest.getInstance(algorithm); @@ -181,7 +212,7 @@ public class Utility { throw new RuntimeException(e); } - FileInputStream i = null; + FileInputStream i; try { i = new FileInputStream(path); @@ -190,14 +221,14 @@ public class Utility { } byte[] buf = new byte[1024]; - int len = 0; + int len; try { while ((len = i.read(buf)) != -1) { md.update(buf, 0, len); } - } catch (IOException ignored) { - + } catch (IOException e) { + // nothing to do } byte[] digest = md.digest(); @@ -211,4 +242,16 @@ public class Utility { return sb.toString(); } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static boolean mkdir(File path, boolean allDirs) { + if (path.exists()) return true; + + if (allDirs) + path.mkdirs(); + else + path.mkdir(); + + return path.exists(); + } } diff --git a/app/src/main/res/drawable-hdpi/grid.png b/app/src/main/res/drawable-hdpi/grid.png index 254f1d300ab07382ea3bf99f51c74e616fe46487..26fa36c070f8a64dd45b446f92cd34cc5b159fbb 100644 GIT binary patch delta 3278 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 pSh002ovPDHLkV1ilPy>I{k diff --git a/app/src/main/res/drawable-hdpi/list.png b/app/src/main/res/drawable-hdpi/list.png index 0b3f54c20a192167c1908651dd9357e0266885ef..16da863e2e21606532a4a0880c30ee609e142fe8 100644 GIT binary patch delta 3092 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 + + - + + android:scaleType="center" + android:contentDescription="TODO"/> @@ -51,8 +51,8 @@ android:layout_centerHorizontal="true" android:scaleType="fitXY" android:gravity="center" - android:padding="10dp" - android:contentDescription="TODO" /> + android:contentDescription="TODO" + android:padding="10dp"/> + android:textColor="@color/white" + android:singleLine="true" + android:ellipsize="marquee" + android:marqueeRepeatLimit="marquee_forever" + android:scrollHorizontally="true"/> - + + + - - + android:layout_height="match_parent" + /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/missions_header.xml b/app/src/main/res/layout/missions_header.xml new file mode 100644 index 000000000..99b7c6b1a --- /dev/null +++ b/app/src/main/res/layout/missions_header.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/download_menu.xml b/app/src/main/res/menu/download_menu.xml index e71eaf152..2d486d617 100644 --- a/app/src/main/res/menu/download_menu.xml +++ b/app/src/main/res/menu/download_menu.xml @@ -1,11 +1,25 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + + + + + + - - \ No newline at end of file diff --git a/app/src/main/res/menu/mission.xml b/app/src/main/res/menu/mission.xml index b76d1a923..4f7eba626 100644 --- a/app/src/main/res/menu/mission.xml +++ b/app/src/main/res/menu/mission.xml @@ -1,37 +1,50 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9c06e228b..c13fbe54f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -492,7 +492,7 @@ abrir en modo popup Minimizar al reproductor de fondo Minimizar el reproductor emergente -Avance rápido durante el silencio + Avance rápido durante el silencio Paso Reiniciar @@ -500,4 +500,54 @@ abrir en modo popup Usuarios Listas de reproducción Pistas + + Finalizadas + En cola + + pausado + en cola + post-procesado + + Encolar + + Acción denegada por el sistema + + Archivo borrado + + + Descarga finalizada: %s + %s descargas finalizadas + + + Generar nombre único + Sobrescribir + Ya existe un archivo descargado con este nombre + Hay una descarga en curso con este nombre + + Mostrar como grilla + Mostrar como lista + Limpiar descargas finalizadas + Tienes %s descargas pendientes, ve a Descargas para continuarlas + Detener + Intentos maximos + Cantidad máxima de intentos antes de cancelar la descarga + Pausar al cambiar a datos moviles + No todas las descargas se pueden suspender, en esos casos, se reiniciaran + + + + Mostrar error + Codigo + No se puede crear la carpeta de destino + No se puede crear el archivo + Permiso denegado por el sistema + Fallo la conexión segura + No se puede encontrar el servidor + No se puede conectar con el servidor + El servidor no devolvio datos + El servidor no acepta descargas multi-hilos, intente de nuevo con @string/msg_threads = 1 + Rango solicitado no satisfactorio + No encontrado + Fallo el post-procesado + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 515f1d46f..5741d1b4f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -63,6 +63,8 @@ #000000 #CD5656 #BC211D + #008ea4 + #005a71 #FFFFFF diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 133a679c9..7234a6639 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -174,6 +174,12 @@ @string/charset_most_special_characters_value + + downloads_max_retry + 3 + cross_network_downloads + + default_download_threads preferred_open_action_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac95d98a4..855c2d092 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,7 +16,7 @@ Download stream file Search Settings - Did you mean: %1$s\? + Did you mean: %1$s? Share with Choose browser rotation @@ -143,6 +143,7 @@ Resizing Best resolution Undo + File deleted Play All Always Just Once @@ -520,9 +521,55 @@ None Minimize to background player Minimize to popup player - List view mode + List view mode List Grid - Auto + Auto Switch View + + + Finished + In queue + + paused + queued + post-processing + + Queue + + Action denied by the system + + + Download finished: %s + %s downloads finished + + + Generate unique name + Overwrite + A downloaded file with this name already exists + There is a download in progress with this name + + + Show error + Code + The file can not be created + The destination folder can not be created + Permission denied by the system + Secure connection failed + Can not found the server + Can not connect to the server + The server does not send data + The server does not accept multi-threaded downloads, retry with @string/msg_threads = 1 + Requested Range Not Satisfiable + Not found + Post-processing failed + + Clear finished downloads + You have %s pending downloads, goto Downloads to continue + Stop + Maximum retry + Maximum number of attempts before canceling the download + Pause on switching to mobile data + Not all downloads can be suspended, in those cases, will be restarted + diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index 0a8768e9e..ed38acbb7 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -29,4 +29,17 @@ android:summary="@string/settings_file_replacement_character_summary" android:title="@string/settings_file_replacement_character_title"/> + + + + From eb1f56488f46597a936a8741a94fefbd7ab0178f Mon Sep 17 00:00:00 2001 From: kapodamy Date: Thu, 8 Nov 2018 19:03:30 -0300 Subject: [PATCH 26/68] resbase (08/11/2018) --- .../java/org/schabi/newpipe/Downloader.java | 6 +- .../download/DeleteDownloadManager.java | 163 -------- .../newpipe/download/DownloadActivity.java | 1 - .../newpipe/download/DownloadDialog.java | 33 +- .../fragments/detail/VideoDetailFragment.java | 3 + .../resolver/VideoPlaybackResolver.java | 2 +- .../newpipe/util/StreamItemAdapter.java | 6 +- .../shandian/giga/get/DownloadDataSource.java | 40 -- .../giga/get/DownloadManagerImpl.java | 395 ------------------ .../us/shandian/giga/get/DownloadMission.java | 2 +- .../shandian/giga/get/DownloadRunnable.java | 2 +- .../giga/get/DownloadRunnableFallback.java | 2 +- .../giga/service/DownloadManagerService.java | 1 + .../giga/ui/adapter/MissionAdapter.java | 2 +- .../us/shandian/giga/ui/common/Deleter.java | 3 +- .../giga/ui/common/ProgressDrawable.java | 2 +- .../giga/ui/fragment/AllMissionsFragment.java | 12 - .../giga/ui/fragment/MissionsFragment.java | 73 ++-- .../java/us/shandian/giga/util/Utility.java | 1 + app/src/main/res/layout/mission_item.xml | 18 +- app/src/main/res/layout/missions.xml | 10 +- app/src/main/res/layout/missions_header.xml | 2 +- app/src/main/res/menu/download_menu.xml | 24 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values/strings.xml | 6 +- .../giga/get/DownloadManagerImplTest.java | 186 --------- 26 files changed, 97 insertions(+), 900 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java delete mode 100644 app/src/main/java/us/shandian/giga/get/DownloadDataSource.java delete mode 100755 app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java delete mode 100644 app/src/main/java/us/shandian/giga/ui/fragment/AllMissionsFragment.java delete mode 100644 app/src/test/java/us/shandian/giga/get/DownloadManagerImplTest.java diff --git a/app/src/main/java/org/schabi/newpipe/Downloader.java b/app/src/main/java/org/schabi/newpipe/Downloader.java index 177f1f624..32e8bd414 100644 --- a/app/src/main/java/org/schabi/newpipe/Downloader.java +++ b/app/src/main/java/org/schabi/newpipe/Downloader.java @@ -105,13 +105,13 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { * but set the HTTP header field "Accept-Language" to the supplied string. * * @param siteUrl the URL of the text file to return the contents of - * @param localisation the language and country (usually a 2-character code) to set + * @param localization the language and country (usually a 2-character code) to set * @return the contents of the specified text file */ @Override - public String download(String siteUrl, Localization localisation) throws IOException, ReCaptchaException { + public String download(String siteUrl, Localization localization) throws IOException, ReCaptchaException { Map requestProperties = new HashMap<>(); - requestProperties.put("Accept-Language", localisation.getLanguage()); + requestProperties.put("Accept-Language", localization.getLanguage()); return download(siteUrl, requestProperties); } diff --git a/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java b/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java deleted file mode 100644 index 2f539e343..000000000 --- a/app/src/main/java/org/schabi/newpipe/download/DeleteDownloadManager.java +++ /dev/null @@ -1,163 +0,0 @@ -package org.schabi.newpipe.download; - -import android.app.Activity; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.BaseTransientBottomBar; -import android.support.design.widget.Snackbar; -import android.view.View; - -import org.schabi.newpipe.R; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import io.reactivex.subjects.PublishSubject; -import us.shandian.giga.get.DownloadManager; -import us.shandian.giga.get.DownloadMission; - -public class DeleteDownloadManager { - - private static final String KEY_STATE = "delete_manager_state"; - - private View mView; - private ArrayList mPendingMap; - private List mDisposableList; - private DownloadManager mDownloadManager; - private final PublishSubject publishSubject = PublishSubject.create(); - - DeleteDownloadManager(Activity activity) { - mPendingMap = new ArrayList<>(); - mDisposableList = new ArrayList<>(); - mView = activity.findViewById(android.R.id.content); - } - - public Observable getUndoObservable() { - return publishSubject; - } - - public boolean contains(@NonNull DownloadMission mission) { - return mPendingMap.contains(mission.timestamp); - } - - public void add(@NonNull DownloadMission mission) { - mPendingMap.add(mission.timestamp); - - if (mPendingMap.size() == 1) { - showUndoDeleteSnackbar(mission); - } - } - - public void setDownloadManager(@NonNull DownloadManager downloadManager) { - mDownloadManager = downloadManager; - - if (mPendingMap.size() < 1) return; - - showUndoDeleteSnackbar(); - } - - public void restoreState(@Nullable Bundle savedInstanceState) { - if (savedInstanceState == null) return; - - long[] list = savedInstanceState.getLongArray(KEY_STATE); - if (list != null) { - mPendingMap.ensureCapacity(mPendingMap.size() + list.length); - for (long timestamp : list) mPendingMap.add(timestamp); - } - } - - public void saveState(@Nullable Bundle outState) { - if (outState == null) return; - - for (Disposable disposable : mDisposableList) { - disposable.dispose(); - } - - long[] list = new long[mPendingMap.size()]; - for (int i = 0; i < mPendingMap.size(); i++) list[i] = mPendingMap.get(i); - - outState.putLongArray(KEY_STATE, list); - } - - private void showUndoDeleteSnackbar() { - if (mPendingMap.size() < 1) return; - - long timestamp = mPendingMap.iterator().next(); - - for (int i = 0; i < mDownloadManager.getCount(); i++) { - DownloadMission mission = mDownloadManager.getMission(i); - if (timestamp == mission.timestamp) { - showUndoDeleteSnackbar(mission); - break; - } - } - } - - private void showUndoDeleteSnackbar(@NonNull DownloadMission mission) { - final Snackbar snackbar = Snackbar.make(mView, mission.name, Snackbar.LENGTH_INDEFINITE); - final Disposable disposable = Observable.timer(3, TimeUnit.SECONDS) - .subscribeOn(AndroidSchedulers.mainThread()) - .subscribe(l -> snackbar.dismiss()); - - mDisposableList.add(disposable); - - snackbar.setAction(R.string.undo, v -> { - mPendingMap.remove(mission.timestamp); - publishSubject.onNext(mission); - disposable.dispose(); - snackbar.dismiss(); - }); - - snackbar.addCallback(new BaseTransientBottomBar.BaseCallback() { - @Override - public void onDismissed(Snackbar transientBottomBar, int event) { - // TODO: disposable.isDisposed() is always true. fix this - if (!disposable.isDisposed()) { - Completable.fromAction(() -> deletePending(mission)) - .subscribeOn(Schedulers.io()) - .subscribe(); - } - mPendingMap.remove(mission.timestamp); - snackbar.removeCallback(this); - mDisposableList.remove(disposable); - showUndoDeleteSnackbar(); - } - }); - - snackbar.show(); - } - - public void deletePending() { - if (mPendingMap.size() < 1) return; - - HashSet idSet = new HashSet<>(); - for (int i = 0; i < mDownloadManager.getCount(); i++) { - if (contains(mDownloadManager.getMission(i))) { - idSet.add(i); - } - } - - for (Integer id : idSet) { - mDownloadManager.deleteMission(id); - } - - mPendingMap.clear(); - } - - private void deletePending(@NonNull DownloadMission mission) { - for (int i = 0; i < mDownloadManager.getCount(); i++) { - if (mission.timestamp == mDownloadManager.getMission(i).timestamp) { - mDownloadManager.deleteMission(i); - break; - } - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index 29940f802..251e4c730 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -24,7 +24,6 @@ public class DownloadActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { - // Service Intent i = new Intent(); i.setClass(this, DownloadManagerService.class); diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index aab6da1a4..d68db11e5 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -55,20 +55,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private static final String TAG = "DialogFragment"; private static final boolean DEBUG = MainActivity.DEBUG; - @State - protected StreamInfo currentInfo; - @State - protected StreamSizeWrapper wrappedAudioStreams = StreamSizeWrapper.empty(); - @State - protected StreamSizeWrapper wrappedVideoStreams = StreamSizeWrapper.empty(); - @State - protected StreamSizeWrapper wrappedSubtitleStreams = StreamSizeWrapper.empty(); - @State - protected int selectedVideoIndex = 0; - @State - protected int selectedAudioIndex = 0; - @State - protected int selectedSubtitleIndex = 0; + @State protected StreamInfo currentInfo; + @State protected StreamSizeWrapper wrappedAudioStreams = StreamSizeWrapper.empty(); + @State protected StreamSizeWrapper wrappedVideoStreams = StreamSizeWrapper.empty(); + @State protected StreamSizeWrapper wrappedSubtitleStreams = StreamSizeWrapper.empty(); + @State protected int selectedVideoIndex = 0; + @State protected int selectedAudioIndex = 0; + @State protected int selectedSubtitleIndex = 0; private StreamItemAdapter audioStreamsAdapter; private StreamItemAdapter videoStreamsAdapter; @@ -151,8 +144,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (DEBUG) - Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { getDialog().dismiss(); return; @@ -168,8 +160,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - if (DEBUG) - Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); + if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); return inflater.inflate(R.layout.download_dialog, container); } @@ -302,8 +293,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck @Override public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) { - if (DEBUG) - Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]"); + if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]"); boolean flag = true; switch (checkedId) { @@ -328,8 +318,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (DEBUG) - Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); + if (DEBUG) Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); switch (radioVideoAudioGroup.getCheckedRadioButtonId()) { case R.id.audio_button: selectedAudioIndex = position; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index ea5300a2e..c7c668f40 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -572,6 +572,9 @@ public class VideoDetailFragment .show(getFragmentManager(), TAG); } break; + case 3: + shareUrl(item.getName(), item.getUrl()); + break; default: break; } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java index 84eeedead..ad2b79523 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -10,10 +10,10 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.util.ListHelper; diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java index 5ee04ef76..6a1e80fea 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -97,7 +97,7 @@ public class StreamItemAdapter extends BaseAdapter { } else if (((VideoStream) stream).isVideoOnly()) { switch (stream.getFormat()) { case WEBM:// fully supported - case MPEG_4:// ¿is DASH MPEG-4? + case MPEG_4:// ¿is DASH MPEG-4 format? woSoundIconVisibility = View.INVISIBLE; break; default: @@ -143,7 +143,7 @@ public class StreamItemAdapter extends BaseAdapter { public static class StreamSizeWrapper implements Serializable { private static final StreamSizeWrapper EMPTY = new StreamSizeWrapper<>(Collections.emptyList(), null); private final List streamsList; - private long[] streamSizes; + private final long[] streamSizes; private final String unknownSize; public StreamSizeWrapper(List streamsList, Context context) { @@ -221,4 +221,4 @@ public class StreamItemAdapter extends BaseAdapter { return (StreamSizeWrapper) EMPTY; } } -} \ No newline at end of file +} diff --git a/app/src/main/java/us/shandian/giga/get/DownloadDataSource.java b/app/src/main/java/us/shandian/giga/get/DownloadDataSource.java deleted file mode 100644 index 2a8a9e129..000000000 --- a/app/src/main/java/us/shandian/giga/get/DownloadDataSource.java +++ /dev/null @@ -1,40 +0,0 @@ -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 download 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/DownloadManagerImpl.java b/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java deleted file mode 100755 index a377d861c..000000000 --- a/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java +++ /dev/null @@ -1,395 +0,0 @@ -package us.shandian.giga.get; - -import android.content.Context; -import android.content.Intent; -import android.os.Handler; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.Log; - -import org.schabi.newpipe.download.ExtSDDownloadFailedActivity; - -import java.io.File; -import java.io.FilenameFilter; -import java.io.IOException; -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; - -public class DownloadManagerImpl implements DownloadManager { - private static final String TAG = DownloadManagerImpl.class.getSimpleName(); - private final DownloadDataSource mDownloadDataSource; - - private final ArrayList mMissions = new ArrayList<>(); - @NonNull - private final Context context; - - /** - * 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; - this.context = null; - loadMissions(searchLocations); - } - - public DownloadManagerImpl(Collection searchLocations, DownloadDataSource downloadDataSource, Context context) { - mDownloadDataSource = downloadDataSource; - this.context = context; - loadMissions(searchLocations); - } - - @Override - 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; - mission.addListener(new MissionListener(mission)); - new Initializer(mission).start(); - return insertMission(mission); - } - - @Override - public void resumeMission(int i) { - DownloadMission d = getMission(i); - if (!d.running && d.errCode == -1) { - d.start(); - } - } - - @Override - public void pauseMission(int i) { - DownloadMission d = getMission(i); - if (d.running) { - d.pause(); - } - } - - @Override - public void deleteMission(int i) { - DownloadMission mission = getMission(i); - if (mission.finished) { - mDownloadDataSource.deleteMission(mission); - } - mission.delete(); - mMissions.remove(i); - } - - private void loadMissions(Iterable searchLocations) { - mMissions.clear(); - loadFinishedMissions(); - for (String location : searchLocations) { - loadMissions(location); - } - - } - - /** - * Sort a list of mission by its timestamp. Oldest first - * @param missions the missions to sort - */ - static void sortByTimestamp(List missions) { - Collections.sort(missions, new Comparator() { - @Override - public int compare(DownloadMission o1, DownloadMission o2) { - return Long.compare(o1.timestamp, o2.timestamp); - } - }); - } - - /** - * Loads finished missions from the data source - */ - private void loadFinishedMissions() { - List finishedMissions = mDownloadDataSource.loadMissions(); - if (finishedMissions == null) { - finishedMissions = new ArrayList<>(); - } - // Ensure its sorted - sortByTimestamp(finishedMissions); - - 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.isFile() && sub.getName().endsWith(".giga")) { - DownloadMission mis = Utility.readFromFile(sub.getAbsolutePath()); - if (mis != null) { - if (mis.finished) { - if (!sub.delete()) { - Log.w(TAG, "Unable to delete .giga file: " + sub.getPath()); - } - continue; - } - - mis.running = false; - mis.recovered = true; - insertMission(mis); - } - } - } - } - } - - @Override - public DownloadMission getMission(int i) { - return mMissions.get(i); - } - - @Override - public int getCount() { - return mMissions.size(); - } - - private int insertMission(DownloadMission mission) { - int i = -1; - - DownloadMission m = null; - - if (mMissions.size() > 0) { - do { - m = mMissions.get(++i); - } while (m.timestamp > mission.timestamp && i < mMissions.size() - 1); - - //if (i > 0) i--; - } else { - i = 0; - } - - mMissions.add(i, mission); - - return i; - } - - /** - * Get a mission by its location and name - * - * @param location the location - * @param name the name - * @return the mission or null if no such mission exists - */ - private - @Nullable - DownloadMission getMissionByLocation(String location, String name) { - for (DownloadMission mission : mMissions) { - if (location.equals(mission.location) && name.equals(mission.name)) { - return mission; - } - } - return null; - } - - /** - * Splits the filename into name and extension - *

- * Dots are ignored if they appear: not at all, at the beginning of the file, - * at the end of the file - * - * @param name the name to split - * @return a string array with a length of 2 containing the name and the extension - */ - private static String[] splitName(String name) { - int dotIndex = name.lastIndexOf('.'); - if (dotIndex <= 0 || (dotIndex == name.length() - 1)) { - return new String[]{name, ""}; - } else { - return new String[]{name.substring(0, dotIndex), name.substring(dotIndex + 1)}; - } - } - - /** - * Generates a unique file name. - *

- * e.g. "myname (1).txt" if the name "myname.txt" exists. - * - * @param location the location (to check for existing files) - * @param name the name of the file - * @return the unique file name - * @throws IllegalArgumentException if the location is not a directory - * @throws SecurityException if the location is not readable - */ - private static String generateUniqueName(String location, String name) { - if (location == null) throw new NullPointerException("location is null"); - if (name == null) throw new NullPointerException("name is null"); - File destination = new File(location); - if (!destination.isDirectory()) { - throw new IllegalArgumentException("location is not a directory: " + location); - } - final String[] nameParts = splitName(name); - String[] existingName = destination.list(new FilenameFilter() { - @Override - 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 final DownloadMission mission; - private final Handler handler; - - public Initializer(DownloadMission mission) { - this.mission = mission; - this.handler = new Handler(); - } - - @Override - public void run() { - try { - URL url = new URL(mission.url); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - mission.length = conn.getContentLength(); - - if (mission.length <= 0) { - mission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED; - //mission.notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED); - return; - } - - // Open again - conn = (HttpURLConnection) url.openConnection(); - conn.setRequestProperty("Range", "bytes=" + (mission.length - 10) + "-" + mission.length); - - if (conn.getResponseCode() != 206) { - // Fallback to single thread if no partial content support - mission.fallback = true; - - if (DEBUG) { - Log.d(TAG, "falling back"); - } - } - - if (DEBUG) { - Log.d(TAG, "response = " + conn.getResponseCode()); - } - - mission.blocks = mission.length / BLOCK_SIZE; - - if (mission.threadCount > mission.blocks) { - mission.threadCount = (int) mission.blocks; - } - - if (mission.threadCount <= 0) { - mission.threadCount = 1; - } - - if (mission.blocks * BLOCK_SIZE < mission.length) { - mission.blocks++; - } - - - new File(mission.location).mkdirs(); - new File(mission.location + "/" + mission.name).createNewFile(); - RandomAccessFile af = new RandomAccessFile(mission.location + "/" + mission.name, "rw"); - af.setLength(mission.length); - af.close(); - - mission.start(); - } catch (IOException ie) { - if(context == null) throw new RuntimeException(ie); - - if(ie.getMessage().contains("Permission denied")) { - handler.post(() -> - context.startActivity(new Intent(context, ExtSDDownloadFailedActivity.class))); - } else throw new RuntimeException(ie); - } catch (Exception e) { - // TODO Notify - throw new RuntimeException(e); - } - } - } - - /** - * 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 73df11ecb..d27046c76 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -24,7 +24,7 @@ import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; public class DownloadMission extends Mission { - private static final long serialVersionUID = 3L;// last bump: 16 october 2018 + private static final long serialVersionUID = 3L;// last bump: 8 november 2018 static final int BUFFER_SIZE = 64 * 1024; final static int BLOCK_SIZE = 512 * 1024; 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 ad2fa7113..b6617cfa4 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -146,7 +146,7 @@ public class DownloadRunnable implements Runnable { try { f.close(); } catch (Exception err) { - // ¿ejected media storage? ¿file deleted? ¿storage ran out of space? + // ¿ejected media storage? ¿file deleted? ¿storage ran out of space? } try { 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 a7c48c170..c484f5158 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -14,7 +14,7 @@ import static org.schabi.newpipe.BuildConfig.DEBUG; // Single-threaded fallback mode public class DownloadRunnableFallback implements Runnable { - private static final String TAG = "DownloadRunnableFallbac"; + private static final String TAG = "DownloadRunnableFallback"; private final DownloadMission mMission; private int retryCount = 0; 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 797fb1c1d..bddc41718 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -73,6 +73,7 @@ public class DownloadManagerService extends Service { private StringBuilder downloadDoneList = null; NotificationManager notificationManager = null; private boolean mForeground = false; + private final ArrayList mEchoObservers = new ArrayList<>(1); private BroadcastReceiver mNetworkStateListener; 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 c40c215b8..3e6a58415 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 @@ -68,8 +68,8 @@ public class MissionAdapter extends RecyclerView.Adapter { private Deleter mDeleter; private int mLayout; private DownloadManager.MissionIterator mIterator; - private Handler mHandler; private ArrayList mPendingDownloadsItems = new ArrayList<>(); + private Handler mHandler; private MenuItem mClear; private View mEmptyMessage; diff --git a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java index 636c5bdd4..c56e1c703 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java +++ b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java @@ -19,6 +19,7 @@ import us.shandian.giga.ui.adapter.MissionAdapter; public class Deleter { private static final int TIMEOUT = 5000;// ms private static final int DELAY = 350;// ms + private static final int DELAY_RESUME = 400;// ms private static final String BUNDLE_NAMES = "us.shandian.giga.ui.common.deleter.names"; private static final String BUNDLE_LOCATIONS = "us.shandian.giga.ui.common.deleter.locations"; @@ -140,7 +141,7 @@ public class Deleter { public void resume() { if (running) return; - mHandler.postDelayed(rShow, (int) (DELAY * 1.5f));// 150% of the delay + mHandler.postDelayed(rShow, DELAY_RESUME); } public void dispose(Bundle bundle) { diff --git a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java index 6ecc843a4..33eba22eb 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java +++ b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java @@ -1,4 +1,4 @@ -package us.shandian.giga.ui.common;// TODO: ¡git it! +package us.shandian.giga.ui.common; import android.graphics.Canvas; import android.graphics.ColorFilter; diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/AllMissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/AllMissionsFragment.java deleted file mode 100644 index ec8d7fc22..000000000 --- a/app/src/main/java/us/shandian/giga/ui/fragment/AllMissionsFragment.java +++ /dev/null @@ -1,12 +0,0 @@ -package us.shandian.giga.ui.fragment; - -import us.shandian.giga.get.DownloadManager; -import us.shandian.giga.service.DownloadManagerService; - -public class AllMissionsFragment extends MissionsFragment { - - @Override - protected DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder) { - return binder.getDownloadManager(); - } -} diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index c3a60f6d0..00d7f9695 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -15,7 +15,6 @@ import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -47,7 +46,7 @@ public class MissionsFragment extends Fragment { private Bundle mBundle; private boolean mForceUpdate; - private final ServiceConnection mConnection = new ServiceConnection() { + private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder binder) { @@ -111,15 +110,6 @@ public class MissionsFragment extends Fragment { return v; } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - if (menu != null) { - mSwitch = menu.findItem(R.id.switch_mode); - mClear = menu.findItem(R.id.clear_list); - } - } - /** * Added in API level 23. */ @@ -129,7 +119,7 @@ public class MissionsFragment extends Fragment { // Bug: in api< 23 this is never called // so mActivity=null - // so app crashes with null-pointer exception + // so app crashes with nullpointer exception mActivity = activity; } @@ -140,9 +130,11 @@ public class MissionsFragment extends Fragment { @Override public void onAttach(Activity activity) { super.onAttach(activity); + mActivity = activity; } + @Override public void onDestroy() { super.onDestroy(); @@ -157,28 +149,10 @@ public class MissionsFragment extends Fragment { } @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (mAdapter != null) { - mAdapter.deleterDispose(outState); - mForceUpdate = true; - mBinder.removeMissionEventListener(mAdapter.getMessenger()); - } - } - - @Override - public void onResume() { - super.onResume(); - if (mAdapter != null) { - mAdapter.deleterResume(); - - if (mForceUpdate) { - mForceUpdate = false; - mAdapter.forceUpdate(); - } - - mBinder.addMissionEventListener(mAdapter.getMessenger()); - } + public void onPrepareOptionsMenu(Menu menu) { + mSwitch = menu.findItem(R.id.switch_mode); + mClear = menu.findItem(R.id.clear_list); + super.onPrepareOptionsMenu(menu); } @Override @@ -203,8 +177,11 @@ public class MissionsFragment extends Fragment { mList.setLayoutManager(mGridManager); } + // destroy all created views in the recycler mList.setAdapter(null); mAdapter.notifyDataSetChanged(); + + // re-attach the adapter in grid/lineal mode mAdapter.setLinear(mLinear); mList.setAdapter(mAdapter); @@ -214,4 +191,32 @@ public class MissionsFragment extends Fragment { mPrefs.edit().putBoolean("linear", mLinear).apply(); } } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + if (mAdapter != null) { + mAdapter.deleterDispose(outState); + mForceUpdate = true; + mBinder.removeMissionEventListener(mAdapter.getMessenger()); + + } + } + + @Override + public void onResume() { + super.onResume(); + + if (mAdapter != null) { + mAdapter.deleterResume(); + + if (mForceUpdate) { + mForceUpdate = false; + mAdapter.forceUpdate(); + } + + mBinder.addMissionEventListener(mAdapter.getMessenger()); + } + } } diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index ac690be10..6cd5ef2c5 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -13,6 +13,7 @@ import android.widget.Toast; import org.schabi.newpipe.R; import java.io.BufferedOutputStream; +import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; diff --git a/app/src/main/res/layout/mission_item.xml b/app/src/main/res/layout/mission_item.xml index 6906dd17f..45e4d44e2 100644 --- a/app/src/main/res/layout/mission_item.xml +++ b/app/src/main/res/layout/mission_item.xml @@ -2,7 +2,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="wrap_content" android:layout_width="match_parent"> - + + android:contentDescription="TODO" /> @@ -51,8 +51,8 @@ android:layout_centerHorizontal="true" android:scaleType="fitXY" android:gravity="center" - android:contentDescription="TODO" - android:padding="10dp"/> + android:padding="10dp" + android:contentDescription="TODO" /> + android:scrollHorizontally="true" + android:text="XXX.xx" + android:textSize="16sp" + android:textStyle="bold" + android:textColor="@color/white"/> @@ -11,7 +12,6 @@ - - \ No newline at end of file + android:layout_height="match_parent"/> + + diff --git a/app/src/main/res/layout/missions_header.xml b/app/src/main/res/layout/missions_header.xml index 99b7c6b1a..f5226e3dd 100644 --- a/app/src/main/res/layout/missions_header.xml +++ b/app/src/main/res/layout/missions_header.xml @@ -27,4 +27,4 @@ android:layout_height="2dp" android:background="@color/black_settings_accent_color" /> - \ No newline at end of file + diff --git a/app/src/main/res/menu/download_menu.xml b/app/src/main/res/menu/download_menu.xml index 2d486d617..e79367135 100644 --- a/app/src/main/res/menu/download_menu.xml +++ b/app/src/main/res/menu/download_menu.xml @@ -1,25 +1,19 @@

+ xmlns:app="http://schemas.android.com/apk/res-auto"> - - + - - - - \ No newline at end of file + app:showAsAction="ifRoom" + android:title="@string/clear_finished_download"/> + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c13fbe54f..debf4a112 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -492,7 +492,7 @@ abrir en modo popup
Minimizar al reproductor de fondo Minimizar el reproductor emergente - Avance rápido durante el silencio +Avance rápido durante el silencio Paso Reiniciar diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 855c2d092..04656aefa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,7 +16,7 @@ Download stream file Search Settings - Did you mean: %1$s? + Did you mean: %1$s\? Share with Choose browser rotation @@ -521,10 +521,10 @@ None Minimize to background player Minimize to popup player - List view mode + List view mode List Grid - Auto + Auto Switch View diff --git a/app/src/test/java/us/shandian/giga/get/DownloadManagerImplTest.java b/app/src/test/java/us/shandian/giga/get/DownloadManagerImplTest.java deleted file mode 100644 index c755ba2e9..000000000 --- a/app/src/test/java/us/shandian/giga/get/DownloadManagerImplTest.java +++ /dev/null @@ -1,186 +0,0 @@ -package us.shandian.giga.get; - -import org.junit.Ignore; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.util.ArrayList; - -import us.shandian.giga.get.DownloadDataSource; -import us.shandian.giga.get.DownloadManagerImpl; -import us.shandian.giga.get.DownloadMission; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Test for {@link DownloadManagerImpl} - * - * TODO: test loading from .giga files, startMission and improve tests - */ -public class DownloadManagerImplTest { - - private DownloadManagerImpl downloadManager; - private DownloadDataSource downloadDataSource; - private ArrayList missions; - - @org.junit.Before - public void setUp() throws Exception { - downloadDataSource = mock(DownloadDataSource.class); - missions = new ArrayList<>(); - for(int i = 0; i < 50; ++i){ - missions.add(generateFinishedDownloadMission()); - } - when(downloadDataSource.loadMissions()).thenReturn(new ArrayList<>(missions)); - downloadManager = new DownloadManagerImpl(new ArrayList<>(), downloadDataSource); - } - - @Test(expected = NullPointerException.class) - public void testConstructorWithNullAsDownloadDataSource() { - new DownloadManagerImpl(new ArrayList<>(), null); - } - - - private static DownloadMission generateFinishedDownloadMission() throws IOException { - File file = File.createTempFile("newpipetest", ".mp4"); - file.deleteOnExit(); - RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); - randomAccessFile.setLength(1000); - randomAccessFile.close(); - DownloadMission downloadMission = new DownloadMission(file.getName(), - "http://google.com/?q=how+to+google", file.getParent()); - downloadMission.blocks = 1000; - downloadMission.done = 1000; - downloadMission.finished = true; - return spy(downloadMission); - } - - private static void assertMissionEquals(String message, DownloadMission expected, DownloadMission actual) { - if(expected == actual) return; - assertEquals(message + ": Name", expected.name, actual.name); - assertEquals(message + ": Location", expected.location, actual.location); - assertEquals(message + ": Url", expected.url, actual.url); - } - - @Test - public void testThatMissionsAreLoaded() throws IOException { - ArrayList missions = new ArrayList<>(); - long millis = System.currentTimeMillis(); - for(int i = 0; i < 50; ++i){ - DownloadMission mission = generateFinishedDownloadMission(); - mission.timestamp = millis - i; // reverse order by timestamp - missions.add(mission); - } - - downloadDataSource = mock(DownloadDataSource.class); - when(downloadDataSource.loadMissions()).thenReturn(new ArrayList<>(missions)); - downloadManager = new DownloadManagerImpl(new ArrayList<>(), downloadDataSource); - verify(downloadDataSource, times(1)).loadMissions(); - - assertEquals(50, downloadManager.getCount()); - - for(int i = 0; i < 50; ++i) { - assertMissionEquals("mission " + i, missions.get(50 - 1 - i), downloadManager.getMission(i)); - } - } - - @Ignore - @Test - public void startMission() throws Exception { - DownloadMission mission = missions.get(0); - mission = spy(mission); - missions.set(0, mission); - String url = "https://github.com/favicon.ico"; - // create a temp file and delete it so we have a temp directory - File tempFile = File.createTempFile("favicon",".ico"); - String name = tempFile.getName(); - String location = tempFile.getParent(); - assertTrue(tempFile.delete()); - int id = downloadManager.startMission(url, location, name, true, 10); - } - - @Test - public void resumeMission() { - DownloadMission mission = missions.get(0); - mission.running = true; - verify(mission, never()).start(); - downloadManager.resumeMission(0); - verify(mission, never()).start(); - mission.running = false; - downloadManager.resumeMission(0); - verify(mission, times(1)).start(); - } - - @Test - public void pauseMission() { - DownloadMission mission = missions.get(0); - mission.running = false; - downloadManager.pauseMission(0); - verify(mission, never()).pause(); - mission.running = true; - downloadManager.pauseMission(0); - verify(mission, times(1)).pause(); - } - - @Test - public void deleteMission() { - DownloadMission mission = missions.get(0); - assertEquals(mission, downloadManager.getMission(0)); - downloadManager.deleteMission(0); - verify(mission, times(1)).delete(); - assertNotEquals(mission, downloadManager.getMission(0)); - assertEquals(49, downloadManager.getCount()); - } - - @Test(expected = RuntimeException.class) - public void getMissionWithNegativeIndex() { - downloadManager.getMission(-1); - } - - @Test - public void getMission() { - assertSame(missions.get(0), downloadManager.getMission(0)); - assertSame(missions.get(1), downloadManager.getMission(1)); - } - - @Test - public void sortByTimestamp() { - ArrayList downloadMissions = new ArrayList<>(); - DownloadMission mission = new DownloadMission(); - mission.timestamp = 0; - - DownloadMission mission1 = new DownloadMission(); - mission1.timestamp = Integer.MAX_VALUE + 1L; - - DownloadMission mission2 = new DownloadMission(); - mission2.timestamp = 2L * Integer.MAX_VALUE ; - - DownloadMission mission3 = new DownloadMission(); - mission3.timestamp = 2L * Integer.MAX_VALUE + 5L; - - - downloadMissions.add(mission3); - downloadMissions.add(mission1); - downloadMissions.add(mission2); - downloadMissions.add(mission); - - - DownloadManagerImpl.sortByTimestamp(downloadMissions); - - assertEquals(mission, downloadMissions.get(0)); - assertEquals(mission1, downloadMissions.get(1)); - assertEquals(mission2, downloadMissions.get(2)); - assertEquals(mission3, downloadMissions.get(3)); - } - -} \ No newline at end of file From 47c3da131c9629ea24e5387a4b9b6f253ebf12cb Mon Sep 17 00:00:00 2001 From: shivanju Date: Sun, 11 Nov 2018 16:24:49 +0530 Subject: [PATCH 27/68] issue:1336 Fix for inserting new streams when auto queuing is enabled --- .../schabi/newpipe/player/helper/PlayerHelper.java | 10 ++++++++-- .../schabi/newpipe/player/playqueue/PlayQueue.java | 10 ++++++++-- .../newpipe/player/playqueue/PlayQueueItem.java | 11 ++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 16dffc3de..d55c81200 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -145,7 +145,7 @@ public class PlayerHelper { final StreamInfoItem nextVideo = info.getNextVideo(); if (nextVideo != null && !urls.contains(nextVideo.getUrl())) { - return new SinglePlayQueue(nextVideo); + return getAutoQueuedSinglePlayQueue(nextVideo); } final List relatedItems = info.getRelatedStreams(); @@ -158,7 +158,7 @@ public class PlayerHelper { } } Collections.shuffle(autoQueueItems); - return autoQueueItems.isEmpty() ? null : new SinglePlayQueue(autoQueueItems.get(0)); + return autoQueueItems.isEmpty() ? null : getAutoQueuedSinglePlayQueue(autoQueueItems.get(0)); } //////////////////////////////////////////////////////////////////////////// @@ -358,4 +358,10 @@ public class PlayerHelper { return getPreferences(context).getString(context.getString(R.string.minimize_on_exit_key), key); } + + private static SinglePlayQueue getAutoQueuedSinglePlayQueue(StreamInfoItem streamInfoItem) { + SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem); + singlePlayQueue.getItem().setAutoQueued(true); + return singlePlayQueue; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java index c9e07c96a..13a550f2e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java @@ -233,7 +233,11 @@ public abstract class PlayQueue implements Serializable { backup.addAll(itemList); Collections.shuffle(itemList); } - streams.addAll(itemList); + if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued() && !itemList.get(0).isAutoQueued()) { + streams.addAll(streams.size() - 1, itemList); + } else { + streams.addAll(itemList); + } broadcast(new AppendEvent(itemList.size())); } @@ -314,7 +318,9 @@ public abstract class PlayQueue implements Serializable { queueIndex.incrementAndGet(); } - streams.add(target, streams.remove(source)); + PlayQueueItem playQueueItem = streams.remove(source); + playQueueItem.setAutoQueued(false); + streams.add(target, playQueueItem); broadcast(new MoveEvent(source, target)); } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java index 8cbc3ed1c..bd0218454 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java @@ -25,9 +25,10 @@ public class PlayQueueItem implements Serializable { @NonNull final private String uploader; @NonNull final private StreamType streamType; + private boolean isAutoQueued; + private long recoveryPosition; private Throwable error; - PlayQueueItem(@NonNull final StreamInfo info) { this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(), info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType()); @@ -105,6 +106,14 @@ public class PlayQueueItem implements Serializable { .doOnError(throwable -> error = throwable); } + public boolean isAutoQueued() { + return isAutoQueued; + } + + public void setAutoQueued(boolean autoQueued) { + isAutoQueued = autoQueued; + } + //////////////////////////////////////////////////////////////////////////// // Item States, keep external access out //////////////////////////////////////////////////////////////////////////// From 25d6806ebdf761c5b596b47ebc940480b00d970a Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Tue, 13 Nov 2018 17:27:47 +0100 Subject: [PATCH 28/68] set minSdk to 19 and deprecate old player --- app/build.gradle | 2 +- .../org/schabi/newpipe/RouterActivity.java | 6 +- .../fragments/detail/VideoDetailFragment.java | 26 +- .../newpipe/player/PopupVideoPlayer.java | 34 +- .../newpipe/player/helper/PlayerHelper.java | 8 - .../newpipe/player/old/PlayVideoActivity.java | 369 ------------------ .../schabi/newpipe/util/NavigationHelper.java | 21 - app/src/main/res/values-ar/strings.xml | 2 - app/src/main/res/values/settings_keys.xml | 1 - app/src/main/res/values/strings.xml | 2 - app/src/main/res/xml/video_audio_settings.xml | 6 - 11 files changed, 20 insertions(+), 457 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/player/old/PlayVideoActivity.java diff --git a/app/build.gradle b/app/build.gradle index b507fd860..b385015f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ android { defaultConfig { applicationId "org.schabi.newpipe" - minSdkVersion 15 + minSdkVersion 19 targetSdkVersion 28 versionCode 69 versionName "0.14.2" diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index e22e2f474..b8941670f 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -542,8 +542,7 @@ public class RouterActivity extends AppCompatActivity { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); - boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this); + boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);; PlayQueue playQueue; String playerChoice = choice.playerChoice; @@ -555,9 +554,6 @@ public class RouterActivity extends AppCompatActivity { } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); - } else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) { - NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info); - } else { playQueue = new SinglePlayQueue((StreamInfo) info); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 9ab40e81c..f5675dcb2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -73,7 +73,6 @@ import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayer; import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.old.PlayVideoActivity; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.ErrorActivity; @@ -921,7 +920,7 @@ public class VideoDetailFragment .getBoolean(this.getString(R.string.use_external_video_player_key), false)) { startOnExternalPlayer(activity, currentInfo, selectedVideoStream); } else { - openNormalPlayer(selectedVideoStream); + openNormalPlayer(); } } @@ -934,24 +933,13 @@ public class VideoDetailFragment } } - private void openNormalPlayer(VideoStream selectedVideoStream) { + private void openNormalPlayer() { Intent mIntent; - boolean useOldPlayer = PlayerHelper.isUsingOldPlayer(activity) || (Build.VERSION.SDK_INT < 16); - if (!useOldPlayer) { - // ExoPlayer - final PlayQueue playQueue = new SinglePlayQueue(currentInfo); - mIntent = NavigationHelper.getPlayerIntent(activity, - MainVideoPlayer.class, - playQueue, - getSelectedVideoStream().getResolution()); - } else { - // Internal Player - mIntent = new Intent(activity, PlayVideoActivity.class) - .putExtra(PlayVideoActivity.VIDEO_TITLE, currentInfo.getName()) - .putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.getUrl()) - .putExtra(PlayVideoActivity.VIDEO_URL, currentInfo.getUrl()) - .putExtra(PlayVideoActivity.START_POSITION, currentInfo.getStartPosition()); - } + final PlayQueue playQueue = new SinglePlayQueue(currentInfo); + mIntent = NavigationHelper.getPlayerIntent(activity, + MainVideoPlayer.class, + playQueue, + getSelectedVideoStream().getResolution()); startActivity(mIntent); } diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index a36a0576c..f5c731ed9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -68,7 +68,6 @@ import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.old.PlayVideoActivity; import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.ListHelper; @@ -80,7 +79,6 @@ import java.util.List; import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; -import static org.schabi.newpipe.player.helper.PlayerHelper.isUsingOldPlayer; import static org.schabi.newpipe.util.AnimationUtils.animateView; /** @@ -554,27 +552,17 @@ public final class PopupVideoPlayer extends Service { if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); setRecovery(); - Intent intent; - if (!isUsingOldPlayer(getApplicationContext())) { - intent = NavigationHelper.getPlayerIntent( - context, - MainVideoPlayer.class, - this.getPlayQueue(), - this.getRepeatMode(), - this.getPlaybackSpeed(), - this.getPlaybackPitch(), - this.getPlaybackSkipSilence(), - this.getPlaybackQuality() - ); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } else { - intent = new Intent(PopupVideoPlayer.this, PlayVideoActivity.class) - .putExtra(PlayVideoActivity.VIDEO_TITLE, getVideoTitle()) - .putExtra(PlayVideoActivity.STREAM_URL, getSelectedVideoStream().getUrl()) - .putExtra(PlayVideoActivity.VIDEO_URL, getVideoUrl()) - .putExtra(PlayVideoActivity.START_POSITION, Math.round(getPlayer().getCurrentPosition() / 1000f)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } + final Intent intent = NavigationHelper.getPlayerIntent( + context, + MainVideoPlayer.class, + this.getPlayQueue(), + this.getRepeatMode(), + this.getPlaybackSpeed(), + this.getPlaybackPitch(), + this.getPlaybackSkipSilence(), + this.getPlaybackQuality() + ); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); closePopup(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 16dffc3de..ac57666cc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -177,10 +177,6 @@ public class PlayerHelper { return isBrightnessGestureEnabled(context, true); } - public static boolean isUsingOldPlayer(@NonNull final Context context) { - return isUsingOldPlayer(context, false); - } - public static boolean isRememberingPopupDimensions(@NonNull final Context context) { return isRememberingPopupDimensions(context, true); } @@ -318,10 +314,6 @@ public class PlayerHelper { return getPreferences(context).getBoolean(context.getString(R.string.brightness_gesture_control_key), b); } - private static boolean isUsingOldPlayer(@NonNull final Context context, final boolean b) { - return getPreferences(context).getBoolean(context.getString(R.string.use_old_player_key), b); - } - private static boolean isRememberingPopupDimensions(@NonNull final Context context, final boolean b) { return getPreferences(context).getBoolean(context.getString(R.string.popup_remember_size_pos_key), b); } diff --git a/app/src/main/java/org/schabi/newpipe/player/old/PlayVideoActivity.java b/app/src/main/java/org/schabi/newpipe/player/old/PlayVideoActivity.java deleted file mode 100644 index 092f82aad..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/old/PlayVideoActivity.java +++ /dev/null @@ -1,369 +0,0 @@ -package org.schabi.newpipe.player.old; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.media.AudioManager; -import android.media.MediaPlayer; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.Display; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.MediaController; -import android.widget.ProgressBar; -import android.widget.VideoView; - -import org.schabi.newpipe.R; - -/* - * Copyright (C) Christian Schabesberger 2015 - * PlayVideoActivity.java is part of NewPipe. - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - -public class PlayVideoActivity extends AppCompatActivity { - - //// TODO: 11.09.15 add "choose stream" menu - - private static final String TAG = PlayVideoActivity.class.toString(); - public static final String VIDEO_URL = "video_url"; - public static final String STREAM_URL = "stream_url"; - public static final String VIDEO_TITLE = "video_title"; - private static final String POSITION = "position"; - public static final String START_POSITION = "start_position"; - - private static final long HIDING_DELAY = 3000; - - private String videoUrl = ""; - - private ActionBar actionBar; - private VideoView videoView; - private int position; - private MediaController mediaController; - private ProgressBar progressBar; - private View decorView; - private boolean uiIsHidden; - private static long lastUiShowTime; - private boolean isLandscape = true; - private boolean hasSoftKeys; - - private SharedPreferences prefs; - private static final String PREF_IS_LANDSCAPE = "is_landscape"; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_play_video); - setVolumeControlStream(AudioManager.STREAM_MUSIC); - - //set background arrow style - getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_arrow_back_white_24dp); - - isLandscape = checkIfLandscape(); - hasSoftKeys = checkIfHasSoftKeys(); - - actionBar = getSupportActionBar(); - assert actionBar != null; - actionBar.setDisplayHomeAsUpEnabled(true); - Intent intent = getIntent(); - if(mediaController == null) { - //prevents back button hiding media controller controls (after showing them) - //instead of exiting video - //see http://stackoverflow.com/questions/6051825 - //also solves https://github.com/theScrabi/NewPipe/issues/99 - mediaController = new MediaController(this) { - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - int keyCode = event.getKeyCode(); - final boolean uniqueDown = event.getRepeatCount() == 0 - && event.getAction() == KeyEvent.ACTION_DOWN; - if (keyCode == KeyEvent.KEYCODE_BACK) { - if (uniqueDown) - { - if (isShowing()) { - finish(); - } else { - hide(); - } - } - return true; - } - return super.dispatchKeyEvent(event); - } - }; - } - - position = intent.getIntExtra(START_POSITION, 0)*1000;//convert from seconds to milliseconds - - videoView = findViewById(R.id.video_view); - progressBar = findViewById(R.id.play_video_progress_bar); - try { - videoView.setMediaController(mediaController); - videoView.setVideoURI(Uri.parse(intent.getStringExtra(STREAM_URL))); - } catch (Exception e) { - e.printStackTrace(); - } - videoView.requestFocus(); - videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { - @Override - public void onPrepared(MediaPlayer mp) { - progressBar.setVisibility(View.GONE); - videoView.seekTo(position); - if (position <= 0) { - videoView.start(); - showUi(); - } else { - videoView.pause(); - } - } - }); - videoUrl = intent.getStringExtra(VIDEO_URL); - - Button button = findViewById(R.id.content_button); - button.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if(uiIsHidden) { - showUi(); - } else { - hideUi(); - } - } - }); - decorView = getWindow().getDecorView(); - decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() { - @Override - public void onSystemUiVisibilityChange(int visibility) { - if (visibility == View.VISIBLE && uiIsHidden) { - showUi(); - } - } - }); - - if (android.os.Build.VERSION.SDK_INT >= 17) { - decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); - } - - prefs = getPreferences(Context.MODE_PRIVATE); - if(prefs.getBoolean(PREF_IS_LANDSCAPE, false) && !isLandscape) { - toggleOrientation(); - } - } - - @Override - public boolean onCreatePanelMenu(int featured, Menu menu) { - super.onCreatePanelMenu(featured, menu); - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.video_player, menu); - - return true; - } - - @Override - public void onPause() { - super.onPause(); - videoView.pause(); - } - - @Override - public void onResume() { - super.onResume(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - prefs = getPreferences(Context.MODE_PRIVATE); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - switch(id) { - case android.R.id.home: - finish(); - break; - case R.id.menu_item_share: - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_TEXT, videoUrl); - intent.setType("text/plain"); - startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); - break; - case R.id.menu_item_screen_rotation: - toggleOrientation(); - break; - default: - Log.e(TAG, "Error: MenuItem not known"); - return false; - } - return true; - } - - @Override - public void onConfigurationChanged(Configuration config) { - super.onConfigurationChanged(config); - - if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { - isLandscape = true; - adjustMediaControlMetrics(); - } else if (config.orientation == Configuration.ORIENTATION_PORTRAIT){ - isLandscape = false; - adjustMediaControlMetrics(); - } - } - - @Override - public void onSaveInstanceState(Bundle savedInstanceState) { - super.onSaveInstanceState(savedInstanceState); - //savedInstanceState.putInt(POSITION, videoView.getCurrentPosition()); - //videoView.pause(); - } - - @Override - public void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - position = savedInstanceState.getInt(POSITION); - //videoView.seekTo(position); - } - - private void showUi() { - try { - uiIsHidden = false; - mediaController.show(100000); - actionBar.show(); - adjustMediaControlMetrics(); - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - @Override - public void run() { - if ((System.currentTimeMillis() - lastUiShowTime) >= HIDING_DELAY) { - hideUi(); - } - } - }, HIDING_DELAY); - lastUiShowTime = System.currentTimeMillis(); - }catch(Exception e) { - e.printStackTrace(); - } - } - - private void hideUi() { - uiIsHidden = true; - actionBar.hide(); - mediaController.hide(); - if (android.os.Build.VERSION.SDK_INT >= 17) { - decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); - } - getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN); - } - - private void adjustMediaControlMetrics() { - MediaController.LayoutParams mediaControllerLayout - = new MediaController.LayoutParams(MediaController.LayoutParams.MATCH_PARENT, - MediaController.LayoutParams.WRAP_CONTENT); - - if(!hasSoftKeys) { - mediaControllerLayout.setMargins(20, 0, 20, 20); - } else { - int width = getNavigationBarWidth(); - int height = getNavigationBarHeight(); - mediaControllerLayout.setMargins(width + 20, 0, width + 20, height + 20); - } - mediaController.setLayoutParams(mediaControllerLayout); - } - - private boolean checkIfHasSoftKeys(){ - return Build.VERSION.SDK_INT >= 17 || - getNavigationBarHeight() != 0 || - getNavigationBarWidth() != 0; - } - - private int getNavigationBarHeight() { - if(Build.VERSION.SDK_INT >= 17) { - Display d = getWindowManager().getDefaultDisplay(); - - DisplayMetrics realDisplayMetrics = new DisplayMetrics(); - d.getRealMetrics(realDisplayMetrics); - DisplayMetrics displayMetrics = new DisplayMetrics(); - d.getMetrics(displayMetrics); - - int realHeight = realDisplayMetrics.heightPixels; - int displayHeight = displayMetrics.heightPixels; - return realHeight - displayHeight; - } else { - return 50; - } - } - - private int getNavigationBarWidth() { - if(Build.VERSION.SDK_INT >= 17) { - Display d = getWindowManager().getDefaultDisplay(); - - DisplayMetrics realDisplayMetrics = new DisplayMetrics(); - d.getRealMetrics(realDisplayMetrics); - DisplayMetrics displayMetrics = new DisplayMetrics(); - d.getMetrics(displayMetrics); - - int realWidth = realDisplayMetrics.widthPixels; - int displayWidth = displayMetrics.widthPixels; - return realWidth - displayWidth; - } else { - return 50; - } - } - - private boolean checkIfLandscape() { - DisplayMetrics displayMetrics = new DisplayMetrics(); - getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - return displayMetrics.heightPixels < displayMetrics.widthPixels; - } - - private void toggleOrientation() { - if(isLandscape) { - isLandscape = false; - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - } else { - isLandscape = true; - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); - } - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(PREF_IS_LANDSCAPE, isLandscape); - editor.apply(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 5b953697d..5df2e8be4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -49,7 +49,6 @@ import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayerActivity; import org.schabi.newpipe.player.VideoPlayer; -import org.schabi.newpipe.player.old.PlayVideoActivity; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.settings.SettingsActivity; @@ -117,26 +116,6 @@ public class NavigationHelper { context.startActivity(playerIntent); } - public static void playOnOldVideoPlayer(Context context, StreamInfo info) { - ArrayList videoStreamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false)); - int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList); - - if (index == -1) { - Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show(); - return; - } - - VideoStream videoStream = videoStreamsList.get(index); - Intent intent = new Intent(context, PlayVideoActivity.class) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(PlayVideoActivity.VIDEO_TITLE, info.getName()) - .putExtra(PlayVideoActivity.STREAM_URL, videoStream.getUrl()) - .putExtra(PlayVideoActivity.VIDEO_URL, info.getUrl()) - .putExtra(PlayVideoActivity.START_POSITION, info.getStartPosition()); - - context.startActivity(intent); - } - public static void playOnPopupPlayer(final Context context, final PlayQueue queue) { if (!PermissionHelper.isPopupEnabled(context)) { PermissionHelper.showPopupEnablementToast(context); diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index d9826bf5f..d9bd02a43 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -165,8 +165,6 @@ الصوت إعادة المحاولة تم رفض إذن الوصول إلى التخزين - استخدام المشغل القديم - المشغل القديم المدمج في إطار Mediaframework ألف مليون diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 133a679c9..b1ff9471d 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -17,7 +17,6 @@ use_external_video_player use_external_audio_player autoplay_through_intent - use_oldplayer volume_gesture_control brightness_gesture_control diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac95d98a4..1a2265e79 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -239,8 +239,6 @@ Audio Retry Storage access permission denied - Use old player - Old built-in Mediaframework player K M diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml index 5ac5bb733..5bd5a45eb 100644 --- a/app/src/main/res/xml/video_audio_settings.xml +++ b/app/src/main/res/xml/video_audio_settings.xml @@ -64,12 +64,6 @@ android:key="@string/use_external_audio_player_key" android:title="@string/use_external_audio_player_title"/> - - Date: Tue, 13 Nov 2018 17:30:04 +0100 Subject: [PATCH 29/68] put autoplay next stream setting into behaviour page --- app/src/main/res/xml/content_settings.xml | 6 ------ app/src/main/res/xml/video_audio_settings.xml | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index c863c37f3..33e04890e 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -29,12 +29,6 @@ android:summary="@string/show_search_suggestions_summary" android:title="@string/show_search_suggestions_title"/> - - + + Date: Thu, 15 Nov 2018 20:17:22 -0300 Subject: [PATCH 30/68] misc utils Also this include: * Mp4 DASH reader/writter * WebM reader/writter * a subtitle converter for Timed Text Markup Language v1 and TranScript (v1, v2 and v3) * SharpStream to wrap IntputStream and OutputStream in one interface * custom implementation of DataInputStream --- .../schabi/newpipe/streams/DataReader.java | 103 +++ .../schabi/newpipe/streams/Mp4DashReader.java | 817 ++++++++++++++++++ .../schabi/newpipe/streams/Mp4DashWriter.java | 623 +++++++++++++ .../newpipe/streams/SubtitleConverter.java | 370 ++++++++ .../newpipe/streams/TrackDataChunk.java | 65 ++ .../schabi/newpipe/streams/WebMReader.java | 507 +++++++++++ .../schabi/newpipe/streams/WebMWriter.java | 728 ++++++++++++++++ .../newpipe/streams/io/SharpStream.java | 47 + .../giga/postprocessing/Mp4DashMuxer.java | 4 +- .../giga/postprocessing/Postprocessing.java | 2 +- .../giga/postprocessing/TestAlgo.java | 2 +- .../giga/postprocessing/TttmlConverter.java | 45 +- .../giga/postprocessing/WebMMuxer.java | 8 +- .../io/ChunkFileInputStream.java | 2 +- .../giga/postprocessing/io/CircularFile.java | 2 +- .../giga/postprocessing/io/FileStream.java | 2 +- .../postprocessing/io/SharpInputStream.java | 2 +- 17 files changed, 3308 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/streams/DataReader.java create mode 100644 app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java create mode 100644 app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java create mode 100644 app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java create mode 100644 app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java create mode 100644 app/src/main/java/org/schabi/newpipe/streams/WebMReader.java create mode 100644 app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java create mode 100644 app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java diff --git a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java new file mode 100644 index 000000000..d0e946eb7 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java @@ -0,0 +1,103 @@ +package org.schabi.newpipe.streams; + +import java.io.EOFException; +import java.io.IOException; + +import org.schabi.newpipe.streams.io.SharpStream; + +/** + * @author kapodamy + */ +public class DataReader { + + public final static int SHORT_SIZE = 2; + public final static int LONG_SIZE = 8; + public final static int INTEGER_SIZE = 4; + public final static int FLOAT_SIZE = 4; + + private long pos; + public final SharpStream stream; + private final boolean rewind; + + public DataReader(SharpStream stream) { + this.rewind = stream.canRewind(); + this.stream = stream; + this.pos = 0L; + } + + public long position() { + return pos; + } + + public final int readInt() throws IOException { + primitiveRead(INTEGER_SIZE); + return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; + } + + public final int read() throws IOException { + int value = stream.read(); + if (value == -1) { + throw new EOFException(); + } + + pos++; + return value; + } + + public final long skipBytes(long amount) throws IOException { + amount = stream.skip(amount); + pos += amount; + return amount; + } + + public final long readLong() throws IOException { + primitiveRead(LONG_SIZE); + long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; + long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7]; + return high << 32 | low; + } + + public final short readShort() throws IOException { + primitiveRead(SHORT_SIZE); + return (short) (primitive[0] << 8 | primitive[1]); + } + + public final int read(byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); + } + + public final int read(byte[] buffer, int offset, int count) throws IOException { + int res = stream.read(buffer, offset, count); + pos += res; + + return res; + } + + public final boolean available() { + return stream.available() > 0; + } + + public void rewind() throws IOException { + stream.rewind(); + pos = 0; + } + + public boolean canRewind() { + return rewind; + } + + private short[] primitive = new short[LONG_SIZE]; + + private void primitiveRead(int amount) throws IOException { + byte[] buffer = new byte[amount]; + int read = stream.read(buffer, 0, amount); + pos += read; + if (read != amount) { + throw new EOFException("Truncated data, missing " + String.valueOf(amount - read) + " bytes"); + } + + for (int i = 0; i < buffer.length; i++) { + primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" datatype is signed and is very annoying + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java new file mode 100644 index 000000000..ec2419734 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java @@ -0,0 +1,817 @@ +package org.schabi.newpipe.streams; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +import java.nio.ByteBuffer; + +import java.util.ArrayList; +import java.util.NoSuchElementException; + +import org.schabi.newpipe.streams.io.SharpStream; + +/** + * @author kapodamy + */ +public class Mp4DashReader { + + // + private static final int ATOM_MOOF = 0x6D6F6F66; + private static final int ATOM_MFHD = 0x6D666864; + private static final int ATOM_TRAF = 0x74726166; + private static final int ATOM_TFHD = 0x74666864; + private static final int ATOM_TFDT = 0x74666474; + private static final int ATOM_TRUN = 0x7472756E; + private static final int ATOM_MDIA = 0x6D646961; + private static final int ATOM_FTYP = 0x66747970; + private static final int ATOM_SIDX = 0x73696478; + private static final int ATOM_MOOV = 0x6D6F6F76; + private static final int ATOM_MDAT = 0x6D646174; + private static final int ATOM_MVHD = 0x6D766864; + private static final int ATOM_TRAK = 0x7472616B; + private static final int ATOM_MVEX = 0x6D766578; + private static final int ATOM_TREX = 0x74726578; + private static final int ATOM_TKHD = 0x746B6864; + private static final int ATOM_MFRA = 0x6D667261; + private static final int ATOM_TFRA = 0x74667261; + private static final int ATOM_MDHD = 0x6D646864; + private static final int BRAND_DASH = 0x64617368; + // + + private final DataReader stream; + + private Mp4Track[] tracks = null; + + private Box box; + private Moof moof; + + private boolean chunkZero = false; + + private int selectedTrack = -1; + + public enum TrackKind { + Audio, Video, Other + } + + public Mp4DashReader(SharpStream source) { + this.stream = new DataReader(source); + } + + public void parse() throws IOException, NoSuchElementException { + if (selectedTrack > -1) { + return; + } + + box = readBox(ATOM_FTYP); + if (parse_ftyp() != BRAND_DASH) { + throw new NoSuchElementException("Main Brand is not dash"); + } + + Moov moov = null; + int i; + + while (box.type != ATOM_MOOF) { + ensure(box); + box = readBox(); + + switch (box.type) { + case ATOM_MOOV: + moov = parse_moov(box); + break; + case ATOM_SIDX: + break; + case ATOM_MFRA: + break; + case ATOM_MDAT: + throw new IOException("Expected moof, found mdat"); + } + } + + if (moov == null) { + throw new IOException("The provided Mp4 doesn't have the 'moov' box"); + } + + tracks = new Mp4Track[moov.trak.length]; + + for (i = 0; i < tracks.length; i++) { + tracks[i] = new Mp4Track(); + tracks[i].trak = moov.trak[i]; + + if (moov.mvex_trex != null) { + for (Trex mvex_trex : moov.mvex_trex) { + if (tracks[i].trak.tkhd.trackId == mvex_trex.trackId) { + tracks[i].trex = mvex_trex; + } + } + } + + if (moov.trak[i].tkhd.bHeight == 0 && moov.trak[i].tkhd.bWidth == 0) { + tracks[i].kind = moov.trak[i].tkhd.bVolume == 0 ? TrackKind.Other : TrackKind.Audio; + } else { + tracks[i].kind = TrackKind.Video; + } + } + } + + public Mp4Track selectTrack(int index) { + selectedTrack = index; + return tracks[index]; + } + + /** + * Count all fragments present. This operation requires a seekable stream + * + * @return list with a basic info + * @throws IOException if the source stream is not seekeable + */ + public int getFragmentsCount() throws IOException { + if (selectedTrack < 0) { + throw new IllegalStateException("track no selected"); + } + if (!stream.canRewind()) { + throw new IOException("The provided stream doesn't allow seek"); + } + + Box tmp; + int count = 0; + long orig_offset = stream.position(); + + if (box.type == ATOM_MOOF) { + tmp = box; + } else { + ensure(box); + tmp = readBox(); + } + + do { + if (tmp.type == ATOM_MOOF) { + ensure(readBox(ATOM_MFHD)); + Box traf; + while ((traf = untilBox(tmp, ATOM_TRAF)) != null) { + Box tfhd = readBox(ATOM_TFHD); + if (parse_tfhd(tracks[selectedTrack].trak.tkhd.trackId) != null) { + count++; + break; + } + ensure(tfhd); + ensure(traf); + } + } + ensure(tmp); + } while (stream.available() && (tmp = readBox()) != null); + + stream.rewind(); + stream.skipBytes((int) orig_offset); + + return count; + } + + public Mp4Track[] getAvailableTracks() { + return tracks; + } + + public Mp4TrackChunk getNextChunk() throws IOException { + Mp4Track track = tracks[selectedTrack]; + + while (stream.available()) { + + if (chunkZero) { + ensure(box); + if (!stream.available()) { + break; + } + box = readBox(); + } else { + chunkZero = true; + } + + switch (box.type) { + case ATOM_MOOF: + if (moof != null) { + throw new IOException("moof found without mdat"); + } + + moof = parse_moof(box, track.trak.tkhd.trackId); + + if (moof.traf != null) { + + if (hasFlag(moof.traf.trun.bFlags, 0x0001)) { + moof.traf.trun.dataOffset -= box.size + 8; + if (moof.traf.trun.dataOffset < 0) { + throw new IOException("trun box has wrong data offset, points outside of concurrent mdat box"); + } + } + + if (moof.traf.trun.chunkSize < 1) { + if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) { + moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount; + } else { + moof.traf.trun.chunkSize = box.size - 8; + } + } + if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) { + if (hasFlag(moof.traf.tfhd.bFlags, 0x20)) { + moof.traf.trun.chunkDuration = moof.traf.tfhd.defaultSampleDuration * moof.traf.trun.entryCount; + } + } + } + break; + case ATOM_MDAT: + if (moof == null) { + throw new IOException("mdat found without moof"); + } + + if (moof.traf == null) { + moof = null; + continue;// find another chunk + } + + Mp4TrackChunk chunk = new Mp4TrackChunk(); + chunk.moof = moof; + chunk.data = new TrackDataChunk(stream, moof.traf.trun.chunkSize); + moof = null; + + stream.skipBytes(chunk.moof.traf.trun.dataOffset); + return chunk; + default: + } + } + + return null; + } + + // + private long readUint() throws IOException { + return stream.readInt() & 0xffffffffL; + } + + public static boolean hasFlag(int flags, int mask) { + return (flags & mask) == mask; + } + + private String boxName(Box ref) { + return boxName(ref.type); + } + + private String boxName(int type) { + try { + return new String(ByteBuffer.allocate(4).putInt(type).array(), "US-ASCII"); + } catch (UnsupportedEncodingException e) { + return "0x" + Integer.toHexString(type); + } + } + + private Box readBox() throws IOException { + Box b = new Box(); + b.offset = stream.position(); + b.size = stream.readInt(); + b.type = stream.readInt(); + + return b; + } + + private Box readBox(int expected) throws IOException { + Box b = readBox(); + if (b.type != expected) { + throw new NoSuchElementException("expected " + boxName(expected) + " found " + boxName(b)); + } + return b; + } + + private void ensure(Box ref) throws IOException { + long skip = ref.offset + ref.size - stream.position(); + + if (skip == 0) { + return; + } else if (skip < 0) { + throw new EOFException(String.format( + "parser go beyond limits of the box. type=%s offset=%s size=%s position=%s", + boxName(ref), ref.offset, ref.size, stream.position() + )); + } + + stream.skipBytes((int) skip); + } + + private Box untilBox(Box ref, int... expected) throws IOException { + Box b; + while (stream.position() < (ref.offset + ref.size)) { + b = readBox(); + for (int type : expected) { + if (b.type == type) { + return b; + } + } + ensure(b); + } + + return null; + } + + // + + // + + private Moof parse_moof(Box ref, int trackId) throws IOException { + Moof obj = new Moof(); + + Box b = readBox(ATOM_MFHD); + obj.mfhd_SequenceNumber = parse_mfhd(); + ensure(b); + + while ((b = untilBox(ref, ATOM_TRAF)) != null) { + obj.traf = parse_traf(b, trackId); + ensure(b); + + if (obj.traf != null) { + return obj; + } + } + + return obj; + } + + private int parse_mfhd() throws IOException { + // version + // flags + stream.skipBytes(4); + + return stream.readInt(); + } + + private Traf parse_traf(Box ref, int trackId) throws IOException { + Traf traf = new Traf(); + + Box b = readBox(ATOM_TFHD); + traf.tfhd = parse_tfhd(trackId); + ensure(b); + + if (traf.tfhd == null) { + return null; + } + + b = untilBox(ref, ATOM_TRUN, ATOM_TFDT); + + if (b.type == ATOM_TFDT) { + traf.tfdt = parse_tfdt(); + ensure(b); + b = readBox(ATOM_TRUN); + } + + traf.trun = parse_trun(); + ensure(b); + + return traf; + } + + private Tfhd parse_tfhd(int trackId) throws IOException { + Tfhd obj = new Tfhd(); + + obj.bFlags = stream.readInt(); + obj.trackId = stream.readInt(); + + if (trackId != -1 && obj.trackId != trackId) { + return null; + } + + if (hasFlag(obj.bFlags, 0x01)) { + stream.skipBytes(8); + } + if (hasFlag(obj.bFlags, 0x02)) { + stream.skipBytes(4); + } + if (hasFlag(obj.bFlags, 0x08)) { + obj.defaultSampleDuration = stream.readInt(); + } + if (hasFlag(obj.bFlags, 0x10)) { + obj.defaultSampleSize = stream.readInt(); + } + if (hasFlag(obj.bFlags, 0x20)) { + obj.defaultSampleFlags = stream.readInt(); + } + + return obj; + } + + private long parse_tfdt() throws IOException { + int version = stream.read(); + stream.skipBytes(3);// flags + return version == 0 ? readUint() : stream.readLong(); + } + + private Trun parse_trun() throws IOException { + Trun obj = new Trun(); + obj.bFlags = stream.readInt(); + obj.entryCount = stream.readInt();// unsigned int + + obj.entries_rowSize = 0; + if (hasFlag(obj.bFlags, 0x0100)) { + obj.entries_rowSize += 4; + } + if (hasFlag(obj.bFlags, 0x0200)) { + obj.entries_rowSize += 4; + } + if (hasFlag(obj.bFlags, 0x0400)) { + obj.entries_rowSize += 4; + } + if (hasFlag(obj.bFlags, 0x0800)) { + obj.entries_rowSize += 4; + } + obj.bEntries = new byte[obj.entries_rowSize * obj.entryCount]; + + if (hasFlag(obj.bFlags, 0x0001)) { + obj.dataOffset = stream.readInt(); + } + if (hasFlag(obj.bFlags, 0x0004)) { + obj.bFirstSampleFlags = stream.readInt(); + } + + stream.read(obj.bEntries); + + for (int i = 0; i < obj.entryCount; i++) { + TrunEntry entry = obj.getEntry(i); + if (hasFlag(obj.bFlags, 0x0100)) { + obj.chunkDuration += entry.sampleDuration; + } + if (hasFlag(obj.bFlags, 0x0200)) { + obj.chunkSize += entry.sampleSize; + } + if (hasFlag(obj.bFlags, 0x0800)) { + if (!hasFlag(obj.bFlags, 0x0100)) { + obj.chunkDuration += entry.sampleCompositionTimeOffset; + } + } + } + + return obj; + } + + private int parse_ftyp() throws IOException { + int brand = stream.readInt(); + stream.skipBytes(4);// minor version + + return brand; + } + + private Mvhd parse_mvhd() throws IOException { + int version = stream.read(); + stream.skipBytes(3);// flags + + // creation entries_time + // modification entries_time + stream.skipBytes(2 * (version == 0 ? 4 : 8)); + + Mvhd obj = new Mvhd(); + obj.timeScale = readUint(); + + // chunkDuration + stream.skipBytes(version == 0 ? 4 : 8); + + // rate + // volume + // reserved + // matrix array + // predefined + stream.skipBytes(76); + + obj.nextTrackId = readUint(); + + return obj; + } + + private Tkhd parse_tkhd() throws IOException { + int version = stream.read(); + + Tkhd obj = new Tkhd(); + + // flags + // creation entries_time + // modification entries_time + stream.skipBytes(3 + (2 * (version == 0 ? 4 : 8))); + + obj.trackId = stream.readInt(); + + stream.skipBytes(4);// reserved + + obj.duration = version == 0 ? readUint() : stream.readLong(); + + stream.skipBytes(2 * 4);// reserved + + obj.bLayer = stream.readShort(); + obj.bAlternateGroup = stream.readShort(); + obj.bVolume = stream.readShort(); + + stream.skipBytes(2);// reserved + + obj.matrix = new byte[9 * 4]; + stream.read(obj.matrix); + + obj.bWidth = stream.readInt(); + obj.bHeight = stream.readInt(); + + return obj; + } + + private Trak parse_trak(Box ref) throws IOException { + Trak trak = new Trak(); + + Box b = readBox(ATOM_TKHD); + trak.tkhd = parse_tkhd(); + ensure(b); + + b = untilBox(ref, ATOM_MDIA); + trak.mdia = new byte[b.size]; + + ByteBuffer buffer = ByteBuffer.wrap(trak.mdia); + buffer.putInt(b.size); + buffer.putInt(ATOM_MDIA); + stream.read(trak.mdia, 8, b.size - 8); + + trak.mdia_mdhd_timeScale = parse_mdia(buffer); + + return trak; + } + + private int parse_mdia(ByteBuffer data) { + while (data.hasRemaining()) { + int end = data.position() + data.getInt(); + if (data.getInt() == ATOM_MDHD) { + byte version = data.get(); + data.position(data.position() + 3 + ((version == 0 ? 4 : 8) * 2)); + return data.getInt(); + } + + data.position(end); + } + + return 0;// this NEVER should happen + } + + private Moov parse_moov(Box ref) throws IOException { + Box b = readBox(ATOM_MVHD); + Moov moov = new Moov(); + moov.mvhd = parse_mvhd(); + ensure(b); + + ArrayList tmp = new ArrayList<>((int) moov.mvhd.nextTrackId); + while ((b = untilBox(ref, ATOM_TRAK, ATOM_MVEX)) != null) { + + switch (b.type) { + case ATOM_TRAK: + tmp.add(parse_trak(b)); + break; + case ATOM_MVEX: + moov.mvex_trex = parse_mvex(b, (int) moov.mvhd.nextTrackId); + break; + } + + ensure(b); + } + + moov.trak = tmp.toArray(new Trak[tmp.size()]); + + return moov; + } + + private Trex[] parse_mvex(Box ref, int possibleTrackCount) throws IOException { + ArrayList tmp = new ArrayList<>(possibleTrackCount); + + Box b; + while ((b = untilBox(ref, ATOM_TREX)) != null) { + tmp.add(parse_trex()); + ensure(b); + } + + return tmp.toArray(new Trex[tmp.size()]); + } + + private Trex parse_trex() throws IOException { + // version + // flags + stream.skipBytes(4); + + Trex obj = new Trex(); + obj.trackId = stream.readInt(); + obj.defaultSampleDescriptionIndex = stream.readInt(); + obj.defaultSampleDuration = stream.readInt(); + obj.defaultSampleSize = stream.readInt(); + obj.defaultSampleFlags = stream.readInt(); + + return obj; + } + + private Tfra parse_tfra() throws IOException { + int version = stream.read(); + + stream.skipBytes(3);// flags + + Tfra tfra = new Tfra(); + tfra.trackId = stream.readInt(); + + stream.skipBytes(3);// reserved + int bFlags = stream.read(); + int size_tts = ((bFlags >> 4) & 3) + ((bFlags >> 2) & 3) + (bFlags & 3); + + tfra.entries_time = new int[stream.readInt()]; + + for (int i = 0; i < tfra.entries_time.length; i++) { + tfra.entries_time[i] = version == 0 ? stream.readInt() : (int) stream.readLong(); + stream.skipBytes(size_tts + (version == 0 ? 4 : 8)); + } + + return tfra; + } + + private Sidx parse_sidx() throws IOException { + int version = stream.read(); + + stream.skipBytes(3);// flags + + Sidx obj = new Sidx(); + obj.referenceId = stream.readInt(); + obj.timescale = stream.readInt(); + + // earliest presentation entries_time + // first offset + // reserved + stream.skipBytes((2 * (version == 0 ? 4 : 8)) + 2); + + obj.entries_subsegmentDuration = new int[stream.readShort()]; + + for (int i = 0; i < obj.entries_subsegmentDuration.length; i++) { + // reference type + // referenced size + stream.skipBytes(4); + obj.entries_subsegmentDuration[i] = stream.readInt();// unsigned int + + // starts with SAP + // SAP type + // SAP delta entries_time + stream.skipBytes(4); + } + + return obj; + } + + private Tfra[] parse_mfra(Box ref, int trackCount) throws IOException { + ArrayList tmp = new ArrayList<>(trackCount); + long limit = ref.offset + ref.size; + + while (stream.position() < limit) { + box = readBox(); + + if (box.type == ATOM_TFRA) { + tmp.add(parse_tfra()); + } + + ensure(box); + } + + return tmp.toArray(new Tfra[tmp.size()]); + } + + // + + // + class Box { + + int type; + long offset; + int size; + } + + class Sidx { + + int timescale; + int referenceId; + int[] entries_subsegmentDuration; + } + + public class Moof { + + int mfhd_SequenceNumber; + public Traf traf; + } + + public class Traf { + + public Tfhd tfhd; + long tfdt; + public Trun trun; + } + + public class Tfhd { + + int bFlags; + public int trackId; + int defaultSampleDuration; + int defaultSampleSize; + int defaultSampleFlags; + } + + public class TrunEntry { + + public int sampleDuration; + public int sampleSize; + public int sampleFlags; + public int sampleCompositionTimeOffset; + } + + public class Trun { + + public int chunkDuration; + public int chunkSize; + + public int bFlags; + int bFirstSampleFlags; + int dataOffset; + + public int entryCount; + byte[] bEntries; + int entries_rowSize; + + public TrunEntry getEntry(int i) { + ByteBuffer buffer = ByteBuffer.wrap(bEntries, i * entries_rowSize, entries_rowSize); + TrunEntry entry = new TrunEntry(); + + if (hasFlag(bFlags, 0x0100)) { + entry.sampleDuration = buffer.getInt(); + } + if (hasFlag(bFlags, 0x0200)) { + entry.sampleSize = buffer.getInt(); + } + if (hasFlag(bFlags, 0x0400)) { + entry.sampleFlags = buffer.getInt(); + } + if (hasFlag(bFlags, 0x0800)) { + entry.sampleCompositionTimeOffset = buffer.getInt(); + } + + return entry; + } + } + + public class Tkhd { + + int trackId; + long duration; + short bVolume; + int bWidth; + int bHeight; + byte[] matrix; + short bLayer; + short bAlternateGroup; + } + + public class Trak { + + public Tkhd tkhd; + public int mdia_mdhd_timeScale; + + byte[] mdia; + } + + class Mvhd { + + long timeScale; + long nextTrackId; + } + + class Moov { + + Mvhd mvhd; + Trak[] trak; + Trex[] mvex_trex; + } + + class Tfra { + + int trackId; + int[] entries_time; + } + + public class Trex { + + private int trackId; + int defaultSampleDescriptionIndex; + int defaultSampleDuration; + int defaultSampleSize; + int defaultSampleFlags; + } + + public class Mp4Track { + + public TrackKind kind; + public Trak trak; + public Trex trex; + } + + public class Mp4TrackChunk { + + public InputStream data; + public Moof moof; + } +// +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java new file mode 100644 index 000000000..babb2e24c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java @@ -0,0 +1,623 @@ +package org.schabi.newpipe.streams; + +import org.schabi.newpipe.streams.io.SharpStream; + +import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track; +import org.schabi.newpipe.streams.Mp4DashReader.Mp4TrackChunk; +import org.schabi.newpipe.streams.Mp4DashReader.Trak; +import org.schabi.newpipe.streams.Mp4DashReader.Trex; + + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import static org.schabi.newpipe.streams.Mp4DashReader.hasFlag; + +/** + * + * @author kapodamy + */ +public class Mp4DashWriter { + + private final static byte DIMENSIONAL_FIVE = 5; + private final static byte DIMENSIONAL_TWO = 2; + private final static short DEFAULT_TIMESCALE = 1000; + private final static int BUFFER_SIZE = 8 * 1024; + private final static byte DEFAULT_TREX_SIZE = 32; + private final static byte[] TFRA_TTS_DEFAULT = new byte[]{0x01, 0x01, 0x01}; + private final static int EPOCH_OFFSET = 2082844800; + + private Mp4Track[] infoTracks; + private SharpStream[] sourceTracks; + + private Mp4DashReader[] readers; + private final long time; + + private boolean done = false; + private boolean parsed = false; + + private long written = 0; + private ArrayList> chunkTimes; + private ArrayList moofOffsets; + private ArrayList fragSizes; + + public Mp4DashWriter(SharpStream... source) { + sourceTracks = source; + readers = new Mp4DashReader[sourceTracks.length]; + infoTracks = new Mp4Track[sourceTracks.length]; + time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET; + } + + public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException { + if (!parsed) { + throw new IllegalStateException("All sources must be parsed first"); + } + + return readers[sourceIndex].getAvailableTracks(); + } + + public void parseSources() throws IOException, IllegalStateException { + if (done) { + throw new IllegalStateException("already done"); + } + if (parsed) { + throw new IllegalStateException("already parsed"); + } + + try { + for (int i = 0; i < readers.length; i++) { + readers[i] = new Mp4DashReader(sourceTracks[i]); + readers[i].parse(); + } + + } finally { + parsed = true; + } + } + + public void selectTracks(int... trackIndex) throws IOException { + if (done) { + throw new IOException("already done"); + } + if (chunkTimes != null) { + throw new IOException("tracks already selected"); + } + + try { + chunkTimes = new ArrayList<>(readers.length); + moofOffsets = new ArrayList<>(32); + fragSizes = new ArrayList<>(32); + + for (int i = 0; i < readers.length; i++) { + infoTracks[i] = readers[i].selectTrack(trackIndex[i]); + + chunkTimes.add(new ArrayList(32)); + } + + } finally { + parsed = true; + } + } + + public long getBytesWritten() { + return written; + } + + public void build(SharpStream out) throws IOException, RuntimeException { + if (done) { + throw new RuntimeException("already done"); + } + if (!out.canWrite()) { + throw new IOException("the provided output is not writable"); + } + + long sidxOffsets = -1; + int maxFrags = 0; + + for (SharpStream stream : sourceTracks) { + if (!stream.canRewind()) { + sidxOffsets = -2;// sidx not available + } + } + + try { + dump(make_ftyp(), out); + dump(make_moov(), out); + + if (sidxOffsets == -1 && out.canRewind()) { + // + int reserved = 0; + for (Mp4DashReader reader : readers) { + int count = reader.getFragmentsCount(); + if (count > maxFrags) { + maxFrags = count; + } + reserved += 12 + calcSidxBodySize(count); + } + if (maxFrags > 0xFFFF) { + sidxOffsets = -3;// TODO: to many fragments, needs a multi-sidx implementation + } else { + sidxOffsets = written; + dump(make_free(reserved), out); + } + // + } + ArrayList chunks = new ArrayList<>(readers.length); + chunks.add(null); + + int read; + byte[] buffer = new byte[BUFFER_SIZE]; + int sequenceNumber = 1; + + while (true) { + chunks.clear(); + + for (int i = 0; i < readers.length; i++) { + Mp4TrackChunk chunk = readers[i].getNextChunk(); + if (chunk == null || chunk.moof.traf.trun.chunkSize < 1) { + continue; + } + chunk.moof.traf.tfhd.trackId = i + 1; + chunks.add(chunk); + + if (sequenceNumber == 1) { + if (chunk.moof.traf.trun.entryCount > 0 && hasFlag(chunk.moof.traf.trun.bFlags, 0x0800)) { + chunkTimes.get(i).add(chunk.moof.traf.trun.getEntry(0).sampleCompositionTimeOffset); + } else { + chunkTimes.get(i).add(0); + } + } + + chunkTimes.get(i).add(chunk.moof.traf.trun.chunkDuration); + } + + if (chunks.size() < 1) { + break; + } + + long offset = written; + moofOffsets.add(offset); + + dump(make_moof(sequenceNumber++, chunks, offset), out); + dump(make_mdat(chunks), out); + + for (Mp4TrackChunk chunk : chunks) { + while ((read = chunk.data.read(buffer)) > 0) { + out.write(buffer, 0, read); + written += read; + } + } + + fragSizes.add((int) (written - offset)); + } + + dump(make_mfra(), out); + + if (sidxOffsets > 0 && moofOffsets.size() == maxFrags) { + long len = written; + + out.rewind(); + out.skip(sidxOffsets); + + written = sidxOffsets; + sidxOffsets = moofOffsets.get(0); + + for (int i = 0; i < readers.length; i++) { + dump(make_sidx(i, sidxOffsets - written), out); + } + + written = len; + } + } finally { + done = true; + } + } + + public boolean isDone() { + return done; + } + + public boolean isParsed() { + return parsed; + } + + public void close() { + done = true; + parsed = true; + + for (SharpStream src : sourceTracks) { + src.dispose(); + } + + sourceTracks = null; + readers = null; + infoTracks = null; + moofOffsets = null; + chunkTimes = null; + } + + // + private void dump(byte[][] buffer, SharpStream stream) throws IOException { + for (byte[] buff : buffer) { + stream.write(buff); + written += buff.length; + } + } + + private byte[][] lengthFor(byte[][] buffer) { + int length = 0; + for (byte[] buff : buffer) { + length += buff.length; + } + + ByteBuffer.wrap(buffer[0]).putInt(length); + + return buffer; + } + + private int calcSidxBodySize(int entryCount) { + return 4 + 4 + 8 + 8 + 4 + (entryCount * 12); + } + // + + // + private byte[][] make_moof(int sequence, ArrayList chunks, long referenceOffset) { + int pos = 2; + TrunExtra[] extra = new TrunExtra[chunks.size()]; + + byte[][] buffer = new byte[pos + (extra.length * DIMENSIONAL_FIVE)][]; + buffer[0] = new byte[]{ + 0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x66,// info header + 0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00//mfhd + }; + buffer[1] = new byte[4]; + ByteBuffer.wrap(buffer[1]).putInt(sequence); + + for (int i = 0; i < extra.length; i++) { + extra[i] = new TrunExtra(); + for (byte[] buff : make_traf(chunks.get(i), extra[i], referenceOffset)) { + buffer[pos++] = buff; + } + } + + lengthFor(buffer); + + int offset = 8 + ByteBuffer.wrap(buffer[0]).getInt(); + + for (int i = 0; i < extra.length; i++) { + extra[i].byteBuffer.putInt(offset); + offset += chunks.get(i).moof.traf.trun.chunkSize; + } + + return buffer; + } + + private byte[][] make_traf(Mp4TrackChunk chunk, TrunExtra extra, long moofOffset) { + byte[][] buffer = new byte[DIMENSIONAL_FIVE][]; + buffer[0] = new byte[]{ + 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x66, + 0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x68, 0x64 + }; + + int flags = (chunk.moof.traf.tfhd.bFlags & 0x38) | 0x01; + byte tfhdBodySize = 8 + 8; + if (hasFlag(flags, 0x08)) { + tfhdBodySize += 4; + } + if (hasFlag(flags, 0x10)) { + tfhdBodySize += 4; + } + if (hasFlag(flags, 0x20)) { + tfhdBodySize += 4; + } + buffer[1] = new byte[tfhdBodySize]; + ByteBuffer set = ByteBuffer.wrap(buffer[1]); + set.position(4); + set.putInt(chunk.moof.traf.tfhd.trackId); + set.putLong(moofOffset); + if (hasFlag(flags, 0x08)) { + set.putInt(chunk.moof.traf.tfhd.defaultSampleDuration); + } + if (hasFlag(flags, 0x10)) { + set.putInt(chunk.moof.traf.tfhd.defaultSampleSize); + } + if (hasFlag(flags, 0x20)) { + set.putInt(chunk.moof.traf.tfhd.defaultSampleFlags); + } + set.putInt(0, flags); + ByteBuffer.wrap(buffer[0]).putInt(8, 8 + tfhdBodySize); + + buffer[2] = new byte[]{ + 0x00, 0x00, 0x00, 0x14, + 0x74, 0x66, 0x64, 0x74, + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + }; + + ByteBuffer.wrap(buffer[2]).putLong(12, chunk.moof.traf.tfdt); + + buffer[3] = new byte[]{ + 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x75, 0x6E, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + }; + + buffer[4] = chunk.moof.traf.trun.bEntries; + + lengthFor(buffer); + + set = ByteBuffer.wrap(buffer[3]); + set.putInt(buffer[3].length + buffer[4].length); + set.position(8); + set.putInt((chunk.moof.traf.trun.bFlags | 0x01) & 0x0F01); + set.putInt(chunk.moof.traf.trun.entryCount); + extra.byteBuffer = set; + + return buffer; + } + + private byte[][] make_mdat(ArrayList chunks) { + byte[][] buffer = new byte[][]{ + { + 0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x61, 0x74 + } + }; + + int length = 0; + + for (Mp4TrackChunk chunk : chunks) { + length += chunk.moof.traf.trun.chunkSize; + } + + ByteBuffer.wrap(buffer[0]).putInt(length + 8); + + return buffer; + } + + private byte[][] make_ftyp() { + return new byte[][]{ + { + 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00, + 0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x36, 0x69, 0x73, 0x6F, 0x32 + } + }; + } + + private byte[][] make_mvhd() { + byte[][] buffer = new byte[DIMENSIONAL_FIVE][]; + + buffer[0] = new byte[]{ + 0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00 + }; + buffer[1] = new byte[28]; + buffer[2] = new byte[]{ + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values + // default matrix + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00 + }; + buffer[3] = new byte[24];// predefined + buffer[4] = ByteBuffer.allocate(4).putInt(infoTracks.length + 1).array(); + + long longestTrack = 0; + + for (Mp4Track track : infoTracks) { + long tmp = (long) ((track.trak.tkhd.duration / (double) track.trak.mdia_mdhd_timeScale) * DEFAULT_TIMESCALE); + if (tmp > longestTrack) { + longestTrack = tmp; + } + } + + ByteBuffer.wrap(buffer[1]) + .putLong(time) + .putLong(time) + .putInt(DEFAULT_TIMESCALE) + .putLong(longestTrack); + + return buffer; + } + + private byte[][] make_trak(int trackId, Trak trak) throws RuntimeException { + if (trak.tkhd.matrix.length != 36) { + throw new RuntimeException("bad track matrix length (expected 36)"); + } + + byte[][] buffer = new byte[DIMENSIONAL_FIVE][]; + + buffer[0] = new byte[]{ + 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header + 0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header + }; + buffer[1] = new byte[48]; + buffer[2] = trak.tkhd.matrix; + buffer[3] = new byte[8]; + buffer[4] = trak.mdia; + + ByteBuffer set = ByteBuffer.wrap(buffer[1]); + set.putLong(time); + set.putLong(time); + set.putInt(trackId); + set.position(24); + set.putLong(trak.tkhd.duration); + set.position(40); + set.putShort(trak.tkhd.bLayer); + set.putShort(trak.tkhd.bAlternateGroup); + set.putShort(trak.tkhd.bVolume); + + ByteBuffer.wrap(buffer[3]) + .putInt(trak.tkhd.bWidth) + .putInt(trak.tkhd.bHeight); + + return lengthFor(buffer); + } + + private byte[][] make_moov() throws RuntimeException { + int pos = 1; + byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length) + (DIMENSIONAL_FIVE * infoTracks.length) + DIMENSIONAL_FIVE + 1][]; + + buffer[0] = new byte[]{ + 0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76 + }; + + for (byte[] buff : make_mvhd()) { + buffer[pos++] = buff; + } + + for (int i = 0; i < infoTracks.length; i++) { + for (byte[] buff : make_trak(i + 1, infoTracks[i].trak)) { + buffer[pos++] = buff; + } + } + + buffer[pos] = new byte[]{ + 0x00, 0x00, 0x00, 0x00, 0x6D, 0x76, 0x65, 0x78 + }; + + ByteBuffer.wrap(buffer[pos++]).putInt((infoTracks.length * DEFAULT_TREX_SIZE) + 8); + + for (int i = 0; i < infoTracks.length; i++) { + for (byte[] buff : make_trex(i + 1, infoTracks[i].trex)) { + buffer[pos++] = buff; + } + } + + // default udta + buffer[pos] = new byte[]{ + 0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, + 0x1F, (byte) 0xA9, 0x63, 0x6D, 0x74, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, + 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string + }; + + return lengthFor(buffer); + } + + private byte[][] make_trex(int trackId, Trex trex) { + byte[][] buffer = new byte[][]{ + { + 0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00 + }, + new byte[20] + }; + + ByteBuffer.wrap(buffer[1]) + .putInt(trackId) + .putInt(trex.defaultSampleDescriptionIndex) + .putInt(trex.defaultSampleDuration) + .putInt(trex.defaultSampleSize) + .putInt(trex.defaultSampleFlags); + + return buffer; + } + + private byte[][] make_tfra(int trackId, List times, List moofOffsets) { + int entryCount = times.size() - 1; + byte[][] buffer = new byte[DIMENSIONAL_TWO][]; + buffer[0] = new byte[]{ + 0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x72, 0x61, 0x01, 0x00, 0x00, 0x00 + }; + buffer[1] = new byte[12 + ((16 + TFRA_TTS_DEFAULT.length) * entryCount)]; + + ByteBuffer set = ByteBuffer.wrap(buffer[1]); + set.putInt(trackId); + set.position(8); + set.putInt(entryCount); + + long decodeTime = 0; + + for (int i = 0; i < entryCount; i++) { + decodeTime += times.get(i); + set.putLong(decodeTime); + set.putLong(moofOffsets.get(i)); + set.put(TFRA_TTS_DEFAULT);// default values: traf number/trun number/sample number + } + + return lengthFor(buffer); + } + + private byte[][] make_mfra() { + byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length)][]; + buffer[0] = new byte[]{ + 0x00, 0x00, 0x00, 0x00, 0x6D, 0x66, 0x72, 0x61 + }; + int pos = 1; + + for (int i = 0; i < infoTracks.length; i++) { + for (byte[] buff : make_tfra(i + 1, chunkTimes.get(i), moofOffsets)) { + buffer[pos++] = buff; + } + } + + buffer[pos] = new byte[]{// mfro + 0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x72, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + lengthFor(buffer); + + ByteBuffer set = ByteBuffer.wrap(buffer[pos]); + set.position(12); + set.put(buffer[0], 0, 4); + + return buffer; + + } + + private byte[][] make_sidx(int internalTrackId, long firstOffset) { + List times = chunkTimes.get(internalTrackId); + int count = times.size() - 1;// the first item is ignored (composition time) + + if (count > 65535) { + throw new OutOfMemoryError("to many fragments. sidx limit is 65535, found " + String.valueOf(count)); + } + + byte[][] buffer = new byte[][]{ + new byte[]{ + 0x00, 0x00, 0x00, 0x00, 0x73, 0x69, 0x64, 0x78, 0x01, 0x00, 0x00, 0x00 + }, + new byte[calcSidxBodySize(count)] + }; + + lengthFor(buffer); + + ByteBuffer set = ByteBuffer.wrap(buffer[1]); + set.putInt(internalTrackId + 1); + set.putInt(infoTracks[internalTrackId].trak.mdia_mdhd_timeScale); + set.putLong(0); + set.putLong(firstOffset - ByteBuffer.wrap(buffer[0]).getInt()); + set.putInt(0xFFFF & count);// unsigned + + int i = 0; + while (i < count) { + set.putInt(fragSizes.get(i) & 0x7fffffff);// default reference type is 0 + set.putInt(times.get(i + 1)); + set.putInt(0x90000000);// default SAP settings + i++; + } + + return buffer; + } + + private byte[][] make_free(int totalSize) { + return lengthFor(new byte[][]{ + new byte[]{0x00, 0x00, 0x00, 0x00, 0x66, 0x72, 0x65, 0x65}, + new byte[totalSize - 8]// this is waste of RAM + }); + + } + +// + + class TrunExtra { + + ByteBuffer byteBuffer; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java b/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java new file mode 100644 index 000000000..26aaf49a5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java @@ -0,0 +1,370 @@ +package org.schabi.newpipe.streams; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.text.ParseException; +import java.util.Locale; + +import org.schabi.newpipe.streams.io.SharpStream; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPathExpressionException; + +/** + * @author kapodamy + */ +public class SubtitleConverter { + private static final String NEW_LINE = "\r\n"; + + public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines + ) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException { + + final FrameWriter callback = new FrameWriter() { + int frameIndex = 0; + final Charset charset = Charset.forName("utf-8"); + + @Override + public void yield(SubtitleFrame frame) throws IOException { + if (ignoreEmptyFrames && frame.isEmptyText()) { + return; + } + out.write(String.valueOf(frameIndex++).getBytes(charset)); + out.write(NEW_LINE.getBytes(charset)); + out.write(getTime(frame.start, true).getBytes(charset)); + out.write(" --> ".getBytes(charset)); + out.write(getTime(frame.end, true).getBytes(charset)); + out.write(NEW_LINE.getBytes(charset)); + out.write(frame.text.getBytes(charset)); + out.write(NEW_LINE.getBytes(charset)); + out.write(NEW_LINE.getBytes(charset)); + } + }; + + read_xml_based(in, callback, detectYoutubeDuplicateLines, + "tt", "xmlns", "http://www.w3.org/ns/ttml", + new String[]{"timedtext", "head", "wp"}, + new String[]{"body", "div", "p"}, + "begin", "end", true + ); + } + + private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines, + String root, String formatAttr, String formatVersion, String[] cuePath, String[] framePath, + String timeAttr, String durationAttr, boolean hasTimestamp + ) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException { + /* + * XML based subtitles parser with BASIC support + * multiple CUE is not supported + * styling is not supported + * tag timestamps (in auto-generated subtitles) are not supported, maybe in the future + * also TimestampTagOption enum is not applicable + * Language parsing is not supported + */ + + byte[] buffer = new byte[source.available()]; + source.read(buffer); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document xml = builder.parse(new ByteArrayInputStream(buffer)); + + String attr; + + // get the format version or namespace + Element node = xml.getDocumentElement(); + + if (node == null) { + throw new ParseException("Can't get the format version. ¿wrong namespace?", -1); + } else if (!node.getNodeName().equals(root)) { + throw new ParseException("Invalid root", -1); + } + + if (formatAttr.equals("xmlns")) { + if (!node.getNamespaceURI().equals(formatVersion)) { + throw new UnsupportedOperationException("Expected xml namespace: " + formatVersion); + } + } else { + attr = node.getAttributeNS(formatVersion, formatAttr); + if (attr == null) { + throw new ParseException("Can't get the format attribute", -1); + } + if (!attr.equals(formatVersion)) { + throw new ParseException("Invalid format version : " + attr, -1); + } + } + + NodeList node_list; + + int line_break = 0;// Maximum characters per line if present (valid for TranScript v3) + + if (!hasTimestamp) { + node_list = selectNodes(xml, cuePath, formatVersion); + + if (node_list != null) { + // if the subtitle has multiple CUEs, use the highest value + for (int i = 0; i < node_list.getLength(); i++) { + try { + int tmp = Integer.parseInt(((Element) node_list.item(i)).getAttributeNS(formatVersion, "ah")); + if (tmp > line_break) { + line_break = tmp; + } + } catch (Exception err) { + } + } + } + } + + // parse every frame + node_list = selectNodes(xml, framePath, formatVersion); + + if (node_list == null) { + return;// no frames detected + } + + int fs_ff = -1;// first timestamp of first frame + boolean limit_lines = false; + + for (int i = 0; i < node_list.getLength(); i++) { + Element elem = (Element) node_list.item(i); + SubtitleFrame obj = new SubtitleFrame(); + obj.text = elem.getTextContent(); + + attr = elem.getAttribute(timeAttr);// ¡this cant be null! + obj.start = hasTimestamp ? parseTimestamp(attr) : Integer.parseInt(attr); + + attr = elem.getAttribute(durationAttr); + if (obj.text == null || attr == null) { + continue;// normally is a blank line (on auto-generated subtitles) ignore + } + + if (hasTimestamp) { + obj.end = parseTimestamp(attr); + + if (detectYoutubeDuplicateLines) { + if (limit_lines) { + int swap = obj.end; + obj.end = fs_ff; + fs_ff = swap; + } else { + if (fs_ff < 0) { + fs_ff = obj.end; + } else { + if (fs_ff < obj.start) { + limit_lines = true;// the subtitles has duplicated lines + } else { + detectYoutubeDuplicateLines = false; + } + } + } + } + } else { + obj.end = obj.start + Integer.parseInt(attr); + } + + if (/*node.getAttribute("w").equals("1") &&*/line_break > 1 && obj.text.length() > line_break) { + + // implement auto line breaking (once) + StringBuilder text = new StringBuilder(obj.text); + obj.text = null; + + switch (text.charAt(line_break)) { + case ' ': + case '\t': + putBreakAt(line_break, text); + break; + default:// find the word start position + for (int j = line_break - 1; j > 0; j--) { + switch (text.charAt(j)) { + case ' ': + case '\t': + putBreakAt(j, text); + j = -1; + break; + case '\r': + case '\n': + j = -1;// long word, just ignore + break; + } + } + break; + } + + obj.text = text.toString();// set the processed text + } + + callback.yield(obj); + } + } + + private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) throws XPathExpressionException { + Element ref = xml.getDocumentElement(); + + for (int i = 0; i < path.length - 1; i++) { + NodeList nodes = ref.getChildNodes(); + if (nodes.getLength() < 1) { + return null; + } + + Element elem; + for (int j = 0; j < nodes.getLength(); j++) { + if (nodes.item(j).getNodeType() == Node.ELEMENT_NODE) { + elem = (Element) nodes.item(j); + if (elem.getNodeName().equals(path[i]) && elem.getNamespaceURI().equals(namespaceUri)) { + ref = elem; + break; + } + } + } + } + + return ref.getElementsByTagNameNS(namespaceUri, path[path.length - 1]); + } + + private static int parseTimestamp(String multiImpl) throws NumberFormatException, ParseException { + if (multiImpl.length() < 1) { + return 0; + } else if (multiImpl.length() == 1) { + return Integer.parseInt(multiImpl) * 1000;// ¡this must be a number in seconds! + } + + // detect wallclock-time + if (multiImpl.startsWith("wallclock(")) { + throw new UnsupportedOperationException("Parsing wallclock timestamp is not implemented"); + } + + // detect offset-time + if (multiImpl.indexOf(':') < 0) { + int multiplier = 1000; + char metric = multiImpl.charAt(multiImpl.length() - 1); + switch (metric) { + case 'h': + multiplier *= 3600000; + break; + case 'm': + multiplier *= 60000; + break; + case 's': + if (multiImpl.charAt(multiImpl.length() - 2) == 'm') { + multiplier = 1;// ms + } + break; + default: + if (!Character.isDigit(metric)) { + throw new NumberFormatException("Invalid metric suffix found on : " + multiImpl); + } + metric = '\0'; + break; + } + try { + String offset_time = multiImpl; + + if (multiplier == 1) { + offset_time = offset_time.substring(0, offset_time.length() - 2); + } else if (metric != '\0') { + offset_time = offset_time.substring(0, offset_time.length() - 1); + } + + double time_metric_based = Double.parseDouble(offset_time); + if (Math.abs(time_metric_based) <= Double.MAX_VALUE) { + return (int) (time_metric_based * multiplier); + } + } catch (Exception err) { + throw new UnsupportedOperationException("Invalid or not implemented timestamp on: " + multiImpl); + } + } + + // detect clock-time + int time = 0; + String[] units = multiImpl.split(":"); + + if (units.length < 3) { + throw new ParseException("Invalid clock-time timestamp", -1); + } + + time += Integer.parseInt(units[0]) * 3600000;// hours + time += Integer.parseInt(units[1]) * 60000;//minutes + time += Float.parseFloat(units[2]) * 1000f;// seconds and milliseconds (if present) + + // frames and sub-frames are ignored (not implemented) + // time += units[3] * fps; + return time; + } + + private static void putBreakAt(int idx, StringBuilder str) { + // this should be optimized at compile time + + if (NEW_LINE.length() > 1) { + str.delete(idx, idx + 1);// remove after replace + str.insert(idx, NEW_LINE); + } else { + str.setCharAt(idx, NEW_LINE.charAt(0)); + } + } + + private static String getTime(int time, boolean comma) { + // cast every value to integer to avoid auto-round in ToString("00"). + StringBuilder str = new StringBuilder(12); + str.append(numberToString(time / 1000 / 3600, 2));// hours + str.append(':'); + str.append(numberToString(time / 1000 / 60 % 60, 2));// minutes + str.append(':'); + str.append(numberToString(time / 1000 % 60, 2));// seconds + str.append(comma ? ',' : '.'); + str.append(numberToString(time % 1000, 3));// miliseconds + + return str.toString(); + } + + private static String numberToString(int nro, int pad) { + return String.format(Locale.ENGLISH, "%0".concat(String.valueOf(pad)).concat("d"), nro); + } + + + /****************** + * helper classes * + ******************/ + + private interface FrameWriter { + + void yield(SubtitleFrame frame) throws IOException; + } + + private static class SubtitleFrame { + //Java no support unsigned int + + public int end; + public int start; + public String text = ""; + + private boolean isEmptyText() { + if (text == null) { + return true; + } + + for (int i = 0; i < text.length(); i++) { + switch (text.charAt(i)) { + case ' ': + case '\t': + case '\r': + case '\n': + break; + default: + return false; + } + } + + return true; + } + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java b/app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java new file mode 100644 index 000000000..86eb5ff4f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java @@ -0,0 +1,65 @@ +package org.schabi.newpipe.streams; + +import java.io.InputStream; +import java.io.IOException; + +public class TrackDataChunk extends InputStream { + + private final DataReader base; + private int size; + + public TrackDataChunk(DataReader base, int size) { + this.base = base; + this.size = size; + } + + @Override + public int read() throws IOException { + if (size < 1) { + return -1; + } + + int res = base.read(); + + if (res >= 0) { + size--; + } + + return res; + } + + @Override + public int read(byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); + } + + @Override + public int read(byte[] buffer, int offset, int count) throws IOException { + count = Math.min(size, count); + int read = base.read(buffer, offset, count); + size -= count; + return read; + } + + @Override + public long skip(long amount) throws IOException { + long res = base.skipBytes(Math.min(amount, size)); + size -= res; + return res; + } + + @Override + public int available() { + return size; + } + + @Override + public void close() { + size = 0; + } + + @Override + public boolean markSupported() { + return false; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java new file mode 100644 index 000000000..f61ef14c5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java @@ -0,0 +1,507 @@ +package org.schabi.newpipe.streams; + +import java.io.EOFException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.NoSuchElementException; +import java.util.Objects; + +import org.schabi.newpipe.streams.io.SharpStream; + +/** + * + * @author kapodamy + */ +public class WebMReader { + + // + private final static int ID_EMBL = 0x0A45DFA3; + private final static int ID_EMBLReadVersion = 0x02F7; + private final static int ID_EMBLDocType = 0x0282; + private final static int ID_EMBLDocTypeReadVersion = 0x0285; + + private final static int ID_Segment = 0x08538067; + + private final static int ID_Info = 0x0549A966; + private final static int ID_TimecodeScale = 0x0AD7B1; + private final static int ID_Duration = 0x489; + + private final static int ID_Tracks = 0x0654AE6B; + private final static int ID_TrackEntry = 0x2E; + private final static int ID_TrackNumber = 0x57; + private final static int ID_TrackType = 0x03; + private final static int ID_CodecID = 0x06; + private final static int ID_CodecPrivate = 0x23A2; + private final static int ID_Video = 0x60; + private final static int ID_Audio = 0x61; + private final static int ID_DefaultDuration = 0x3E383; + private final static int ID_FlagLacing = 0x1C; + + private final static int ID_Cluster = 0x0F43B675; + private final static int ID_Timecode = 0x67; + private final static int ID_SimpleBlock = 0x23; +// + + public enum TrackKind { + Audio/*2*/, Video/*1*/, Other + } + + private DataReader stream; + private Segment segment; + private WebMTrack[] tracks; + private int selectedTrack; + private boolean done; + private boolean firstSegment; + + public WebMReader(SharpStream source) { + this.stream = new DataReader(source); + } + + public void parse() throws IOException { + Element elem = readElement(ID_EMBL); + if (!readEbml(elem, 1, 2)) { + throw new UnsupportedOperationException("Unsupported EBML data (WebM)"); + } + ensure(elem); + + elem = untilElement(null, ID_Segment); + if (elem == null) { + throw new IOException("Fragment element not found"); + } + segment = readSegment(elem, 0, true); + tracks = segment.tracks; + selectedTrack = -1; + done = false; + firstSegment = true; + } + + public WebMTrack[] getAvailableTracks() { + return tracks; + } + + public WebMTrack selectTrack(int index) { + selectedTrack = index; + return tracks[index]; + } + + public Segment getNextSegment() throws IOException { + if (done) { + return null; + } + + if (firstSegment && segment != null) { + firstSegment = false; + return segment; + } + + ensure(segment.ref); + + Element elem = untilElement(null, ID_Segment); + if (elem == null) { + done = true; + return null; + } + segment = readSegment(elem, 0, false); + + return segment; + } + + // + private long readNumber(Element parent) throws IOException { + int length = (int) parent.contentSize; + long value = 0; + while (length-- > 0) { + int read = stream.read(); + if (read == -1) { + throw new EOFException(); + } + value = (value << 8) | read; + } + return value; + } + + private String readString(Element parent) throws IOException { + return new String(readBlob(parent), "utf-8"); + } + + private byte[] readBlob(Element parent) throws IOException { + long length = parent.contentSize; + byte[] buffer = new byte[(int) length]; + int read = stream.read(buffer); + if (read < length) { + throw new EOFException(); + } + return buffer; + } + + private long readEncodedNumber() throws IOException { + int value = stream.read(); + + if (value > 0) { + byte size = 1; + int mask = 0x80; + + while (size < 9) { + if ((value & mask) == mask) { + mask = 0xFF; + mask >>= size; + + long number = value & mask; + + for (int i = 1; i < size; i++) { + value = stream.read(); + number <<= 8; + number |= value; + } + + return number; + } + + mask >>= 1; + size++; + } + } + + throw new IOException("Invalid encoded length"); + } + + private Element readElement() throws IOException { + Element elem = new Element(); + elem.offset = stream.position(); + elem.type = (int) readEncodedNumber(); + elem.contentSize = readEncodedNumber(); + elem.size = elem.contentSize + stream.position() - elem.offset; + + return elem; + } + + private Element readElement(int expected) throws IOException { + Element elem = readElement(); + if (expected != 0 && elem.type != expected) { + throw new NoSuchElementException("expected " + elementID(expected) + " found " + elementID(elem.type)); + } + + return elem; + } + + private Element untilElement(Element ref, int... expected) throws IOException { + Element elem; + while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) { + elem = readElement(); + for (int type : expected) { + if (elem.type == type) { + return elem; + } + } + ensure(elem); + } + + return null; + } + + private String elementID(long type) { + return "0x".concat(Long.toHexString(type)); + } + + private void ensure(Element ref) throws IOException { + long skip = (ref.offset + ref.size) - stream.position(); + + if (skip == 0) { + return; + } else if (skip < 0) { + throw new EOFException(String.format( + "parser go beyond limits of the Element. type=%s offset=%s size=%s position=%s", + elementID(ref.type), ref.offset, ref.size, stream.position() + )); + } + + stream.skipBytes(skip); + } +// + + // + private boolean readEbml(Element ref, int minReadVersion, int minDocTypeVersion) throws IOException { + Element elem = untilElement(ref, ID_EMBLReadVersion); + if (elem == null) { + return false; + } + if (readNumber(elem) > minReadVersion) { + return false; + } + + elem = untilElement(ref, ID_EMBLDocType); + if (elem == null) { + return false; + } + if (!readString(elem).equals("webm")) { + return false; + } + elem = untilElement(ref, ID_EMBLDocTypeReadVersion); + + return elem != null && readNumber(elem) <= minDocTypeVersion; + } + + private Info readInfo(Element ref) throws IOException { + Element elem; + Info info = new Info(); + + while ((elem = untilElement(ref, ID_TimecodeScale, ID_Duration)) != null) { + switch (elem.type) { + case ID_TimecodeScale: + info.timecodeScale = readNumber(elem); + break; + case ID_Duration: + info.duration = readNumber(elem); + break; + } + ensure(elem); + } + + if (info.timecodeScale == 0) { + throw new NoSuchElementException("Element Timecode not found"); + } + + return info; + } + + private Segment readSegment(Element ref, int trackLacingExpected, boolean metadataExpected) throws IOException { + Segment obj = new Segment(ref); + Element elem; + while ((elem = untilElement(ref, ID_Info, ID_Tracks, ID_Cluster)) != null) { + if (elem.type == ID_Cluster) { + obj.currentCluster = elem; + break; + } + switch (elem.type) { + case ID_Info: + obj.info = readInfo(elem); + break; + case ID_Tracks: + obj.tracks = readTracks(elem, trackLacingExpected); + break; + } + ensure(elem); + } + + if (metadataExpected && (obj.info == null || obj.tracks == null)) { + throw new RuntimeException("Cluster element found without Info and/or Tracks element at position " + String.valueOf(ref.offset)); + } + + return obj; + } + + private WebMTrack[] readTracks(Element ref, int lacingExpected) throws IOException { + ArrayList trackEntries = new ArrayList<>(2); + Element elem_trackEntry; + + while ((elem_trackEntry = untilElement(ref, ID_TrackEntry)) != null) { + WebMTrack entry = new WebMTrack(); + boolean drop = false; + Element elem; + while ((elem = untilElement(elem_trackEntry, + ID_TrackNumber, ID_TrackType, ID_CodecID, ID_CodecPrivate, ID_FlagLacing, ID_DefaultDuration, ID_Audio, ID_Video + )) != null) { + switch (elem.type) { + case ID_TrackNumber: + entry.trackNumber = readNumber(elem); + break; + case ID_TrackType: + entry.trackType = (int)readNumber(elem); + break; + case ID_CodecID: + entry.codecId = readString(elem); + break; + case ID_CodecPrivate: + entry.codecPrivate = readBlob(elem); + break; + case ID_Audio: + case ID_Video: + entry.bMetadata = readBlob(elem); + break; + case ID_DefaultDuration: + entry.defaultDuration = readNumber(elem); + break; + case ID_FlagLacing: + drop = readNumber(elem) != lacingExpected; + break; + default: + System.out.println(); + break; + } + ensure(elem); + } + if (!drop) { + trackEntries.add(entry); + } + ensure(elem_trackEntry); + } + + WebMTrack[] entries = new WebMTrack[trackEntries.size()]; + trackEntries.toArray(entries); + + for (WebMTrack entry : entries) { + switch (entry.trackType) { + case 1: + entry.kind = TrackKind.Video; + break; + case 2: + entry.kind = TrackKind.Audio; + break; + default: + entry.kind = TrackKind.Other; + break; + } + } + + return entries; + } + + private SimpleBlock readSimpleBlock(Element ref) throws IOException { + SimpleBlock obj = new SimpleBlock(ref); + obj.dataSize = stream.position(); + obj.trackNumber = readEncodedNumber(); + obj.relativeTimeCode = stream.readShort(); + obj.flags = (byte) stream.read(); + obj.dataSize = (ref.offset + ref.size) - stream.position(); + + if (obj.dataSize < 0) { + throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize)); + } + return obj; + } + + private Cluster readCluster(Element ref) throws IOException { + Cluster obj = new Cluster(ref); + + Element elem = untilElement(ref, ID_Timecode); + if (elem == null) { + throw new NoSuchElementException("Cluster at " + String.valueOf(ref.offset) + " without Timecode element"); + } + obj.timecode = readNumber(elem); + + return obj; + } +// + + // + class Element { + + int type; + long offset; + long contentSize; + long size; + } + + public class Info { + + public long timecodeScale; + public long duration; + } + + public class WebMTrack { + + public long trackNumber; + protected int trackType; + public String codecId; + public byte[] codecPrivate; + public byte[] bMetadata; + public TrackKind kind; + public long defaultDuration; + } + + public class Segment { + + Segment(Element ref) { + this.ref = ref; + this.firstClusterInSegment = true; + } + + public Info info; + WebMTrack[] tracks; + private Element currentCluster; + private final Element ref; + boolean firstClusterInSegment; + + public Cluster getNextCluster() throws IOException { + if (done) { + return null; + } + if (firstClusterInSegment && segment.currentCluster != null) { + firstClusterInSegment = false; + return readCluster(segment.currentCluster); + } + ensure(segment.currentCluster); + + Element elem = untilElement(segment.ref, ID_Cluster); + if (elem == null) { + return null; + } + + segment.currentCluster = elem; + + return readCluster(segment.currentCluster); + } + } + + public class SimpleBlock { + + public TrackDataChunk data; + + SimpleBlock(Element ref) { + this.ref = ref; + } + + public long trackNumber; + public short relativeTimeCode; + public byte flags; + public long dataSize; + private final Element ref; + + public boolean isKeyframe() { + return (flags & 0x80) == 0x80; + } + } + + public class Cluster { + + Element ref; + SimpleBlock currentSimpleBlock = null; + public long timecode; + + Cluster(Element ref) { + this.ref = ref; + } + + boolean check() { + return stream.position() >= (ref.offset + ref.size); + } + + public SimpleBlock getNextSimpleBlock() throws IOException { + if (check()) { + return null; + } + if (currentSimpleBlock != null) { + ensure(currentSimpleBlock.ref); + } + + while (!check()) { + Element elem = untilElement(ref, ID_SimpleBlock); + if (elem == null) { + return null; + } + + currentSimpleBlock = readSimpleBlock(elem); + if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) { + currentSimpleBlock.data = new TrackDataChunk(stream, (int) currentSimpleBlock.dataSize); + return currentSimpleBlock; + } + + ensure(elem); + } + + return null; + } + + } +// +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java new file mode 100644 index 000000000..ea038c607 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java @@ -0,0 +1,728 @@ +package org.schabi.newpipe.streams; + +import org.schabi.newpipe.streams.WebMReader.Cluster; +import org.schabi.newpipe.streams.WebMReader.Segment; +import org.schabi.newpipe.streams.WebMReader.SimpleBlock; +import org.schabi.newpipe.streams.WebMReader.WebMTrack; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +import org.schabi.newpipe.streams.io.SharpStream; + +/** + * + * @author kapodamy + */ +public class WebMWriter { + + private final static int BUFFER_SIZE = 8 * 1024; + private final static int DEFAULT_TIMECODE_SCALE = 1000000; + private final static int INTERV = 100;// 100ms on 1000000us timecode scale + private final static int DEFAULT_CUES_EACH_MS = 5000;// 100ms on 1000000us timecode scale + + private WebMReader.WebMTrack[] infoTracks; + private SharpStream[] sourceTracks; + + private WebMReader[] readers; + + private boolean done = false; + private boolean parsed = false; + + private long written = 0; + + private Segment[] readersSegment; + private Cluster[] readersCluter; + + private int[] predefinedDurations; + + private byte[] outBuffer; + + public WebMWriter(SharpStream... source) { + sourceTracks = source; + readers = new WebMReader[sourceTracks.length]; + infoTracks = new WebMTrack[sourceTracks.length]; + outBuffer = new byte[BUFFER_SIZE]; + } + + public WebMTrack[] getTracksFromSource(int sourceIndex) throws IllegalStateException { + if (done) { + throw new IllegalStateException("already done"); + } + if (!parsed) { + throw new IllegalStateException("All sources must be parsed first"); + } + + return readers[sourceIndex].getAvailableTracks(); + } + + public void parseSources() throws IOException, IllegalStateException { + if (done) { + throw new IllegalStateException("already done"); + } + if (parsed) { + throw new IllegalStateException("already parsed"); + } + + try { + for (int i = 0; i < readers.length; i++) { + readers[i] = new WebMReader(sourceTracks[i]); + readers[i].parse(); + } + + } finally { + parsed = true; + } + } + + public void selectTracks(int... trackIndex) throws IOException { + try { + readersSegment = new Segment[readers.length]; + readersCluter = new Cluster[readers.length]; + predefinedDurations = new int[readers.length]; + + for (int i = 0; i < readers.length; i++) { + infoTracks[i] = readers[i].selectTrack(trackIndex[i]); + predefinedDurations[i] = -1; + readersSegment[i] = readers[i].getNextSegment(); + } + } finally { + parsed = true; + } + } + + public long getBytesWritten() { + return written; + } + + public boolean isDone() { + return done; + } + + public boolean isParsed() { + return parsed; + } + + public void close() { + done = true; + parsed = true; + + for (SharpStream src : sourceTracks) { + src.dispose(); + } + + sourceTracks = null; + readers = null; + infoTracks = null; + readersSegment = null; + readersCluter = null; + outBuffer = null; + } + + public void build(SharpStream out) throws IOException, RuntimeException { + if (!out.canRewind()) { + throw new IOException("The output stream must be allow seek"); + } + + makeEBML(out); + + long offsetSegmentSizeSet = written + 5; + long offsetInfoDurationSet = written + 94; + long offsetClusterSet = written + 58; + long offsetCuesSet = written + 75; + + ArrayList listBuffer = new ArrayList<>(4); + + /* segment */ + listBuffer.add(new byte[]{ + 0x18, 0x53, (byte) 0x80, 0x67, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size + }); + + long baseSegmentOffset = written + listBuffer.get(0).length; + + /* seek head */ + listBuffer.add(new byte[]{ + 0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe, + 0x4d, (byte) 0xbb, (byte) 0x8b, + 0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53, + (byte) 0xac, (byte) 0x81, /*info offset*/ 0x43, + 0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab, + (byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81, + /*tracks offset*/ 0x6a, + 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f, + 0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00, + 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53, + (byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00 + }); + + /* info */ + listBuffer.add(new byte[]{ + 0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1 + }); + listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true));// this value MUST NOT exceed 4 bytes + listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84, + 0x00, 0x00, 0x00, 0x00,// info.duration + + /* MuxingApp */ + 0x4d, (byte) 0x80, (byte) 0x87, 0x4E, + 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string + + /* WritingApp */ + 0x57, 0x41, (byte) 0x87, 0x4E, + 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string + }); + + /* tracks */ + listBuffer.addAll(makeTracks()); + + for (byte[] buff : listBuffer) { + dump(buff, out); + } + + // reserve space for Cues element, but is a waste of space (actually is 64 KiB) + // TODO: better Cue maker + long cueReservedOffset = written; + dump(new byte[]{(byte) 0xec, 0x20, (byte) 0xff, (byte) 0xfb}, out); + int reserved = (1024 * 63) - 4; + while (reserved > 0) { + int write = Math.min(reserved, outBuffer.length); + out.write(outBuffer, 0, write); + reserved -= write; + written += write; + } + + // Select a track for the cue + int cuesForTrackId = selectTrackForCue(); + long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0; + ArrayList keyFrames = new ArrayList<>(32); + + //ArrayList chunks = new ArrayList<>(readers.length); + ArrayList clusterOffsets = new ArrayList<>(32); + ArrayList clusterSizes = new ArrayList<>(32); + + long duration = 0; + int durationFromTrackId = 0; + + byte[] bTimecode = makeTimecode(0); + + int firstClusterOffset = (int) written; + long currentClusterOffset = makeCluster(out, bTimecode, 0, clusterOffsets, clusterSizes); + + long baseTimecode = 0; + long limitTimecode = -1; + int limitTimecodeByTrackId = cuesForTrackId; + + int blockWritten = Integer.MAX_VALUE; + + int newClusterByTrackId = -1; + + while (blockWritten > 0) { + blockWritten = 0; + int i = 0; + while (i < readers.length) { + Block bloq = getNextBlockFrom(i); + if (bloq == null) { + i++; + continue; + } + + if (bloq.data == null) { + blockWritten = 1;// fake block + newClusterByTrackId = i; + i++; + continue; + } + + if (newClusterByTrackId == i) { + limitTimecodeByTrackId = i; + newClusterByTrackId = -1; + baseTimecode = bloq.absoluteTimecode; + limitTimecode = baseTimecode + INTERV; + bTimecode = makeTimecode(baseTimecode); + currentClusterOffset = makeCluster(out, bTimecode, currentClusterOffset, clusterOffsets, clusterSizes); + } + + if (cuesForTrackId == i) { + if ((nextCueTime > -1 && bloq.absoluteTimecode >= nextCueTime) || (nextCueTime < 0 && bloq.isKeyframe())) { + if (nextCueTime > -1) { + nextCueTime += DEFAULT_CUES_EACH_MS; + } + keyFrames.add( + new KeyFrame(baseSegmentOffset, currentClusterOffset - 7, written, bTimecode.length, bloq.absoluteTimecode) + ); + } + } + + writeBlock(out, bloq, baseTimecode); + blockWritten++; + + if (bloq.absoluteTimecode > duration) { + duration = bloq.absoluteTimecode; + durationFromTrackId = bloq.trackNumber; + } + + if (limitTimecode < 0) { + limitTimecode = bloq.absoluteTimecode + INTERV; + continue; + } + + if (bloq.absoluteTimecode >= limitTimecode) { + if (limitTimecodeByTrackId != i) { + limitTimecode += INTERV - (bloq.absoluteTimecode - limitTimecode); + } + i++; + } + } + } + + makeCluster(out, null, currentClusterOffset, null, clusterSizes); + + long segmentSize = written - offsetSegmentSizeSet - 7; + + // final step write offsets and sizes + out.rewind(); + written = 0; + + skipTo(out, offsetSegmentSizeSet); + writeLong(out, segmentSize); + + if (predefinedDurations[durationFromTrackId] > -1) { + duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method + } + skipTo(out, offsetInfoDurationSet); + writeFloat(out, duration); + + firstClusterOffset -= baseSegmentOffset; + skipTo(out, offsetClusterSet); + writeInt(out, firstClusterOffset); + + skipTo(out, cueReservedOffset); + + /* Cue */ + dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out); + + for (KeyFrame keyFrame : keyFrames) { + for (byte[] buffer : makeCuePoint(cuesForTrackId, keyFrame)) { + dump(buffer, out); + if (written >= (cueReservedOffset + 65535 - 16)) { + throw new IOException("Too many Cues"); + } + } + } + short cueSize = (short) (written - cueReservedOffset - 7); + + /* EBML Void */ + ByteBuffer voidBuffer = ByteBuffer.allocate(4); + voidBuffer.putShort((short) 0xec20); + voidBuffer.putShort((short) (firstClusterOffset - written - 4)); + dump(voidBuffer.array(), out); + + out.rewind(); + written = 0; + + skipTo(out, offsetCuesSet); + writeInt(out, (int) (cueReservedOffset - baseSegmentOffset)); + + skipTo(out, cueReservedOffset + 5); + writeShort(out, cueSize); + + for (int i = 0; i < clusterSizes.size(); i++) { + skipTo(out, clusterOffsets.get(i)); + byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array(); + out.write(size, 1, 3); + written += 3; + } + } + + private Block getNextBlockFrom(int internalTrackId) throws IOException { + if (readersSegment[internalTrackId] == null) { + readersSegment[internalTrackId] = readers[internalTrackId].getNextSegment(); + if (readersSegment[internalTrackId] == null) { + return null;// no more blocks in the selected track + } + } + + if (readersCluter[internalTrackId] == null) { + readersCluter[internalTrackId] = readersSegment[internalTrackId].getNextCluster(); + if (readersCluter[internalTrackId] == null) { + readersSegment[internalTrackId] = null; + return getNextBlockFrom(internalTrackId); + } + } + + SimpleBlock res = readersCluter[internalTrackId].getNextSimpleBlock(); + if (res == null) { + readersCluter[internalTrackId] = null; + return new Block();// fake block to indicate the end of the cluster + } + + Block bloq = new Block(); + bloq.data = res.data; + bloq.dataSize = (int) res.dataSize; + bloq.trackNumber = internalTrackId; + bloq.flags = res.flags; + bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale, DEFAULT_TIMECODE_SCALE); + bloq.absoluteTimecode += readersCluter[internalTrackId].timecode; + + return bloq; + } + + private short convertTimecode(int time, long oldTimeScale, int newTimeScale) { + return (short) (time * (newTimeScale / oldTimeScale)); + } + + private void skipTo(SharpStream stream, long absoluteOffset) throws IOException { + absoluteOffset -= written; + written += absoluteOffset; + stream.skip(absoluteOffset); + } + + private void writeLong(SharpStream stream, long number) throws IOException { + byte[] buffer = ByteBuffer.allocate(DataReader.LONG_SIZE).putLong(number).array(); + stream.write(buffer, 1, buffer.length - 1); + written += buffer.length - 1; + } + + private void writeFloat(SharpStream stream, float number) throws IOException { + byte[] buffer = ByteBuffer.allocate(DataReader.FLOAT_SIZE).putFloat(number).array(); + dump(buffer, stream); + } + + private void writeShort(SharpStream stream, short number) throws IOException { + byte[] buffer = ByteBuffer.allocate(DataReader.SHORT_SIZE).putShort(number).array(); + dump(buffer, stream); + } + + private void writeInt(SharpStream stream, int number) throws IOException { + byte[] buffer = ByteBuffer.allocate(DataReader.INTEGER_SIZE).putInt(number).array(); + dump(buffer, stream); + } + + private void writeBlock(SharpStream stream, Block bloq, long clusterTimecode) throws IOException { + long relativeTimeCode = bloq.absoluteTimecode - clusterTimecode; + + if (relativeTimeCode < Short.MIN_VALUE || relativeTimeCode > Short.MAX_VALUE) { + throw new IndexOutOfBoundsException("SimpleBlock timecode overflow."); + } + + ArrayList listBuffer = new ArrayList<>(5); + listBuffer.add(new byte[]{(byte) 0xa3}); + listBuffer.add(null);// block size + listBuffer.add(encode(bloq.trackNumber + 1, false)); + listBuffer.add(ByteBuffer.allocate(DataReader.SHORT_SIZE).putShort((short) relativeTimeCode).array()); + listBuffer.add(new byte[]{bloq.flags}); + + int blockSize = bloq.dataSize; + for (int i = 2; i < listBuffer.size(); i++) { + blockSize += listBuffer.get(i).length; + } + listBuffer.set(1, encode(blockSize, false)); + + for (byte[] buff : listBuffer) { + dump(buff, stream); + } + + int read; + while ((read = bloq.data.read(outBuffer)) > 0) { + stream.write(outBuffer, 0, read); + written += read; + } + } + + private byte[] makeTimecode(long timecode) { + ByteBuffer buffer = ByteBuffer.allocate(9); + buffer.put((byte) 0xe7); + buffer.put(encode(timecode, true)); + + byte[] res = new byte[buffer.position()]; + System.arraycopy(buffer.array(), 0, res, 0, res.length); + + return res; + } + + private long makeCluster(SharpStream stream, byte[] bTimecode, long startOffset, ArrayList clusterOffsets, ArrayList clusterSizes) throws IOException { + if (startOffset > 0) { + clusterSizes.add((int) (written - startOffset));// size for last offset + } + + if (clusterOffsets != null) { + /* cluster */ + dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream); + clusterOffsets.add(written);// warning: max cluster size is 256 MiB + dump(new byte[]{0x20, 0x00, 0x00}, stream); + + startOffset = written;// size for the this cluster + + dump(bTimecode, stream); + + return startOffset; + } + + return -1; + } + + private void makeEBML(SharpStream stream) throws IOException { + // deafult values + dump(new byte[]{ + 0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01, + 0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04, + 0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77, + 0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02, + 0x42, (byte) 0x85, (byte) 0x81, 0x02 + }, stream); + } + + private ArrayList makeTracks() { + ArrayList buffer = new ArrayList<>(1); + buffer.add(new byte[]{0x16, 0x54, (byte) 0xae, 0x6b}); + buffer.add(null); + + for (int i = 0; i < infoTracks.length; i++) { + buffer.addAll(makeTrackEntry(i, infoTracks[i])); + } + + return lengthFor(buffer); + } + + private ArrayList makeTrackEntry(int internalTrackId, WebMTrack track) { + byte[] id = encode(internalTrackId + 1, true); + ArrayList buffer = new ArrayList<>(12); + + /* track */ + buffer.add(new byte[]{(byte) 0xae}); + buffer.add(null); + + /* track number */ + buffer.add(new byte[]{(byte) 0xd7}); + buffer.add(id); + + /* track uid */ + buffer.add(new byte[]{0x73, (byte) 0xc5}); + buffer.add(id); + + /* flag lacing */ + buffer.add(new byte[]{(byte) 0x9c, (byte) 0x81, 0x00}); + + /* lang */ + buffer.add(new byte[]{0x22, (byte) 0xb5, (byte) 0x9c, (byte) 0x83, 0x75, 0x6e, 0x64}); + + /* codec id */ + buffer.add(new byte[]{(byte) 0x86}); + buffer.addAll(encode(track.codecId)); + + /* type */ + buffer.add(new byte[]{(byte) 0x83}); + buffer.add(encode(track.trackType, true)); + + /* default duration */ + if (track.defaultDuration != 0) { + predefinedDurations[internalTrackId] = (int) Math.ceil(track.defaultDuration / (float) DEFAULT_TIMECODE_SCALE); + buffer.add(new byte[]{0x23, (byte) 0xe3, (byte) 0x83}); + buffer.add(encode(track.defaultDuration, true)); + } + + /* audio/video */ + if ((track.trackType == 1 || track.trackType == 2) && valid(track.bMetadata)) { + buffer.add(new byte[]{(byte) (track.trackType == 1 ? 0xe0 : 0xe1)}); + buffer.add(encode(track.bMetadata.length, false)); + buffer.add(track.bMetadata); + } + + /* codec private*/ + if (valid(track.codecPrivate)) { + buffer.add(new byte[]{0x63, (byte) 0xa2}); + buffer.add(encode(track.codecPrivate.length, false)); + buffer.add(track.codecPrivate); + } + + return lengthFor(buffer); + + } + + private ArrayList makeCuePoint(int internalTrackId, KeyFrame keyFrame) { + ArrayList buffer = new ArrayList<>(5); + + /* CuePoint */ + buffer.add(new byte[]{(byte) 0xbb}); + buffer.add(null); + + /* CueTime */ + buffer.add(new byte[]{(byte) 0xb3}); + buffer.add(encode(keyFrame.atTimecode, true)); + + /* CueTrackPosition */ + buffer.addAll(makeCueTrackPosition(internalTrackId, keyFrame)); + + return lengthFor(buffer); + } + + private ArrayList makeCueTrackPosition(int internalTrackId, KeyFrame keyFrame) { + ArrayList buffer = new ArrayList<>(8); + + /* CueTrackPositions */ + buffer.add(new byte[]{(byte) 0xb7}); + buffer.add(null); + + /* CueTrack */ + buffer.add(new byte[]{(byte) 0xf7}); + buffer.add(encode(internalTrackId + 1, true)); + + /* CueClusterPosition */ + buffer.add(new byte[]{(byte) 0xf1}); + buffer.add(encode(keyFrame.atCluster, true)); + + /* CueRelativePosition */ + if (keyFrame.atBlock > 0) { + buffer.add(new byte[]{(byte) 0xf0}); + buffer.add(encode(keyFrame.atBlock, true)); + } + + return lengthFor(buffer); + } + + private void dump(byte[] buffer, SharpStream stream) throws IOException { + stream.write(buffer); + written += buffer.length; + } + + private ArrayList lengthFor(ArrayList buffer) { + long size = 0; + for (int i = 2; i < buffer.size(); i++) { + size += buffer.get(i).length; + } + buffer.set(1, encode(size, false)); + return buffer; + } + + private byte[] encode(long number, boolean withLength) { + int length = -1; + for (int i = 1; i <= 7; i++) { + if (number < Math.pow(2, 7 * i)) { + length = i; + break; + } + } + + if (length < 1) { + throw new ArithmeticException("Can't encode a number of bigger than 7 bytes"); + } + + if (number == (Math.pow(2, 7 * length)) - 1) { + length++; + } + + int offset = withLength ? 1 : 0; + byte[] buffer = new byte[offset + length]; + long marker = (long) Math.floor((length - 1) / 8); + + for (int i = length - 1, mul = 1; i >= 0; i--, mul *= 0x100) { + long b = (long) Math.floor(number / mul); + if (!withLength && i == marker) { + b = b | (0x80 >> (length - 1)); + } + buffer[offset + i] = (byte) b; + } + + if (withLength) { + buffer[0] = (byte) (0x80 | length); + } + + return buffer; + } + + private ArrayList encode(String value) { + byte[] str; + try { + str = value.getBytes("utf-8"); + } catch (UnsupportedEncodingException err) { + str = value.getBytes(); + } + + ArrayList buffer = new ArrayList<>(2); + buffer.add(encode(str.length, false)); + buffer.add(str); + + return buffer; + } + + private boolean valid(byte[] buffer) { + return buffer != null && buffer.length > 0; + } + + private int selectTrackForCue() { + int i = 0; + int videoTracks = 0; + int audioTracks = 0; + + for (; i < infoTracks.length; i++) { + switch (infoTracks[i].trackType) { + case 1: + videoTracks++; + break; + case 2: + audioTracks++; + break; + } + } + + int kind; + if (audioTracks == infoTracks.length) { + kind = 2; + } else if (videoTracks == infoTracks.length) { + kind = 1; + } else if (videoTracks > 0) { + kind = 1; + } else if (audioTracks > 0) { + kind = 2; + } else { + return 0; + } + + // TODO: in the adove code, find and select the shortest track for the desired kind + for (i = 0; i < infoTracks.length; i++) { + if (kind == infoTracks[i].trackType) { + return i; + } + } + + return 0; + } + + class KeyFrame { + + KeyFrame(long segment, long cluster, long block, int bTimecodeLength, long timecode) { + atCluster = cluster - segment; + if ((block - bTimecodeLength) > cluster) { + atBlock = (int) (block - cluster); + } + atTimecode = timecode; + } + + long atCluster; + int atBlock; + long atTimecode; + } + + class Block { + + InputStream data; + int trackNumber; + byte flags; + int dataSize; + long absoluteTimecode; + + boolean isKeyframe() { + return (flags & 0x80) == 0x80; + } + + @Override + public String toString() { + return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, (flags & 0x80) == 0x80, absoluteTimecode); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java new file mode 100644 index 000000000..48bea06f6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java @@ -0,0 +1,47 @@ +package org.schabi.newpipe.streams.io; + +import java.io.IOException; + +/** + * based c# + */ +public abstract class SharpStream { + + public abstract int read() throws IOException; + + public abstract int read(byte buffer[]) throws IOException; + + public abstract int read(byte buffer[], int offset, int count) throws IOException; + + public abstract long skip(long amount) throws IOException; + + + public abstract int available(); + + public abstract void rewind() throws IOException; + + + public abstract void dispose(); + + public abstract boolean isDisposed(); + + + public abstract boolean canRewind(); + + public abstract boolean canRead(); + + public abstract boolean canWrite(); + + + public abstract void write(byte value) throws IOException; + + public abstract void write(byte[] buffer) throws IOException; + + public abstract void write(byte[] buffer, int offset, int count) throws IOException; + + public abstract void flush() throws IOException; + + public void setLength(long length) throws IOException { + throw new IOException("Not implemented"); + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java index 738135253..5e7a5f80d 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java @@ -1,7 +1,7 @@ package us.shandian.giga.postprocessing; -import org.schabi.newpipe.extractor.utils.Mp4DashWriter; -import org.schabi.newpipe.extractor.utils.io.SharpStream; +import org.schabi.newpipe.streams.Mp4DashWriter; +import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 811ec70d7..2c6dc776b 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -2,7 +2,7 @@ package us.shandian.giga.postprocessing; import android.os.Message; -import org.schabi.newpipe.extractor.utils.io.SharpStream; +import org.schabi.newpipe.streams.io.SharpStream; import java.io.File; import java.io.IOException; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java b/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java index 996f02d97..66b235d7c 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java @@ -2,7 +2,7 @@ package us.shandian.giga.postprocessing; import android.util.Log; -import org.schabi.newpipe.extractor.utils.io.SharpStream; +import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; import java.util.Random; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java b/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java index d05440d70..4c9d44548 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java @@ -1,16 +1,25 @@ package us.shandian.giga.postprocessing; -import org.schabi.newpipe.extractor.utils.io.SharpStream; -import org.schabi.newpipe.extractor.utils.SubtitleConverter; +import android.util.Log; + +import org.schabi.newpipe.streams.io.SharpStream; +import org.schabi.newpipe.streams.SubtitleConverter; +import org.xml.sax.SAXException; import java.io.IOException; +import java.text.ParseException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPathExpressionException; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.postprocessing.io.SharpInputStream; + /** * @author kapodamy */ class TttmlConverter extends Postprocessing { + private static final String TAG = "TttmlConverter"; TttmlConverter(DownloadMission mission) { super(mission); @@ -26,14 +35,32 @@ class TttmlConverter extends Postprocessing { if (format == null || format.equals("ttml")) { SubtitleConverter ttmlDumper = new SubtitleConverter(); - int res = ttmlDumper.dumpTTML( - sources[0], - out, - getArgumentAt(1, "true").equals("true"), - getArgumentAt(2, "true").equals("true") - ); + try { + ttmlDumper.dumpTTML( + sources[0], + out, + getArgumentAt(1, "true").equals("true"), + getArgumentAt(2, "true").equals("true") + ); + } catch (Exception err) { + Log.e(TAG, "subtitle parse failed", err); - return res == 0 ? OK_RESULT : res; + if (err instanceof IOException) { + return 1; + } else if (err instanceof ParseException) { + return 2; + } else if (err instanceof SAXException) { + return 3; + } else if (err instanceof ParserConfigurationException) { + return 4; + } else if (err instanceof XPathExpressionException) { + return 7; + } + + return 8; + } + + return OK_RESULT; } else if (format.equals("srt")) { byte[] buffer = new byte[8 * 1024]; int read; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java index d73fdc3b7..c69809e00 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java @@ -1,9 +1,9 @@ package us.shandian.giga.postprocessing; -import org.schabi.newpipe.extractor.utils.WebMReader.TrackKind; -import org.schabi.newpipe.extractor.utils.WebMReader.WebMTrack; -import org.schabi.newpipe.extractor.utils.WebMWriter; -import org.schabi.newpipe.extractor.utils.io.SharpStream; +import org.schabi.newpipe.streams.WebMReader.TrackKind; +import org.schabi.newpipe.streams.WebMReader.WebMTrack; +import org.schabi.newpipe.streams.WebMWriter; +import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java index f3e3ccdda..cd62c5d22 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java @@ -1,6 +1,6 @@ package us.shandian.giga.postprocessing.io; -import org.schabi.newpipe.extractor.utils.io.SharpStream; +import org.schabi.newpipe.streams.io.SharpStream; import java.io.File; import java.io.IOException; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java index 3d4f2931f..531e0587e 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java @@ -1,6 +1,6 @@ package us.shandian.giga.postprocessing.io; -import org.schabi.newpipe.extractor.utils.io.SharpStream; +import org.schabi.newpipe.streams.io.SharpStream; import java.io.File; import java.io.IOException; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java index dd3f8c697..c1b675eef 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java @@ -1,6 +1,6 @@ package us.shandian.giga.postprocessing.io; -import org.schabi.newpipe.extractor.utils.io.SharpStream; +import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; import java.io.RandomAccessFile; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java index 831afbfc2..52e0775da 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java @@ -7,7 +7,7 @@ package us.shandian.giga.postprocessing.io; import android.support.annotation.NonNull; -import org.schabi.newpipe.extractor.utils.io.SharpStream; +import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; import java.io.InputStream; From 23309e6fdfd11e0c19cfee35b8cd1d38b6295205 Mon Sep 17 00:00:00 2001 From: krtkush Date: Sun, 18 Nov 2018 19:15:50 +0530 Subject: [PATCH 31/68] Pull request changes. --- app/src/main/java/org/schabi/newpipe/App.java | 20 +++- .../newpipe/CheckForNewAppVersionTask.java | 91 ++++++++----------- 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 16fa66e2a..a1f5076fb 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -1,5 +1,6 @@ package org.schabi.newpipe; +import android.annotation.TargetApi; import android.app.Application; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -63,7 +64,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 context; + private static App app; @SuppressWarnings("unchecked") private static final Class[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class}; @@ -86,7 +87,7 @@ public class App extends Application { } refWatcher = installLeakCanary(); - context = this; + app = this; // Initialize settings first because others inits can use its values SettingsActivity.initSettings(this); @@ -209,7 +210,16 @@ public class App extends Application { NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mNotificationManager.createNotificationChannel(mChannel); - // Set up notification channel for app update. + 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 @@ -240,7 +250,7 @@ public class App extends Application { return false; } - public static App getContext() { - return context; + public static App getApp() { + return app; } } diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index e122a5688..5c55ee8ba 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -12,10 +12,11 @@ import android.os.AsyncTask; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; -import android.util.Log; 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; @@ -30,6 +31,12 @@ 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. @@ -38,12 +45,13 @@ import java.security.cert.X509Certificate; */ public class CheckForNewAppVersionTask extends AsyncTask { - private static final Application app = App.getContext(); + 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 = 10000; + private static final int timeoutPeriod = 30; private SharedPreferences mPrefs; + private OkHttpClient client; @Override protected void onPreExecute() { @@ -62,55 +70,29 @@ public class CheckForNewAppVersionTask extends AsyncTask { protected String doInBackground(Void... voids) { // Make a network request to get latest NewPipe data. - String response; - HttpURLConnection connection = null; + + if (client == null) { + + client = new OkHttpClient + .Builder() + .readTimeout(timeoutPeriod, TimeUnit.SECONDS) + .build(); + } + + Request request = new Request.Builder() + .url(newPipeApiUrl) + .build(); try { - URL url = new URL(newPipeApiUrl); + Response response = client.newCall(request).execute(); + return response.body().string(); - connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - connection.setConnectTimeout(timeoutPeriod); - connection.setReadTimeout(timeoutPeriod); - connection.setRequestProperty("Content-length", "0"); - connection.setUseCaches(false); - connection.setAllowUserInteraction(false); - connection.connect(); - - int responseStatus = connection.getResponseCode(); - - switch (responseStatus) { - - case 200: - case 201: - BufferedReader bufferedReader = new BufferedReader( - new InputStreamReader(connection.getInputStream())); - - StringBuilder stringBuilder = new StringBuilder(); - - String line; - - while ((line = bufferedReader.readLine()) != null) { - stringBuilder.append(line); - stringBuilder.append("\n"); - } - - bufferedReader.close(); - response = stringBuilder.toString(); - - return response; - } } catch (IOException ex) { - ex.printStackTrace(); - } finally { - if (connection != null) { - try { - connection.disconnect(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } + + ErrorActivity.reportError(app, ex, null, null, + ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", + "app update API fail", R.string.app_ui_crash)); } return null; @@ -136,6 +118,9 @@ public class CheckForNewAppVersionTask extends AsyncTask { } catch (JSONException ex) { ex.printStackTrace(); + 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)); } } } @@ -211,10 +196,14 @@ public class CheckForNewAppVersionTask extends AsyncTask { MessageDigest md = MessageDigest.getInstance("SHA1"); byte[] publicKey = md.digest(c.getEncoded()); hexString = byte2HexFormatted(publicKey); - } catch (NoSuchAlgorithmException e1) { - e1.printStackTrace(); - } catch (CertificateEncodingException e) { - e.printStackTrace(); + } 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; From 939cc56951fadfb4ca612b4432dbeb931b5d19d9 Mon Sep 17 00:00:00 2001 From: krtkush Date: Sun, 18 Nov 2018 19:18:16 +0530 Subject: [PATCH 32/68] Pull request changes v2. --- .../schabi/newpipe/CheckForNewAppVersionTask.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 5c55ee8ba..cd41ac07e 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -89,7 +89,6 @@ public class CheckForNewAppVersionTask extends AsyncTask { 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)); @@ -117,7 +116,6 @@ public class CheckForNewAppVersionTask extends AsyncTask { compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode); } catch (JSONException ex) { - ex.printStackTrace(); 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)); @@ -172,8 +170,10 @@ public class CheckForNewAppVersionTask extends AsyncTask { try { packageInfo = pm.getPackageInfo(packageName, flags); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); + } 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; @@ -186,8 +186,10 @@ public class CheckForNewAppVersionTask extends AsyncTask { try { cf = CertificateFactory.getInstance("X509"); c = (X509Certificate) cf.generateCertificate(input); - } catch (CertificateException e) { - e.printStackTrace(); + } 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; From ad5535af814afc43b77d70237177c6ec13583204 Mon Sep 17 00:00:00 2001 From: krtkush Date: Mon, 19 Nov 2018 23:27:13 +0530 Subject: [PATCH 33/68] Code refactoring, PR changes. --- .../org/schabi/newpipe/CheckForNewAppVersionTask.java | 3 --- .../newpipe/settings/UpdateSettingsFragment.java | 11 ++++------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index cd41ac07e..af9b88ac1 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -70,7 +70,6 @@ public class CheckForNewAppVersionTask extends AsyncTask { protected String doInBackground(Void... voids) { // Make a network request to get latest NewPipe data. - if (client == null) { client = new OkHttpClient @@ -84,10 +83,8 @@ public class CheckForNewAppVersionTask extends AsyncTask { .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", diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java index 157f52a0e..76d887dd1 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -23,14 +23,11 @@ public class UpdateSettingsFragment extends BasePreferenceFragment { } private Preference.OnPreferenceChangeListener updatePreferenceChange - = new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { + = (preference, newValue) -> { - defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), - (boolean) newValue).apply(); + defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), + (boolean) newValue).apply(); - return true; - } + return true; }; } From fef9d541ede88701b5519160fa4a176d6467248f Mon Sep 17 00:00:00 2001 From: kapodamy Date: Thu, 15 Nov 2018 22:30:00 -0300 Subject: [PATCH 34/68] misc fixes * use getPreferredLocalization() instead of getLocalization() * use lastest commit in build.gradle * fix missing cast in MissionAdapter.java --- app/build.gradle | 2 +- .../main/java/org/schabi/newpipe/download/DownloadDialog.java | 2 +- .../main/java/us/shandian/giga/ui/adapter/MissionAdapter.java | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b507fd860..aab3ae17c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,7 +54,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.TeamNewPipe:NewPipeExtractor:32d316330c26' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:91b1efc97e' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index d68db11e5..493ed44ea 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -376,7 +376,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } private int getSubtitleIndexBy(List streams) { - Localization loc = NewPipe.getLocalization(); + Localization loc = NewPipe.getPreferredLocalization(); for (int j = 0; j < 2; j++) { for (int i = 0; i < streams.size(); i++) { Locale streamLocale = streams.get(i).getLocale(); 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 7827c822e..bb5af1b0d 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 @@ -577,7 +577,8 @@ public class MissionAdapter extends RecyclerView.Adapter { checksum = menu.findItem(R.id.checksum); itemView.setOnClickListener((v) -> { - if(h.mission.finished) viewWithFileProvider(h); + if(((DownloadMission)item.mission).isFinished()) + viewWithFileProvider(item.mission.getDownloadedFile()); }); //h.itemView.setOnClickListener(v -> showDetail(h)); From d647555e3a8d17eac2cbe7858b356612165ff231 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Tue, 20 Nov 2018 19:10:50 -0300 Subject: [PATCH 35/68] more fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * use bold style in status (mission_item_linear.xml) * fix download attemps not begin updated * dont stop the queue if a download fails * implement partial wake-lock & wifi-lock * show notifications for failed downloads * ¿proper bitmap dispose? (DownloadManagerService.java) * improve buffer filling (CircularFile.java) * [Mp4Dash] increment reserved space from 2MiB to 15MiB. This is expensive but useful for devices with low ram * [WebM] use 2MiB of reserved space * fix debug warning if one thread is used * fix wrong download speed when the activity is suspended * Fix "Queue" menu item that appears in post-processing errors * fix mission length dont being updated (missing commit) --- .../newpipe/download/DownloadDialog.java | 109 +++++++++------- .../fragments/detail/VideoDetailFragment.java | 4 +- .../schabi/newpipe/streams/Mp4DashReader.java | 2 +- .../newpipe/util/SecondaryStreamHelper.java | 66 ++++++++++ .../newpipe/util/StreamItemAdapter.java | 48 ++++--- .../giga/get/DownloadInitializer.java | 2 +- .../us/shandian/giga/get/DownloadMission.java | 14 +- .../giga/postprocessing/Mp4DashMuxer.java | 2 +- .../giga/postprocessing/Postprocessing.java | 2 + .../giga/postprocessing/WebMMuxer.java | 2 +- .../giga/postprocessing/io/CircularFile.java | 74 ++++++++--- .../giga/service/DownloadManager.java | 35 +++-- .../giga/service/DownloadManagerService.java | 123 ++++++++++++++---- .../giga/ui/adapter/MissionAdapter.java | 47 ++++--- .../giga/ui/fragment/MissionsFragment.java | 3 +- .../main/res/layout/mission_item_linear.xml | 1 + app/src/main/res/layout/missions_header.xml | 6 +- app/src/main/res/values-es/strings.xml | 5 +- app/src/main/res/values/strings.xml | 5 +- 19 files changed, 400 insertions(+), 150 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 493ed44ea..ace143b13 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -11,6 +11,7 @@ import android.support.v4.app.DialogFragment; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; import android.util.Log; +import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -37,6 +38,7 @@ import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.util.FilenameUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.SecondaryStreamHelper; import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.ThemeHelper; @@ -55,17 +57,24 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private static final String TAG = "DialogFragment"; private static final boolean DEBUG = MainActivity.DEBUG; - @State protected StreamInfo currentInfo; - @State protected StreamSizeWrapper wrappedAudioStreams = StreamSizeWrapper.empty(); - @State protected StreamSizeWrapper wrappedVideoStreams = StreamSizeWrapper.empty(); - @State protected StreamSizeWrapper wrappedSubtitleStreams = StreamSizeWrapper.empty(); - @State protected int selectedVideoIndex = 0; - @State protected int selectedAudioIndex = 0; - @State protected int selectedSubtitleIndex = 0; + @State + protected StreamInfo currentInfo; + @State + protected StreamSizeWrapper wrappedAudioStreams = StreamSizeWrapper.empty(); + @State + protected StreamSizeWrapper wrappedVideoStreams = StreamSizeWrapper.empty(); + @State + protected StreamSizeWrapper wrappedSubtitleStreams = StreamSizeWrapper.empty(); + @State + protected int selectedVideoIndex = 0; + @State + protected int selectedAudioIndex = 0; + @State + protected int selectedSubtitleIndex = 0; - private StreamItemAdapter audioStreamsAdapter; - private StreamItemAdapter videoStreamsAdapter; - private StreamItemAdapter subtitleStreamsAdapter; + private StreamItemAdapter audioStreamsAdapter; + private StreamItemAdapter videoStreamsAdapter; + private StreamItemAdapter subtitleStreamsAdapter; private final CompositeDisposable disposables = new CompositeDisposable(); @@ -144,7 +153,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + if (DEBUG) + Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { getDialog().dismiss(); return; @@ -153,14 +163,29 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext())); Icepick.restoreInstanceState(this, savedInstanceState); - this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, true); + SparseArray> secondaryStreams = new SparseArray<>(4); + List videoStreams = wrappedVideoStreams.getStreamsList(); + + for (int i = 0; i < videoStreams.size(); i++) { + if (!videoStreams.get(i).isVideoOnly()) continue; + AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i)); + + if (audioStream != null) { + secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream)); + } else if (DEBUG) { + Log.w(TAG, "No audio stream candidates for video format " + videoStreams.get(i).getFormat().name()); + } + } + + this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, secondaryStreams); this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams); this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); + if (DEBUG) + Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); return inflater.inflate(R.layout.download_dialog, container); } @@ -293,7 +318,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck @Override public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) { - if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]"); + if (DEBUG) + Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]"); boolean flag = true; switch (checkedId) { @@ -318,7 +344,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (DEBUG) Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); + if (DEBUG) + Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); switch (radioVideoAudioGroup.getCheckedRadioButtonId()) { case R.id.audio_button: selectedAudioIndex = position; @@ -458,57 +485,41 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck String[] urls; String psName = null; String[] psArgs = null; - String secondaryStream = null; + String secondaryStreamUrl = null; + long nearLength = 0; if (selectedStream instanceof VideoStream) { - VideoStream videoStream = (VideoStream) selectedStream; - if (videoStream.isVideoOnly() && videoStream.getFormat() != MediaFormat.v3GPP) { - boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4; + SecondaryStreamHelper secondaryStream = videoStreamsAdapter + .getAllSecondary() + .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); - for (AudioStream audio : audioStreamsAdapter.getAll()) { - if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) { - secondaryStream = audio.getUrl(); - break; - } - } + if (secondaryStream != null) { + secondaryStreamUrl = secondaryStream.getStream().getUrl(); + psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; + psArgs = null; + long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream); - if (secondaryStream == null) { - // retry, but this time in reverse order - List audioStreams = audioStreamsAdapter.getAll(); - for (int i = audioStreams.size() - 1; i >= 0; i--) { - AudioStream audio = audioStreams.get(i); - if (audio.getFormat() == (m4v ? MediaFormat.MP3 : MediaFormat.OPUS)) { - secondaryStream = audio.getUrl(); - break; - } - } - } - - if (secondaryStream == null) { - Log.w(TAG, "No audio stream candidates for video format " + videoStream.getFormat().name()); - psName = null; - psArgs = null; - } else { - psName = m4v ? Postprocessing.ALGORITHM_MP4_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; - psArgs = null; + // set nearLength, only, if both sizes are fetched or known. this probably does not work on weak internet connections + if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) { + nearLength = secondaryStream.getSizeInBytes() + videoSize; } } } else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) { psName = Postprocessing.ALGORITHM_TTML_CONVERTER; psArgs = new String[]{ selectedStream.getFormat().getSuffix(), - "false",//ignore empty frames - "false",// detect youtube duplicateLines + "false",// ignore empty frames + "false",// detect youtube duplicate lines }; } - if (secondaryStream == null) { + if (secondaryStreamUrl == null) { urls = new String[]{selectedStream.getUrl()}; } else { - urls = new String[]{selectedStream.getUrl(), secondaryStream}; + urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; } - DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs); + DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); getDialog().dismiss(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index c7c668f40..8bcd2c66d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -746,7 +746,7 @@ public class VideoDetailFragment sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false); selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams); - final StreamItemAdapter streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled); + final StreamItemAdapter streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled); spinnerToolbar.setAdapter(streamsAdapter); spinnerToolbar.setSelection(selectedVideoStreamIndex); spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @@ -1335,4 +1335,4 @@ public class VideoDetailFragment relatedStreamRootLayout.setVisibility(visibility); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java index ec2419734..271929d47 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java @@ -257,7 +257,7 @@ public class Mp4DashReader { private String boxName(int type) { try { - return new String(ByteBuffer.allocate(4).putInt(type).array(), "US-ASCII"); + return new String(ByteBuffer.allocate(4).putInt(type).array(), "UTF-8"); } catch (UnsupportedEncodingException e) { return "0x" + Integer.toHexString(type); } diff --git a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java new file mode 100644 index 000000000..a5d3ea3eb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java @@ -0,0 +1,66 @@ +package org.schabi.newpipe.util; + +import android.support.annotation.NonNull; + +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.Stream; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; + +import java.util.List; + +public class SecondaryStreamHelper { + private final int position; + private final StreamSizeWrapper streams; + + public SecondaryStreamHelper(StreamSizeWrapper streams, T selectedStream) { + this.streams = streams; + this.position = streams.getStreamsList().indexOf(selectedStream); + if (this.position < 0) throw new RuntimeException("selected stream not found"); + } + + public T getStream() { + return streams.getStreamsList().get(position); + } + + public long getSizeInBytes() { + return streams.getSizeInBytes(position); + } + + /** + * find the correct audio stream for the desired video stream + * + * @param audioStreams list of audio streams + * @param videoStream desired video ONLY stream + * @return selected audio stream or null if a candidate was not found + */ + public static AudioStream getAudioStreamFor(@NonNull List audioStreams, @NonNull VideoStream videoStream) { + // TODO: check if m4v and m4a selected streams are DASH compliant + switch (videoStream.getFormat()) { + case WEBM: + case MPEG_4: + break; + default: + return null; + } + + boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4; + + for (AudioStream audio : audioStreams) { + if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) { + return audio; + } + } + + // retry, but this time in reverse order + for (int i = audioStreams.size() - 1; i >= 0; i--) { + AudioStream audio = audioStreams.get(i); + if (audio.getFormat() == (m4v ? MediaFormat.MP3 : MediaFormat.OPUS)) { + return audio; + } + } + + return null; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java index 6a1e80fea..eb106f91d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.util; import android.content.Context; +import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -29,26 +30,34 @@ import us.shandian.giga.util.Utility; /** * A list adapter for a list of {@link Stream streams}, currently supporting {@link VideoStream} and {@link AudioStream}. */ -public class StreamItemAdapter extends BaseAdapter { +public class StreamItemAdapter extends BaseAdapter { private final Context context; private final StreamSizeWrapper streamsWrapper; - private final boolean showIconNoAudio; + private final SparseArray> secondaryStreams; - public StreamItemAdapter(Context context, StreamSizeWrapper streamsWrapper, boolean showIconNoAudio) { + public StreamItemAdapter(Context context, StreamSizeWrapper streamsWrapper, SparseArray> secondaryStreams) { this.context = context; this.streamsWrapper = streamsWrapper; - this.showIconNoAudio = showIconNoAudio; + this.secondaryStreams = secondaryStreams; + } + + public StreamItemAdapter(Context context, StreamSizeWrapper streamsWrapper, boolean showIconNoAudio) { + this(context, streamsWrapper, showIconNoAudio ? new SparseArray<>() : null); } public StreamItemAdapter(Context context, StreamSizeWrapper streamsWrapper) { - this(context, streamsWrapper, false); + this(context, streamsWrapper, null); } public List getAll() { return streamsWrapper.getStreamsList(); } + public SparseArray> getAllSecondary() { + return secondaryStreams; + } + @Override public int getCount() { return streamsWrapper.getStreamsList().size(); @@ -90,22 +99,15 @@ public class StreamItemAdapter extends BaseAdapter { String qualityString; if (stream instanceof VideoStream) { - qualityString = ((VideoStream) stream).getResolution(); + VideoStream videoStream = ((VideoStream) stream); + qualityString = videoStream.getResolution(); - if (!showIconNoAudio) { - woSoundIconVisibility = View.GONE; - } else if (((VideoStream) stream).isVideoOnly()) { - switch (stream.getFormat()) { - case WEBM:// fully supported - case MPEG_4:// ¿is DASH MPEG-4 format? - woSoundIconVisibility = View.INVISIBLE; - break; - default: - woSoundIconVisibility = View.VISIBLE; - break; + if (secondaryStreams != null) { + if (videoStream.isVideoOnly()) { + woSoundIconVisibility = secondaryStreams.get(position) == null ? View.VISIBLE : View.INVISIBLE; + } else if (isDropdownItem) { + woSoundIconVisibility = View.INVISIBLE; } - } else if (isDropdownItem) { - woSoundIconVisibility = View.INVISIBLE; } } else if (stream instanceof AudioStream) { qualityString = ((AudioStream) stream).getAverageBitrate() + "kbps"; @@ -119,7 +121,13 @@ public class StreamItemAdapter extends BaseAdapter { } if (streamsWrapper.getSizeInBytes(position) > 0) { - sizeView.setText(streamsWrapper.getFormattedSize(position)); + SecondaryStreamHelper secondary = secondaryStreams == null ? null : secondaryStreams.get(position); + if (secondary != null) { + long size = secondary.getSizeInBytes() + streamsWrapper.getSizeInBytes(position); + sizeView.setText(Utility.formatBytes(size)); + } else { + sizeView.setText(streamsWrapper.getFormattedSize(position)); + } sizeView.setVisibility(View.VISIBLE); } else { sizeView.setVisibility(View.GONE); diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index 190bac285..2ea097062 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -74,7 +74,7 @@ public class DownloadInitializer implements Runnable { } } else { // if one thread is solicited don't calculate blocks, is useless - mMission.blocks = 0; + mMission.blocks = 1; mMission.fallback = true; mMission.unknownLength = false; } 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 d27046c76..8e34981cc 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -103,6 +103,11 @@ public class DownloadMission extends Mission { */ public int maxRetry; + /** + * Approximated final length, this represent the sum of all resources sizes + */ + public long nearLength; + public int threadCount = 3; boolean fallback; private int finishCount; @@ -432,7 +437,7 @@ public class DownloadMission extends Mission { return; } - if (DEBUG && blocks < 1) { + if (DEBUG && blocks == 0) { Log.w(TAG, "pausing a download that can not be resumed."); } @@ -507,6 +512,13 @@ public class DownloadMission extends Mission { return current >= urls.length && postprocessingName == null; } + public long getLength() { + long near = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; + near -= offsets[0];// don't count reserved space + + return near > nearLength ? near : nearLength; + } + private boolean doPostprocessing() { if (postprocessingName == null) return true; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java index 5e7a5f80d..b303b66cd 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java @@ -14,7 +14,7 @@ class Mp4DashMuxer extends Postprocessing { Mp4DashMuxer(DownloadMission mission) { super(mission); - recommendedReserve = 2048 * 1024;// 2 MiB + recommendedReserve = 15360 * 1024;// 15 MiB worksOnSameFile = true; } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 2c6dc776b..88cc337fd 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -91,6 +91,8 @@ public abstract class Postprocessing { out = new CircularFile(file, 0, this::progressReport, checker); mission.done = 0; + mission.length = mission.getLength(); + int result = process(out, sources); if (result == OK_RESULT) { diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java index c69809e00..009a9a66b 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java @@ -16,7 +16,7 @@ class WebMMuxer extends Postprocessing { WebMMuxer(DownloadMission mission) { super(mission); - recommendedReserve = (1024 + 512) * 1024;// 1.50 MiB + recommendedReserve = 2048 * 1024;// 2 MiB worksOnSameFile = true; } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java index 531e0587e..1454c1f2d 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java @@ -10,7 +10,7 @@ import java.util.ArrayList; public class CircularFile extends SharpStream { private final static int AUX_BUFFER_SIZE = 1024 * 1024;// 1 MiB - private final static int AUX2_BUFFER_SIZE = 256 * 1024;// 256 KiB + private final static int NOTIFY_BYTES_INTERVAL = 256 * 1024;// 256 KiB private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB private RandomAccessFile out; @@ -108,32 +108,56 @@ public class CircularFile extends SharpStream { } long end = callback.check(); - int available; + long available; if (end == -1) { - available = Integer.MAX_VALUE; + available = Long.MAX_VALUE; } else { if (end < startOffset) { throw new IOException("The reported offset is invalid. reported offset is " + String.valueOf(end)); } - available = (int) (end - position); + available = end - position; } while (available > 0 && auxiliaryBuffers.size() > 0) { ManagedBuffer aux = auxiliaryBuffers.get(0); - if ((queue.size + aux.size) > available) { - available = 0;// wait for next check - break; + // check if there is enough space to dump the auxiliar buffer + if (available >= (aux.size + queue.size)) { + available -= aux.size; + writeQueue(aux.buffer, 0, aux.size); + aux.dereference(); + auxiliaryBuffers.remove(0); + continue; } - writeQueue(aux.buffer, 0, aux.size); - available -= aux.size; - aux.dereference(); - auxiliaryBuffers.remove(0); + // try flush contents to avoid allocate another auxiliar buffer + if (aux.available() < len && available > queue.size) { + int size = Math.min(len, aux.available()); + aux.write(b, off, size); + + off += size; + len -= size; + + size = Math.min(aux.size, (int) available - queue.size); + if (size < 1) { + break; + } + + writeQueue(aux.buffer, 0, size); + aux.dereference(size); + + available -= size; + } + + break; } - if (available > (len + queue.size)) { + if (len < 1) { + return; + } + + if (auxiliaryBuffers.size() < 1 && available > (len + queue.size)) { writeQueue(b, off, len); } else { int i = auxiliaryBuffers.size() - 1; @@ -150,14 +174,14 @@ public class CircularFile extends SharpStream { if (available < 1) { // secondary auxiliary buffer available = len; - aux = new ManagedBuffer(Math.max(len, AUX2_BUFFER_SIZE)); + aux = new ManagedBuffer(Math.max(len, AUX_BUFFER_SIZE)); auxiliaryBuffers.add(aux); i++; } else { available = Math.min(len, available); } - aux.write(b, off, available); + aux.write(b, off, (int) available); len -= available; if (len < 1) { @@ -173,7 +197,7 @@ public class CircularFile extends SharpStream { position += length; if (onProgress != null && position > reportPosition) { - reportPosition = position + AUX2_BUFFER_SIZE;// notify every 256 KiB (approx) + reportPosition = position + NOTIFY_BYTES_INTERVAL; onProgress.report(position); } } @@ -195,6 +219,10 @@ public class CircularFile extends SharpStream { offset += size; length -= size; } + + if (queue.size >= queue.buffer.length) { + flushQueue(); + } } private void flushQueue() throws IOException { @@ -238,7 +266,9 @@ public class CircularFile extends SharpStream { flush(); out.seek(startOffset); - if (onProgress != null) onProgress.report(-position); + if (onProgress != null) { + onProgress.report(-position); + } position = startOffset; reportPosition = startOffset; @@ -327,6 +357,18 @@ public class CircularFile extends SharpStream { size = 0; } + void dereference(int amount) { + if (amount > size) { + throw new IndexOutOfBoundsException("Invalid dereference amount (" + amount + ">=" + size + ")"); + } + + size -= amount; + + for (int i = 0; i < size; i++) { + buffer[i] = buffer[amount + i]; + } + } + protected int available() { return buffer.length - size; } diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 97a0da523..52b49a0ae 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -116,7 +116,6 @@ public class DownloadManager { return result; } - @SuppressWarnings("ResultOfMethodCallIgnored") private void loadPendingMissions() { File[] subs = mPendingMissionsDir.listFiles(); @@ -136,9 +135,11 @@ public class DownloadManager { DownloadMission mis = Utility.readFromFile(sub); if (mis == null) { + //noinspection ResultOfMethodCallIgnored sub.delete(); } else { if (mis.isFinished()) { + //noinspection ResultOfMethodCallIgnored sub.delete(); continue; } @@ -173,6 +174,7 @@ public class DownloadManager { m.threadCount = mis.threadCount; m.source = mis.source; m.maxRetry = mis.maxRetry; + m.nearLength = mis.nearLength; mis = m; } @@ -204,7 +206,7 @@ public class DownloadManager { * @param postProcessingArgs the arguments for the post-processing algorithm. */ void startMission(String[] urls, String location, String name, char kind, int threads, String source, - String postprocessingName, String[] postProcessingArgs) { + String postprocessingName, String[] postProcessingArgs, long nearLength) { synchronized (this) { // check for existing pending download DownloadMission pendingMission = getPendingMission(location, name); @@ -229,6 +231,7 @@ public class DownloadManager { mission.source = source; mission.mHandler = mHandler; mission.maxRetry = mPrefs.getInt(mPrefMaxRetry, 3); + mission.nearLength = nearLength; while (true) { mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp)); @@ -406,26 +409,30 @@ public class DownloadManager { * Set a pending download as finished * * @param mission the desired mission - * @return true if exits pending missions running, otherwise, false */ - boolean setFinished(DownloadMission mission) { + void setFinished(DownloadMission mission) { synchronized (this) { - int i = mMissionsPending.indexOf(mission); - mMissionsPending.remove(i); - + mMissionsPending.remove(mission); mMissionsFinished.add(0, new FinishedMission(mission)); mDownloadDataSource.addMission(mission); + } + } + /** + * runs another mission in queue if possible + * @return true if exits pending missions running or a mission was started, otherwise, false + */ + boolean runAnotherMission() { + synchronized (this) { if (mMissionsPending.size() < 1) return false; - i = getRunningMissionsCount(); + int i = getRunningMissionsCount(); if (i > 0) return true; - // before returning, check the queue if (!canDownloadInCurrentNetwork()) return false; - for (DownloadMission mission1 : mMissionsPending) { - if (!mission1.running && mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED && mission1.enqueued) { + for (DownloadMission mission : mMissionsPending) { + if (!mission.running && mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED && mission.enqueued) { resumeMission(mMissionsPending.get(i)); return true; } @@ -481,6 +488,12 @@ public class DownloadManager { if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); } + void updateMaximumAttempts(int maxRetry) { + synchronized (this) { + for (DownloadMission mission : mMissionsPending) mission.maxRetry = maxRetry; + } + } + /** * Fast check for pending downloads. If exists, the user will be notified * TODO: call this method in somewhere 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 bddc41718..52485e9d8 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -11,6 +11,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; +import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.ConnectivityManager; @@ -22,6 +23,8 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.preference.PreferenceManager; +import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.content.PermissionChecker; import android.util.Log; @@ -29,6 +32,7 @@ import android.widget.Toast; import org.schabi.newpipe.R; import org.schabi.newpipe.download.DownloadActivity; +import org.schabi.newpipe.player.helper.LockManager; import java.io.File; import java.util.ArrayList; @@ -61,6 +65,7 @@ public class DownloadManagerService extends Service { private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName"; private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; + private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; private static final String ACTION_RESET_DOWNLOAD_COUNT = APPLICATION_ID + ".reset_download_count"; @@ -73,11 +78,22 @@ public class DownloadManagerService extends Service { private StringBuilder downloadDoneList = null; NotificationManager notificationManager = null; private boolean mForeground = false; - + private final ArrayList mEchoObservers = new ArrayList<>(1); private BroadcastReceiver mNetworkStateListener; + private SharedPreferences mPrefs = null; + private final SharedPreferences.OnSharedPreferenceChangeListener mPrefChangeListener = this::handlePreferenceChange; + + private boolean wakeLockAcquired = false; + private LockManager wakeLock = null; + + private int downloadFailedNotificationID = DOWNLOADS_NOTIFICATION_ID + 1; + + private Bitmap icLauncher; + private Bitmap icDownloadDone; + /** * notify media scanner on downloaded media file ... * @@ -112,12 +128,12 @@ public class DownloadManagerService extends Service { openDownloadListIntent, PendingIntent.FLAG_UPDATE_CURRENT); - Bitmap iconBitmap = BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher); + icLauncher = BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher); Builder builder = new Builder(this, getString(R.string.notification_channel_id)) .setContentIntent(pendingIntent) .setSmallIcon(android.R.drawable.stat_sys_download) - .setLargeIcon(iconBitmap) + .setLargeIcon(icLauncher) .setContentTitle(getString(R.string.msg_running)) .setContentText(getString(R.string.msg_running_detail)); @@ -135,6 +151,11 @@ public class DownloadManagerService extends Service { } }; registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + + mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener); + + wakeLock = new LockManager(this); } @Override @@ -158,8 +179,9 @@ public class DownloadManagerService extends Service { String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); String source = intent.getStringExtra(EXTRA_SOURCE); + long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); - mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs)); + mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength)); } else if (downloadDoneNotification != null && action.equals(ACTION_RESET_DOWNLOAD_COUNT)) { downloadDoneCount = 0; @@ -184,10 +206,15 @@ public class DownloadManagerService extends Service { notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); } - unregisterReceiver(mNetworkStateListener); - mManager.pauseAllMissions(); + if (wakeLockAcquired) wakeLock.releaseWifiAndCpu(); + + unregisterReceiver(mNetworkStateListener); + mPrefs.unregisterOnSharedPreferenceChangeListener(mPrefChangeListener); + + icDownloadDone.recycle(); + icLauncher.recycle(); } @Override @@ -209,19 +236,24 @@ public class DownloadManagerService extends Service { } public void handleMessage(Message msg) { + DownloadMission mission = (DownloadMission) msg.obj; + switch (msg.what) { case MESSAGE_FINISHED: - DownloadMission mission = (DownloadMission) msg.obj; notifyMediaScanner(mission.getDownloadedFile()); notifyFinishedDownload(mission.name); - updateForegroundState(mManager.setFinished(mission)); + mManager.setFinished(mission); + updateForegroundState(mManager.runAnotherMission()); break; case MESSAGE_RUNNING: case MESSAGE_PROGRESS: updateForegroundState(true); break; - case MESSAGE_PAUSED: case MESSAGE_ERROR: + notifyFailedDownload(mission.name); + updateForegroundState(mManager.runAnotherMission()); + break; + case MESSAGE_PAUSED: updateForegroundState(mManager.getRunningMissionsCount() > 0); break; } @@ -272,21 +304,28 @@ public class DownloadManagerService extends Service { mManager.handleConnectivityChange(status); } + private void handlePreferenceChange(SharedPreferences prefs, String key) { + if (key.equals(getString(R.string.downloads_max_retry))) { + mManager.updateMaximumAttempts(prefs.getInt(key, 3)); + } + } + public void updateForegroundState(boolean state) { if (state == mForeground) return; if (state) { startForeground(FOREGROUND_NOTIFICATION_ID, mNotification); + if (!wakeLockAcquired) wakeLock.acquireWifiAndCpu(); } else { stopForeground(true); + if (wakeLockAcquired) wakeLock.releaseWifiAndCpu(); } mForeground = state; } - public static void startMission(Context context, String urls[], String location, String name, - char kind, int threads, String source, String postprocessingName, - String[] postprocessingArgs) { + public static void startMission(Context context, String urls[], String location, String name, char kind, + int threads, String source, String psName, String[] psArgs, long nearLength) { Intent intent = new Intent(context, DownloadManagerService.class); intent.setAction(Intent.ACTION_RUN); intent.putExtra(EXTRA_URLS, urls); @@ -295,8 +334,9 @@ public class DownloadManagerService extends Service { intent.putExtra(EXTRA_KIND, kind); intent.putExtra(EXTRA_THREADS, threads); intent.putExtra(EXTRA_SOURCE, source); - intent.putExtra(EXTRA_POSTPROCESSING_NAME, postprocessingName); - intent.putExtra(EXTRA_POSTPROCESSING_ARGS, postprocessingArgs); + intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); + intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); + intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); context.startService(intent); } @@ -330,16 +370,19 @@ public class DownloadManagerService extends Service { if (downloadDoneNotification == null) { downloadDoneList = new StringBuilder(name.length()); - Bitmap icon = BitmapFactory.decodeResource(this.getResources(), android.R.drawable.stat_sys_download_done); + icDownloadDone = BitmapFactory.decodeResource(this.getResources(), android.R.drawable.stat_sys_download_done); downloadDoneNotification = new Builder(this, getString(R.string.notification_channel_id)) .setAutoCancel(true) - .setLargeIcon(icon) + .setLargeIcon(icDownloadDone) .setSmallIcon(android.R.drawable.stat_sys_download_done) .setDeleteIntent(PendingIntent.getService(this, (int) System.currentTimeMillis(), new Intent(this, DownloadManagerService.class) .setAction(ACTION_RESET_DOWNLOAD_COUNT) , PendingIntent.FLAG_UPDATE_CURRENT)) - .setContentIntent(mNotification.contentIntent); + .setContentIntent(PendingIntent.getService(this, (int) System.currentTimeMillis() + 1, + new Intent(this, DownloadActivity.class) + .setAction(Intent.ACTION_MAIN), + PendingIntent.FLAG_UPDATE_CURRENT)); } if (downloadDoneCount < 1) { @@ -347,27 +390,61 @@ public class DownloadManagerService extends Service { if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { downloadDoneNotification.setContentTitle(getString(R.string.app_name)); - downloadDoneNotification.setContentText(getString(R.string.download_finished, name)); } else { - downloadDoneNotification.setContentTitle(getString(R.string.download_finished, name)); - downloadDoneNotification.setContentText(null); + downloadDoneNotification.setContentTitle(null); } + + downloadDoneNotification.setContentText(getString(R.string.download_finished)); + downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle() + .setBigContentTitle(getString(R.string.download_finished)) + .bigText(name) + ); } else { - downloadDoneList.append(", "); + downloadDoneList.append('\n'); downloadDoneList.append(name); + downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle().bigText(downloadDoneList)); downloadDoneNotification.setContentTitle(getString(R.string.download_finished_more, String.valueOf(downloadDoneCount + 1))); - downloadDoneNotification.setContentText(downloadDoneList.toString()); + downloadDoneNotification.setContentText(downloadDoneList); } notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); downloadDoneCount++; } + public void notifyFailedDownload(String name) { + if (icDownloadDone == null) { + // TODO: use a proper icon for failed downloads + icDownloadDone = BitmapFactory.decodeResource(this.getResources(), android.R.drawable.stat_sys_download_done); + } + + Builder notification = new Builder(this, getString(R.string.notification_channel_id)) + .setAutoCancel(true) + .setLargeIcon(icDownloadDone) + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .setContentIntent(PendingIntent.getService(this, (int) System.currentTimeMillis() + 1, + new Intent(this, DownloadActivity.class) + .setAction(Intent.ACTION_MAIN), + PendingIntent.FLAG_UPDATE_CURRENT)); + + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + notification.setContentTitle(getString(R.string.app_name)); + notification.setStyle(new NotificationCompat.BigTextStyle() + .bigText(getString(R.string.download_failed).concat(": ").concat(name))); + } else { + notification.setContentTitle(getString(R.string.download_failed)); + notification.setContentText(name); + notification.setStyle(new NotificationCompat.BigTextStyle() + .bigText(name)); + } + + notificationManager.notify(downloadFailedNotificationID++, notification.build()); + } + private void manageObservers(Handler handler, boolean add) { synchronized (mEchoObservers) { if (add) { - mEchoObservers.add(handler); + mEchoObservers.add(handler); } else { mEchoObservers.remove(handler); } 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 bb5af1b0d..c4eb663f9 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 @@ -142,7 +142,7 @@ public class MissionAdapter extends RecyclerView.Adapter { str = R.string.missions_header_pending; } else { str = R.string.missions_header_finished; - mClear.setVisible(true); + setClearButtonVisibility(true); } ((ViewHolderHeader) view).header.setText(str); @@ -233,8 +233,7 @@ public class MissionAdapter extends RecyclerView.Adapter { } } - long length = mission.offsets[mission.current < mission.offsets.length ? mission.current : (mission.offsets.length - 1)]; - length += mission.length; + long length = mission.getLength(); int state = 0; if (!mission.isFinished()) { @@ -274,7 +273,7 @@ public class MissionAdapter extends RecyclerView.Adapter { return; } - + if (deltaTime > 1000 && deltaDone > 0) { float speed = (float) deltaDone / deltaTime; String speedStr = Utility.formatSpeed(speed * 1000); @@ -297,7 +296,7 @@ public class MissionAdapter extends RecyclerView.Adapter { Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); Uri uri = FileProvider.getUriForFile(mContext, BuildConfig.APPLICATION_ID + ".provider", file); - + Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.setDataAndType(uri, mimeType); @@ -390,7 +389,7 @@ public class MissionAdapter extends RecyclerView.Adapter { str.append(mContext.getString(R.string.error_connect_host)); break; case DownloadMission.ERROR_POSTPROCESSING_FAILED: - str.append(R.string.error_postprocessing_failed); + str.append(mContext.getString(R.string.error_postprocessing_failed)); case DownloadMission.ERROR_UNKNOWN_EXCEPTION: break; default: @@ -418,7 +417,7 @@ public class MissionAdapter extends RecyclerView.Adapter { public void clearFinishedDownloads() { mDownloadManager.forgetFinishedDownloads(); applyChanges(); - mClear.setVisible(false); + setClearButtonVisibility(false); } private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) { @@ -429,7 +428,7 @@ public class MissionAdapter extends RecyclerView.Adapter { switch (id) { case R.id.start: h.state = -1; - h.size.setText(Utility.formatBytes(mission.length)); + h.size.setText(Utility.formatBytes(mission.getLength())); mDownloadManager.resumeMission(mission); return true; case R.id.pause: @@ -466,11 +465,11 @@ public class MissionAdapter extends RecyclerView.Adapter { new ChecksumTask(mContext).execute(h.item.mission.getDownloadedFile().getAbsolutePath(), ALGORITHMS.get(id)); return true; case R.id.source: - /*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source)); - mContext.startActivity(intent);*/ + /*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source)); + mContext.startActivity(intent);*/ try { Intent intent = NavigationHelper.getIntentByLink(mContext, h.item.mission.source); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); mContext.startActivity(intent); } catch (Exception e) { Log.w(TAG, "Selected item has a invalid source", e); @@ -490,7 +489,7 @@ public class MissionAdapter extends RecyclerView.Adapter { if (mIterator.getOldListSize() > 0) { int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1); - mClear.setVisible(lastItemType == DownloadManager.SPECIAL_FINISHED); + setClearButtonVisibility(lastItemType == DownloadManager.SPECIAL_FINISHED); } } @@ -498,6 +497,10 @@ public class MissionAdapter extends RecyclerView.Adapter { mIterator.start(); mIterator.end(); + for (ViewHolderItem item: mPendingDownloadsItems) { + item.lastTimeStamp = -1; + } + notifyDataSetChanged(); } @@ -505,6 +508,18 @@ public class MissionAdapter extends RecyclerView.Adapter { mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item; } + public void setClearButton(MenuItem clearButton) { + if (mClear == null) { + int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1); + clearButton.setVisible(lastItemType == DownloadManager.SPECIAL_FINISHED); + } + 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); @@ -577,8 +592,8 @@ public class MissionAdapter extends RecyclerView.Adapter { checksum = menu.findItem(R.id.checksum); itemView.setOnClickListener((v) -> { - if(((DownloadMission)item.mission).isFinished()) - viewWithFileProvider(item.mission.getDownloadedFile()); + if (((DownloadMission) item.mission).isFinished()) + viewWithFileProvider(item.mission.getDownloadedFile()); }); //h.itemView.setOnClickListener(v -> showDetail(h)); @@ -607,9 +622,9 @@ public class MissionAdapter extends RecyclerView.Adapter { queue.setChecked(mission.enqueued); - start.setVisible(mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED); delete.setVisible(true); - queue.setVisible(true); + start.setVisible(mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED); + queue.setVisible(mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED); } } } else { diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 00d7f9695..f04361f19 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -33,7 +33,7 @@ public class MissionsFragment extends Fragment { private SharedPreferences mPrefs; private boolean mLinear; private MenuItem mSwitch; - private MenuItem mClear; + private MenuItem mClear = null; private RecyclerView mList; private View mEmpty; @@ -152,6 +152,7 @@ public class MissionsFragment extends Fragment { public void onPrepareOptionsMenu(Menu menu) { mSwitch = menu.findItem(R.id.switch_mode); mClear = menu.findItem(R.id.clear_list); + if (mAdapter != null) mAdapter.setClearButton(mClear); super.onPrepareOptionsMenu(menu); } diff --git a/app/src/main/res/layout/mission_item_linear.xml b/app/src/main/res/layout/mission_item_linear.xml index 0133d0c3f..7fff76235 100644 --- a/app/src/main/res/layout/mission_item_linear.xml +++ b/app/src/main/res/layout/mission_item_linear.xml @@ -56,6 +56,7 @@ android:layout_toRightOf="@id/item_size" android:padding="6dp" android:singleLine="true" + android:textStyle="bold" android:text="0%" android:textColor="@color/white" android:textSize="12sp" /> diff --git a/app/src/main/res/layout/missions_header.xml b/app/src/main/res/layout/missions_header.xml index f5226e3dd..9505a2fce 100644 --- a/app/src/main/res/layout/missions_header.xml +++ b/app/src/main/res/layout/missions_header.xml @@ -2,7 +2,6 @@ + android:textStyle="bold" + android:text="relative header"/> Archivo borrado - - Descarga finalizada: %s + + Descarga fallida + Descarga finalizada %s descargas finalizadas diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 04656aefa..ade72ccad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -539,8 +539,9 @@ Action denied by the system - - Download finished: %s + + Download failed + Download finished %s downloads finished From 7b5ba3bdc2a41ca87a170b21928a09791d4ab5fc Mon Sep 17 00:00:00 2001 From: Ping20002015 Date: Fri, 23 Nov 2018 19:38:01 +0100 Subject: [PATCH 36/68] Fix NPE for issue #1901 --- app/src/main/java/org/schabi/newpipe/player/BasePlayer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 0e4d07179..b71976eda 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -1106,6 +1106,7 @@ public abstract class BasePlayer implements } public boolean isPlaying() { + if (simpleExoPlayer == null) return false; final int state = simpleExoPlayer.getPlaybackState(); return (state == Player.STATE_READY || state == Player.STATE_BUFFERING) && simpleExoPlayer.getPlayWhenReady(); From 5bbb0cd666a843c25ad5ec4a44aaa7db700262b7 Mon Sep 17 00:00:00 2001 From: shivanju Date: Sat, 24 Nov 2018 17:20:57 +0530 Subject: [PATCH 37/68] issue:1336 Remove auto queued stream if a new stream gets appended --- .../java/org/schabi/newpipe/player/playqueue/PlayQueue.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java index 13a550f2e..2a7c9f127 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java @@ -234,10 +234,9 @@ public abstract class PlayQueue implements Serializable { Collections.shuffle(itemList); } if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued() && !itemList.get(0).isAutoQueued()) { - streams.addAll(streams.size() - 1, itemList); - } else { - streams.addAll(itemList); + streams.remove(streams.size() - 1); } + streams.addAll(itemList); broadcast(new AppendEvent(itemList.size())); } From f3d4d4747a162c23da1669514ad2213752705ec1 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Sat, 24 Nov 2018 00:14:37 -0300 Subject: [PATCH 38/68] and more fixes * fix content length reading * use float overflow. Expensive, double is used instead * fix invalid cast after click the mission body * use a list for maximum attemps (downloads) * minor clean up (DownloadManager.java) * dont pass SharedPreferences instace to DownloadManager * use a switch instead of checkbox for cross_network_downloads * notify media scanner after deleting a finished download --- .../newpipe/download/DownloadDialog.java | 19 +++++++- .../giga/get/DownloadInitializer.java | 6 ++- .../us/shandian/giga/get/DownloadMission.java | 6 +-- .../shandian/giga/get/DownloadRunnable.java | 2 +- .../giga/get/DownloadRunnableFallback.java | 15 +++++-- .../giga/service/DownloadManager.java | 44 ++++++++----------- .../giga/service/DownloadManagerService.java | 7 ++- .../giga/ui/adapter/MissionAdapter.java | 14 +++--- .../us/shandian/giga/ui/common/Deleter.java | 9 +++- .../java/us/shandian/giga/util/Utility.java | 23 ++++++++-- app/src/main/res/values/settings_keys.xml | 11 +++++ app/src/main/res/xml/download_settings.xml | 7 +-- 12 files changed, 111 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index ace143b13..0418eadae 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -404,11 +404,26 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private int getSubtitleIndexBy(List streams) { Localization loc = NewPipe.getPreferredLocalization(); + + for (int i = 0; i < streams.size(); i++) { + Locale streamLocale = streams.get(i).getLocale(); + String tag = streamLocale.getLanguage().concat("-").concat(streamLocale.getCountry()); + if (tag.equalsIgnoreCase(loc.getLanguage())) { + return i; + } + } + + // fallback + // 1st loop match country & language + // 2nd loop match language only + String lang = loc.getLanguage().substring(0, loc.getLanguage().indexOf("-")); + for (int j = 0; j < 2; j++) { for (int i = 0; i < streams.size(); i++) { Locale streamLocale = streams.get(i).getLocale(); - if (streamLocale.getLanguage().equals(loc.getLanguage())) { - if (j > 0 || streamLocale.getCountry().equals(loc.getCountry())) { + + if (streamLocale.getLanguage().equalsIgnoreCase(lang)) { + if (j > 0 || streamLocale.getCountry().equalsIgnoreCase(loc.getCountry())) { return i; } } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index 2ea097062..298e7be37 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -35,7 +35,9 @@ public class DownloadInitializer implements Runnable { HttpURLConnection conn = mMission.openConnection(mId, -1, -1); if (!mMission.running || Thread.interrupted()) return; - mMission.length = conn.getContentLength(); + mMission.length = Utility.getContentLength(conn); + + if (mMission.length == 0) { mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null); return; @@ -97,7 +99,7 @@ public class DownloadInitializer implements Runnable { for (long i = 0; i < mMission.currentThreadCount; i++) { mMission.threadBlockPositions.add(i); - mMission.threadBytePositions.add(0); + mMission.threadBytePositions.add(0L); } File file; 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 8e34981cc..851b5cb1b 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -124,7 +124,7 @@ public class DownloadMission extends Mission { @SuppressWarnings("UseSparseArrays")// LongSparseArray is not serializable private final HashMap blockState = new HashMap<>(); final List threadBlockPositions = new ArrayList<>(); - final List threadBytePositions = new ArrayList<>(); + final List threadBytePositions = new ArrayList<>(); private transient boolean deleted; int currentThreadCount; @@ -216,7 +216,7 @@ public class DownloadMission extends Mission { * @param threadId the identifier of the thread * @param position the relative position in bytes or zero */ - void setThreadBytePosition(int threadId, int position) { + void setThreadBytePosition(int threadId, long position) { threadBytePositions.set(threadId, position); } @@ -226,7 +226,7 @@ public class DownloadMission extends Mission { * @param threadId the identifier of the thread * @return the relative position in bytes or zero */ - int getBlockBytePosition(int threadId) { + long getBlockBytePosition(int threadId) { return threadBytePositions.get(threadId); } 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 b6617cfa4..95f4758f9 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -89,7 +89,7 @@ public class DownloadRunnable implements Runnable { end = mMission.length - 1; } - int total = 0; + long total = 0; try { HttpURLConnection conn = mMission.openConnection(mId, start, end); 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 c484f5158..b648dd812 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -1,5 +1,6 @@ package us.shandian.giga.get; +import android.annotation.SuppressLint; import android.support.annotation.NonNull; import android.util.Log; @@ -10,9 +11,13 @@ import java.net.HttpURLConnection; import java.nio.channels.ClosedByInterruptException; +import us.shandian.giga.util.Utility; + import static org.schabi.newpipe.BuildConfig.DEBUG; -// Single-threaded fallback mode +/** + * Single-threaded fallback mode + */ public class DownloadRunnableFallback implements Runnable { private static final String TAG = "DownloadRunnableFallback"; @@ -43,10 +48,11 @@ public class DownloadRunnableFallback implements Runnable { } @Override + @SuppressLint("LongLogTag") public void run() { boolean done; - int start = 0; + long start = 0; if (!mMission.unknownLength) { start = mMission.getBlockBytePosition(0); @@ -56,11 +62,12 @@ public class DownloadRunnableFallback implements Runnable { } try { - int rangeStart = (mMission.unknownLength || start < 1) ? -1 : start; + long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start; HttpURLConnection conn = mMission.openConnection(1, rangeStart, -1); // secondary check for the file length - if (!mMission.unknownLength) mMission.unknownLength = conn.getContentLength() == -1; + if (!mMission.unknownLength) + mMission.unknownLength = Utility.getContentLength(conn) == -1; f = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); f.seek(mMission.offsets[mMission.current] + start); diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 52b49a0ae..31b5b16a9 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -1,9 +1,7 @@ package us.shandian.giga.service; import android.content.Context; -import android.content.SharedPreferences; import android.os.Handler; -import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.util.DiffUtil; @@ -46,9 +44,8 @@ public class DownloadManager { private NetworkState mLastNetworkStatus = NetworkState.Unavailable; - private SharedPreferences mPrefs; - private String mPrefMaxRetry; - private String mPrefCrossNetwork; + int mPrefMaxRetry; + boolean mPrefCrossNetwork; /** * Create a new instance @@ -65,9 +62,6 @@ public class DownloadManager { mHandler = handler; mMissionsFinished = loadFinishedMissions(); mPendingMissionsDir = getPendingDir(context); - mPrefs = PreferenceManager.getDefaultSharedPreferences(context); - mPrefMaxRetry = context.getString(R.string.downloads_max_retry); - mPrefCrossNetwork = context.getString(R.string.cross_network_downloads); if (!Utility.mkdir(mPendingMissionsDir, false)) { throw new RuntimeException("failed to create pending_downloads in data directory"); @@ -196,17 +190,17 @@ public class DownloadManager { /** * Start a new download mission * - * @param urls the list of urls to download - * @param location the location - * @param name the name of the file to create - * @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined) - * @param threads the number of threads maximal used to download chunks of the file. - * @param postprocessingName the name of the required post-processing algorithm, or {@code null} to ignore. - * @param source source url of the resource - * @param postProcessingArgs the arguments for the post-processing algorithm. + * @param urls the list of urls to download + * @param location the location + * @param name the name of the file to create + * @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined) + * @param threads the number of threads maximal used to download chunks of the file. + * @param psName the name of the required post-processing algorithm, or {@code null} to ignore. + * @param source source url of the resource + * @param psArgs the arguments for the post-processing algorithm. */ - void startMission(String[] urls, String location, String name, char kind, int threads, String source, - String postprocessingName, String[] postProcessingArgs, long nearLength) { + void startMission(String[] urls, String location, String name, char kind, int threads, + String source, String psName, String[] psArgs, long nearLength) { synchronized (this) { // check for existing pending download DownloadMission pendingMission = getPendingMission(location, name); @@ -225,12 +219,12 @@ public class DownloadManager { if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index)); } - DownloadMission mission = new DownloadMission(urls, name, location, kind, postprocessingName, postProcessingArgs); + DownloadMission mission = new DownloadMission(urls, name, location, kind, psName, psArgs); mission.timestamp = System.currentTimeMillis(); mission.threadCount = threads; mission.source = source; mission.mHandler = mHandler; - mission.maxRetry = mPrefs.getInt(mPrefMaxRetry, 3); + mission.maxRetry = mPrefMaxRetry; mission.nearLength = nearLength; while (true) { @@ -420,6 +414,7 @@ public class DownloadManager { /** * runs another mission in queue if possible + * * @return true if exits pending missions running or a mission was started, otherwise, false */ boolean runAnotherMission() { @@ -460,18 +455,17 @@ public class DownloadManager { private boolean canDownloadInCurrentNetwork() { if (mLastNetworkStatus == NetworkState.Unavailable) return false; - return !(mPrefs.getBoolean(mPrefCrossNetwork, false) && mLastNetworkStatus == NetworkState.MobileOperating); + return !(mPrefCrossNetwork && mLastNetworkStatus == NetworkState.MobileOperating); } void handleConnectivityChange(NetworkState currentStatus) { if (currentStatus == mLastNetworkStatus) return; mLastNetworkStatus = currentStatus; - boolean pauseOnMobile = mPrefs.getBoolean(mPrefCrossNetwork, false); if (currentStatus == NetworkState.Unavailable) { return; - } else if (currentStatus != NetworkState.MobileOperating || !pauseOnMobile) { + } else if (currentStatus != NetworkState.MobileOperating || !mPrefCrossNetwork) { return; } @@ -488,9 +482,9 @@ public class DownloadManager { if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); } - void updateMaximumAttempts(int maxRetry) { + void updateMaximumAttempts() { synchronized (this) { - for (DownloadMission mission : mMissionsPending) mission.maxRetry = maxRetry; + for (DownloadMission mission : mMissionsPending) mission.maxRetry = mPrefMaxRetry; } } 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 52485e9d8..7b30740d5 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -306,7 +306,12 @@ public class DownloadManagerService extends Service { private void handlePreferenceChange(SharedPreferences prefs, String key) { if (key.equals(getString(R.string.downloads_max_retry))) { - mManager.updateMaximumAttempts(prefs.getInt(key, 3)); + mManager.mPrefMaxRetry = Integer.parseInt( + prefs.getString(key, getString(R.string.default_max_retry)) + ); + mManager.updateMaximumAttempts(); + } else if (key.equals(getString(R.string.cross_network_downloads))) { + mManager.mPrefCrossNetwork = prefs.getBoolean(key, false); } } 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 c4eb663f9..4dc40c420 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 @@ -183,6 +183,7 @@ public class MissionAdapter extends RecyclerView.Adapter { return mIterator.getSpecialAtItem(position); } + @SuppressLint("DefaultLocale") private void updateProgress(ViewHolderItem h) { if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return; @@ -216,14 +217,15 @@ public class MissionAdapter extends RecyclerView.Adapter { progress = Float.NaN; h.progress.setProgress(0f); } else { - progress = (float) mission.done / mission.length; + progress = (float) ((double) mission.done / mission.length); if (mission.urls.length > 1 && mission.current < mission.urls.length) { progress = (progress / mission.urls.length) + ((float) mission.current / mission.urls.length); } } if (hasError) { - if (Float.isNaN(progress) || Float.isInfinite(progress)) h.progress.setProgress(1f); + if (Float.isNaN(progress) || Float.isInfinite(progress)) + h.progress.setProgress(1f); h.status.setText(R.string.msg_error); } else if (Float.isNaN(progress) || Float.isInfinite(progress)) { h.status.setText("--.-%"); @@ -275,7 +277,7 @@ public class MissionAdapter extends RecyclerView.Adapter { if (deltaTime > 1000 && deltaDone > 0) { - float speed = (float) deltaDone / deltaTime; + float speed = (float) ((double) deltaDone / deltaTime); String speedStr = Utility.formatSpeed(speed * 1000); String sizeStr = Utility.formatBytes(length); @@ -497,7 +499,7 @@ public class MissionAdapter extends RecyclerView.Adapter { mIterator.start(); mIterator.end(); - for (ViewHolderItem item: mPendingDownloadsItems) { + for (ViewHolderItem item : mPendingDownloadsItems) { item.lastTimeStamp = -1; } @@ -592,11 +594,9 @@ public class MissionAdapter extends RecyclerView.Adapter { checksum = menu.findItem(R.id.checksum); itemView.setOnClickListener((v) -> { - if (((DownloadMission) item.mission).isFinished()) + if (item.mission instanceof FinishedMission) viewWithFileProvider(item.mission.getDownloadedFile()); }); - - //h.itemView.setOnClickListener(v -> showDetail(h)); } private void showPopupMenu() { diff --git a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java index c56e1c703..6407ab019 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java +++ b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java @@ -1,7 +1,9 @@ package us.shandian.giga.ui.common; import android.content.Context; +import android.content.Intent; import android.graphics.Color; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.design.widget.Snackbar; @@ -11,6 +13,7 @@ import org.schabi.newpipe.R; import java.util.ArrayList; +import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManager.MissionIterator; @@ -120,6 +123,10 @@ public class Deleter { mIterator.unHide(mission); mDownloadManager.deleteMission(mission); + + if (mission instanceof FinishedMission) { + mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mission.getDownloadedFile()))); + } break; } @@ -167,4 +174,4 @@ public class Deleter { bundle.putStringArray(BUNDLE_NAMES, names); bundle.putStringArray(BUNDLE_LOCATIONS, locations); } -} \ No newline at end of file +} diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index 6cd5ef2c5..e5149cf9b 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -3,6 +3,7 @@ package us.shandian.giga.util; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; +import android.os.Build; import android.support.annotation.ColorInt; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; @@ -21,6 +22,7 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; +import java.net.HttpURLConnection; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Locale; @@ -38,11 +40,11 @@ public class Utility { if (bytes < 1024) { return String.format("%d B", bytes); } else if (bytes < 1024 * 1024) { - return String.format("%.2f kB", (float) bytes / 1024); + return String.format("%.2f kB", bytes / 1024d); } else if (bytes < 1024 * 1024 * 1024) { - return String.format("%.2f MB", (float) bytes / 1024 / 1024); + return String.format("%.2f MB", bytes / 1024d / 1024d); } else { - return String.format("%.2f GB", (float) bytes / 1024 / 1024 / 1024); + return String.format("%.2f GB", bytes / 1024d / 1024d / 1024d); } } @@ -255,4 +257,19 @@ public class Utility { return path.exists(); } + + public static long getContentLength(HttpURLConnection connection) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return connection.getContentLengthLong(); + } + + try { + long length = Long.parseLong(connection.getHeaderField("Content-Length")); + if (length >= 0) return length; + } catch (Exception err) { + // nothing to do + } + + return -1; + } } diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 7234a6639..2973b69ef 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -176,6 +176,17 @@ @string/charset_most_special_characters_value downloads_max_retry + + 1 + 2 + 3 + 4 + 5 + 7 + 10 + 15 + + 3 cross_network_downloads diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index ed38acbb7..7175ae711 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -29,14 +29,15 @@ android:summary="@string/settings_file_replacement_character_summary" android:title="@string/settings_file_replacement_character_title"/> - - Date: Mon, 26 Nov 2018 01:18:02 +0530 Subject: [PATCH 39/68] vector -> png --- app/src/main/ic_settings_update_white-web.png | Bin 0 -> 9830 bytes .../newpipe/CheckForNewAppVersionTask.java | 2 +- .../drawable-hdpi/ic_settings_update_black.png | Bin 0 -> 720 bytes .../drawable-hdpi/ic_settings_update_white.png | Bin 0 -> 720 bytes .../ic_settings_update_black.png | Bin 0 -> 512 bytes .../drawable-mdpi/ic_settings_update_black.png | Bin 0 -> 379 bytes .../drawable-mdpi/ic_settings_update_white.png | Bin 0 -> 562 bytes .../drawable-xhdpi/ic_settings_update_black.png | Bin 0 -> 954 bytes .../drawable-xhdpi/ic_settings_update_white.png | Bin 0 -> 1010 bytes .../drawable-xxhdpi/ic_settings_update_black.png | Bin 0 -> 1340 bytes .../drawable-xxhdpi/ic_settings_update_white.png | Bin 0 -> 1468 bytes .../ic_settings_update_black.png | Bin 0 -> 1769 bytes .../ic_settings_update_white.png | Bin 0 -> 2037 bytes .../drawable-hdpi/ic_settings_update_black.png | Bin 0 -> 431 bytes .../drawable-mdpi/ic_settings_update_black.png | Bin 0 -> 379 bytes .../drawable-xhdpi/ic_settings_update_black.png | Bin 0 -> 710 bytes .../drawable-xxhdpi/ic_settings_update_black.png | Bin 0 -> 1091 bytes .../ic_settings_update_black.png | Bin 0 -> 2029 bytes .../res/drawable/ic_settings_update_black.xml | 9 --------- .../drawable-hdpi/ic_settings_update_black.png | Bin 0 -> 431 bytes .../drawable-mdpi/ic_settings_update_black.png | Bin 0 -> 379 bytes .../drawable-xhdpi/ic_settings_update_black.png | Bin 0 -> 710 bytes .../drawable-xxhdpi/ic_settings_update_black.png | Bin 0 -> 1091 bytes .../ic_settings_update_black.png | Bin 0 -> 2029 bytes .../res/drawable/ic_settings_update_white.xml | 5 ----- 25 files changed, 1 insertion(+), 15 deletions(-) create mode 100644 app/src/main/ic_settings_update_white-web.png create mode 100755 app/src/main/res/drawable-hdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable-hdpi/ic_settings_update_white.png create mode 100755 app/src/main/res/drawable-ldrtl-mdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable-mdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable-mdpi/ic_settings_update_white.png create mode 100755 app/src/main/res/drawable-xhdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable-xhdpi/ic_settings_update_white.png create mode 100755 app/src/main/res/drawable-xxhdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable-xxhdpi/ic_settings_update_white.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_settings_update_white.png create mode 100755 app/src/main/res/drawable/drawable-hdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable/drawable-mdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable/drawable-xhdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable/drawable-xxhdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable/drawable-xxxhdpi/ic_settings_update_black.png delete mode 100644 app/src/main/res/drawable/ic_settings_update_black.xml create mode 100755 app/src/main/res/drawable/ic_settings_update_black/drawable-hdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable/ic_settings_update_black/drawable-mdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable/ic_settings_update_black/drawable-xhdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable/ic_settings_update_black/drawable-xxhdpi/ic_settings_update_black.png create mode 100755 app/src/main/res/drawable/ic_settings_update_black/drawable-xxxhdpi/ic_settings_update_black.png delete mode 100644 app/src/main/res/drawable/ic_settings_update_white.xml diff --git a/app/src/main/ic_settings_update_white-web.png b/app/src/main/ic_settings_update_white-web.png new file mode 100644 index 0000000000000000000000000000000000000000..60383b7901b4d341b2428782840b828ef3c65f9d GIT binary patch literal 9830 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 literal 0 HcmV?d00001 diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index af9b88ac1..706fec186 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -225,6 +225,6 @@ public class CheckForNewAppVersionTask extends AsyncTask { public static boolean isGithubApk() { - return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1); + return true; } } diff --git a/app/src/main/res/drawable-hdpi/ic_settings_update_black.png b/app/src/main/res/drawable-hdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..cdd51d35fe0bb929bc605d41b98e3bd31580b0e7 GIT binary patch literal 720 zcmV;>0x$iEP)Px%ib+I4R9Fe^m%D2eK^(^~DiWXI1z%W)SZM?i5)myFMJo}ET8h2tlvDg8EG;BO zGz76y6ve{IMzoQjAt;Cl=0t+8)A%^^`K~+1?6`X~ySEm~{or#uGr!0Dc4mLGdzDI8 zx-0OXtw2wQ_p<{3b+9sy^O5T;e2M$HFGUiPPd^-o-LM9hfusTN;0{c}EBI@oL{>u$ zKEN*zb@ZoTIg~Doh`1B(gD(+Re;uDeD6$*4rOOtEr=bb%guebyd>(_gxfIzzlqv8{ z?9)FYh6_}(q-0K5$o1ksgX=Prv3E~D- zzi2`x)^lQP19Pa-{4IKL#~^P>cbbq%>>6|LfR=2h^%;yo=(VhbG~ZJgf=Dj%8=8b| zS7t(rdkdyz2 zk#AO9d;ErChbAOPJ@_?P1wN6DZ1WV$TLpaqc9y=jPVWowN4YX)L^|2X))&v#M~}gH zPA3KU0i$4^6Phj2m~^tSCktuo)lxYK`s568ux-BMa~|yNQ(I%w*|wqWoA_1w;C8_P zI={3vy)kMRIn$JI6OKS62l#yrp?6~2se(?&gjSR7Z<4u7jI|Jn_Wc+XO-hHPa3>1w z&)~ZY5-(hJw!%XQ4OE8R6!Q&0luRNc1||AiGXjTUJ?Ny>M}C>2t*wJz7nk5YM3R<} z(djJN2>R=jR^G-g?^Ua+s(u4=d7ZoV-4*!fR$u|=aQRftDBw{500000x$iEP)Px%ib+I4R9Fe^m(NQSVI0PHEy7BL4EurS$WuQ+f(ofaQRFEED-Ru_d)*2v|Bw#S zK{p~g6^YTIQy0;Tgi0ug2xTl`W@@#rpJ#e!chuQw-gl-z*#|y5Gw=8PzV9>dJMTNQ znM_sH5!hxUkWF$w#bR+U`3IoRw6nnRb2gh@GS}7zYeSe&&Jnl)=U^Y~2I&XnVGy2S zY=+~%k&HkCbi+6Jo93u+8}_8`h(>GREW86-y&&NPSkFeq8=@Fd}%Q~!c?PH*~OT7k;IA^>Y!|50C|w|UK7*^D5JxLi zx);@ASWG#f+&Fq^V@M6^!PncqKCJU#nd+FjHjCwp3{8QJw0rl!`sEsMqnN3qx;84v zP@hePsr#GbUGSfik{y+qnCjRnFGE*g$u^Vvzu+O1rcdXlOfeU|lA--D;F3*__AgCe ztwHB5dm3rp^V#7IH5Zp*R;3Qo3c6#6yP7t3vr54mXz~qd-minJDZNaiUZ{j4^^s^B zlwY{Ifa5R(|5N%JJqn(gvLmxj2D~5kRQO9 z_L;-b0GpP#U(exKBu9TFKVjsnLw+@-Is#j31l9n>h3cDEjU>SU0000Px$x=BPqR7ef&l*=nbQ5?rFu`nLxQ6w9RvS1^5m58#pv9e%mqhx6z;=i!4wy?7y zW?`X_k`-H0UU|PCpYLtXbex$xXJ$XY`rLDV@0okg?*u_!Ja3@DD2cU;)y_Xe?+5*`6lue<3Q@1* z3*VrFZ1z9s@L?y@p7aavnJSH%!CfbOMS<%5J%ScfgR zHVxQ^qBE^KUmr1QHp0M&hXz*1{v6&TV)EwyZ{QF1J=`8(Lv9!V0000 CMBifo literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_settings_update_black.png b/app/src/main/res/drawable-mdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..964553137f8d2d1138c49f86c6414fe6af9bbad9 GIT binary patch literal 379 zcmV->0fhdEP)Px$HAzH4R9Fe^l)Fj;Q4ogX-9ig1v9+?Ww6*ay6blPMQi<6348b~Gn&cTQ^8yxv zU~3^-i-9D@>-WPMGAxIH*-hmPe9XTyb7q#|?AB^wv4Pk?Y~a6dV4=hUHaNQrTQE%E zW^b#ExP%Y*NsRCpumUr+Wx99Z6KuG7y-*vLpmJNJ;|?^y27aO66VetxM^|cVbZbH% zY>50F93-SIcZ=SH%It(r-(UmGcijYOi<`eDrk5{m4|K4m-gjMRX`hqUJ-Kowr<~MnFE)QAcAStd=Jl-4 zvb5>P(}LUpef$202Cl literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_settings_update_white.png b/app/src/main/res/drawable-mdpi/ic_settings_update_white.png new file mode 100755 index 0000000000000000000000000000000000000000..cf4642f9720cf549caa4d3b429cff1e6fbf63549 GIT binary patch literal 562 zcmV-20?qx2P)Px$>`6pHR7ef&l(9=3K@^8~)kD0fkV_yEQV9_(v`~(y5fzN2w-6FY;T1M%gm9&W zApQ|4EUi-5S!j?HDH1d&hm{s-B7#vta~c)he80`iv3K0;-t|8C_Pu%U&3m&uv*UU9 zlJEumgt!+3fpRkN2wF`3CHlzs{Zlh`s@Mh;Z)0XJjKKhOfaCza!WSse=mK4zQ2a5x zhkbC#E`c08tgAW`9>5&@g(UeyLLSnIO-M>X7rcWa)GFsBPF8DB+5-}vlkf^&!XR{l zl>pNK|g0nKZNe&e(13C1iG^oeu=T(VB!H4NFAR?BB%%5llMR^VE_YW2Y|>}XIS@+qbq zmt^$@o38f8e~ey$(vKwD_!NtiGd1`SmtkTmveS z8hZwvF}6!x5qKL0X_AFiND{5OmX+(8(OuSR|77WI&{Z)G&maZ$$~6=AUR literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_update_black.png b/app/src/main/res/drawable-xhdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..0304e6fd140ffb51dd739b070f914e295fbc09eb GIT binary patch literal 954 zcmV;r14aCaP)Px&bV)=(RA>e5m^o+_Q4~PGYc%dL5!6&R3L+>33vFx^YefVbK@eM45Yq%~6txgc zVQFcjD1wa$HX&jmL_|>$_qeZdU(fku!espOXa3CpiNahsnKy6lz3;yJ-@LgmrBWe9 z28s+687MN4BLib{*v6SQ^I$Htz$9>tPw)z!z*nf7)N2A0h-fKvz+#vKGoc9_<1@U0 ze&~bCa1(w(F3CFq?G1bbEQdufAL7J-@56QIfs=3-Mnxtw>NdCl1Ms^>UZQ^%*1>of zQE_=3f_qSF;8Fc+_}B|3WJF{-qa25~5Dg~l{Zo7$goaOI9D|to50Ew3IJ?*Q?1Hh- zFxiA>2Z{?KZ~t?AZ)`;LzGUri8}bI3WPgh>S|PXiQ!a!~=!L7`HkcGZ-sg_d1*s(N zJK$b^7?y$Amu4K5i=fm*~eVM&64+h$XO zj)_uHSTZDuI%)j7Rr~H$N^-?9R?=&|lE!(QPCX}J0ci2}0Iad9 zt_FY7euxV!ZGSx{5FXQyrq%3fs3=G24I5-_H);YwJo-NZ!YT5vw=KxP}fp4uv+d-zz&!OX+>Ra@+8j~eGf~Qpq79u>ADOJXa*#S zI_hR!y5aH56WEWlaA(%G&XOh(1g~z^al&?%8Ra^|vNil5Yujzqb?u=)D)#wIovi+& zc4r*KgUh)(I{kvL=p=&ReHINO>%BYI7AVUK{yu`N&Z2gHk$1tYL2TO>)ys2#2Y*8! zO^c&FP|pLq%hEkw>a|3J3BSYVRv0GM+XukoT%efuI)E?9>nCh)GLAvT^MN0iPUwTM z(ZAC6o7D}iP!|7D6&AO*Uhr_^OJafLI`%~$O?l7YJ#Yd%54hCJGVEXwbPitUaa{!+ z&<-tN?J`o6F@Y{Wm-G#|2v@+;V@Rq^AW*XObpmbB3?n%KGw-=LIPTIcq{u*#fg%G% c2I`oBKfw!FKOJ^iGynhq07*qoM6N<$f)wPjaR2}S literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_update_white.png b/app/src/main/res/drawable-xhdpi/ic_settings_update_white.png new file mode 100755 index 0000000000000000000000000000000000000000..9c71b13f7982ba89e77f96beeed0cbaf96a5b69e GIT binary patch literal 1010 zcmVPx&tVu*cRA>e5naOJuK@^8)CQ%dDXjEM0q=JG7DuV~TcoHvOj1mz<2#6OiZXo93 z(HOm%g9rZrFNzC#Py|H;5ibd%s7Tzhxs1_lGcg&zU+kVtW_o%$-8Ewl{ev&PRJ~X4 zRrOR=cOsEcVGo2o5cWXW0|9y<8o<`kG?U59rC=p2ff`U7L+~BCqS0uIqcIstfS_j) z(Pl`%MpzDYFbCAeFeITH+Tj_zCYf=LffOhK0++o4-w)eh1FVK(QotYJB|L>|B=MeO zA3o&Zj(Ar5b$+|zbL{#!U&9(Pm~+qZw7?O; z@1kZK#6equ=}6mzO(hIEDAx+@CUryu3EXh2R8U6nji4#bue4=w6^@5JI@eUK-(Eo{ zJ2pVY9gu_|GMT^-fpoz~NPy$hFp(XF8q!W!;h3HGntaM(O42bHmuC7Y8-oM* zzn@#ro`o)OGidO+eodb*!lAb3+B;o>$;XO|dKd!}GWiGVg6-MW~j($pW#vq|xcx~>_tr#G|wX;d#nv-5nFy6mMwJc7e7;74aM zng^QdvXp8yx2|}>$oK(Tg!)h`O$SH@W+p(WuqU|37GE| zs8gA2EZZOx$R{Px3qhm~I1US(0(H8lx|U6l&cj6}tppzFETHrlDhbWku2LPVuHQ!& z^Q_XEk{-LZ=&evM!L|mWldIH6EE^T8UgJy2El|p-z?24XBuq-3aTyA?7mdGwyKt2aW6}<$AOX98L{@RW6B3{oe7#U; z-xGsMNMEuCKwl_c!9#dX@Om@Ls%Qds4HD7TR|)8Ii2iv|$pqB%UC>S7`ce@p?18Wc g!X5~Fz-$lv1&a_iItI`d4gdfE07*qoM6N<$f*x(b4FCWD literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_update_black.png b/app/src/main/res/drawable-xxhdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..7316dbc88983770f1f9f3e5c166336813c796787 GIT binary patch literal 1340 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 F`~%XSQ9l3x literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_update_white.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_update_white.png new file mode 100755 index 0000000000000000000000000000000000000000..8b5e6fa38c671e03b2a0b8eb31772031d01a6216 GIT binary patch literal 2037 zcmaLY`#;l-0tfK#Hny2%OLKYLnp;-6vrHyiCU)kQlw*{=Tr#7O9GCEHQ$lz~i6gv5 zL?uMXy>*bg5oc0kxjcyJIdbQz)AJ8JKYTv#Up~Ko(%fAg4bB#N}(&72hvh-2U&_dm+OgsS4>~n=$?ty(-_Gc?=?ZK;He(ZJ$RuEy7a^XHS z^(%Of`;u{p03;05;C#WuKs(7j*Q9|39Cl$~V%0T3)B$585t+ii;d6YXy>Mbe%>|^R8s41p$`z7!oZ8z3A1^kou^Q`PjYwe|n!B4Bl;K z&rRbsWe`Hhr$-kn>8FXe>zmk=&Md|K@7e@8um4AR?Z*=I|(Bl2nBLkrZ)W(bda)k+~{X$?a2gVuwLO4FIcLB4;i>=eOg7mRx=VEZpM~fW2bBRnKO% zC{v$s4)dpj%t1Tj!P+zg$#ZT*z!S$t`*-kU&fv8Wyi`=8XVRIrG*7ei#`n~n zG!O(iN~g{%L+S}hvuf!li{#G6!A)Xre(!k}Zoi4C)b zePZL(A%gtml}nQ15g)ZEv4ZmQBKad3A7s*_we78Tv}OJXFMJ}$&vd*DV4kX(s)HrF z?O=!RD(r&iFW?>Q-l^5(MGQ;cIF_U@3AGOH5P0!uoueC>SEMttl|F6#nUM59a(ks6 z%n6103dK<^`;fp}Anyg5nmHXLd>cbAO|$zGTl$5Z8aDi{Bf*K5avb=zJjk~0xmhY# za;<{=a-Gsh=?JSho~*>48QZ|t8R`6fQ5T+_88=CLl%Uj31?^BBL8*;dgLkmfw@wrt zanyNZA8n_rayKr(iz#QA^aNK$a_Y@t@~;m28N{dtHzP)JaCz>tC~|D0hr?Q2Z(g;q zH}95>ugdsp;UT??=#7B2^knpMer5-c)$zEwS^mh`J>a3jl%2KGt}; z;wj=OmtXU^^y_V$TdK*YfRK2V>6lh!{-YU=SJse{FmCjryDr}E0qf)(?KD#!7!^9; z#V=Y8SK{DmRSV8O6p>}GN3n3hNaMil9ThPOKgMMlJ2R>SM>{n`<+)j$>Iv>a+Wlag z^{9tmUVkWh)Wr274<~LjfaRGTUPw=wwsTc?d7$zyGyHdbVN*jiQ?gJeQ@P?^$Hpw( z1vj(i6kc$lzZ@f#H8)8)NL(vv0|{NYde@T=@1l0>OBwRO zO$)Y;=;S`x#yE|$IbV$0-P;N`xaLQeR-ZE<*5>`kN)RnsT&mMAH>=HAjrx{tf|vDS z*W1&C-%GS5+NLNBQe)diO^uHItW|6LtT)a&O$o52q8~I_se(453XfyAzRgPwOKa3O z)#77L*(V7;+{uyCLr*n|jv!%8oMj0{_`yNEN$0=dsSU2(J>YhtblrCG`J_tqFHlTI zHbdjJYN5Oq>yv{R~~?`u5{7lF!FK6R;@LNh=CZ~hi|Hs=TMSQFcsRs_fW{BeNl;7X}J%i#VW D7xIJBsuwEZMZhtXJ?ObIx*K(VRbg-8F4%|1~c7*Kp%@WaM&h2M(2~V#_+`1@rJ6 zzBe;=)8@QFJ(*{P@8?#(yLp_4%}7&$bxDKB1g0PdE)PZ!$6+_uoQFHq6PVXDxF_&@ zxp(g12k9TCA6oz3Gb_rSurBKv*T%h@xHqg+%6j`&p#D}nU!TJxxd*EgCvN=~|9^L5 zJI~7*@uAAXrc4iiXZlG+Hi>ipxnA>*w@35>TkCse7OMT* z?l*(o^@qy6=&VP27TYhD+?W0!aMkUAp#jURTVkB^4=_ErJjoO6c|=HDn9Eepu;Oc! U)=K@?z!+fgboFyt=akR{04@o<&Hw-a literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/drawable-mdpi/ic_settings_update_black.png b/app/src/main/res/drawable/drawable-mdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..964553137f8d2d1138c49f86c6414fe6af9bbad9 GIT binary patch literal 379 zcmV->0fhdEP)Px$HAzH4R9Fe^l)Fj;Q4ogX-9ig1v9+?Ww6*ay6blPMQi<6348b~Gn&cTQ^8yxv zU~3^-i-9D@>-WPMGAxIH*-hmPe9XTyb7q#|?AB^wv4Pk?Y~a6dV4=hUHaNQrTQE%E zW^b#ExP%Y*NsRCpumUr+Wx99Z6KuG7y-*vLpmJNJ;|?^y27aO66VetxM^|cVbZbH% zY>50F93-SIcZ=SH%It(r-(UmGcijYOi<`eDrk5{m4|K4m-gjMRX`hqUJ-Kowr<~MnFE)QAcAStd=Jl-4 zvb5>P(}LUpef$202Cl literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/drawable-xhdpi/ic_settings_update_black.png b/app/src/main/res/drawable/drawable-xhdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..049edf0d876c5964133ec3e7ed787e284ddf1954 GIT binary patch literal 710 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-HD>U~2GmaSW+o zd^^oHThvjcP1u)(d$AyQAU7w+v@D&_Yi=$%e>DHGtQBbzV`I4(ub}D7WVTV{;F4~I z8xbp3ENIfun)SR@Hsamf!t}|r-`sm${UEjMe9hd?XO6$SG2<+Y&jFn1pEA>9-6vKj z=eS=`+_HVnf9t30U-A-UHr{$l=@)iu-dIb)r6U-Y)Ji1fCcop7``WO_yZU*lgg?#{XF9se|6`RaYIZQU1~q2c?yLhh;B?5y&B!;*TQ z)|YMe-Fb^mrxZU6?qvTjeay0P+fg+(i&rMeXBTFBod3V+bqH6$Rlko#+h1wS|9aA< zKhE`))#FQ%@eYYsU!77C+F7%*&VPmQg2O9%>&2J(-j-V>w=m>`VZETF-@b_GpZ^w| z&ycw_*a+5(kV_od_{gK=MU2ERK zvi=+Y?dKQm|DEm%zrFMNljxNe{-4$u&)?J?GRL`lC7bor>-M!dv%_lhGbb}^9jTeg z{&i)aV0E30)t1P*lN=!jwH4P!%@s8{;c3?sX?}0p_4P*1Ii*SazHhnuYMD;?)5ACJ z2Qj~0@?ULoYeSKjW5?STruuiM+AF_A=G;9eaL`?(>6GTwj8b)z*q;W+=U&^a&hYoY zy^G?CJ3#_nLB8Q}#V_tZHhI9}Iaf{Qp-??F8bk@_U ze=J_j$#gAIzvG}E@J@Y`-cR|SrQH7MakuvcYx~=n2^%otL=t>V((ikkB%aAm*bYpX N44$rjF6*2UngDR^J5c}t literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/drawable-xxhdpi/ic_settings_update_black.png b/app/src/main/res/drawable/drawable-xxhdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..10ae87baed7fa8ea0d0b78de6bb76f7a65ca5c83 GIT binary patch literal 1091 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-HD>VE*gr;uuoF z_;%Lm)xLo;$JduXs_L0?oaOp#BgYlH+3&fqxv;UF>Zxy-wBLcPt5aplB%=ckbF|Hu zXbX2Ws`T?1d^n-J{`~fLXWy2bi>rRW`Mq8G{ojADE`N73d+q&OuQz|YmA<|)+qtb-oso0l4VTvA6GF@Omi3=lIrTw@;f4uD zv1`Aom{tAJ+i9KjOa78;vc%uajF6(3-}X=JSr;E@Y%Gi5-RRxUzxDrU+LvP zf9D<9x&GpOVr*~d$jdVTOsC2G6Wv}sxk2~r-P71}E?3%kUac!-W zdgUUKonKs*>^)f$|Y|M-g?PbeHW6-*jseB zdodf&id@!LPx)o{Uv;>zd-;nmGn0x3)8;iTE4Jr)6vgcP#<{fL=HC0|v7Ca}ObQE& zCQo_!Hs|GI3uO8M&IeQNrWKcE2`mfls?F&pX{?y?Wi$K=qgF8=EJU z)|al%QlGS6jH^?y^~GJ+*WzN7-fx*+&&t;ucLK6UgrSE?L literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/drawable-xxxhdpi/ic_settings_update_black.png b/app/src/main/res/drawable/drawable-xxxhdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..49ac8ea654e4f48096c13cdb32633705825c5981 GIT binary patch literal 2029 zcmc(g{Xf$S8^^yJ+ZqRrh~?p+4doWcLt=7=bL9CXr1G@fg%+j<&tkJBloAJFiiMnt zo!irHlG!kh40jBXeaDG|X-i}>9Oth%ug~Xmy??oWxL(&UpX{H#U6mBj3IG5oxt(_L z-DAez*$3a7oRj#0JwR^wx;g>%JncmQK-9XqobpSAtduQ32tKQsbunS-|3wsJ8mDxegtUoa*Ima^3#>+cyAUe#-l9o=_vzPf1 zHBRW6>R_o&CwrZt6K4)~1v9iovUiogZ(O-pOCsEj3ezISZz3<~_`6?qqHH+Abjevl zv@LuoCp=1uD^V`WYRJ;N*p7`%f!ZF?qh2POj81M^rGHNZ45cJg|(tU-xGQ~iM^45a+( z2d@2+H_5VVyMr!JZ`+-5VPcuU8;UhJ%kZ}dux<=BiT{RUqRiy0haj$D1#Y5dT3UVP z7(?}fI>v|cew5m%o#~nzqjUGTYvxl z*fEbI5bw|YZ(JdocuPh-X5+k{ey)&bUKuubRPO$wh(|-a=89MlF_+ z&5-lVXaE(7WlU7g7Eo6xPYe~amrq6dD}B6eZ;@|=U1gaUWnviWA=KlbrBo|t(xlKg zRA*hW(#!hEl*1+FkGMmo1fQ<(;qE$2@ro|yEx_L)2o;Pm`t4Kb%~+cHO%ZAA7jUvS zR$_CF(o3hv6L&?6Gi$xkqbYKEsRdytL{N`Ya{?n{e0S2s9Qr9GMQC$X$z$3;LLBIh z#koKHv?kX5wyRo1odV+{Wyb9kRdYpD#m@SnvM5kaQ+`<_6rLeAd^SCn+Axso@ceq~ zY}MGxOeBXYtOk>~Tzd4goX(^Akn)}FYaKZfRu89mNXR{jw#xnklnm$k|r|%;)O^hPjoEh}I(Uoe`JX z`Kis8^B+;omszXhk%bR|DT}9(uCe|?=-@9cjVv)k;itBRPopyLg^k8P=T>(W#Dg0j z{@$yfaZp-&!T0Y4?tf59$vU)6;f6#&?R#MYI@lEYv9I=jl?>7OU(xqfRYAk=$`vgM z=J<{`Dme13--96?E;`n`lI6WeO6&0SGfvRdyCf=jml1PLs~)M{(IP&cs~wj)yi%SK zs^Zm8<2AI+PwjMlR&;#nC_U(fYgM+^@gDesxAALcpOiT~mOsmaRnd7XRfNn;PH88g z3$gE9D)lMI>c-wq)|)x;M{Bsu8Ho&Y^@N}6zJWIwMa8u95CdH{6Ur5in*3CIL23Fi z&n!F6WFK|$1*3(hs8hG1s>({adGt@?_5Zy6uirUU=9pDKE@>J@Gnqh%`&z%ys7q}p zy;OR9WC@tp`%<3%W{iOpg3lROhDg8W-HBRy$Ozx&^ZXdvn!3HFas{mX(1e8>*_Bbd z*19xU(3X`(_2CSf_yQ~60w|)m{d_J^*^}G|x-%h5^_xMLB0bvjxW&R?;yl61-3`Dn zH5g`FlNp9_Xxj_R=zJPIFamfou2bQ9O>TK-nE^*Xc%=Got(bQLQ(=>jwcot0eolIoU?Sdr^W?r>SUlPkLg*BH@e85fz+nCz7f}rQ1!+g_uukr zK$s14)mjOc+s`JQwxd${0hIX zKd7%e8>k#Z;gJV#u@}P(m?@EVxU)q4b~!#L!hGLtkA8~zDOHR8a{rizdsvPucn{Go T%FJM|zX7)&y - - diff --git a/app/src/main/res/drawable/ic_settings_update_black/drawable-hdpi/ic_settings_update_black.png b/app/src/main/res/drawable/ic_settings_update_black/drawable-hdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..1e318eecc2817d0da257bfbfd8b96aabca214f04 GIT binary patch literal 431 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^TIJBsuwEZMZhtXJ?ObIx*K(VRbg-8F4%|1~c7*Kp%@WaM&h2M(2~V#_+`1@rJ6 zzBe;=)8@QFJ(*{P@8?#(yLp_4%}7&$bxDKB1g0PdE)PZ!$6+_uoQFHq6PVXDxF_&@ zxp(g12k9TCA6oz3Gb_rSurBKv*T%h@xHqg+%6j`&p#D}nU!TJxxd*EgCvN=~|9^L5 zJI~7*@uAAXrc4iiXZlG+Hi>ipxnA>*w@35>TkCse7OMT* z?l*(o^@qy6=&VP27TYhD+?W0!aMkUAp#jURTVkB^4=_ErJjoO6c|=HDn9Eepu;Oc! U)=K@?z!+fgboFyt=akR{04@o<&Hw-a literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_settings_update_black/drawable-mdpi/ic_settings_update_black.png b/app/src/main/res/drawable/ic_settings_update_black/drawable-mdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..964553137f8d2d1138c49f86c6414fe6af9bbad9 GIT binary patch literal 379 zcmV->0fhdEP)Px$HAzH4R9Fe^l)Fj;Q4ogX-9ig1v9+?Ww6*ay6blPMQi<6348b~Gn&cTQ^8yxv zU~3^-i-9D@>-WPMGAxIH*-hmPe9XTyb7q#|?AB^wv4Pk?Y~a6dV4=hUHaNQrTQE%E zW^b#ExP%Y*NsRCpumUr+Wx99Z6KuG7y-*vLpmJNJ;|?^y27aO66VetxM^|cVbZbH% zY>50F93-SIcZ=SH%It(r-(UmGcijYOi<`eDrk5{m4|K4m-gjMRX`hqUJ-Kowr<~MnFE)QAcAStd=Jl-4 zvb5>P(}LUpef$202Cl literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_settings_update_black/drawable-xhdpi/ic_settings_update_black.png b/app/src/main/res/drawable/ic_settings_update_black/drawable-xhdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..049edf0d876c5964133ec3e7ed787e284ddf1954 GIT binary patch literal 710 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-HD>U~2GmaSW+o zd^^oHThvjcP1u)(d$AyQAU7w+v@D&_Yi=$%e>DHGtQBbzV`I4(ub}D7WVTV{;F4~I z8xbp3ENIfun)SR@Hsamf!t}|r-`sm${UEjMe9hd?XO6$SG2<+Y&jFn1pEA>9-6vKj z=eS=`+_HVnf9t30U-A-UHr{$l=@)iu-dIb)r6U-Y)Ji1fCcop7``WO_yZU*lgg?#{XF9se|6`RaYIZQU1~q2c?yLhh;B?5y&B!;*TQ z)|YMe-Fb^mrxZU6?qvTjeay0P+fg+(i&rMeXBTFBod3V+bqH6$Rlko#+h1wS|9aA< zKhE`))#FQ%@eYYsU!77C+F7%*&VPmQg2O9%>&2J(-j-V>w=m>`VZETF-@b_GpZ^w| z&ycw_*a+5(kV_od_{gK=MU2ERK zvi=+Y?dKQm|DEm%zrFMNljxNe{-4$u&)?J?GRL`lC7bor>-M!dv%_lhGbb}^9jTeg z{&i)aV0E30)t1P*lN=!jwH4P!%@s8{;c3?sX?}0p_4P*1Ii*SazHhnuYMD;?)5ACJ z2Qj~0@?ULoYeSKjW5?STruuiM+AF_A=G;9eaL`?(>6GTwj8b)z*q;W+=U&^a&hYoY zy^G?CJ3#_nLB8Q}#V_tZHhI9}Iaf{Qp-??F8bk@_U ze=J_j$#gAIzvG}E@J@Y`-cR|SrQH7MakuvcYx~=n2^%otL=t>V((ikkB%aAm*bYpX N44$rjF6*2UngDR^J5c}t literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_settings_update_black/drawable-xxhdpi/ic_settings_update_black.png b/app/src/main/res/drawable/ic_settings_update_black/drawable-xxhdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..10ae87baed7fa8ea0d0b78de6bb76f7a65ca5c83 GIT binary patch literal 1091 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-HD>VE*gr;uuoF z_;%Lm)xLo;$JduXs_L0?oaOp#BgYlH+3&fqxv;UF>Zxy-wBLcPt5aplB%=ckbF|Hu zXbX2Ws`T?1d^n-J{`~fLXWy2bi>rRW`Mq8G{ojADE`N73d+q&OuQz|YmA<|)+qtb-oso0l4VTvA6GF@Omi3=lIrTw@;f4uD zv1`Aom{tAJ+i9KjOa78;vc%uajF6(3-}X=JSr;E@Y%Gi5-RRxUzxDrU+LvP zf9D<9x&GpOVr*~d$jdVTOsC2G6Wv}sxk2~r-P71}E?3%kUac!-W zdgUUKonKs*>^)f$|Y|M-g?PbeHW6-*jseB zdodf&id@!LPx)o{Uv;>zd-;nmGn0x3)8;iTE4Jr)6vgcP#<{fL=HC0|v7Ca}ObQE& zCQo_!Hs|GI3uO8M&IeQNrWKcE2`mfls?F&pX{?y?Wi$K=qgF8=EJU z)|al%QlGS6jH^?y^~GJ+*WzN7-fx*+&&t;ucLK6UgrSE?L literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_settings_update_black/drawable-xxxhdpi/ic_settings_update_black.png b/app/src/main/res/drawable/ic_settings_update_black/drawable-xxxhdpi/ic_settings_update_black.png new file mode 100755 index 0000000000000000000000000000000000000000..49ac8ea654e4f48096c13cdb32633705825c5981 GIT binary patch literal 2029 zcmc(g{Xf$S8^^yJ+ZqRrh~?p+4doWcLt=7=bL9CXr1G@fg%+j<&tkJBloAJFiiMnt zo!irHlG!kh40jBXeaDG|X-i}>9Oth%ug~Xmy??oWxL(&UpX{H#U6mBj3IG5oxt(_L z-DAez*$3a7oRj#0JwR^wx;g>%JncmQK-9XqobpSAtduQ32tKQsbunS-|3wsJ8mDxegtUoa*Ima^3#>+cyAUe#-l9o=_vzPf1 zHBRW6>R_o&CwrZt6K4)~1v9iovUiogZ(O-pOCsEj3ezISZz3<~_`6?qqHH+Abjevl zv@LuoCp=1uD^V`WYRJ;N*p7`%f!ZF?qh2POj81M^rGHNZ45cJg|(tU-xGQ~iM^45a+( z2d@2+H_5VVyMr!JZ`+-5VPcuU8;UhJ%kZ}dux<=BiT{RUqRiy0haj$D1#Y5dT3UVP z7(?}fI>v|cew5m%o#~nzqjUGTYvxl z*fEbI5bw|YZ(JdocuPh-X5+k{ey)&bUKuubRPO$wh(|-a=89MlF_+ z&5-lVXaE(7WlU7g7Eo6xPYe~amrq6dD}B6eZ;@|=U1gaUWnviWA=KlbrBo|t(xlKg zRA*hW(#!hEl*1+FkGMmo1fQ<(;qE$2@ro|yEx_L)2o;Pm`t4Kb%~+cHO%ZAA7jUvS zR$_CF(o3hv6L&?6Gi$xkqbYKEsRdytL{N`Ya{?n{e0S2s9Qr9GMQC$X$z$3;LLBIh z#koKHv?kX5wyRo1odV+{Wyb9kRdYpD#m@SnvM5kaQ+`<_6rLeAd^SCn+Axso@ceq~ zY}MGxOeBXYtOk>~Tzd4goX(^Akn)}FYaKZfRu89mNXR{jw#xnklnm$k|r|%;)O^hPjoEh}I(Uoe`JX z`Kis8^B+;omszXhk%bR|DT}9(uCe|?=-@9cjVv)k;itBRPopyLg^k8P=T>(W#Dg0j z{@$yfaZp-&!T0Y4?tf59$vU)6;f6#&?R#MYI@lEYv9I=jl?>7OU(xqfRYAk=$`vgM z=J<{`Dme13--96?E;`n`lI6WeO6&0SGfvRdyCf=jml1PLs~)M{(IP&cs~wj)yi%SK zs^Zm8<2AI+PwjMlR&;#nC_U(fYgM+^@gDesxAALcpOiT~mOsmaRnd7XRfNn;PH88g z3$gE9D)lMI>c-wq)|)x;M{Bsu8Ho&Y^@N}6zJWIwMa8u95CdH{6Ur5in*3CIL23Fi z&n!F6WFK|$1*3(hs8hG1s>({adGt@?_5Zy6uirUU=9pDKE@>J@Gnqh%`&z%ys7q}p zy;OR9WC@tp`%<3%W{iOpg3lROhDg8W-HBRy$Ozx&^ZXdvn!3HFas{mX(1e8>*_Bbd z*19xU(3X`(_2CSf_yQ~60w|)m{d_J^*^}G|x-%h5^_xMLB0bvjxW&R?;yl61-3`Dn zH5g`FlNp9_Xxj_R=zJPIFamfou2bQ9O>TK-nE^*Xc%=Got(bQLQ(=>jwcot0eolIoU?Sdr^W?r>SUlPkLg*BH@e85fz+nCz7f}rQ1!+g_uukr zK$s14)mjOc+s`JQwxd${0hIX zKd7%e8>k#Z;gJV#u@}P(m?@EVxU)q4b~!#L!hGLtkA8~zDOHR8a{rizdsvPucn{Go T%FJM|zX7)&y - - From c864b15c34524bdfe83eb6faf3c6d438b3a453a6 Mon Sep 17 00:00:00 2001 From: krtkush Date: Mon, 26 Nov 2018 01:18:33 +0530 Subject: [PATCH 40/68] Test code revert. --- .../main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 706fec186..af9b88ac1 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -225,6 +225,6 @@ public class CheckForNewAppVersionTask extends AsyncTask { public static boolean isGithubApk() { - return true; + return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1); } } From eba3b327082e6a9a37b0f866f32d1d8e6762b784 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Mon, 26 Nov 2018 00:20:25 -0300 Subject: [PATCH 41/68] misc improvements * don't show notifications while download activity * proper icon in failed download notifications * re-write list auto-refresh (MissionAdapter.java) * improve I/O performance (CircularFile.java) * fix implementation of "save thread position" on multi-thread downloads --- .../newpipe/download/DownloadDialog.java | 3 +- .../us/shandian/giga/get/DownloadMission.java | 10 +- .../shandian/giga/get/DownloadRunnable.java | 22 ++- .../giga/get/DownloadRunnableFallback.java | 21 +-- .../giga/postprocessing/io/CircularFile.java | 54 +++--- .../giga/service/DownloadManager.java | 5 +- .../giga/service/DownloadManagerService.java | 168 +++++++++++------- .../giga/ui/adapter/MissionAdapter.java | 141 +++++++++------ .../giga/ui/fragment/MissionsFragment.java | 19 +- app/src/main/res/values/settings_keys.xml | 7 +- app/src/main/res/xml/download_settings.xml | 4 +- 11 files changed, 270 insertions(+), 184 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 0418eadae..4f98f7f28 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -416,7 +416,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck // fallback // 1st loop match country & language // 2nd loop match language only - String lang = loc.getLanguage().substring(0, loc.getLanguage().indexOf("-")); + int index = loc.getLanguage().indexOf("-"); + String lang = index > 0 ? loc.getLanguage().substring(0, index) : loc.getLanguage(); for (int j = 0; j < 2; j++) { for (int i = 0; i < streams.size(); i++) { 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 851b5cb1b..79e01b8cc 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -221,12 +221,12 @@ public class DownloadMission extends Mission { } /** - * Get position inside of the block, where thread will be resumed + * Get position inside of the thread, where thread will be resumed * * @param threadId the identifier of the thread * @return the relative position in bytes or zero */ - long getBlockBytePosition(int threadId) { + long getThreadBytePosition(int threadId) { return threadBytePositions.get(threadId); } @@ -256,6 +256,8 @@ public class DownloadMission extends Mission { } } + conn.connect(); + int statusCode = conn.getResponseCode(); switch (statusCode) { case 204: @@ -446,6 +448,8 @@ public class DownloadMission extends Mission { return; } + if (postprocessingRunning) return; + // wait for all threads are suspended before save the state runAsync(-1, () -> { try { @@ -590,7 +594,7 @@ public class DownloadMission extends Mission { @Override public String getMessage() { - return "Http status code" + String.valueOf(statusCode); + return "Http status code: " + String.valueOf(statusCode); } } } 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 95f4758f9..336bc13ee 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -2,11 +2,10 @@ package us.shandian.giga.get; import android.util.Log; -import java.io.BufferedInputStream; import java.io.FileNotFoundException; +import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; -import java.net.URL; import java.nio.channels.ClosedByInterruptException; import static org.schabi.newpipe.BuildConfig.DEBUG; @@ -38,8 +37,8 @@ public class DownloadRunnable implements Runnable { Log.d(TAG, mId + ":recovered: " + mMission.recovered); } - BufferedInputStream ipt = null; RandomAccessFile f; + InputStream is = null; try { f = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); @@ -82,9 +81,11 @@ public class DownloadRunnable implements Runnable { mMission.preserveBlock(blockPosition); mMission.setBlockPosition(mId, blockPosition); - long start = (blockPosition * DownloadMission.BLOCK_SIZE) + mMission.getBlockBytePosition(mId); + long start = blockPosition * DownloadMission.BLOCK_SIZE; long end = start + DownloadMission.BLOCK_SIZE - 1; + start += mMission.getThreadBytePosition(mId); + if (end >= mMission.length) { end = mMission.length - 1; } @@ -107,11 +108,11 @@ public class DownloadRunnable implements Runnable { f.seek(mMission.offsets[mMission.current] + start); - ipt = new BufferedInputStream(conn.getInputStream()); + is = conn.getInputStream(); byte[] buf = new byte[DownloadMission.BUFFER_SIZE]; int len; - while (start < end && mMission.running && (len = ipt.read(buf, 0, buf.length)) != -1) { + while (start < end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) { f.write(buf, 0, len); start += len; total += len; @@ -119,7 +120,8 @@ public class DownloadRunnable implements Runnable { } if (DEBUG && mMission.running) { - Log.d(TAG, mId + ":position " + blockPosition + " finished, total length " + total); + Log.d(TAG, mId + ":position " + blockPosition + " finished, " + total + " bytes downloaded"); + mMission.setThreadBytePosition(mId, 0L); } // if the download is paused, save progress for this thread @@ -132,7 +134,7 @@ public class DownloadRunnable implements Runnable { if (e instanceof ClosedByInterruptException) break; - if (retryCount++ > mMission.maxRetry) { + if (retryCount++ >= mMission.maxRetry) { mMission.notifyError(e); break; } @@ -140,6 +142,8 @@ public class DownloadRunnable implements Runnable { if (DEBUG) { Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e); } + + retry = true; } } @@ -150,7 +154,7 @@ public class DownloadRunnable implements Runnable { } try { - if (ipt != null) ipt.close(); + if (is != null) is.close(); } catch (Exception err) { // nothing to do } 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 b648dd812..5ef4ed90e 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -4,8 +4,8 @@ import android.annotation.SuppressLint; import android.support.annotation.NonNull; import android.util.Log; -import java.io.BufferedInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.nio.channels.ClosedByInterruptException; @@ -24,18 +24,18 @@ public class DownloadRunnableFallback implements Runnable { private final DownloadMission mMission; private int retryCount = 0; - private BufferedInputStream ipt; + private InputStream is; private RandomAccessFile f; DownloadRunnableFallback(@NonNull DownloadMission mission) { mMission = mission; - ipt = null; + is = null; f = null; } private void dispose() { try { - if (ipt != null) ipt.close(); + if (is != null) is.close(); } catch (IOException e) { // nothing to do } @@ -55,7 +55,7 @@ public class DownloadRunnableFallback implements Runnable { long start = 0; if (!mMission.unknownLength) { - start = mMission.getBlockBytePosition(0); + start = mMission.getThreadBytePosition(0); if (DEBUG && start > 0) { Log.i(TAG, "Resuming a single-thread download at " + start); } @@ -72,18 +72,15 @@ public class DownloadRunnableFallback implements Runnable { f = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); f.seek(mMission.offsets[mMission.current] + start); - ipt = new BufferedInputStream(conn.getInputStream()); + is = conn.getInputStream(); - byte[] buf = new byte[DownloadMission.BUFFER_SIZE]; + byte[] buf = new byte[64 * 1024]; int len = 0; - while (mMission.running && (len = ipt.read(buf, 0, buf.length)) != -1) { + while (mMission.running && (len = is.read(buf, 0, buf.length)) != -1) { f.write(buf, 0, len); start += len; - mMission.notifyProgress(len); - - if (Thread.interrupted()) break; } // if thread goes interrupted check if the last part is written. This avoid re-download the whole file @@ -96,7 +93,7 @@ public class DownloadRunnableFallback implements Runnable { if (e instanceof ClosedByInterruptException) return; - if (retryCount++ > mMission.maxRetry) { + if (retryCount++ >= mMission.maxRetry) { mMission.notifyError(e); return; } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java index 1454c1f2d..7e5ad9929 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java @@ -10,8 +10,10 @@ import java.util.ArrayList; public class CircularFile extends SharpStream { private final static int AUX_BUFFER_SIZE = 1024 * 1024;// 1 MiB - private final static int NOTIFY_BYTES_INTERVAL = 256 * 1024;// 256 KiB + private final static int AUX_BUFFER_SIZE2 = 512 * 1024;// 512 KiB + private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB + private final static boolean IMMEDIATE_AUX_BUFFER_FLUSH = false; private RandomAccessFile out; private long position; @@ -45,7 +47,7 @@ public class CircularFile extends SharpStream { throw err; } - auxiliaryBuffers = new ArrayList<>(1); + auxiliaryBuffers = new ArrayList<>(15); callback = checker; startOffset = offset; reportPosition = offset; @@ -122,7 +124,7 @@ public class CircularFile extends SharpStream { while (available > 0 && auxiliaryBuffers.size() > 0) { ManagedBuffer aux = auxiliaryBuffers.get(0); - // check if there is enough space to dump the auxiliar buffer + // check if there is enough space to dump the auxiliary buffer if (available >= (aux.size + queue.size)) { available -= aux.size; writeQueue(aux.buffer, 0, aux.size); @@ -131,26 +133,27 @@ public class CircularFile extends SharpStream { continue; } - // try flush contents to avoid allocate another auxiliar buffer - if (aux.available() < len && available > queue.size) { - int size = Math.min(len, aux.available()); - aux.write(b, off, size); + if (IMMEDIATE_AUX_BUFFER_FLUSH) { + // try flush contents to avoid allocate another auxiliary buffer + if (aux.available() < len && available > queue.size) { + int size = Math.min(len, aux.available()); + aux.write(b, off, size); - off += size; - len -= size; + off += size; + len -= size; - size = Math.min(aux.size, (int) available - queue.size); - if (size < 1) { - break; + size = Math.min(aux.size, (int) available - queue.size); + if (size < 1) { + break; + } + + writeQueue(aux.buffer, 0, size); + aux.dereference(size); + + available -= size; } - - writeQueue(aux.buffer, 0, size); - aux.dereference(size); - - available -= size; + break; } - - break; } if (len < 1) { @@ -174,7 +177,7 @@ public class CircularFile extends SharpStream { if (available < 1) { // secondary auxiliary buffer available = len; - aux = new ManagedBuffer(Math.max(len, AUX_BUFFER_SIZE)); + aux = new ManagedBuffer(Math.max(len, AUX_BUFFER_SIZE2)); auxiliaryBuffers.add(aux); i++; } else { @@ -184,10 +187,7 @@ public class CircularFile extends SharpStream { aux.write(b, off, (int) available); len -= available; - if (len < 1) { - break; - } - off += available; + if (len > 0) off += available; } } } @@ -361,12 +361,8 @@ public class CircularFile extends SharpStream { if (amount > size) { throw new IndexOutOfBoundsException("Invalid dereference amount (" + amount + ">=" + size + ")"); } - size -= amount; - - for (int i = 0; i < size; i++) { - buffer[i] = buffer[amount + i]; - } + System.arraycopy(buffer, amount, buffer, 0, size); } protected int available() { diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 31b5b16a9..55a22c8c5 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -277,6 +277,7 @@ public class DownloadManager { mDownloadDataSource.deleteMission(mission); } + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); mission.delete(); } } @@ -427,8 +428,8 @@ public class DownloadManager { if (!canDownloadInCurrentNetwork()) return false; for (DownloadMission mission : mMissionsPending) { - if (!mission.running && mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED && mission.enqueued) { - resumeMission(mMissionsPending.get(i)); + if (!mission.running && mission.errCode == DownloadMission.ERROR_NOTHING && mission.enqueued) { + resumeMission(mission); return true; } } 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 7b30740d5..7d88d9e2a 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -28,6 +28,7 @@ import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.content.PermissionChecker; import android.util.Log; +import android.util.SparseArray; import android.widget.Toast; import org.schabi.newpipe.R; @@ -36,7 +37,6 @@ import org.schabi.newpipe.player.helper.LockManager; import java.io.File; import java.util.ArrayList; -import java.util.Iterator; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.service.DownloadManager.NetworkState; @@ -46,13 +46,14 @@ import static org.schabi.newpipe.BuildConfig.DEBUG; public class DownloadManagerService extends Service { - private static final String TAG = DownloadManagerService.class.getSimpleName(); + private static final String TAG = "DownloadManagerService"; - public static final int MESSAGE_RUNNING = 1; - public static final int MESSAGE_PAUSED = 2; - public static final int MESSAGE_FINISHED = 3; - public static final int MESSAGE_PROGRESS = 4; - public static final int MESSAGE_ERROR = 5; + public static final int MESSAGE_RUNNING = 0; + public static final int MESSAGE_PAUSED = 1; + public static final int MESSAGE_FINISHED = 2; + public static final int MESSAGE_PROGRESS = 3; + public static final int MESSAGE_ERROR = 4; + public static final int MESSAGE_DELETED = 5; private static final int FOREGROUND_NOTIFICATION_ID = 1000; private static final int DOWNLOADS_NOTIFICATION_ID = 1001; @@ -67,17 +68,20 @@ public class DownloadManagerService extends Service { private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; - private static final String ACTION_RESET_DOWNLOAD_COUNT = APPLICATION_ID + ".reset_download_count"; + private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; + private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; private DMBinder mBinder; private DownloadManager mManager; private Notification mNotification; private Handler mHandler; + private boolean mForeground = false; + private NotificationManager notificationManager = null; + private boolean mDownloadNotificationEnable = true; + private int downloadDoneCount = 0; private Builder downloadDoneNotification = null; private StringBuilder downloadDoneList = null; - NotificationManager notificationManager = null; - private boolean mForeground = false; private final ArrayList mEchoObservers = new ArrayList<>(1); @@ -90,9 +94,14 @@ public class DownloadManagerService extends Service { private LockManager wakeLock = null; private int downloadFailedNotificationID = DOWNLOADS_NOTIFICATION_ID + 1; + private Builder downloadFailedNotification = null; + private SparseArray mFailedDownloads = new SparseArray<>(5); private Bitmap icLauncher; private Bitmap icDownloadDone; + private Bitmap icDownloadFailed; + + private PendingIntent mOpenDownloadList; /** * notify media scanner on downloaded media file ... @@ -124,14 +133,14 @@ public class DownloadManagerService extends Service { Intent openDownloadListIntent = new Intent(this, DownloadActivity.class) .setAction(Intent.ACTION_MAIN); - PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, + mOpenDownloadList = PendingIntent.getActivity(this, 0, openDownloadListIntent, PendingIntent.FLAG_UPDATE_CURRENT); icLauncher = BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher); Builder builder = new Builder(this, getString(R.string.notification_channel_id)) - .setContentIntent(pendingIntent) + .setContentIntent(mOpenDownloadList) .setSmallIcon(android.R.drawable.stat_sys_download) .setLargeIcon(icLauncher) .setContentTitle(getString(R.string.msg_running)) @@ -155,6 +164,9 @@ public class DownloadManagerService extends Service { mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener); + handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network)); + handlePreferenceChange(mPrefs, getString(R.string.downloads_max_retry)); + wakeLock = new LockManager(this); } @@ -183,9 +195,17 @@ public class DownloadManagerService extends Service { mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength)); - } else if (downloadDoneNotification != null && action.equals(ACTION_RESET_DOWNLOAD_COUNT)) { - downloadDoneCount = 0; - downloadDoneList.setLength(0); + } else if (downloadDoneNotification != null) { + if (action.equals(ACTION_RESET_DOWNLOAD_FINISHED) || action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) { + downloadDoneCount = 0; + downloadDoneList.setLength(0); + } + if (action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) { + startActivity(new Intent(this, DownloadActivity.class) + .setAction(Intent.ACTION_MAIN) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ); + } } } return START_NOT_STICKY; @@ -213,7 +233,8 @@ public class DownloadManagerService extends Service { unregisterReceiver(mNetworkStateListener); mPrefs.unregisterOnSharedPreferenceChangeListener(mPrefChangeListener); - icDownloadDone.recycle(); + if (icDownloadDone != null) icDownloadDone.recycle(); + if (icDownloadFailed != null) icDownloadFailed.recycle(); icLauncher.recycle(); } @@ -250,7 +271,7 @@ public class DownloadManagerService extends Service { updateForegroundState(true); break; case MESSAGE_ERROR: - notifyFailedDownload(mission.name); + notifyFailedDownload(mission); updateForegroundState(mManager.runAnotherMission()); break; case MESSAGE_PAUSED: @@ -258,19 +279,16 @@ public class DownloadManagerService extends Service { break; } + if (msg.what != MESSAGE_ERROR) + mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission)); synchronized (mEchoObservers) { - Iterator iterator = mEchoObservers.iterator(); - while (iterator.hasNext()) { - Handler handler = iterator.next(); - if (handler.getLooper().getThread().isAlive()) { - Message echo = new Message(); - echo.what = msg.what; - echo.obj = msg.obj; - handler.sendMessage(echo); - } else { - iterator.remove();// ¿missing call to removeMissionEventListener()? - } + for (Handler handler : mEchoObservers) { + Message echo = new Message(); + echo.what = msg.what; + echo.obj = msg.obj; + + handler.sendMessage(echo); } } } @@ -306,11 +324,14 @@ public class DownloadManagerService extends Service { private void handlePreferenceChange(SharedPreferences prefs, String key) { if (key.equals(getString(R.string.downloads_max_retry))) { - mManager.mPrefMaxRetry = Integer.parseInt( - prefs.getString(key, getString(R.string.default_max_retry)) - ); + try { + String value = prefs.getString(key, getString(R.string.downloads_max_retry_default)); + mManager.mPrefMaxRetry = Integer.parseInt(value); + } catch (Exception e) { + mManager.mPrefMaxRetry = 0; + } mManager.updateMaximumAttempts(); - } else if (key.equals(getString(R.string.cross_network_downloads))) { + } else if (key.equals(getString(R.string.downloads_cross_network))) { mManager.mPrefCrossNetwork = prefs.getBoolean(key, false); } } @@ -368,7 +389,7 @@ public class DownloadManagerService extends Service { } public void notifyFinishedDownload(String name) { - if (notificationManager == null) { + if (!mDownloadNotificationEnable || notificationManager == null) { return; } @@ -380,14 +401,8 @@ public class DownloadManagerService extends Service { .setAutoCancel(true) .setLargeIcon(icDownloadDone) .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setDeleteIntent(PendingIntent.getService(this, (int) System.currentTimeMillis(), - new Intent(this, DownloadManagerService.class) - .setAction(ACTION_RESET_DOWNLOAD_COUNT) - , PendingIntent.FLAG_UPDATE_CURRENT)) - .setContentIntent(PendingIntent.getService(this, (int) System.currentTimeMillis() + 1, - new Intent(this, DownloadActivity.class) - .setAction(Intent.ACTION_MAIN), - PendingIntent.FLAG_UPDATE_CURRENT)); + .setDeleteIntent(makePendingIntent(ACTION_RESET_DOWNLOAD_FINISHED)) + .setContentIntent(makePendingIntent(ACTION_OPEN_DOWNLOADS_FINISHED)); } if (downloadDoneCount < 1) { @@ -417,33 +432,38 @@ public class DownloadManagerService extends Service { downloadDoneCount++; } - public void notifyFailedDownload(String name) { - if (icDownloadDone == null) { - // TODO: use a proper icon for failed downloads - icDownloadDone = BitmapFactory.decodeResource(this.getResources(), android.R.drawable.stat_sys_download_done); - } + public void notifyFailedDownload(DownloadMission mission) { + if (!mDownloadNotificationEnable || mFailedDownloads.indexOfValue(mission) >= 0) return; - Builder notification = new Builder(this, getString(R.string.notification_channel_id)) - .setAutoCancel(true) - .setLargeIcon(icDownloadDone) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setContentIntent(PendingIntent.getService(this, (int) System.currentTimeMillis() + 1, - new Intent(this, DownloadActivity.class) - .setAction(Intent.ACTION_MAIN), - PendingIntent.FLAG_UPDATE_CURRENT)); + int id = downloadFailedNotificationID++; + mFailedDownloads.put(id, mission); + + if (downloadFailedNotification == null) { + icDownloadFailed = BitmapFactory.decodeResource(this.getResources(), android.R.drawable.stat_sys_warning); + downloadFailedNotification = new Builder(this, getString(R.string.notification_channel_id)) + .setAutoCancel(true) + .setLargeIcon(icDownloadFailed) + .setSmallIcon(android.R.drawable.stat_sys_warning) + .setContentIntent(mOpenDownloadList); + } if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - notification.setContentTitle(getString(R.string.app_name)); - notification.setStyle(new NotificationCompat.BigTextStyle() - .bigText(getString(R.string.download_failed).concat(": ").concat(name))); + downloadFailedNotification.setContentTitle(getString(R.string.app_name)); + downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() + .bigText(getString(R.string.download_failed).concat(": ").concat(mission.name))); } else { - notification.setContentTitle(getString(R.string.download_failed)); - notification.setContentText(name); - notification.setStyle(new NotificationCompat.BigTextStyle() - .bigText(name)); + downloadFailedNotification.setContentTitle(getString(R.string.download_failed)); + downloadFailedNotification.setContentText(mission.name); + downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() + .bigText(mission.name)); } - notificationManager.notify(downloadFailedNotificationID++, notification.build()); + notificationManager.notify(id, downloadFailedNotification.build()); + } + + private PendingIntent makePendingIntent(String action) { + Intent intent = new Intent(this, DownloadManagerService.class).setAction(action); + return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); } private void manageObservers(Handler handler, boolean add) { @@ -470,12 +490,26 @@ public class DownloadManagerService extends Service { manageObservers(handler, false); } - public void resetFinishedDownloadCount() { - if (notificationManager == null || downloadDoneNotification == null) return; - notificationManager.cancel(DOWNLOADS_NOTIFICATION_ID); - downloadDoneList.setLength(0); - downloadDoneCount = 0; + public void clearDownloadNotifications() { + if (notificationManager == null) return; + if (downloadDoneNotification != null) { + notificationManager.cancel(DOWNLOADS_NOTIFICATION_ID); + downloadDoneList.setLength(0); + downloadDoneCount = 0; + } + if (downloadFailedNotification != null) { + for (; downloadFailedNotificationID > DOWNLOADS_NOTIFICATION_ID; downloadFailedNotificationID--) { + notificationManager.cancel(downloadFailedNotificationID); + } + mFailedDownloads.clear(); + downloadFailedNotificationID++; + } } + + public void enableNotifications(boolean enable) { + mDownloadNotificationEnable = enable; + } + } public interface DMChecker { 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 4dc40c420..8c332565b 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 @@ -20,6 +20,7 @@ import android.support.v7.app.AlertDialog; import android.support.v7.util.DiffUtil; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.ViewHolder; +import android.support.v7.widget.RecyclerView.Adapter; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; @@ -40,7 +41,6 @@ import org.schabi.newpipe.util.NavigationHelper; import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Locale; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; @@ -53,9 +53,10 @@ 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 { +public class MissionAdapter extends Adapter { private static final SparseArray ALGORITHMS = new SparseArray<>(); private static final String TAG = "MissionAdapter"; + private static final String UNDEFINED_SPEED = "--.-%"; static { ALGORITHMS.put(R.id.md5, "MD5"); @@ -89,6 +90,7 @@ public class MissionAdapter extends RecyclerView.Adapter { case DownloadManagerService.MESSAGE_ERROR: case DownloadManagerService.MESSAGE_FINISHED: onServiceMessage(msg); + break; } } }; @@ -120,7 +122,10 @@ public class MissionAdapter extends RecyclerView.Adapter { if (view instanceof ViewHolderHeader) return; ViewHolderItem h = (ViewHolderItem) view; - if (h.item.mission instanceof DownloadMission) mPendingDownloadsItems.remove(h); + if (h.item.mission instanceof DownloadMission) { + mPendingDownloadsItems.remove(h); + if (mPendingDownloadsItems.size() < 1) setAutoRefresh(false); + } h.popupMenu.dismiss(); h.item = null; @@ -153,10 +158,11 @@ public class MissionAdapter extends RecyclerView.Adapter { h.item = item; Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.name); + long length = item.mission instanceof FinishedMission ? item.mission.length : ((DownloadMission) item.mission).getLength(); h.icon.setImageResource(Utility.getIconForFileType(type)); h.name.setText(item.mission.name); - h.size.setText(Utility.formatBytes(item.mission.length)); + h.size.setText(Utility.formatBytes(length)); h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type)); @@ -187,66 +193,60 @@ public class MissionAdapter extends RecyclerView.Adapter { private void updateProgress(ViewHolderItem h) { if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return; + long now = System.currentTimeMillis(); DownloadMission mission = (DownloadMission) h.item.mission; - long now = System.currentTimeMillis(); - - if (h.lastTimeStamp == -1) { - h.lastTimeStamp = now; - } - - if (h.lastDone == -1) { - h.lastDone = mission.done; - } if (h.lastCurrent != mission.current) { h.lastCurrent = mission.current; - h.lastDone = 0; h.lastTimeStamp = now; + h.lastDone = 0; + } else { + if (h.lastTimeStamp == -1) h.lastTimeStamp = now; + if (h.lastDone == -1) h.lastDone = mission.done; } long deltaTime = now - h.lastTimeStamp; long deltaDone = mission.done - h.lastDone; boolean hasError = mission.errCode != DownloadMission.ERROR_NOTHING; - if (hasError || deltaTime == 0 || deltaTime > 1000) { - // on error hide marquee or show if condition (mission.done < 1 || mission.unknownLength) is true - h.progress.setMarquee(!hasError && (mission.done < 1 || mission.unknownLength)); + // on error hide marquee or show if condition (mission.done < 1 || mission.unknownLength) is true + h.progress.setMarquee(!hasError && (mission.done < 1 || mission.unknownLength)); - float progress; - if (mission.unknownLength) { - progress = Float.NaN; - h.progress.setProgress(0f); - } else { - progress = (float) ((double) mission.done / mission.length); - if (mission.urls.length > 1 && mission.current < mission.urls.length) { - progress = (progress / mission.urls.length) + ((float) mission.current / mission.urls.length); - } + float progress; + if (mission.unknownLength) { + progress = Float.NaN; + h.progress.setProgress(0f); + } else { + progress = (float) ((double) mission.done / mission.length); + if (mission.urls.length > 1 && mission.current < mission.urls.length) { + progress = (progress / mission.urls.length) + ((float) mission.current / mission.urls.length); } + } - if (hasError) { - if (Float.isNaN(progress) || Float.isInfinite(progress)) - h.progress.setProgress(1f); - h.status.setText(R.string.msg_error); - } else if (Float.isNaN(progress) || Float.isInfinite(progress)) { - h.status.setText("--.-%"); - } else { - h.status.setText(String.format("%.2f%%", progress * 100)); - h.progress.setProgress(progress); - } + if (hasError) { + if (Float.isNaN(progress) || Float.isInfinite(progress)) + h.progress.setProgress(1f); + h.status.setText(R.string.msg_error); + } else if (Float.isNaN(progress) || Float.isInfinite(progress)) { + h.status.setText(UNDEFINED_SPEED); + } else { + h.status.setText(String.format("%.2f%%", progress * 100)); + h.progress.setProgress(progress); } long length = mission.getLength(); - int state = 0; - if (!mission.isFinished()) { - if (!mission.running) { - state = mission.enqueued ? 1 : 2; - } else if (mission.postprocessingRunning) { - state = 3; - } + int state; + if (!mission.running) { + state = mission.enqueued ? 1 : 2; + } else if (mission.postprocessingRunning) { + state = 3; + } else { + state = 0; } if (state != 0) { + // update state without download speed if (h.state != state) { String statusStr; h.state = state; @@ -267,7 +267,7 @@ public class MissionAdapter extends RecyclerView.Adapter { } h.size.setText(Utility.formatBytes(length).concat(" (").concat(statusStr).concat(")")); - } else if (deltaTime > 1000 && deltaDone > 0) { + } else if (deltaDone > 0) { h.lastTimeStamp = now; h.lastDone = mission.done; } @@ -275,10 +275,10 @@ public class MissionAdapter extends RecyclerView.Adapter { return; } + if (deltaDone > 0 && deltaTime > 0) { + float speed = (deltaDone * 1000f) / deltaTime; - if (deltaTime > 1000 && deltaDone > 0) { - float speed = (float) ((double) deltaDone / deltaTime); - String speedStr = Utility.formatSpeed(speed * 1000); + String speedStr = Utility.formatSpeed(speed); String sizeStr = Utility.formatBytes(length); h.size.setText(sizeStr.concat(" ").concat(speedStr)); @@ -325,6 +325,8 @@ public class MissionAdapter extends RecyclerView.Adapter { private void onServiceMessage(@NonNull Message msg) { switch (msg.what) { case DownloadManagerService.MESSAGE_PROGRESS: + setAutoRefresh(true); + return; case DownloadManagerService.MESSAGE_ERROR: case DownloadManagerService.MESSAGE_FINISHED: break; @@ -339,8 +341,6 @@ public class MissionAdapter extends RecyclerView.Adapter { if (msg.what == DownloadManagerService.MESSAGE_FINISHED) { // DownloadManager should mark the download as finished applyChanges(); - - mPendingDownloadsItems.remove(i); return; } @@ -396,7 +396,9 @@ public class MissionAdapter extends RecyclerView.Adapter { break; default: if (mission.errCode >= 100 && mission.errCode < 600) { - str.append("HTTP"); + str = new StringBuilder(8); + str.append("HTTP "); + str.append(mission.errCode); } else if (mission.errObject == null) { str.append("(not_decelerated_error_code)"); } @@ -436,7 +438,7 @@ public class MissionAdapter extends RecyclerView.Adapter { case R.id.pause: h.state = -1; mDownloadManager.pauseMission(mission); - notifyItemChanged(h.getAdapterPosition()); + updateProgress(h); h.lastTimeStamp = -1; h.lastDone = -1; return true; @@ -542,6 +544,43 @@ public class MissionAdapter extends RecyclerView.Adapter { } + private boolean mUpdaterRunning = false; + private final Runnable rUpdater = this::updater; + + public void onPaused() { + setAutoRefresh(false); + } + + private void setAutoRefresh(boolean enabled) { + if (enabled && !mUpdaterRunning) { + mUpdaterRunning = true; + updater(); + } else if (!enabled && mUpdaterRunning) { + mUpdaterRunning = false; + mHandler.removeCallbacks(rUpdater); + } + } + + private void updater() { + if (!mUpdaterRunning) return; + + boolean running = false; + for (ViewHolderItem h : mPendingDownloadsItems) { + // check if the mission is running first + if (!((DownloadMission) h.item.mission).running) continue; + + updateProgress(h); + running = true; + } + + if (running) { + mHandler.postDelayed(rUpdater, 1000); + } else { + mUpdaterRunning = false; + } + } + + class ViewHolderItem extends RecyclerView.ViewHolder { DownloadManager.MissionItem item; diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index f04361f19..aa9c497f1 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -51,7 +51,7 @@ public class MissionsFragment extends Fragment { @Override public void onServiceConnected(ComponentName name, IBinder binder) { mBinder = (DownloadManagerService.DMBinder) binder; - mBinder.resetFinishedDownloadCount(); + mBinder.clearDownloadNotifications(); mAdapter = new MissionAdapter(mActivity, mBinder.getDownloadManager(), mClear, mEmpty); mAdapter.deleterLoad(mBundle, getView()); @@ -59,6 +59,7 @@ public class MissionsFragment extends Fragment { mBundle = null; mBinder.addMissionEventListener(mAdapter.getMessenger()); + mBinder.enableNotifications(false); updateList(); } @@ -130,7 +131,7 @@ public class MissionsFragment extends Fragment { @Override public void onAttach(Activity activity) { super.onAttach(activity); - + mActivity = activity; } @@ -141,6 +142,7 @@ public class MissionsFragment extends Fragment { if (mBinder == null || mAdapter == null) return; mBinder.removeMissionEventListener(mAdapter.getMessenger()); + mBinder.enableNotifications(true); mActivity.unbindService(mConnection); mAdapter.deleterDispose(null); @@ -181,7 +183,7 @@ public class MissionsFragment extends Fragment { // destroy all created views in the recycler mList.setAdapter(null); mAdapter.notifyDataSetChanged(); - + // re-attach the adapter in grid/lineal mode mAdapter.setLinear(mLinear); mList.setAdapter(mAdapter); @@ -201,14 +203,13 @@ public class MissionsFragment extends Fragment { mAdapter.deleterDispose(outState); mForceUpdate = true; mBinder.removeMissionEventListener(mAdapter.getMessenger()); - } } @Override public void onResume() { super.onResume(); - + if (mAdapter != null) { mAdapter.deleterResume(); @@ -219,5 +220,13 @@ public class MissionsFragment extends Fragment { mBinder.addMissionEventListener(mAdapter.getMessenger()); } + if (mBinder != null) mBinder.enableNotifications(false); + } + + @Override + public void onPause() { + super.onPause(); + if (mAdapter != null) mAdapter.onPaused(); + if (mBinder != null) mBinder.enableNotifications(true); } } diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 2973b69ef..5125752b4 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -176,7 +176,9 @@ @string/charset_most_special_characters_value downloads_max_retry + 3 + @string/minimize_on_exit_none_description 1 2 3 @@ -186,9 +188,8 @@ 10 15 - - 3 - cross_network_downloads + + cross_network_downloads default_download_threads diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index 7175ae711..e7faf40dd 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -30,7 +30,7 @@ android:title="@string/settings_file_replacement_character_title"/> From b8293f134d63419fafde01fffc25889b8144d220 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Thu, 29 Nov 2018 15:16:46 -0300 Subject: [PATCH 42/68] Update settings_keys.xml * remane max_try -> maximum_try --- .../us/shandian/giga/service/DownloadManagerService.java | 6 +++--- app/src/main/res/values/settings_keys.xml | 7 ++++--- app/src/main/res/xml/download_settings.xml | 8 ++++---- 3 files changed, 11 insertions(+), 10 deletions(-) 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 7d88d9e2a..557c5a28a 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -165,7 +165,7 @@ public class DownloadManagerService extends Service { mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener); handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network)); - handlePreferenceChange(mPrefs, getString(R.string.downloads_max_retry)); + handlePreferenceChange(mPrefs, getString(R.string.downloads_maximum_retry)); wakeLock = new LockManager(this); } @@ -323,9 +323,9 @@ public class DownloadManagerService extends Service { } private void handlePreferenceChange(SharedPreferences prefs, String key) { - if (key.equals(getString(R.string.downloads_max_retry))) { + if (key.equals(getString(R.string.downloads_maximum_retry))) { try { - String value = prefs.getString(key, getString(R.string.downloads_max_retry_default)); + String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default)); mManager.mPrefMaxRetry = Integer.parseInt(value); } catch (Exception e) { mManager.mPrefMaxRetry = 0; diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index abc2f0b8e..300217c09 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -17,6 +17,7 @@ use_external_video_player use_external_audio_player autoplay_through_intent + use_oldplayer volume_gesture_control brightness_gesture_control @@ -174,9 +175,9 @@ @string/charset_most_special_characters_value - downloads_max_retry - 3 - + downloads_max_retry + 3 + @string/minimize_on_exit_none_description 1 2 diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index e7faf40dd..e5d2031fe 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -30,10 +30,10 @@ android:title="@string/settings_file_replacement_character_title"/> From c69b224c65a7960c640b80ad98792ef8f85b4a0a Mon Sep 17 00:00:00 2001 From: Akash Agarwal Date: Sat, 1 Dec 2018 00:35:27 +0530 Subject: [PATCH 43/68] Issue 1505: Delete on right swipe --- .../org/schabi/newpipe/player/MainVideoPlayer.java | 5 +++++ .../schabi/newpipe/player/ServicePlayerActivity.java | 5 +++++ .../player/playqueue/PlayQueueItemTouchCallback.java | 10 +++++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 07a9ac71c..f4fea5165 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -900,6 +900,11 @@ public final class MainVideoPlayer extends AppCompatActivity public void onMove(int sourceIndex, int targetIndex) { if (playQueue != null) playQueue.move(sourceIndex, targetIndex); } + + @Override + public void onSwiped(int index) { + if(index != -1) playQueue.remove(index); + } }; } diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 94305e6c4..2ec4275fc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -375,6 +375,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity public void onMove(int sourceIndex, int targetIndex) { if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex); } + + @Override + public void onSwiped(int index) { + if (index != -1) player.getPlayQueue().remove(index); + } }; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java index 6edeff670..26be83b98 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java @@ -8,11 +8,13 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25; public PlayQueueItemTouchCallback() { - super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0); + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT); } public abstract void onMove(final int sourceIndex, final int targetIndex); + public abstract void onSwiped(int index); + @Override public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, int viewSizeOutOfBounds, int totalSize, @@ -44,9 +46,11 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC @Override public boolean isItemViewSwipeEnabled() { - return false; + return true; } @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} + public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { + onSwiped(viewHolder.getAdapterPosition()); + } } From 22b20c15ded0e4a37d2419a183d81c7fba5490a0 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Sat, 1 Dec 2018 09:21:57 +0100 Subject: [PATCH 44/68] update contribution guidelines --- .github/CONTRIBUTING.md | 44 ++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 07cb9f66c..574c87ad3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -5,11 +5,14 @@ PLEASE READ THESE GUIDELINES CAREFULLY BEFORE ANY CONTRIBUTION! ## Crash reporting -Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to send a report via e-mail when a crash occurs. This contains all the data we need for debugging, and allows you to even add a comment to it. You'll see exactly what is sent, the system is 100% transparent. +Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to +send a report via e-mail when a crash occurs. This contains all the data we need for debugging, and allows you to even +add a comment to it. You'll see exactly what is sent, the system is 100% transparent. ## Issue reporting/feature requests -* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature hasn't been reported/requested before +* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature +hasn't been reported/requested before * Check whether your issue/feature is already fixed/implemented * Check if the issue still exists in the latest release/beta version * If you are an Android/Java developer, you are always welcome to fix/implement an issue/a feature yourself. PRs welcome! @@ -19,30 +22,47 @@ Do not report crashes in the GitHub issue tracker. NewPipe has an automated cras * Issues that only contain a generated bug report, but no describtion might be closed. ## Bug Fixing -* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to tnp@newpipe.schabi.org to let me know that you intend to help. We'll send you further instructions. You may, on request, register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information. +* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to +tnp@newpipe.schabi.org to let me know that you intend to help. We'll send you further instructions. You may, on request, +register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information. ## Translation -* NewPipe can be translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there with your GitHub account. +* NewPipe can be translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there +with your GitHub account. ## Code contribution * Stick to NewPipe's style conventions (well, just look the other code and then do it the same way :)) -* Do not bring non-free software (e.g., binary blobs) into the project. Also, make sure you do not introduce Google libraries. +* Do not bring non-free software (e.g., binary blobs) into the project. Also, make sure you do not introduce Google + libraries. * Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy) -* Make changes on a separate branch, not on the master branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request on GitHub. Patches to the email address mentioned in this document might not be considered, GitHub is the primary platform. (This only affects you if you are a member of TeamNewPipe) -* When submitting changes, you confirm that your code is licensed under the terms of the [GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html). -* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged! +* Make changes on a separate branch, not on the master branch. This is commonly known as *feature branch workflow*. You + may then send your changes as a pull request on GitHub. Patches to the email address mentioned in this document might + not be considered, GitHub is the primary platform. (This only affects you if you are a member of TeamNewPipe) +* When submitting changes, you confirm that your code is licensed under the terms of the + [GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html). +* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR + description. Untested code will **not** be merged! * Try to figure out yourself why builds on our CI fail. -* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you are asked to merge the master branch manually and resolve the problems on your own. That will make the maintainers' jobs way easier. -* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again about submission, or clearly state that in the description of your PR. +* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job, + but if not, you are asked to merge the master branch manually and resolve the problems on your own. That will make the + maintainers' jobs way easier. +* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for + the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again + about submission, or clearly state that in the description of your PR. * Respond yourselves if someone requests changes or otherwise raises issues about your PRs. * Check if your contributions align with the [fdroid inclusion guidelines](https://f-droid.org/en/docs/Inclusion_Policy/). * Check if your submission can be build with the current fdroid build server setup. +* Send PR that only cover one specific issue/solution/bug. Do not send PRs that are huge and consists of multiple + independent solutions. ## Communication * WE DO NOW HAVE A MAILING LIST: [newpipe@list.schabi.org](https://list.schabi.org/cgi-bin/mailman/listinfo/newpipe). -* There is an IRC channel on Freenode which is regularly visited by the core team and other developers: [#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=newpipe)! -* If you want to get in touch with the core team or one of our other contributors you can send an email to tnp(at)schabi.org. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue tracker described above! +* There is an IRC channel on Freenode which is regularly visited by the core team and other developers: + [#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=newpipe)! +* If you want to get in touch with the core team or one of our other contributors you can send an email to + tnp(at)schabi.org. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue + tracker described above! * Feel free to post suggestions, changes, ideas etc. on GitHub, IRC or the mailing list! From c1168693fa0fd9eec729db6ac025960a7c0a04d7 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Sat, 1 Dec 2018 09:35:42 +0100 Subject: [PATCH 45/68] add stalebot confic --- .github/stale.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..e556fa985 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false From 9f4a7e664fa754093deef1721eae4a33cc97ae1d Mon Sep 17 00:00:00 2001 From: kapodamy Date: Sat, 1 Dec 2018 22:05:09 -0300 Subject: [PATCH 46/68] more of the same * misc code clean-up * fix weird download speed, before switching the list view * fix CircularFile.java getting stuck on post-processing huge files >2GiB * keep crashed post-processing downloads visible to the user --- .../us/shandian/giga/get/DownloadMission.java | 22 +++++--- .../giga/postprocessing/Postprocessing.java | 2 +- .../giga/postprocessing/io/CircularFile.java | 34 +++++------ .../giga/service/DownloadManager.java | 6 +- .../giga/service/DownloadManagerService.java | 23 ++++++-- .../giga/ui/adapter/MissionAdapter.java | 56 ++++++++++++------- 6 files changed, 84 insertions(+), 59 deletions(-) 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 79e01b8cc..f3a817ba8 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -341,6 +341,12 @@ public class DownloadMission extends Mission { finishCount++; if (finishCount == currentThreadCount) { + if (errCode > ERROR_NOTHING) return; + + if (DEBUG) { + Log.d(TAG, "onFinish" + (current + 1) + "/" + urls.length); + } + if ((current + 1) < urls.length) { // prepare next sub-mission long current_offset = offsets[current++]; @@ -354,10 +360,6 @@ public class DownloadMission extends Mission { if (!doPostprocessing()) return; - if (errCode > ERROR_NOTHING) return; - if (DEBUG) { - Log.d(TAG, "onFinish"); - } running = false; deleteThisFromFile(); @@ -517,10 +519,16 @@ public class DownloadMission extends Mission { } public long getLength() { - long near = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; - near -= offsets[0];// don't count reserved space + long calculated; + if (postprocessingRunning) { + calculated = length; + } else { + calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; + } - return near > nearLength ? near : nearLength; + calculated -= offsets[0];// don't count reserved space + + return calculated > nearLength ? calculated : nearLength; } private boolean doPostprocessing() { diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 88cc337fd..80726f705 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -91,7 +91,7 @@ public abstract class Postprocessing { out = new CircularFile(file, 0, this::progressReport, checker); mission.done = 0; - mission.length = mission.getLength(); + mission.length = file.length(); int result = process(out, sources); diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java index 7e5ad9929..d2fc82d33 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java @@ -121,45 +121,37 @@ public class CircularFile extends SharpStream { available = end - position; } - while (available > 0 && auxiliaryBuffers.size() > 0) { + // Check if possible flush one or more auxiliary buffer + if (auxiliaryBuffers.size() > 0) { ManagedBuffer aux = auxiliaryBuffers.get(0); - // check if there is enough space to dump the auxiliary buffer - if (available >= (aux.size + queue.size)) { + // check if there is enough space to flush it completely + while (available >= (aux.size + queue.size)) { available -= aux.size; writeQueue(aux.buffer, 0, aux.size); aux.dereference(); auxiliaryBuffers.remove(0); - continue; + + if (auxiliaryBuffers.size() < 1) { + aux = null; + break; + } + aux = auxiliaryBuffers.get(0); } if (IMMEDIATE_AUX_BUFFER_FLUSH) { - // try flush contents to avoid allocate another auxiliary buffer - if (aux.available() < len && available > queue.size) { - int size = Math.min(len, aux.available()); - aux.write(b, off, size); - - off += size; - len -= size; - - size = Math.min(aux.size, (int) available - queue.size); - if (size < 1) { - break; - } + // try partial flush to avoid allocate another auxiliary buffer + if (aux != null && aux.available() < len && available > queue.size) { + int size = Math.min(aux.size, (int) available - queue.size); writeQueue(aux.buffer, 0, size); aux.dereference(size); available -= size; } - break; } } - if (len < 1) { - return; - } - if (auxiliaryBuffers.size() < 1 && available > (len + queue.size)) { writeQueue(b, off, len); } else { diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 55a22c8c5..6bcf84745 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -150,10 +150,8 @@ public class DownloadManager { exists = true; mis.postprocessingRunning = false; mis.errCode = DownloadMission.ERROR_POSTPROCESSING_FAILED; - mis.errObject = new RuntimeException("post-processing stopped unexpectedly"); - } - - if (exists && !dl.isFile()) { + mis.errObject = new RuntimeException("stopped unexpectedly"); + } else if (exists && !dl.isFile()) { // probably a folder, this should never happens if (!sub.delete()) { Log.w(TAG, "Unable to delete serialized file: " + sub.getPath()); 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 557c5a28a..1bb28fe95 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -90,8 +90,8 @@ public class DownloadManagerService extends Service { private SharedPreferences mPrefs = null; private final SharedPreferences.OnSharedPreferenceChangeListener mPrefChangeListener = this::handlePreferenceChange; - private boolean wakeLockAcquired = false; - private LockManager wakeLock = null; + private boolean mLockAcquired = false; + private LockManager mLock = null; private int downloadFailedNotificationID = DOWNLOADS_NOTIFICATION_ID + 1; private Builder downloadFailedNotification = null; @@ -167,7 +167,7 @@ public class DownloadManagerService extends Service { handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network)); handlePreferenceChange(mPrefs, getString(R.string.downloads_maximum_retry)); - wakeLock = new LockManager(this); + mLock = new LockManager(this); } @Override @@ -228,7 +228,7 @@ public class DownloadManagerService extends Service { mManager.pauseAllMissions(); - if (wakeLockAcquired) wakeLock.releaseWifiAndCpu(); + manageLock(false); unregisterReceiver(mNetworkStateListener); mPrefs.unregisterOnSharedPreferenceChangeListener(mPrefChangeListener); @@ -341,12 +341,12 @@ public class DownloadManagerService extends Service { if (state) { startForeground(FOREGROUND_NOTIFICATION_ID, mNotification); - if (!wakeLockAcquired) wakeLock.acquireWifiAndCpu(); } else { stopForeground(true); - if (wakeLockAcquired) wakeLock.releaseWifiAndCpu(); } + manageLock(state); + mForeground = state; } @@ -476,6 +476,17 @@ public class DownloadManagerService extends Service { } } + private void manageLock(boolean acquire) { + if (acquire == mLockAcquired) return; + + if (acquire) + mLock.acquireWifiAndCpu(); + else + mLock.releaseWifiAndCpu(); + + mLockAcquired = acquire; + } + // Wrapper of DownloadManager public class DMBinder extends Binder { public DownloadManager getDownloadManager() { 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 8c332565b..df5f9e429 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 @@ -52,6 +52,17 @@ 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; +import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST; +import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION; +import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT; +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_SSL_EXCEPTION; +import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; +import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; public class MissionAdapter extends Adapter { private static final SparseArray ALGORITHMS = new SparseArray<>(); @@ -158,24 +169,27 @@ public class MissionAdapter extends Adapter { h.item = item; Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.name); - long length = item.mission instanceof FinishedMission ? item.mission.length : ((DownloadMission) item.mission).getLength(); h.icon.setImageResource(Utility.getIconForFileType(type)); h.name.setText(item.mission.name); - h.size.setText(Utility.formatBytes(length)); h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type)); if (h.item.mission instanceof DownloadMission) { DownloadMission mission = (DownloadMission) item.mission; - h.progress.setMarquee(mission.done < 1); - updateProgress(h); + String length = Utility.formatBytes(mission.getLength()); + if (mission.running && !mission.postprocessingRunning) length += " --.- kB/s"; + + h.size.setText(length); h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause); + h.lastCurrent = mission.current; + updateProgress(h); mPendingDownloadsItems.add(h); } else { h.progress.setMarquee(false); h.status.setText("100%"); h.progress.setProgress(1f); + h.size.setText(Utility.formatBytes(item.mission.length)); } } @@ -207,7 +221,7 @@ public class MissionAdapter extends Adapter { long deltaTime = now - h.lastTimeStamp; long deltaDone = mission.done - h.lastDone; - boolean hasError = mission.errCode != DownloadMission.ERROR_NOTHING; + boolean hasError = mission.errCode != ERROR_NOTHING; // on error hide marquee or show if condition (mission.done < 1 || mission.unknownLength) is true h.progress.setMarquee(!hasError && (mission.done < 1 || mission.unknownLength)); @@ -237,7 +251,9 @@ public class MissionAdapter extends Adapter { long length = mission.getLength(); int state; - if (!mission.running) { + if (mission.errCode == ERROR_POSTPROCESSING_FAILED) { + state = 0; + } else if (!mission.running) { state = mission.enqueued ? 1 : 2; } else if (mission.postprocessingRunning) { state = 3; @@ -363,36 +379,36 @@ public class MissionAdapter extends Adapter { case 404: str.append(mContext.getString(R.string.error_http_not_found)); break; - case DownloadMission.ERROR_NOTHING: + case ERROR_NOTHING: str.append("¿?"); break; - case DownloadMission.ERROR_FILE_CREATION: + case ERROR_FILE_CREATION: str.append(mContext.getString(R.string.error_file_creation)); break; - case DownloadMission.ERROR_HTTP_NO_CONTENT: + case ERROR_HTTP_NO_CONTENT: str.append(mContext.getString(R.string.error_http_no_content)); break; - case DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE: + case ERROR_HTTP_UNSUPPORTED_RANGE: str.append(mContext.getString(R.string.error_http_unsupported_range)); break; - case DownloadMission.ERROR_PATH_CREATION: + case ERROR_PATH_CREATION: str.append(mContext.getString(R.string.error_path_creation)); break; - case DownloadMission.ERROR_PERMISSION_DENIED: + case ERROR_PERMISSION_DENIED: str.append(mContext.getString(R.string.permission_denied)); break; - case DownloadMission.ERROR_SSL_EXCEPTION: + case ERROR_SSL_EXCEPTION: str.append(mContext.getString(R.string.error_ssl_exception)); break; - case DownloadMission.ERROR_UNKNOWN_HOST: + case ERROR_UNKNOWN_HOST: str.append(mContext.getString(R.string.error_unknown_host)); break; - case DownloadMission.ERROR_CONNECT_HOST: + case ERROR_CONNECT_HOST: str.append(mContext.getString(R.string.error_connect_host)); break; - case DownloadMission.ERROR_POSTPROCESSING_FAILED: + case ERROR_POSTPROCESSING_FAILED: str.append(mContext.getString(R.string.error_postprocessing_failed)); - case DownloadMission.ERROR_UNKNOWN_EXCEPTION: + case ERROR_UNKNOWN_EXCEPTION: break; default: if (mission.errCode >= 100 && mission.errCode < 600) { @@ -655,15 +671,15 @@ public class MissionAdapter extends Adapter { if (mission.running) { pause.setVisible(true); } else { - if (mission.errCode != DownloadMission.ERROR_NOTHING) { + if (mission.errCode != ERROR_NOTHING) { showError.setVisible(true); } queue.setChecked(mission.enqueued); delete.setVisible(true); - start.setVisible(mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED); - queue.setVisible(mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED); + start.setVisible(mission.errCode != ERROR_POSTPROCESSING_FAILED); + queue.setVisible(mission.errCode != ERROR_POSTPROCESSING_FAILED); } } } else { From ea4e8805b7c74755c73ae998bfc8d405048233be Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Mon, 3 Dec 2018 23:30:28 +0100 Subject: [PATCH 47/68] disable stalebot again --- .github/stale.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index e556fa985..000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security -# Label to use when marking an issue as stale -staleLabel: stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false From e2aa36d083c961048bd652e83280799e50ed55fc Mon Sep 17 00:00:00 2001 From: kapodamy Date: Wed, 5 Dec 2018 01:03:56 -0300 Subject: [PATCH 48/68] fast download pausing * fast download pausing * fix UI thread blocking when calling pause() * check running threads before start the download * fix null pointer exception in onDestroy in the download service, without calling onCreate method (android 8) --- .../giga/get/DownloadInitializer.java | 106 ++++++++++------ .../us/shandian/giga/get/DownloadMission.java | 117 +++++++++++++----- .../shandian/giga/get/DownloadRunnable.java | 72 +++++++---- .../giga/get/DownloadRunnableFallback.java | 62 +++++++--- .../giga/service/DownloadManagerService.java | 2 +- 5 files changed, 241 insertions(+), 118 deletions(-) diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index 298e7be37..ce7ae267c 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -5,6 +5,7 @@ import android.util.Log; import java.io.File; import java.io.IOException; +import java.io.InterruptedIOException; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.nio.channels.ClosedByInterruptException; @@ -13,14 +14,16 @@ import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; -public class DownloadInitializer implements Runnable { +public class DownloadInitializer extends Thread { private final static String TAG = "DownloadInitializer"; final static int mId = 0; private DownloadMission mMission; + private HttpURLConnection mConn; DownloadInitializer(@NonNull DownloadMission mission) { mMission = mission; + mConn = null; } @Override @@ -32,10 +35,12 @@ public class DownloadInitializer implements Runnable { try { mMission.currentThreadCount = mMission.threadCount; - HttpURLConnection conn = mMission.openConnection(mId, -1, -1); + mConn = mMission.openConnection(mId, -1, -1); + mMission.establishConnection(mId, mConn); + if (!mMission.running || Thread.interrupted()) return; - mMission.length = Utility.getContentLength(conn); + mMission.length = Utility.getContentLength(mConn); if (mMission.length == 0) { @@ -44,7 +49,7 @@ public class DownloadInitializer implements Runnable { } // check for dynamic generated content - if (mMission.length == -1 && conn.getResponseCode() == 200) { + if (mMission.length == -1 && mConn.getResponseCode() == 200) { mMission.blocks = 0; mMission.length = 0; mMission.fallback = true; @@ -56,50 +61,54 @@ public class DownloadInitializer implements Runnable { } } else { // Open again - conn = mMission.openConnection(mId, mMission.length - 10, mMission.length); + mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length); + mMission.establishConnection(mId, mConn); - int code = conn.getResponseCode(); if (!mMission.running || Thread.interrupted()) return; - if (code == 206) { - if (mMission.currentThreadCount > 1) { - mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE; + synchronized (mMission.blockState) { + if (mConn.getResponseCode() == 206) { + if (mMission.currentThreadCount > 1) { + mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE; - if (mMission.currentThreadCount > mMission.blocks) { - mMission.currentThreadCount = (int) mMission.blocks; + if (mMission.currentThreadCount > mMission.blocks) { + mMission.currentThreadCount = (int) mMission.blocks; + } + if (mMission.currentThreadCount <= 0) { + mMission.currentThreadCount = 1; + } + if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) { + mMission.blocks++; + } + } else { + // if one thread is solicited don't calculate blocks, is useless + mMission.blocks = 1; + mMission.fallback = true; + mMission.unknownLength = false; } - if (mMission.currentThreadCount <= 0) { - mMission.currentThreadCount = 1; - } - if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) { - mMission.blocks++; + + if (DEBUG) { + Log.d(TAG, "http response code = " + mConn.getResponseCode()); } } else { - // if one thread is solicited don't calculate blocks, is useless - mMission.blocks = 1; + // Fallback to single thread + mMission.blocks = 0; mMission.fallback = true; mMission.unknownLength = false; + mMission.currentThreadCount = 1; + + if (DEBUG) { + Log.d(TAG, "falling back due http response code = " + mConn.getResponseCode()); + } } - if (DEBUG) { - Log.d(TAG, "http response code = " + code); - } - } else { - // Fallback to single thread - mMission.blocks = 0; - mMission.fallback = true; - mMission.unknownLength = false; - mMission.currentThreadCount = 1; - - if (DEBUG) { - Log.d(TAG, "falling back due http response code = " + code); + for (long i = 0; i < mMission.currentThreadCount; i++) { + mMission.threadBlockPositions.add(i); + mMission.threadBytePositions.add(0L); } } - } - for (long i = 0; i < mMission.currentThreadCount; i++) { - mMission.threadBlockPositions.add(i); - mMission.threadBytePositions.add(0L); + if (!mMission.running || Thread.interrupted()) return; } File file; @@ -112,7 +121,7 @@ public class DownloadInitializer implements Runnable { file = new File(file, mMission.name); - // if the name is used by "something", delete it + // if the name is used by another process, delete it if (file.exists() && !file.isFile() && !file.delete()) { mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null); return; @@ -131,14 +140,16 @@ public class DownloadInitializer implements Runnable { af.seek(mMission.offsets[mMission.current]); af.close(); - if (Thread.interrupted()) return; + if (!mMission.running || Thread.interrupted()) return; mMission.running = false; break; + } catch (InterruptedIOException | ClosedByInterruptException e) { + return; } catch (Exception e) { - if (e instanceof ClosedByInterruptException) { - return; - } else if (e instanceof IOException && e.getMessage().contains("Permission denied")) { + if (!mMission.running) return; + + if (e instanceof IOException && e.getMessage().contains("Permission denied")) { mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e); return; } @@ -150,11 +161,26 @@ public class DownloadInitializer implements Runnable { return; } - //try again Log.e(TAG, "initializer failed, retrying", e); } } + // hide marquee in the progress bar + mMission.done++; + mMission.start(); } + + @Override + public void interrupt() { + super.interrupt(); + + if (mConn != null) { + try { + mConn.disconnect(); + } catch (Exception e) { + // nothing to do + } + } + } } 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 f3a817ba8..c25d517f1 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -122,13 +122,13 @@ public class DownloadMission extends Mission { private transient boolean mWritingToFile; @SuppressWarnings("UseSparseArrays")// LongSparseArray is not serializable - private final HashMap blockState = new HashMap<>(); + final HashMap blockState = new HashMap<>(); final List threadBlockPositions = new ArrayList<>(); final List threadBytePositions = new ArrayList<>(); private transient boolean deleted; int currentThreadCount; - private transient Thread[] threads = null; + private transient Thread[] threads = new Thread[0]; private transient Thread init = null; @@ -238,9 +238,8 @@ public class DownloadMission extends Mission { * @param rangeEnd range end * @return a {@link java.net.URLConnection URLConnection} linking to the URL. * @throws IOException if an I/O exception occurs. - * @throws HttpError if the the http response is not satisfiable */ - HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException, HttpError { + HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException { URL url = new URL(urls[current]); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setInstanceFollowRedirects(true); @@ -250,29 +249,45 @@ public class DownloadMission extends Mission { if (rangeEnd > 0) req += rangeEnd; conn.setRequestProperty("Range", req); + if (DEBUG) { Log.d(TAG, threadId + ":" + conn.getRequestProperty("Range")); - Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode()); } } - conn.connect(); + return conn; + } + /** + * @param threadId id of the calling thread + * @param conn Opens and establish the communication + * @throws IOException if an error occurred connecting to the server. + * @throws HttpError if the HTTP Status-Code is not satisfiable + */ + void establishConnection(int threadId, HttpURLConnection conn) throws IOException, HttpError { + conn.connect(); int statusCode = conn.getResponseCode(); + + if (DEBUG) { + Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + statusCode); + } + switch (statusCode) { case 204: case 205: case 207: throw new HttpError(conn.getResponseCode()); + case 416: + return;// let the download thread handle this error default: if (statusCode < 200 || statusCode > 299) { throw new HttpError(statusCode); } } - return conn; } + private void notify(int what) { Message m = new Message(); m.what = what; @@ -389,6 +404,11 @@ public class DownloadMission extends Mission { */ public void start() { if (running || current >= urls.length) return; + + // ensure that the previous state is completely paused. + joinForThread(init); + for (Thread thread : threads) joinForThread(thread); + enqueued = false; running = true; errCode = ERROR_NOTHING; @@ -400,7 +420,7 @@ public class DownloadMission extends Mission { init = null; - if (threads == null) { + if (threads.length < 1) { threads = new Thread[currentThreadCount]; } @@ -428,39 +448,37 @@ public class DownloadMission extends Mission { recovered = true; enqueued = false; - if (init != null && init != Thread.currentThread() && init.isAlive()) { - init.interrupt(); - - try { - init.join(); - } catch (InterruptedException e) { - // nothing to do + if (postprocessingRunning) { + if (DEBUG) { + Log.w(TAG, "pause during post-processing is not applicable."); } + return; + } - resetState(); + if (init != null && init.isAlive()) { + init.interrupt(); + synchronized (blockState) { + resetState(); + } return; } if (DEBUG && blocks == 0) { - Log.w(TAG, "pausing a download that can not be resumed."); + Log.w(TAG, "pausing a download that can not be resumed (range requests not allowed by the server)."); } - if (threads == null || Thread.interrupted()) { + if (threads == null || Thread.currentThread().isInterrupted()) { writeThisToFile(); return; } - if (postprocessingRunning) return; - // wait for all threads are suspended before save the state runAsync(-1, () -> { try { for (Thread thread : threads) { - if (thread == Thread.currentThread()) continue; - if (thread.isAlive()) { thread.interrupt(); - thread.join(); + thread.join(5000); } } } catch (Exception e) { @@ -492,7 +510,7 @@ public class DownloadMission extends Mission { threadBlockPositions.clear(); threadBytePositions.clear(); blockState.clear(); - threads = null; + threads = new Thread[0]; Utility.writeToFile(metadata, DownloadMission.this); } @@ -571,28 +589,61 @@ public class DownloadMission extends Mission { } /** - * run a method in a new thread + * run a new thread * * @param id id of new thread (used for debugging only) - * @param who the object whose {@code run} method is invoked when this thread is started - * @return the created thread + * @param who the Runnable whose {@code run} method is invoked. */ - private Thread runAsync(int id, Runnable who) { + private void runAsync(int id, Runnable who) { + runAsync(id, new Thread(who)); + } + + /** + * run a new thread + * + * @param id id of new thread (used for debugging only) + * @param who the Thread whose {@code run} method is invoked when this thread is started + * @return the passed thread + */ + private Thread runAsync(int id, Thread who) { // known thread ids: // -2: state saving by notifyProgress() method // -1: wait for saving the state by pause() method // 0: initializer // >=1: any download thread - Thread thread = new Thread(who); if (DEBUG) { - thread.setName(String.format("[%s] id = %s filename = %s", TAG, id, name)); + who.setName(String.format("%s[%s] %s", TAG, id, name)); } - thread.start(); - return thread; + who.start(); + + return who; } + private void joinForThread(Thread thread) { + if (thread == null || !thread.isAlive()) return; + if (thread == Thread.currentThread()) return; + + if (DEBUG) { + Log.w(TAG, "a thread is !still alive!: " + thread.getName()); + } + + // still alive, this should not happen. + // Possible reasons: + // slow device + // the user is spamming start/pause buttons + // start() method called quickly after pause() + + try { + thread.join(10000); + } catch (InterruptedException e) { + Log.d(TAG, "timeout on join : " + thread.getName()); + throw new RuntimeException("A thread is still running:\n" + thread.getName()); + } + } + + static class HttpError extends Exception { int statusCode; @@ -602,7 +653,7 @@ public class DownloadMission extends Mission { @Override public String getMessage() { - return "Http status code: " + String.valueOf(statusCode); + return "HTTP " + String.valueOf(statusCode); } } } 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 336bc13ee..244fbd47a 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -14,16 +14,19 @@ import static org.schabi.newpipe.BuildConfig.DEBUG; * Runnable to download blocks of a file until the file is completely downloaded, * an error occurs or the process is stopped. */ -public class DownloadRunnable implements Runnable { +public class DownloadRunnable extends Thread { private static final String TAG = DownloadRunnable.class.getSimpleName(); private final DownloadMission mMission; private final int mId; + private HttpURLConnection mConn; + DownloadRunnable(DownloadMission mission, int id) { if (mission == null) throw new NullPointerException("mission is null"); mMission = mission; mId = id; + mConn = null; } @Override @@ -47,12 +50,7 @@ public class DownloadRunnable implements Runnable { return; } - while (mMission.errCode == DownloadMission.ERROR_NOTHING && mMission.running && blockPosition < mMission.blocks) { - - if (Thread.currentThread().isInterrupted()) { - mMission.pause(); - return; - } + while (mMission.running && mMission.errCode == DownloadMission.ERROR_NOTHING && blockPosition < mMission.blocks) { if (DEBUG && retry) { Log.d(TAG, mId + ":retry is true. Resuming at " + blockPosition); @@ -83,8 +81,9 @@ public class DownloadRunnable implements Runnable { long start = blockPosition * DownloadMission.BLOCK_SIZE; long end = start + DownloadMission.BLOCK_SIZE - 1; + long offset = mMission.getThreadBytePosition(mId); - start += mMission.getThreadBytePosition(mId); + start += offset; if (end >= mMission.length) { end = mMission.length - 1; @@ -93,14 +92,21 @@ public class DownloadRunnable implements Runnable { long total = 0; try { - HttpURLConnection conn = mMission.openConnection(mId, start, end); + mConn = mMission.openConnection(mId, start, end); + mMission.establishConnection(mId, mConn); + + // check if the download can be resumed + if (mConn.getResponseCode() == 416 && offset > 0) { + retryCount--; + throw new DownloadMission.HttpError(416); + } // The server may be ignoring the range request - if (conn.getResponseCode() != 206) { - mMission.notifyError(new DownloadMission.HttpError(conn.getResponseCode())); + if (mConn.getResponseCode() != 206) { + mMission.notifyError(new DownloadMission.HttpError(mConn.getResponseCode())); if (DEBUG) { - Log.e(TAG, mId + ":Unsupported " + conn.getResponseCode()); + Log.e(TAG, mId + ":Unsupported " + mConn.getResponseCode()); } break; @@ -108,7 +114,8 @@ public class DownloadRunnable implements Runnable { f.seek(mMission.offsets[mMission.current] + start); - is = conn.getInputStream(); + is = mConn.getInputStream(); + byte[] buf = new byte[DownloadMission.BUFFER_SIZE]; int len; @@ -121,18 +128,17 @@ public class DownloadRunnable implements Runnable { if (DEBUG && mMission.running) { Log.d(TAG, mId + ":position " + blockPosition + " finished, " + total + " bytes downloaded"); - mMission.setThreadBytePosition(mId, 0L); } - // if the download is paused, save progress for this thread - if (!mMission.running) { - mMission.setThreadBytePosition(mId, total); - break; - } + if (mMission.running) + mMission.setThreadBytePosition(mId, 0L);// clear byte position for next block + else + mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block + } catch (Exception e) { mMission.setThreadBytePosition(mId, total); - if (e instanceof ClosedByInterruptException) break; + if (!mMission.running || e instanceof ClosedByInterruptException) break; if (retryCount++ >= mMission.maxRetry) { mMission.notifyError(e); @@ -147,29 +153,43 @@ public class DownloadRunnable implements Runnable { } } - try { - f.close(); - } catch (Exception err) { - // ¿ejected media storage? ¿file deleted? ¿storage ran out of space? - } - try { if (is != null) is.close(); } catch (Exception err) { // nothing to do } + try { + f.close(); + } catch (Exception err) { + // ¿ejected media storage? ¿file deleted? ¿storage ran out of space? + } + if (DEBUG) { Log.d(TAG, "thread " + mId + " exited from main download loop"); } + if (mMission.errCode == DownloadMission.ERROR_NOTHING && mMission.running) { if (DEBUG) { Log.d(TAG, "no error has happened, notifying"); } mMission.notifyFinished(); } + if (DEBUG && !mMission.running) { Log.d(TAG, "The mission has been paused. Passing."); } } + + @Override + public void interrupt() { + super.interrupt(); + + try { + if (mConn != null) mConn.disconnect(); + } catch (Exception e) { + // nothing to do + } + } + } 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 5ef4ed90e..4bcaeaf85 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -18,30 +18,33 @@ import static org.schabi.newpipe.BuildConfig.DEBUG; /** * Single-threaded fallback mode */ -public class DownloadRunnableFallback implements Runnable { +public class DownloadRunnableFallback extends Thread { private static final String TAG = "DownloadRunnableFallback"; private final DownloadMission mMission; - private int retryCount = 0; + private final int mId = 1; - private InputStream is; - private RandomAccessFile f; + private int mRetryCount = 0; + private InputStream mIs; + private RandomAccessFile mF; + private HttpURLConnection mConn; DownloadRunnableFallback(@NonNull DownloadMission mission) { mMission = mission; - is = null; - f = null; + mIs = null; + mF = null; + mConn = null; } private void dispose() { try { - if (is != null) is.close(); + if (mIs != null) mIs.close(); } catch (IOException e) { // nothing to do } try { - if (f != null) f.close(); + if (mF != null) mF.close(); } catch (IOException e) { // ¿ejected media storage? ¿file deleted? ¿storage ran out of space? } @@ -63,27 +66,36 @@ public class DownloadRunnableFallback implements Runnable { try { long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start; - HttpURLConnection conn = mMission.openConnection(1, rangeStart, -1); + + mConn = mMission.openConnection(mId, rangeStart, -1); + mMission.establishConnection(mId, mConn); + + // check if the download can be resumed + if (mConn.getResponseCode() == 416 && start > 0) { + start = 0; + mRetryCount--; + throw new DownloadMission.HttpError(416); + } // secondary check for the file length if (!mMission.unknownLength) - mMission.unknownLength = Utility.getContentLength(conn) == -1; + mMission.unknownLength = Utility.getContentLength(mConn) == -1; - f = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); - f.seek(mMission.offsets[mMission.current] + start); + mF = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); + mF.seek(mMission.offsets[mMission.current] + start); - is = conn.getInputStream(); + mIs = mConn.getInputStream(); byte[] buf = new byte[64 * 1024]; int len = 0; - while (mMission.running && (len = is.read(buf, 0, buf.length)) != -1) { - f.write(buf, 0, len); + while (mMission.running && (len = mIs.read(buf, 0, buf.length)) != -1) { + mF.write(buf, 0, len); start += len; mMission.notifyProgress(len); } - // if thread goes interrupted check if the last part is written. This avoid re-download the whole file + // if thread goes interrupted check if the last part mIs written. This avoid re-download the whole file done = len == -1; } catch (Exception e) { dispose(); @@ -91,9 +103,9 @@ public class DownloadRunnableFallback implements Runnable { // save position mMission.setThreadBytePosition(0, start); - if (e instanceof ClosedByInterruptException) return; + if (!mMission.running || e instanceof ClosedByInterruptException) return; - if (retryCount++ >= mMission.maxRetry) { + if (mRetryCount++ >= mMission.maxRetry) { mMission.notifyError(e); return; } @@ -110,4 +122,18 @@ public class DownloadRunnableFallback implements Runnable { mMission.setThreadBytePosition(0, start); } } + + @Override + public void interrupt() { + super.interrupt(); + + if (mConn != null) { + try { + mConn.disconnect(); + } catch (Exception e) { + // nothing to do + } + + } + } } 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 1bb28fe95..a57fe1734 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -235,7 +235,7 @@ public class DownloadManagerService extends Service { if (icDownloadDone != null) icDownloadDone.recycle(); if (icDownloadFailed != null) icDownloadFailed.recycle(); - icLauncher.recycle(); + if (icLauncher != null) icLauncher.recycle(); } @Override From fab5f26e09eb00a8d939a31d1d922c3500f207de Mon Sep 17 00:00:00 2001 From: Brian <12914037+thisBrian@users.noreply.github.com> Date: Sat, 12 Jan 2019 04:41:16 +1300 Subject: [PATCH 49/68] Correct 'description' spelling --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 574c87ad3..873c1780f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,7 +19,7 @@ hasn't been reported/requested before * We use English for development. Issues in other languages will be closed and ignored. * Please only add *one* issue at a time. Do not put multiple issues into one thread. * When reporting a bug please give us a context, and a description how to reproduce it. -* Issues that only contain a generated bug report, but no describtion might be closed. +* Issues that only contain a generated bug report, but no description might be closed. ## Bug Fixing * If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to From 7564a4934432472efc649d2f6d8d014bfdb22183 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Fri, 18 Jan 2019 17:11:31 +0100 Subject: [PATCH 50/68] fix decrypt error once again --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 78ac4f3e3..abb455e70 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,7 +54,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.TeamNewPipe:NewPipeExtractor:91b1efc97e' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:99915e4527c0' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' From 880a176e657d9f4cf82109bd4335161ffc5c431b Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Fri, 18 Jan 2019 20:34:43 +0100 Subject: [PATCH 51/68] move on to version 0.15.0 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 78ac4f3e3..52c7ca78b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.schabi.newpipe" minSdkVersion 19 targetSdkVersion 28 - versionCode 69 - versionName "0.14.2" + versionCode 70 + versionName "0.15.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true From 8c9492669379b15bbd05bc0957f9a872828d43a0 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Fri, 18 Jan 2019 20:46:50 +0100 Subject: [PATCH 52/68] move on to version 0.15.0 --- .github/CONTRIBUTING.md | 2 +- app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 574c87ad3..873c1780f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,7 +19,7 @@ hasn't been reported/requested before * We use English for development. Issues in other languages will be closed and ignored. * Please only add *one* issue at a time. Do not put multiple issues into one thread. * When reporting a bug please give us a context, and a description how to reproduce it. -* Issues that only contain a generated bug report, but no describtion might be closed. +* Issues that only contain a generated bug report, but no description might be closed. ## Bug Fixing * If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to diff --git a/app/build.gradle b/app/build.gradle index 52c7ca78b..bb993cca0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,7 +54,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.TeamNewPipe:NewPipeExtractor:91b1efc97e' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:99915e4527c0' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' From 2ae99afa211cc28139e7275234b918ad1d13d703 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Fri, 18 Jan 2019 21:13:04 +0100 Subject: [PATCH 53/68] add changelog for version v0.15.0 --- .../metadata/android/en-US/changelogs/70.txt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/70.txt diff --git a/fastlane/metadata/android/en-US/changelogs/70.txt b/fastlane/metadata/android/en-US/changelogs/70.txt new file mode 100644 index 000000000..755fc3469 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/70.txt @@ -0,0 +1,25 @@ +ATTENTION: This version probably is a bugfest, just like the last one. However due to the full shutdown since the 17. a broken version is better then no version. Right? ¯\_(ツ)_/¯ + +### Improvements +* downloaded files can now be opened with one click #1879 +* drop support for android 4.1 - 4.3 #1884 +* remove old player #1884 +* remove streams from current play queue by swiping them to the right #1915 +* remove auto queued stream when a new stream is enqueued manually #1878 +* Postprocessing for downloads and implement missing features #1759 by @kapodamy + * Post-processing infrastructure + * Proper error handling "infrastructure" (for downloader) + * Queue instead of multiple downloads + * Move serialized pending downloads (`.giga` files) to app data + * Implement max download retry + * Proper multi-thread download pausing + * Stop downloads when swicthing to mobile network (never works, see 2nd point) + * Save the thread count for next downloads + * A lot of incoherences fixed + +### Fixed +* Fix crash with default resolution set to best and limited mobile data resolution #1835 +* pop-up player crash fixed #1874 +* NPE when trying to open background player #1901 +* Fix for inserting new streams when auto queuing is enabled #1878 +* Fixed the decypering shuttown issue From 43e4fbfcd0e146413c45e4b03b9d0664f08e86f4 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Sun, 20 Jan 2019 14:23:41 +0100 Subject: [PATCH 54/68] fix livestream issue, and move on to v0.15.1 --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bb993cca0..7bcd9d38a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.schabi.newpipe" minSdkVersion 19 targetSdkVersion 28 - versionCode 70 - versionName "0.15.0" + versionCode 71 + versionName "0.15.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -54,7 +54,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.TeamNewPipe:NewPipeExtractor:99915e4527c0' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:f7c7b9df1a' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' From d02dce556202109e6780eaff5d747d9e1cef671d Mon Sep 17 00:00:00 2001 From: Keegan Given <32858528+gkeegan@users.noreply.github.com> Date: Mon, 21 Jan 2019 02:35:39 +0000 Subject: [PATCH 55/68] Add updating section to README This will help cull the number of "how to update?" Issues whenever we need to push out a fix, like how recently NewPipe got completely broken. This attempts to be an all-inclusive guide to updating NewPipe. If there are any (clean/official) links to additional info, please comment them and I will add it. Also let me know if this section should be relocated within the README. I figured it would be best to keep it up top, so people see it right away, and then don't make a new Issue. --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 15ba3d04b..79fb18eb5 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,20 @@ WARNING: PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS. +## Updates +When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can: + * Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods. + * Download the APK from [releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it. + * Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users. + +When you install an APK from one of these options, it will be incompatible with an APK from one of the other options. This is due to different signing keys being used. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app, and are independent. F-Droid and GitHub use different signing keys, and building an APK debug excludes a key. + +If you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure: +1. Back up your data via "Settings>Content>Export Database" so you keep your history, subscriptions, and playlists +2. Uninstall NewPipe +3. Download the APK from the new source and install it +4. Import the data from step 1 via "Settings>Content>Import Database" + ## Screenshots [](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png) From f97cb821f85019f321fd1f850cd366ff91c8f613 Mon Sep 17 00:00:00 2001 From: Keegan Given <32858528+gkeegan@users.noreply.github.com> Date: Mon, 21 Jan 2019 21:55:45 +0000 Subject: [PATCH 56/68] Update README.md Move Updates underneath Coming Features. Make it linkable. --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 79fb18eb5..019ced085 100644 --- a/README.md +++ b/README.md @@ -12,26 +12,12 @@


-

ScreenshotsDescriptionFeaturesContributionDonateLicense

+

ScreenshotsDescriptionFeaturesUpdatesContributionDonateLicense

WebsiteBlogPress


WARNING: PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS. -## Updates -When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can: - * Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods. - * Download the APK from [releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it. - * Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users. - -When you install an APK from one of these options, it will be incompatible with an APK from one of the other options. This is due to different signing keys being used. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app, and are independent. F-Droid and GitHub use different signing keys, and building an APK debug excludes a key. - -If you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure: -1. Back up your data via "Settings>Content>Export Database" so you keep your history, subscriptions, and playlists -2. Uninstall NewPipe -3. Download the APK from the new source and install it -4. Import the data from step 1 via "Settings>Content>Import Database" - ## Screenshots [](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png) @@ -87,6 +73,20 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit * Show comments * … and many more +## Updates +When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can: + * Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods. + * Download the APK from [releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it. + * Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users. + +When you install an APK from one of these options, it will be incompatible with an APK from one of the other options. This is due to different signing keys being used. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app, and are independent. F-Droid and GitHub use different signing keys, and building an APK debug excludes a key. + +If you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure: +1. Back up your data via "Settings>Content>Export Database" so you keep your history, subscriptions, and playlists +2. Uninstall NewPipe +3. Download the APK from the new source and install it +4. Import the data from step 1 via "Settings>Content>Import Database" + ## Contribution Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome. The more is done the better it gets! From 5d14dca81804ff1dd209f54a738d5478f5ca78b2 Mon Sep 17 00:00:00 2001 From: Keegan Given <32858528+gkeegan@users.noreply.github.com> Date: Mon, 21 Jan 2019 22:21:24 +0000 Subject: [PATCH 57/68] Add link to #1981 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 019ced085..0d615f43c 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,9 @@ When a change to the NewPipe code occurs (due to either adding features or bug f * Download the APK from [releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it. * Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users. -When you install an APK from one of these options, it will be incompatible with an APK from one of the other options. This is due to different signing keys being used. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app, and are independent. F-Droid and GitHub use different signing keys, and building an APK debug excludes a key. +When you install an APK from one of these options, it will be incompatible with an APK from one of the other options. This is due to different signing keys being used. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app, and are independent. F-Droid and GitHub use different signing keys, and building an APK debug excludes a key. The signing key issue is being discussed in issue [#1981](https://github.com/TeamNewPipe/NewPipe/issues/1981), and may be fixed by setting up our own repository on F-Droid. -If you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure: +In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure: 1. Back up your data via "Settings>Content>Export Database" so you keep your history, subscriptions, and playlists 2. Uninstall NewPipe 3. Download the APK from the new source and install it From 6185c4ddcfef5e7a2587c605d968d2d200ceccaf Mon Sep 17 00:00:00 2001 From: kapodamy Date: Fri, 14 Dec 2018 14:11:10 -0300 Subject: [PATCH 58/68] delete list and grid icons * delete all grid.png files * delete all list.png files --- app/src/main/res/drawable-hdpi/grid.png | Bin 3341 -> 0 bytes app/src/main/res/drawable-hdpi/list.png | Bin 3156 -> 0 bytes app/src/main/res/drawable-ldrtl-xhdpi/grid.png | Bin 2960 -> 0 bytes app/src/main/res/drawable-ldrtl-xhdpi/list.png | Bin 3006 -> 0 bytes app/src/main/res/drawable-xxhdpi/grid.png | Bin 18221 -> 0 bytes app/src/main/res/drawable-xxhdpi/list.png | Bin 18220 -> 0 bytes 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/src/main/res/drawable-hdpi/grid.png delete mode 100644 app/src/main/res/drawable-hdpi/list.png delete mode 100644 app/src/main/res/drawable-ldrtl-xhdpi/grid.png delete mode 100644 app/src/main/res/drawable-ldrtl-xhdpi/list.png delete mode 100644 app/src/main/res/drawable-xxhdpi/grid.png delete mode 100644 app/src/main/res/drawable-xxhdpi/list.png diff --git a/app/src/main/res/drawable-hdpi/grid.png b/app/src/main/res/drawable-hdpi/grid.png deleted file mode 100644 index 26fa36c070f8a64dd45b446f92cd34cc5b159fbb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3341 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%0Dy!5 z0Qvv`0D$NK0Cg|`0P0`>06Lfe02gqax=}m;000JJOGiWi{{a60|De66lK=n!32;bR za{vGf6951U69E94oEQKA00(qQO+^Rd3JMb@5C2qaZ~y=T7fD1xR9M69m$6P%Q4of| zxv)sV6)Fo7p)er^^##D#Xzd%QjW2)&iBDj`WAFfWR#0JK4~7O4cN1@<5Lu~M2xBvw zOeW`qdtDY>vXh)-_uPNxoO|Y<|ICH4jqRFERb`m*C9~)g;04eNk17%QXqYA7888i~ zi+2Qk5|Lxu{41agai)MR5!tPK3Tv9S1BZF3s@(+Ur>d?v++ilxT59bgv!`B-eRqN1 ziTP7IwZ8qy_@6b|Q1 z_|ZAJ=uaEUhRo^rr^)ztThPU$||MS58 zg#R7)dmdOwV9F+CpA)u=OaAxg^b*YI3!~J)OUAz#ZQux?{`;04-i57Kix6b|o XjPphH8Dpc*00000NkvXXu0mjff#6e2 diff --git a/app/src/main/res/drawable-hdpi/list.png b/app/src/main/res/drawable-hdpi/list.png deleted file mode 100644 index 16da863e2e21606532a4a0880c30ee609e142fe8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3156 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%0Dy!5 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 A-T(jq diff --git a/app/src/main/res/drawable-xxhdpi/grid.png b/app/src/main/res/drawable-xxhdpi/grid.png deleted file mode 100644 index db749798127e1dd5b9a40603fbe7cca317a8b46e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18221 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 Z62MTuf)7V!Tj8+Y%w)ZBnxV^{e*qu(_>2Gm diff --git a/app/src/main/res/drawable-xxhdpi/list.png b/app/src/main/res/drawable-xxhdpi/list.png deleted file mode 100644 index fbb7c107299cc04d04e03834fcf1f5c785249611..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18220 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 From 8d1d4092aae1ff6bb60d5d3f015e03576fe6f638 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Fri, 14 Dec 2018 14:51:55 -0300 Subject: [PATCH 59/68] add missing icons in bright theme * missing white icons * update attrs.xml and styles.xml --- .../giga/ui/fragment/MissionsFragment.java | 19 ++++++++++++++---- .../drawable-hdpi/ic_delete_black_24dp.png | Bin 0 -> 317 bytes .../drawable-hdpi/ic_delete_white_24dp.png | Bin 0 -> 319 bytes .../res/drawable-hdpi/ic_grid_black_24dp.png | Bin 0 -> 422 bytes .../res/drawable-hdpi/ic_grid_white_24dp.png | Bin 0 -> 415 bytes .../res/drawable-hdpi/ic_list_black_24dp.png | Bin 0 -> 265 bytes .../res/drawable-hdpi/ic_list_white_24dp.png | Bin 0 -> 276 bytes .../drawable-mdpi/ic_delete_black_24dp.png | Bin 0 -> 198 bytes .../drawable-mdpi/ic_delete_white_24dp.png | Bin 0 -> 198 bytes .../res/drawable-mdpi/ic_grid_black_24dp.png | Bin 0 -> 270 bytes .../res/drawable-mdpi/ic_grid_white_24dp.png | Bin 0 -> 279 bytes .../res/drawable-mdpi/ic_list_black_24dp.png | Bin 0 -> 248 bytes .../res/drawable-mdpi/ic_list_white_24dp.png | Bin 0 -> 254 bytes .../drawable-xhdpi/ic_delete_black_24dp.png | Bin 0 -> 270 bytes .../drawable-xhdpi/ic_delete_white_24dp.png | Bin 0 -> 274 bytes .../res/drawable-xhdpi/ic_grid_black_24dp.png | Bin 0 -> 276 bytes .../res/drawable-xhdpi/ic_grid_white_24dp.png | Bin 0 -> 288 bytes .../res/drawable-xhdpi/ic_list_black_24dp.png | Bin 0 -> 247 bytes .../res/drawable-xhdpi/ic_list_white_24dp.png | Bin 0 -> 249 bytes .../drawable-xxhdpi/ic_delete_black_24dp.png | Bin 0 -> 506 bytes .../drawable-xxhdpi/ic_delete_white_24dp.png | Bin 0 -> 495 bytes .../drawable-xxhdpi/ic_grid_black_24dp.png | Bin 0 -> 427 bytes .../drawable-xxhdpi/ic_grid_white_24dp.png | Bin 0 -> 419 bytes .../drawable-xxhdpi/ic_list_black_24dp.png | Bin 0 -> 292 bytes .../drawable-xxhdpi/ic_list_white_24dp.png | Bin 0 -> 295 bytes .../drawable-xxxhdpi/ic_delete_black_24dp.png | Bin 0 -> 541 bytes .../drawable-xxxhdpi/ic_delete_white_24dp.png | Bin 0 -> 547 bytes .../drawable-xxxhdpi/ic_grid_black_24dp.png | Bin 0 -> 363 bytes .../drawable-xxxhdpi/ic_grid_white_24dp.png | Bin 0 -> 382 bytes .../drawable-xxxhdpi/ic_list_black_24dp.png | Bin 0 -> 330 bytes .../drawable-xxxhdpi/ic_list_white_24dp.png | Bin 0 -> 320 bytes .../activity_main_player.xml | 2 +- .../main/res/layout/activity_main_player.xml | 2 +- app/src/main/res/menu/download_menu.xml | 18 ++++++++--------- app/src/main/res/values/attrs.xml | 3 +++ app/src/main/res/values/styles.xml | 8 ++++++-- 36 files changed, 35 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable-hdpi/ic_delete_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_grid_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_grid_white_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_list_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_list_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_delete_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_grid_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_grid_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_list_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_list_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_grid_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_grid_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_list_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_grid_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_grid_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_list_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_grid_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_grid_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_list_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index aa9c497f1..a4be9301e 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -7,9 +7,12 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; +import android.content.res.TypedArray; import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; +import android.support.annotation.AttrRes; +import android.support.annotation.DrawableRes; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -89,7 +92,7 @@ public class MissionsFragment extends Fragment { 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 +106,6 @@ public class MissionsFragment extends Fragment { } } }); - mLinearManager = new LinearLayoutManager(getActivity()); setHasOptionsMenu(true); @@ -120,7 +122,7 @@ public class MissionsFragment extends Fragment { // Bug: in api< 23 this is never called // so mActivity=null - // so app crashes with nullpointer exception + // so app crashes with null-pointer exception mActivity = activity; } @@ -189,12 +191,21 @@ public class MissionsFragment extends Fragment { mList.setAdapter(mAdapter); if (mSwitch != null) { - mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list); + mSwitch.setIcon(getDrawableFromAttribute(mLinear ? R.attr.ic_grid : R.attr.ic_list)); mSwitch.setTitle(mLinear ? R.string.grid : R.string.list); mPrefs.edit().putBoolean("linear", mLinear).apply(); } } + @DrawableRes + private int getDrawableFromAttribute(@AttrRes int ic) { + TypedArray styledAttributes = mActivity.obtainStyledAttributes(new int[]{ic}); + int resId = styledAttributes.getResourceId(0, -1); + styledAttributes.recycle(); + + return resId; + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); diff --git a/app/src/main/res/drawable-hdpi/ic_delete_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_delete_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ba8820363ec185ea85c54720ee4b709ae6c0fc9b GIT binary patch literal 317 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$UKzz{uk1;usRq`u2vQwrd~*`-l1I zl}pVN1U5;`nA+)R);zB_Fn@w?WKGNTn`$T9C!AFA)LgwOfX%MOrmcM0?bkcMTdY6v z^LoAQ|NCpJ+pAA_&R0B>Bwby5>yS;Brd#{P0~rgGBaT>F{djYkPj6$-N4a#dhbD3o?BL)(7O;=LW4@*f)`Jo8BYt zIftya32j?>=yc2(ws}iy%kMGiG#hCkp%?ZIvnsrJs*a}VWQg{6I_gJsv-&<@df#av zzvEDf4#%{D`5a#wrp`}z5jWE(nLSs=ZA)+LUHK!I!VUH-#c$tIcI_qWx=3KKFnGH9 KxvX|k1|%Oc%$UKzz{uw5;usRq`u2vRw{xHb+k>6a zY?~Ia1x6a4u`$RLnC+NW94K}~E#eR7;T&PX`j#0!N=h7I2Nxw=kXL+XxT9oe%zM4( zO^+%cuDxH$Hz#;sl&)HVL8I%N1pDQ+It%3Yd(Jpbc2TIv70J8nPTg7XIo&Z==JC2i^A^O4gt#+L7TuP3N4}#n zXOZ}ZSXcRCwhV6l{DGbteD- literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_grid_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_grid_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2db18582c2906e204f3007cee270ebb97d488f3d GIT binary patch literal 422 zcmV;X0a^ZuP)nP^NG9}0Xg73$+A(`%eX zYzI39{ssql7LECiivmBWLRIL&3vJ;l)4%wY2yGYmE9@sid-zk}4^yF6IL-8D`$mSD z#&Ur_#aqlqW7hGbz;C2NHTPvom>aoHkI6R)-;Bwd7v{;Re#UAd^Sr?C;Dx(qSx;Cv zIesw}nz&_Og{n{$`rks`_9mmc@9tUC2>VQqKfqEeKFkun6!?`?Xr_IlT%U}|ckn3U zJ9v5zUo%z~aGdD??<2Os%L0EaoPCd@F)PBv&hcxh&?YW3J;zDJcJQgdUt(97GsAqv z?*jiV6`HtZUxljBgA?kt7f1C7?;^GlzJYW6K-hQU<6v3%S3~y+=Pi1$(<;cOV(H#lW46F2v!N|ChQ%k7>%-!E3i_6g_mVx5dujOo5y0d zI636}IEAo;^TF4>|L`)*IX}}hHUAjg!?@7bc#^QUaX;Y47-BJ*8Dcl!N4d~gIe+jo zVOvZB9*(e*%zVOGz)!1C6}s_4uW(uD1%6~g`vHH6!%XM{eh2*LT<9@Q3jKyj!Y*JV z;7{-ZcaoXs_!{urO--NZ)O?^ zeS(KLE_8~Q2|K`}fIr3-mXevx*@s=?pXEY(I4|@R-xIdQn}9#Zdn_k2@9-<&KjuPn zckHWB6}oXkBi(&-UB6%_VFy?X_^IyRNq5_y!)Nt?}n0`#~?julWV6q_xt# z+@^!3z>|3ooJku?^YXgu{r4201ZHLp45Xb%^Rh|@W%tTr(0_x@fu(r|l+r3`US4#) z|DMABH*U;1FqC#G&CAPl&@=F3egciOM`>QZr-QP4W!Vk70bb1wFp)Ns=H;T}`3FG| a9F-kHEtdRUpr-Wz0000(&8NluWs++?wI<3<(9<}%Lxe&b)qs9!&MB^6DRV=&HVmA{$B#OOyRr2L$9JE zd(W~*+bF%M%6}EGz*pc{hI1p^_ePV23?Vbpf{s4fV`uu^=CzPUBM4RG+bL}D;p{0m uJK?^8ALjv<>XIEUFA5Img#Np1%UBdDwdP%o@-m>q7(8A5T-G@yGywoR=S(#K literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8f10392cac7644b0c4b09e20f52ce13b0bd60970 GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gjy`C>7wES+6Ote6n0olNO-6--h0jII{NMQ zMy}ueaxKL&dsZH6FiSF+QpS_;E_|3jq z-tICB4OJT`os5MAKy2_72&JcE@I(tr;OXR{1bc&@ftK`~41V|vz7mXpyOXOD%niO% zf~~<9G+Du${E8D(y|d0rZSER8r~U^6gJ)Xc{}lNzNM(c0Q3-N`Pk~5!CI&CGAe7U% z`NSi|BZG%DS$*K<olS)$JH_nJvHzcIY|_&nd#`n_Gtesxp00i_>zopr09-L;Gynhq literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_list_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f0d3f5f7cff9f53b0fb070d4d40d4ca6f64fccee GIT binary patch literal 254 zcmVYH5>z7jL9j@K;A&C})*(v>SY-hgf<>yNbOAPL-GFPb&jM_+fX8A!;ikPZ zW1$D$;=Ik_@^M?OOU@&Bzz~Dz1)jp?(oY9w_{C53TWrJS@|_NRsj`WFk3Yv{l@3hs ziTCJT%){mK*2sMiFDP-1GI3Yoaw*e++HX+%KpQ=D6L%9XmrkvB{TIC81Fz9Ln1;(` z+~^G!*kK+0PW9b)N3cu>`WWFcdV%L~xeOY)?1EO|r68}40 zUw*tDBk^sD*$$bkKW4n^HXg}csAsr6{XYMs|4-Ph*TwJfo+p+M^dN($tDnm{r-UW| DT4rf% literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a1b828bf928a98888a4fbbf9204c1f87e7fef099 GIT binary patch literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUto1QL?AsLNtZ`pb=2THU)?7Y#z zvvFIYXjxM$m!6{Dan>E&;tCNrZ)tvecX)r1ob<#g&z_yTBXFKOzFuy}U7zbqbvbPH z!uM3ZzPtb7xmUJZ?%hq4@0Z|WV4hcK@a(|tro^qg`4<%amoDgCeyn^mUqaj5W9Hgy zIleR2f9*PL#ih`|z{G)txWV9XvV6m(wp$Hy^1@8}3L=CzbWi7GTz_LV+YiS%k8+zn zzTdIJs*bZT=g#skos$EP=_^*vNo-v0wf(?O-(PgSGY6|=t%}oS3j3^ HP6baGgzB8KxQ$Z_4x!K2xpWj9bL^zv58&q<`;ZeoWf)DEOGbhq`oShT0EV zm;77zsn#lfQDy$Z5O;9*#D98U{4eFV{>x}!WRpl}V610fahbpSPTLpjh4vHo3I1Yb zuDEyMIzt6ZzW=Z03-KQI;=e?hQ~pbs9$;YRF*wk$hB5coqZ1py|J-~b-t#`^-&u?{ z`7g9TR0_GQQ~c}m!unEw>%OFhH}Z?Yrn5;*FkRpy@6z|ze!oc7H@kSwzb%Y=^55|0 X$7` zqU(&r2M$L>yVr1SmXe8hQzoBy*8PLvw~p%z-tIqCKY8|U_B@-1Eww%No<9Bz28;o& zddmNTK5#$sXRSZSq;=%7(0|1rvLCY#=F2=_V7EBH$oFG+1CN9BPUesOP4$X(dOv0> z9GJ23^UscX6RidR>-;c(w4Sy8IMb^4;o1wPr(a>@D`;SrdC=g91B&4a&o@$P!+|GYkUKRVB1pUOD3;8<0qn=I5OiIM{{4#GPh)XE(Q`_TNq il40co{U(Ob%5nSJyb}&CK9vgeGJ~h9pUXO@geCw&^>^0* literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_list_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_list_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0f7327fadeacdc39422adcbe44146bc63ad2b13f GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUteV#6kAsLNtr!MSr3KVe__dlv4 zaYU2tzcY_g(?KPR1&#vU|KEt;(D^nqVdB|5eVLb0W!j~_*yzEe?w1AVh)B=4oAjj#u-m?RtcV+|CnnTpCklVl{_vmKH kt&ktD*?@L)DAq{-Z;Fy02N)%w#4?n6R z;nIEZyFhbC!iJcD3kn@a%fE5vi2c5Ce!K6E*XJc?m?iq(TxE1jBDu#?yU=4l6FZCK};Nt#`3;4=`Yc3ke;b~mZPFrSI1ai;~RRj!eY zEAzg8y;Tm~f1WiMUg+4o;Ii7`Ov3{+%o(#5=wAb=KN>5#fg$gaBFJ!Q-XFirUoq+m r@kKEEe-tXYm9j&h4P^hZKZ^Q^pIZ|i?3U>R`h~&M)z4*}Q$iB}8Q5WF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4cb4c08e2d5bc8bbb83fc99bd95d02010b5a9e89 GIT binary patch literal 506 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2U_9vQ;uumf=j|O^eU?Cm)`#Ys zwBiL7ITn60HWA|Ooaf}cIB20`^Q54~YF71aQ34-DcXwuS@9w&pq->b3zOy_n?A+uz zWySAj_x2{Soq02V1q1Vegoe-}yXum^2iQKa>|wtl=J}>Rk>v(kTqD~Bo()nPxO<*e z^lKbzu$`3ex5DD=we-D=?FUlww!b{USTHSdx=Nu_>(9O753C-~^lJ*wESCScsdBB9 zmDldyLNON_mCiRFk?}awcw`aVEybTR-#mNEzW&m({wMd_^5Wt@Xr%?N3HT#_$8Wpb zN=GerAsLSXg@cVOG+=~r{8lMmW2&KX!6@K-#SNt}_G?O}9;W`~HVNN3bdGvX$Ya~V zw9i<^?0qe>`GJ@vY%8@6XnnZwFtPiF@dnN>ZdnSwjK>cgmuJ^0GJP;bXwR;st|yik z(ocO}{f$qCar4J@bI%%VV3@LSxA*fT%8G@kN6hOgcYS? c#@hSkBi)|*CH`6b4j88lp00i_>zopr0G|KJPXGV_ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..bd11b4c66ce16c9380af0941567031454e81d02e GIT binary patch literal 495 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2VBF^E;uumf=j|QCY>`Bfwu=JS zw2mnpT(IcKsriY@H#BZ!Cob-tI`x)|wC~ELy<3x>&%YQI;^J$-xLV}o#v7pmDS1uC zzf%uaKiGEqPI`OfwjXJEdP;pvJsUcNZO^RuYPa`L;hkgYIbRIQH{PgPAD1Bc{R40H zuZ_Lz7FVnG|8EktnRCao_Nd{{HSa5WHn08uhq?d5ABN9aLg^auAD=Z`-(r`tfS*~< zS~r^Wif$tpXNboFCnixg-V*uO#&+4xY*7aPZX2B5Y!I=akM!7HSI3Vtw^l<#WI{6Rr|o>)z@2`(^Ls2)>wf(<0$JXLvMA)Y0rWX0iKk z>)m^l)a{^r=fLr0oV#id%F4{HTHv`v)aGc~WuE`rdefu!?_zqsA?;7D?6ml;yfz10 zmlf6>_KZ2kz2`9hpE;j$9@@q|;+hs+{*(JzZ+u;e)N|>xk~)d|O8Yx*Opm|pYH<0C Tc#eZw?LI_moH?*1-caYw{@t7% z9e;H9N!2l(Kl*Lc&i|}yy8g0$xOZp%^-7+U1q&LR*tn!LGO#cbc(&yIV6+jtZGQHz zX+iv(c&7Dd{~oqFzk{jnm2g2^&U};a^BXrO)%O=pE&I0pwT+UJ(*K#Mhog_Jt?6oh z_D{${?^}7p>u3MgoO|qf%e@zmO<<{G4y)VvuiRhTH~Y8tgDp4iv%Z`4JGxX(P;lb^ zGm9RIK33NVeq-`oyeImnNb~{M;`+_!jQOJMzxEbN%z3nH=l^pS&wUCR_n3VD{@P~V zlG-aA*gXYxg#`0iX}Q4v_p`3unIBxg{Z`7J^Dp1zU#WC?BL0&tnlG_7R(jnJU?ebj My85}Sb4q9e03TGkZU6uP literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_grid_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_grid_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ebe059481e836a0264e83096628fc1b1ce6023db GIT binary patch literal 419 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2V9fG#aSW-L^Y->auVzM(wugxV zZPOZ!0`D{_9O{b5aT62^V{x3Y_3qT~37@3G_HfKD|0tDaCttzSx0ip;uh!`Y>wf%X zXZHN%{^BQ7*wJr`cJgn~`Ig^UT3nyBU{)g&GbF(9e?nGzPS~X-eH)JB2cDuVN;?1mG^7U&HnBEV9^bGre|mW zHXBV)@x0xAcJq<^_iPq=+uYCojefA=hCM^t*}s29GVbmuPYBGpZ~FcEfju|oGp;}T zw~*_~Mq&R;vN(JMvN1uO$ych}?c-iigPeE%8uy|Ze;?SfbALG7q%YI=G5(IxZ9H?7 RIT;xE44$rjF6*2UngGv?vcv!Y literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_list_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_list_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..52d0c00a17994990d99d28d8cc949109e2187854 GIT binary patch literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!uRUEHLn`LHy=j=`6e!U8P+qC) zfw!PE^R7;j&Iep7LQT;e5j%sWt2cVhJfy_kaWvcb^MO~rf8^(UGFiTpSzN_)5(vEv z6Mn!jr)iJM-4(%_?hh=bUkLtS{MWQ*Qr?x7Q{odII`BOKp}#)Q9he`;-e8-l22v=0 zz(U$%(vn?l2c&-Re40|S*l%ghi;8EqB72v}o@kUg(O5P6Tw~h-@eTY&-n#;q2bFD+ z7oVhZcOmlzP{PuJJ8zzopr08o&4p8x;= literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e5f698298456b7cb877e2352de76ad338dcf2a03 GIT binary patch literal 295 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!?>t=`Ln`LHy=Lge93bF&vB6Pr zlE^KM8NN#l8rL|82yR!1*cm+g%R#NBt*zWU%rfuu-1u9-@AGrk?LYQ2JtnDm0@0=Y z48{+HZ?s2?-rgelvg|;vXa-}=#X(|b2V6x@fb>j4=*i%#VYGjE4QwQHfjBeBkw6sml7DY( WbI`(vbM66s&fw|l=d#Wzp$Py9vUe{4 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ab07ea2ae96b3ffd072d9644d118d8b55bda90dc GIT binary patch literal 541 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V0_}~;uumf=k4A7tIik79RK+I zk&Jo_BXg?J9M>Q=<>sX6(gCLqEa1^Q@Jw%t_k=TQrA2D0PyJ^~q%1gGV70jQ{PW*` z{#Mw%DrKDi{qy(N<=dD&C#f9ZOklq;cZ=+UD@p4kjnf;hA2@%qnmI@R%d)K-dG{p$ z`^f)|(_;IH)7K6}CiFk;tKVSz;1JXM=*;Pj=XL7;G}kh&^Q(WVa=c-E#qsE=wak70 zpX&biVC32$q}{NG=>&qf%#_1W-cbGj=ete+6MZJRluG|!Kj~(*!J8;y5r;5_NX8V_ z38DkRxO>R{!kfGDWotR}9>`~W%j4U9jpg}Aev@kP_Sozu-Uqh-CZ@er-eCDAYH|$o zzQ^*}U+NgHv){{{<$S>SPjl}*?gt+ZSo@Z;RCLyF39M$7k*!+DI)`Q6qd)5(1m0$z z$Gp5fF3oZF0q1Y^Gp=hMk^8_{!IHzfzbk=z2a8SG|JQ%YT(@)EFs}R1FJtF9>CZ9! Ysek4lOXgEx0Y*E6r>mdKI;Vst04N09wEzGB literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a8358eb71286cb51ee10a3633a3f24ebfe0a933f GIT binary patch literal 547 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V0`82;uumf=j~nFEYU=U)`uoi z%_q6G1Z>#kp{x`Uk)@cSpXVT;lcX4t!dUjssen`IT9}i;RD~w{gr%(!8s9hWh}j@n zKk1I8uq}TL_m6X@4KANOGHx<) zm^Pq-VtLE2OCJ3D=t@!oSBc}vUXTDV`9x)o^d{jBhLzSSOY{Vgu?8)n`6IlVyk z{vl`G!{v88-v5raXj=Yo+x zWnOhtsEt9rq5Lud<2}cn*DNi+W4`90k=#uCbNOmjWgjKN|93uIy=qm~4gRywycD%B Ss~-nOK7*&LpUXO@geCwAq2jUt literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_grid_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_grid_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..fe78d853e74f89291e5f1002f193801d8fd1138b GIT binary patch literal 363 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U^Mb{aSW-L^Y-q>Tqj2X*Nftf zvFoK5Fx9aKG%!Er(l}IPTh?Gce`=a)cl|mO#aUP9b*L&#RPp@3O84a6NeqY}38*cjl9&RSn5V086#aSW-L^Y-q+%)<^Mu7T^{ zthi?~fol!l8>THC!Hr!4tDdJm%whg~l1K1I%#5X#PRCbs9$7wF#nZ~q^O;Ka^J48c z-|xjuUeo%wpYiv1mFcIrU)&9=V06}cqtE>8yNWQ-CVW7K;lZ?&*EM$B`U}2)n|4e5 zfmOo)iP!8;c~4UDob)B?XzYdk_gU96%i%KvDyQHH)N?6&>hu5SU=;LpaSW-L^Y+eKuhKw~)`yJ$ z?yQwK6ZL1E;}*`O&9htr>?VsVm>E{c-twFKc?oB=E&ram4IfWd~YbdU?pCapWtK=NmQ1i<=1FgO$YU^vvF&zt!)}%T z-R~yLnB|3*hrL|*F$`#!kELf(WXY^23kp~7+U%9yyZzQj-Had66Bv0m>;!8wK2X3W zagO2eThjxTlU6V4{_;;?^}98f_8*I8WMD9OYIQRCqHmS`xwSyC+{bInYPhBTiKQ?w z=jb791{zV#`(e_PR2tQ+DP=@0BoMa8a1Lb+N&0-mmZF6*2UngDQH BgQfrg literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..053a1a0da97aa4ac5f9aa2d4836fe12394d87bc3 GIT binary patch literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U}X1naSW-L^Y)IlSBs-S>qSQS z-J;CNz9qbiW-6pDb5qig+R0!j%zV9&H^!m#utDn=%Yw^Z`!Z%dyq^O!1PmHfRVsZx zg}%`|dGNs6SLd{jcw lu3HAQ_#W6|um>3a-_n=QIhto+n7jfc;OXk;vd$@?2>{=GdBFex literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml index 7d7e1230e..5f484267b 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/activity_main_player.xml @@ -239,7 +239,7 @@ android:focusable="true" android:padding="5dp" android:scaleType="fitXY" - android:src="@drawable/list" + android:src="@drawable/ic_list_white_24dp" android:background="?attr/selectableItemBackground" tools:ignore="ContentDescription,RtlHardcoded"/> diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 3dccc5c4c..8cf9ba32f 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -237,7 +237,7 @@ android:focusable="true" android:padding="5dp" android:scaleType="fitXY" - android:src="@drawable/list" + android:src="@drawable/ic_list_white_24dp" android:background="?attr/selectableItemBackground" tools:ignore="ContentDescription,RtlHardcoded"/> diff --git a/app/src/main/res/menu/download_menu.xml b/app/src/main/res/menu/download_menu.xml index e79367135..4e2b32715 100644 --- a/app/src/main/res/menu/download_menu.xml +++ b/app/src/main/res/menu/download_menu.xml @@ -3,17 +3,17 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> - - + android:icon="?attr/ic_delete" + android:title="@string/clear_finished_download" + app:showAsAction="ifRoom" /> + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 7b879fb4c..c0e7fa532 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -38,6 +38,9 @@ + + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 87e19cede..da1ae0e38 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -22,7 +22,6 @@ @drawable/ic_info_outline_black_24dp @drawable/ic_bug_report_black_24dp @drawable/ic_headset_black_24dp - @drawable/ic_delete_sweep_white_24dp @drawable/ic_file_download_black_24dp @drawable/ic_share_black_24dp @drawable/ic_cast_black_24dp @@ -54,6 +53,9 @@ @drawable/ic_add_black_24dp @drawable/ic_settings_backup_restore_black_24dp @drawable/ic_blank_page_black_24dp + @drawable/ic_list_black_24dp + @drawable/ic_grid_black_24dp + @drawable/ic_delete_black_24dp @color/light_separator_color @color/light_contrast_background_color @@ -82,7 +84,6 @@ @drawable/ic_headset_white_24dp @drawable/ic_info_outline_white_24dp @drawable/ic_bug_report_white_24dp - @drawable/ic_delete_sweep_black_24dp @drawable/ic_file_download_white_24dp @drawable/ic_share_white_24dp @drawable/ic_cast_white_24dp @@ -114,6 +115,9 @@ @drawable/ic_add_white_24dp @drawable/ic_settings_backup_restore_white_24dp @drawable/ic_blank_page_white_24dp + @drawable/ic_list_white_24dp + @drawable/ic_grid_white_24dp + @drawable/ic_delete_white_24dp @color/dark_separator_color @color/dark_contrast_background_color From ecabbb57e62523fdab31e73d0246d3be05bf8cfc Mon Sep 17 00:00:00 2001 From: kapodamy Date: Sat, 15 Dec 2018 20:57:25 -0300 Subject: [PATCH 60/68] Update download_menu.xml * hide clear button by default --- app/src/main/res/menu/download_menu.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/menu/download_menu.xml b/app/src/main/res/menu/download_menu.xml index 4e2b32715..02efde55b 100644 --- a/app/src/main/res/menu/download_menu.xml +++ b/app/src/main/res/menu/download_menu.xml @@ -8,6 +8,7 @@ app:showAsAction="ifRoom" /> From 8fed18b2acdd1844d6a999330dea3f99714a5f23 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Sat, 15 Dec 2018 20:58:28 -0300 Subject: [PATCH 61/68] Update MissionAdapter.java * check if the iterator is initialized --- .../giga/ui/adapter/MissionAdapter.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) 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 df5f9e429..5057ed6db 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 @@ -158,7 +158,7 @@ public class MissionAdapter extends Adapter { str = R.string.missions_header_pending; } else { str = R.string.missions_header_finished; - setClearButtonVisibility(true); + mClear.setVisible(true); } ((ViewHolderHeader) view).header.setText(str); @@ -437,7 +437,7 @@ public class MissionAdapter extends Adapter { public void clearFinishedDownloads() { mDownloadManager.forgetFinishedDownloads(); applyChanges(); - setClearButtonVisibility(false); + mClear.setVisible(false); } private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) { @@ -506,11 +506,7 @@ public class MissionAdapter extends Adapter { mIterator.end(); checkEmptyMessageVisibility(); - - if (mIterator.getOldListSize() > 0) { - int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1); - setClearButtonVisibility(lastItemType == DownloadManager.SPECIAL_FINISHED); - } + checkClearButtonVisibility(mClear); } public void forceUpdate() { @@ -529,15 +525,18 @@ public class MissionAdapter extends Adapter { } public void setClearButton(MenuItem clearButton) { - if (mClear == null) { - int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1); - clearButton.setVisible(lastItemType == DownloadManager.SPECIAL_FINISHED); - } + if (mClear == null) checkClearButtonVisibility(clearButton); mClear = clearButton; } - private void setClearButtonVisibility(boolean flag) { - mClear.setVisible(flag); + private void checkClearButtonVisibility(MenuItem clearButton) { + if (mIterator.getOldListSize() < 1) { + clearButton.setVisible(false); + return; + } + + DownloadManager.MissionItem item = mIterator.getItem(mIterator.getOldListSize() - 1); + clearButton.setVisible(item.special == DownloadManager.SPECIAL_FINISHED || item.mission instanceof FinishedMission); } private void checkEmptyMessageVisibility() { From df4dd0122f60a426a552bfbc03647b978721dc51 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Mon, 14 Jan 2019 22:54:44 -0300 Subject: [PATCH 62/68] Update missions_header.xml make the separator line """"""""simetric"""""""""" --- app/src/main/res/layout/missions_header.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/missions_header.xml b/app/src/main/res/layout/missions_header.xml index 9505a2fce..6de38664d 100644 --- a/app/src/main/res/layout/missions_header.xml +++ b/app/src/main/res/layout/missions_header.xml @@ -4,8 +4,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="30dp" - android:layout_marginRight="16dp" - android:layout_marginEnd="16dp" + android:layout_marginRight="8dp" + android:layout_marginEnd="8dp" android:layout_marginTop="16dp" android:orientation="vertical" android:layout_marginLeft="8dp" From c4a5e8dc86fd6060bc21effc5f6f9242760741d4 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Tue, 15 Jan 2019 14:58:56 -0300 Subject: [PATCH 63/68] misc fixes * add null checks before resuming a download * (MissionAdapter.java) reset percent after resuming a download. prevents the "Error" string get stuck, until the download start --- app/src/main/java/us/shandian/giga/get/DownloadMission.java | 5 +++-- .../java/us/shandian/giga/ui/adapter/MissionAdapter.java | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) 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 c25d517f1..b41cfc350 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -407,7 +407,8 @@ public class DownloadMission extends Mission { // 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; @@ -420,7 +421,7 @@ public class DownloadMission extends Mission { init = null; - if (threads.length < 1) { + if (threads == null || threads.length < 1) { threads = new Thread[currentThreadCount]; } 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 5057ed6db..28e7ef480 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 @@ -447,6 +447,7 @@ public class MissionAdapter extends Adapter { if (mission != null) { switch (id) { case R.id.start: + h.status.setText(UNDEFINED_SPEED); h.state = -1; h.size.setText(Utility.formatBytes(mission.getLength())); mDownloadManager.resumeMission(mission); From 9db272f30ed12ed1c1b389572cac8c500ad07f18 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Fri, 18 Jan 2019 14:32:28 -0300 Subject: [PATCH 64/68] Update MissionsFragment.java use another way to get the "Context" --- .../giga/ui/fragment/MissionsFragment.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index a4be9301e..33e7b0a8a 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -43,7 +43,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; @@ -56,7 +56,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; @@ -82,11 +82,11 @@ 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); @@ -117,13 +117,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 null-pointer exception - mActivity = activity; + mContext = context; } /** @@ -134,7 +134,7 @@ public class MissionsFragment extends Fragment { public void onAttach(Activity activity) { super.onAttach(activity); - mActivity = activity; + mContext = activity.getApplicationContext(); } @@ -145,7 +145,7 @@ public class MissionsFragment extends Fragment { mBinder.removeMissionEventListener(mAdapter.getMessenger()); mBinder.enableNotifications(true); - mActivity.unbindService(mConnection); + mContext.unbindService(mConnection); mAdapter.deleterDispose(null); mBinder = null; @@ -199,7 +199,7 @@ public class MissionsFragment extends Fragment { @DrawableRes private int getDrawableFromAttribute(@AttrRes int ic) { - TypedArray styledAttributes = mActivity.obtainStyledAttributes(new int[]{ic}); + TypedArray styledAttributes = mContext.obtainStyledAttributes(new int[]{ic}); int resId = styledAttributes.getResourceId(0, -1); styledAttributes.recycle(); From 684cb8197435eb08d3a30191be37a9dfa3d93d1c Mon Sep 17 00:00:00 2001 From: kapodamy Date: Sat, 19 Jan 2019 16:22:24 -0300 Subject: [PATCH 65/68] Update MissionsFragment.java work-around for reading the current theme icons --- .../giga/ui/fragment/MissionsFragment.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 33e7b0a8a..25960a419 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -16,6 +16,7 @@ import android.support.annotation.DrawableRes; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -198,12 +199,20 @@ public class MissionsFragment extends Fragment { } @DrawableRes - private int getDrawableFromAttribute(@AttrRes int ic) { - TypedArray styledAttributes = mContext.obtainStyledAttributes(new int[]{ic}); - int resId = styledAttributes.getResourceId(0, -1); + private int getDrawableFromAttribute(@AttrRes int attr) { + TypedArray styledAttributes = mContext.getTheme().obtainStyledAttributes(new int[]{attr}); + int resId = styledAttributes.getResourceId(0, 0); styledAttributes.recycle(); - return resId; + if (resId != 0) { + return resId; + } else { + // work-around + styledAttributes = mContext.obtainStyledAttributes(new int[]{attr}); + resId = styledAttributes.getResourceId(0, 0); + styledAttributes.recycle(); + return resId; + } } @Override From f2285c0b191b70aa25961f7ad19516c715fa522b Mon Sep 17 00:00:00 2001 From: kapodamy Date: Mon, 21 Jan 2019 01:30:03 -0300 Subject: [PATCH 66/68] MP4 muxer +misc modifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * allow retry downloads with "post-processing failed" error in the new muxer * MPEG-4 muxer ¡¡ no DASH output!! * keep the progress if download fails * remove TODO in SecondaryStreamHelper.java * misc clean-up * delete TestAlgo.java * delete ExtSDDownloadFailedActivity.java and remove it from AndroidManifest.xml * use hardcored version for changing icon colors --- app/src/main/AndroidManifest.xml | 1 - .../newpipe/download/DownloadDialog.java | 7 +- .../download/ExtSDDownloadFailedActivity.java | 38 ----- .../newpipe/util/SecondaryStreamHelper.java | 1 - .../giga/get/DownloadInitializer.java | 1 - .../us/shandian/giga/get/DownloadMission.java | 123 +++++++++++----- .../giga/postprocessing/Mp4DashMuxer.java | 4 +- .../giga/postprocessing/Mp4Muxer.java | 136 ++++++++++++++++++ .../giga/postprocessing/Postprocessing.java | 123 +++++++++------- .../giga/postprocessing/TestAlgo.java | 54 ------- ...TttmlConverter.java => TtmlConverter.java} | 15 +- .../giga/postprocessing/WebMMuxer.java | 4 +- .../giga/service/DownloadManager.java | 30 ++-- .../giga/ui/adapter/MissionAdapter.java | 48 +++---- .../giga/ui/fragment/MissionsFragment.java | 32 ++--- 15 files changed, 357 insertions(+), 260 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/download/ExtSDDownloadFailedActivity.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java delete mode 100644 app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java rename app/src/main/java/us/shandian/giga/postprocessing/{TttmlConverter.java => TtmlConverter.java} (83%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1bc205f33..5aa1dc982 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -119,7 +119,6 @@ - { - // 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; } diff --git a/app/src/main/java/org/schabi/newpipe/download/ExtSDDownloadFailedActivity.java b/app/src/main/java/org/schabi/newpipe/download/ExtSDDownloadFailedActivity.java deleted file mode 100644 index c02ef92eb..000000000 --- a/app/src/main/java/org/schabi/newpipe/download/ExtSDDownloadFailedActivity.java +++ /dev/null @@ -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(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java index a5d3ea3eb..b3522aea0 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java @@ -36,7 +36,6 @@ public class SecondaryStreamHelper { * @return selected audio stream or null if a candidate was not found */ public static AudioStream getAudioStreamFor(@NonNull List audioStreams, @NonNull VideoStream videoStream) { - // TODO: check if m4v and m4a selected streams are DASH compliant switch (videoStream.getFormat()) { case WEBM: case MPEG_4: diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index ce7ae267c..b864cf4fb 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -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; } 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 b41cfc350..243a8585a 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -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,7 +413,7 @@ 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); @@ -414,6 +424,19 @@ public class DownloadMission extends Mission { 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; @@ -445,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(); @@ -533,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; @@ -551,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) { @@ -572,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() { diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java index b303b66cd..45c06dd4b 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java @@ -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 diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java new file mode 100644 index 000000000..bf932d5c1 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java @@ -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; + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 80726f705..635140bd3 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -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; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java b/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java deleted file mode 100644 index 66b235d7c..000000000 --- a/app/src/main/java/us/shandian/giga/postprocessing/TestAlgo.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java similarity index 83% rename from app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java rename to app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java index 4c9d44548..390061840 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/TttmlConverter.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java @@ -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; } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java index 009a9a66b..2ffb0f08d 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java @@ -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 diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 6bcf84745..883c26850 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -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 current; ArrayList 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() { 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 28e7ef480..1b5b98145 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 @@ -59,7 +59,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 +67,8 @@ import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; public class MissionAdapter extends Adapter { private static final SparseArray 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"); @@ -178,7 +179,7 @@ public class MissionAdapter extends Adapter { 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 +239,10 @@ public class MissionAdapter extends Adapter { } 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 +251,11 @@ public class MissionAdapter extends Adapter { 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; @@ -406,7 +406,7 @@ public class MissionAdapter extends Adapter { 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; @@ -447,7 +447,7 @@ public class MissionAdapter extends Adapter { if (mission != null) { switch (id) { case R.id.start: - h.status.setText(UNDEFINED_SPEED); + h.status.setText(UNDEFINED_PROGRESS); h.state = -1; h.size.setText(Utility.formatBytes(mission.getLength())); mDownloadManager.resumeMission(mission); @@ -507,7 +507,7 @@ public class MissionAdapter extends Adapter { mIterator.end(); checkEmptyMessageVisibility(); - checkClearButtonVisibility(mClear); + mClear.setVisible(mIterator.hasFinishedMissions()); } public void forceUpdate() { @@ -526,20 +526,10 @@ public class MissionAdapter extends Adapter { } public void setClearButton(MenuItem clearButton) { - if (mClear == null) checkClearButtonVisibility(clearButton); + if (mClear == null) clearButton.setVisible(mIterator.hasFinishedMissions()); mClear = clearButton; } - private void checkClearButtonVisibility(MenuItem clearButton) { - if (mIterator.getOldListSize() < 1) { - clearButton.setVisible(false); - return; - } - - DownloadManager.MissionItem item = mIterator.getItem(mIterator.getOldListSize() - 1); - clearButton.setVisible(item.special == DownloadManager.SPECIAL_FINISHED || item.mission instanceof FinishedMission); - } - private void checkEmptyMessageVisibility() { int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE; if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag); @@ -596,6 +586,10 @@ public class MissionAdapter extends Adapter { } } + private boolean isNotFinite(Float value) { + return Float.isNaN(value) || Float.isInfinite(value); + } + class ViewHolderItem extends RecyclerView.ViewHolder { DownloadManager.MissionItem item; @@ -667,7 +661,7 @@ public class MissionAdapter extends Adapter { 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 +672,10 @@ public class MissionAdapter extends Adapter { 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 { diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 25960a419..c4fd3b5fd 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -7,16 +7,12 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; -import android.content.res.TypedArray; import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; -import android.support.annotation.AttrRes; -import android.support.annotation.DrawableRes; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -24,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; @@ -192,29 +189,20 @@ public class MissionsFragment extends Fragment { mList.setAdapter(mAdapter); if (mSwitch != null) { - mSwitch.setIcon(getDrawableFromAttribute(mLinear ? R.attr.ic_grid : R.attr.ic_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(); } } - @DrawableRes - private int getDrawableFromAttribute(@AttrRes int attr) { - TypedArray styledAttributes = mContext.getTheme().obtainStyledAttributes(new int[]{attr}); - int resId = styledAttributes.getResourceId(0, 0); - styledAttributes.recycle(); - - if (resId != 0) { - return resId; - } else { - // work-around - styledAttributes = mContext.obtainStyledAttributes(new int[]{attr}); - resId = styledAttributes.getResourceId(0, 0); - styledAttributes.recycle(); - return resId; - } - } - @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); From 21eb98a52ca286d009baa5bfe3f6c572f620069c Mon Sep 17 00:00:00 2001 From: kapodamy Date: Thu, 24 Jan 2019 23:23:30 -0300 Subject: [PATCH 67/68] add null check add checks for null. This happens after rotating the screen while is turned off --- .../java/us/shandian/giga/ui/adapter/MissionAdapter.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 1b5b98145..8fca7c470 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 @@ -159,7 +159,7 @@ public class MissionAdapter extends Adapter { str = R.string.missions_header_pending; } else { str = R.string.missions_header_finished; - mClear.setVisible(true); + if (mClear != null) mClear.setVisible(true); } ((ViewHolderHeader) view).header.setText(str); @@ -437,7 +437,6 @@ public class MissionAdapter extends Adapter { public void clearFinishedDownloads() { mDownloadManager.forgetFinishedDownloads(); applyChanges(); - mClear.setVisible(false); } private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) { @@ -507,7 +506,7 @@ public class MissionAdapter extends Adapter { mIterator.end(); checkEmptyMessageVisibility(); - mClear.setVisible(mIterator.hasFinishedMissions()); + if (mClear != null) mClear.setVisible(mIterator.hasFinishedMissions()); } public void forceUpdate() { From a92f776ebe7506182562fc70981c2ed6966fea2f Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Sun, 27 Jan 2019 16:46:44 +0100 Subject: [PATCH 68/68] fix icon messup --- app/src/main/ic_settings_update_white-web.png | Bin 9830 -> 0 bytes .../drawable-hdpi/ic_settings_update_black.png | Bin 431 -> 0 bytes .../drawable-mdpi/ic_settings_update_black.png | Bin 379 -> 0 bytes .../drawable-xhdpi/ic_settings_update_black.png | Bin 710 -> 0 bytes .../drawable-xxhdpi/ic_settings_update_black.png | Bin 1091 -> 0 bytes .../ic_settings_update_black.png | Bin 2029 -> 0 bytes .../drawable-hdpi/ic_settings_update_black.png | Bin 431 -> 0 bytes .../drawable-mdpi/ic_settings_update_black.png | Bin 379 -> 0 bytes .../drawable-xhdpi/ic_settings_update_black.png | Bin 710 -> 0 bytes .../drawable-xxhdpi/ic_settings_update_black.png | Bin 1091 -> 0 bytes .../ic_settings_update_black.png | Bin 2029 -> 0 bytes 11 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/src/main/ic_settings_update_white-web.png delete mode 100755 app/src/main/res/drawable/drawable-hdpi/ic_settings_update_black.png delete mode 100755 app/src/main/res/drawable/drawable-mdpi/ic_settings_update_black.png delete mode 100755 app/src/main/res/drawable/drawable-xhdpi/ic_settings_update_black.png delete mode 100755 app/src/main/res/drawable/drawable-xxhdpi/ic_settings_update_black.png delete mode 100755 app/src/main/res/drawable/drawable-xxxhdpi/ic_settings_update_black.png delete mode 100755 app/src/main/res/drawable/ic_settings_update_black/drawable-hdpi/ic_settings_update_black.png delete mode 100755 app/src/main/res/drawable/ic_settings_update_black/drawable-mdpi/ic_settings_update_black.png delete mode 100755 app/src/main/res/drawable/ic_settings_update_black/drawable-xhdpi/ic_settings_update_black.png delete mode 100755 app/src/main/res/drawable/ic_settings_update_black/drawable-xxhdpi/ic_settings_update_black.png delete mode 100755 app/src/main/res/drawable/ic_settings_update_black/drawable-xxxhdpi/ic_settings_update_black.png diff --git a/app/src/main/ic_settings_update_white-web.png b/app/src/main/ic_settings_update_white-web.png deleted file mode 100644 index 60383b7901b4d341b2428782840b828ef3c65f9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9830 zcmdsd_g_<46ZZ{WP!J2fXySq{RS}RHR6?hy?%|I_IoU|9zKCpTKQRuD8H%Z}+D#W@!I5L3|0-gZv_;@ZZLmyLz>-3IO-!aOEGDa{EWFp*$lq%|Gz#jwzKSw(gF{RonnlV zYDt;UL6|Ete}G$aa7?@UmN!GV+QU20JB@f02uDKn4H;YY-&YjI%or^sE|>@tuj2(G z>Vxlh2;&p9)jnwffajL+@-FZkNQX8fJk(5}j1AInFw(~IK+Zv?HEj?KgUMUBsWBtG zgJ{WM7Zje#K6OGb^jcRh)UkoDtKxwhm20NO;E*Dr+16Yw29x9r%GC2zKihc7=E#o0 zs>5h0^mAvpJcUKn0YJXpFOU6Z0t|k^Vj7S!wJ(n~2$Ku#%YwM4@{&FJVMY2|6h|OML!UZU!NKehc*dJgk z0V|8EAsj<`_V4NeBE#_GdlG`)`MH}U<#JfRQ$py4ve9-3J$rF2B9;3MG_FN8V@|EG zNs*MoU%9Y`y05%8OI_e;J#>l{ap<^=9v8Wto2P*BqCFx(=qsk*1|fiHIp7p>1IOD4 z;62{3B~BVMJDQ5_9F3{4FdQS^IYXPgn;G5Cbn5JtY%EGpz^$;lZoRV zmPa?Ox#J$Pa|y``ob$X64b>H6PoU9VK;%kFCb>maaQkJ=6+6FA@fTML^AdAaXi z)>+P~iUbFgKknHL=+l%rV&%madQB!poPDkR*zqgLy9mK{qSEMOiB=83Gu>$WczNtf zKX|Mh$r}T4sn1nRG#PkO`NqVmad1jdG2!>uFu-Bfo~__+xy&7G-T!o(H4Q!}Wq9>2rDl@biBZWOKg|c;iqEg4!(}$;{YIRY zw{zqTJT8UG$qzWz4V}uS19M9ep8CDJ%s*ZL=7~awjCqNa`v$~%yNz^AdAdI3ROHG1 zKossnFZOpUY(;PVJQY4BRkyc3VQLa;;3Dr(fX&9s2N3DNeoh$jB_apl!|E|wo61*c^NMIqYE&tt4OH0O}+oLf109LU!!PyQxVo^%)jz<^SlhTcuuc zqkZV>G!tPwe85&1lJ-?3OArNYBBs?H7bZA% z$La{YNtpatfr9U59dv3C-1Nz|qW$g>2h!Xik8*L;pIHBZF_*yL8~0}Zu*c;5jXmY> zA_wu+X#W+&#hkOnu2{Hkw~e^eKOM_ZxG$Vw{FF8JT?Qe2wBn$c!m!X38Z@B1SxK}= zt1l|@(0smmQRP;W(C&#O;%Qnx=mjgzVj&uGZ8^(kY4tO7xAp|m9WWC{zz5RmfOhgh z08@(cSjv>IcDq4*`N6%f=S`rJ^W<(Zr`2@0d>{Mo6~FWJ4-qeACVu1_`PLX3!gXs? z6!+*?e#QBl&ERgBdEY5c#D;@D=x*PDg@%Yphyw~x8Bt@N)gx8KJkQxvxsZ-) zCgp>8ST@CYUN*boc&n?ovVrQK13+8SK?7L4Pyib}dP*<#drp3xW&* zmd~{MV0A1_6kLVE8`S&!u+&dnY%1^qB^eh&AHnHjC#z)o=0erFt4{!}Ex$9+%Dgr( zyAEYVtm00eYe%8PX(rFy%MMuS$pMW_yR;SpX92RDJqLMMWe>D&bm3a+aJS9 z*VWXHn0aC^`JwZNaaOu8*dNqmv>afV)QTL5A}T$wZh79po=EEQ{_&-STy2lA|uk=6dS3g(uvr8 zx1FQ+Rc2<1&L;^?wUfZ%V1<3^4^_}U5L3+bqy8NV-CYrry^X6$(7b<@P<=>igI7@V06};;D(xU&1aN5Rf zfjlSBWx;*6Aa#6s1irg58it1!q?^O5&v~aElu-C8bSRifF$hJ_d>2pdE)^}JzHPkf z-SQuYxDmv&N)s!1pR_oq+9VA>2giR9k2 zNRrL68^@W;)zt$%LO?1}G9I#We=`#^sETi+ft5+$Yjv$P0%f3?a% z+w-jv?--Z}9ce)Eto-uDpE)knjrRxxXYGLtdUf>R6eO>&iUD(Dw#K#Og*9bKDWnQb z{UFT@JI+fQ*J?thVbf)v?Vd*5B^~Sq3Y$<@amMjo$w7$S!wYqfA&JL;jT9AMi~AMf z=V^5gCp<42aYMjeMIP(hii=0XiyFZ}TaNdph?cVcBO%W-t|xmk#&4?Luc&=}5Rxk& zDGUf}0}PjbFW87R_XeLHj-oY@`oX-iz;-LixFuNr%m_TZb@EA6{74v|_5saSwysZ) zDI3D_W`^zN{Y^4f9<&q>RpkG6Fr8X4=CS+is9VOc(4Z0&>ndOs*@bC&Yti5J11nsr z`fF3Py_OUYcA2x1^9_tc?R&yXRo5m9IeYkhQ+@AdJSflaR7`Xky~uiP=b<(m zJLV$WHy^ZUgkGQ}qBD8KBJBIkWUwHMOwQI!#K|+yx+*MJ@x{8Hwwv$WB7m^+=Y7he z?Md_zP8eB_ok@u=VBdigI86;5!nY#wMJq$kY+8QlHEV+}##&S+$<;YR#@45!Jm>qq zVlQ~-ku-|(`z9-fuCkJCdQxI6LIY?)0ZRH-cvsmvG4!4HeKLmPQ9 zuW`1~8RC^j)!TkaS#qXcA5gy``F_?LsV%KTlxD^VcbudL#p$l=%e0eE=`pAqXT8b} zD%dpeLlRCTXBBVRX|Hp*<-^B36|~dbE-yF(Bf=k!2C~eu$=SKYHMly{gGMG1Y&ntz zgWK)5os^ijXn%mk&gs`_QgIv@vi5VxSmBDWdHY|T6v(P}dys>dLR&iaLi9+zYf1K6 zH#$?s^JIY#tu$^wxfJk&cq&lo@HZCAy_?&ZIjl!tSmwI80)qMP?+yEvgP0q?(*XsB zkY_T<7Ef4$X=HsNmpv`b;De-nYjg=Fd|E)q+67x;aa4;*b z55kpVlDC$sLZER^T`4cy-TUxCzXDF7ClN}OuW6;p_jpa z+p4^STb=DnyJ!RByaQ%tiGDOMkOJLwlDP~V1z3MkZ-u_;|BI+&$*jN;NSaW%_PXih zcA5B5-~bSNgyI z0ms~_9`2{SwyTJm{z~G7rrfhRYv1~>Jh4?3zPsX*NgH#4iQL;I^#1+nm!hu6o0u>} zSCs{+!)`9idJ_yAq1tA`%TTlpHm-%r3H0Ly=cnuPI`Of*9Oriqs8kG>h&7Sd2` z4QIT6oXg|R8t~Tv2uDPkN&Vt%ki#8l!bK5ws{6)tsuAAR{kEFAN0pyO&9D40dL-Ct z&-oe0tHCz|=+7OrTZ#@!Ssw3xpr_#lwM$)+XP%~A3OgV`a~&*CG)CxV#2AEn(7Nqi z%WEV^Pv5)4?6lTBuX9g@y?b<>!v5T_c`cjFzQW%-g==`q{seYk-#Pkv$H~I{?@oWI zeZw*fWS#di@k~KXw7+#8&fiD-S{erPLs%+dF4BrXRohs1v%@Eb13l4lk9AT zzU%vPbkPKYu54vi(*-=ev4@uClU4m6s zG}I2sebAPu&YcQz-uq0a%<1H(+NEJEXKt1_+9L4Q3(b5<+V_s&21jAuXuhPk@^>-c zbc9!6wDnpi>ENuC!1Wf3P4zu+p%xT=XE<30Z+ zbREAO-{y8w`sg~jQ~9ePTLIRmmGiB3Q$9u5CpT4V-s0r&$fsnDP!|f@eIrqzf#*a{ zjZ@K^e4ygKkS$raXQSgrRY(aM8rChmBkxcz?YrrStv+QY`YBqqk0OQO3d6NR(Z93c z0{%Oc+CFxMy^Q1~!hDx((kdD%4E6JoFeMF zAT5BhFGXm==)mqpuIP=xEVRk*yqW@tHPmry6gKtWOldrxGO~5%-|9XBLsLoC1}Od{Bt@lI!$pDBlZ5-#uo39 zy(>>nsYZ~j`XYk2=`L^(Hn@7OCTG-H)T(V87gn;ul~?}nfqQZdXAa5atOROA^dvk| zXPdmxkz{6s7gVM4HwQR-bTca&20yl55yx*rPr{J6)1xgzkvw^Y!X5Q#PZ1RlRpiEo z(^Mut9#ZG5P}Mk-L4mIXE7o_VO2+rvY&^VRWPtQLPO2x$KOt{KW{0E*G{Y_fe2+-k za{f29KUG;6>2sWf*`zjx0F@aq1?+Y{Z) z%I77Wrq2;Cm8b&6PotVb8~?95qu$LPX!`|^nsdYu8WXHR@0=5oKvUm)qX?PfnInM% z!43=Jay`aLg6um0gugU{hQW}}Is=S;7AWQHZ~USlu*0u@A1xgh1dWW?sh_KblBjey zza;_vC@_DcFJK9d5#!Rviav^(EsBYBP$-552mS&zMd>ZbMfhUZBHg+ordy( zuZtZqRYSoK%v4)gb)GKUuCZ0f&+qS`%eIHltu&P$gd`thi5vagH1cJ+=su956xNJc zJll*qa%!q>s)`=GPE}KtLZQ84!CRB7443W1uM5cqzmyTuCmdMhR=Rw@GkPF!!YgOk zsD76(+kYf+7d_apT>NuK3OY>>?Uhe`+CcX3bbzz(^-9G_8#ze$SwA$;n(wb$erNRQ zrwpb7oevUOXW;}Rhr4lZNBy3uu91|C3)6402HQZ@ulKU-9@1?&5_`>mowLHq-|+W7 zTXRp>onjQ|wWW$>Zq{YnY#fICR)wDsM)$=8#yI-&pMR53bF7=)%CfB&c3 zI2Liy&~^T?A=+QLMn1Q#R@2HaS5d1#`?Jj@W1G9rG$AJO42ypftA(UB(kI`}M9mA9T=?fL&$wENff6FS zj#c$1F%TXRY3!VW1s(*JVneXa@{#9Ita^WaE5CvsyHVgyQ%;SCKlYN{F_!^(A z+m@;ehagMlfQR}5j_%R*9&d%CItk(L^>F3b-9wK_1ad%J3RvZPCGRiigUk)%#FvOi z{Vol)tSXJC$ftE9KqhSZ>nl@`KP#nfc|Nies+Bk~5b)FiF6JuIouYC%T$qhM!O>;} zcqP`oKs*B7YHC@>o#v3z8G*hU-#WuxX_uxG4)ZzRxif@s8FeAoq9uR*ZSc>ff5zFo zKsSBs9gRZ)PaNP$=i8Qhf`tS9e7|*yo;`;@bhg#z*1Ff*@rnGXmFs+B76-(qNhLjy zJDtSHCh`RaEXpND^-gpi(elB1zYOWvI|wC_D=zZgp{w^WFz9hss87Eb>6gGwB+rpQ z_$w}UMAVZ+s}3xD{>$`_6NCSj z;vR&hiQB}-VF$SVAn(f=r#nxf%(jDUacYp(xZ z;0c##zWZEy(ql+Rw0(S|_W-bqcuyR`f^Se~jAPWrr%u|tLOpA5$}SO6Tt4~YWKS`m z;dP9+*JSvv=aT{vJ|{zfa9(5*|2)|KX*If}0orkM06MxIknR9C{n$cs%dCEK3(QuO zNjssF(8>`&%-cxx%I|dk@&-&y7dO2p*Fkmls%7=xVo1GPz1U+vd~E&DR4X|1NmTXM z5i7iBuy{IE3X=>+LQ zL}fIN8L)m9(kGgEC!0TNgDc2~Pi68QH*sgmu2g;Em*iB0LH#ANpS5nfw!ZGXm5WI8 z;;mPFz!MiuY`%^n*>v4FDd>ojeE1>0&~E|gi61OdmTypAiz}k%3;aRIY4JhmXk)+w z2Y6EoK2A*^W30DcXeH0Lv?Z{VhuN{jZ%d$#NYa zN01R7zd+LB*9CFAUvyY9vf{Yq{0SG4WL#|D0+MhSw|vKU{cJNNe;@OD$lgcG(E@XK zWd!CNX-?G1ZtSlkD0uZPT5}Orb#GI;T*r53W&XC=QdPz%-5yNuf@=$iU47-X64Dcb zj9>eo`7g__yHU*7I6IoT*F9aFKp}kxzoXCf=KDDn3&oMQWL4NoFZj^vQQog|+8CWg zTN=QBxEOK!wxAOAmofq=k4cu`Z(sHo`FQrJzLQ?})m|blm#ROhXHLqF4F;mtp^rv{ z>;f}*K_`6Yck-r=)xoxxdAH*EiGV%*%&|rIb>vOip#>zaxqfJIxxsMSk$C9&fgE&q zpI5Df>6$IhlsH5QGB#LsBpGI{I*=jt8W?H>Xsn_U9oDK&5M$-Qaj#?`e$ zxmho=3q-=*cMQp_wU91Fth{t~dt?13JE~^D-V1NpcWnow`Q?1~+@MW0GQ!t^9~*ef zf&TyKef)7DefYzs*7TY^N_gC_tY6bk$g~1cd2>B6tC2q_n25S7m)8jjpKtZTq*U1y z%)k2&;p|xu#uu!azo5Nt<%xw&$$wIcedXmMf_H#B<$Ofzw!AsTHZGIuo2L8-OF51# zP+b2{@Y?IS!O98PG=1np15obhk8CfYXH@7s!n%6f6z$l(#w3tjVHxO z$V{khCFDpf`-tc0f6($Kk(TNh&>cfViKg+VneXF|u)bC9ghr()JY`7{)8dZtmWn%G zE^(@IXc$i+PbvRp06baj$W!;w@{li0cwykS(ir~2ja*v(}m8v#k1&{t~C$<8<#PZx70Q1Au7rpb5&)1ll zam4?8roku-tAXkDeuY;*oTxLCjB8OpM3Bm-qyqDPezhnLKWwLFk(W}J(2n$;Glxp+ zITSce^^*Z0{3wIN`>0Fi-*wp(soiDu+EavUvOcInfU@o%_I7NdX6QZl+HHK?9w^1e zlFYrnV)zN-lwz))Nl0HcGguru-An|Y?ZsT6{j!${AOCv+9S=NmSYs6^GUw`pPpyrd zz2ANnDvrRq#TWo_+cUET#oU^S;D1zWHYMZOF~2m5>d&aU`zsH*f3mQebfIE7xIg%_ zmEYhU`mWbuMBiiIJLA0ERGY$tt2x-r6<^yZb$JwjW z&-i`p(p@(HD#f`3wU2)gT|rxaF({qHh7tV;Ao!Ng6yBVeW8Eg66YVv-Wh& z5F0Y@!i4F<#Wp+nQH30vadMZ{FS(Xl{FG&QxaqH&p$4Xz9ra<-Cc;Q;^~fQ;JzsKP=^7FMQ|b(e#}impPet4B z89nfDT5#`o7IC|_5M8w0Z4meHH-#hv{fzW*kNK8y_)ihWz>D8S9%|sGsP-{a5y13Z z5LaDyeUUItciVjpqW{LHi?mUMz4l9(etZyJRR2FfzdDqjH;H}TdW7-XzV~)|RoHQr zkL^uh30-z@iF7ev3`k3Li{_zzUs1OyIYbJI4^(W?XPjM?^&I_*??wj@GMM`QqU}!{ zL}Dt3x->#PXS3fd|LL&@*nQv4mZ#EqJoFANtjJ?AH-M1KnSXG@Gt_&$ z3gW^8a=E9$i%X}%T_F0$Pm~X7X`&n#j71q5|4XG@YrAqvl2DKK6RzkH%Uss;GH%=Or1u2CJ$zpYmUS86qdOvQoah96zA zm}jQFSAAS3YiwK7?@SMntLWO{u7@9GE$)YYu5%QU!t3o3GVe>(+w?(Rqt0fFvo*-4 z=j+0RnBG%;q~oCNc?Wwp6e#(-1FtMJh)D+$B&hDoJfPng>S%*S`3LN_ zD&`LKBDXQaKj(_D1~Y8?6(q*7WaRCfQiE-YINPXJs{Ko^6is9d&Ui|4F2X+CvJuSF z9iGqjx?WlBTMeOi`nF9Lc>5$9MxS`3qtArADkldd_#vnVFn%;C(3WCWM|6sglYK%s zT2s!92Jz4p!FbH)X0uKH@NmV~^YkPoKcD0}J?V|K7X8rB5@Vl<%g^Njo8ea*Z+Tku zT{B$tbZ>F?bor2eD5Ap#yyzP8X#ekZkf+MLO|vc981Zmj_?!wjIFXH3Md_da9k=gQ zonaOl*jnJ%9&ViCRc=hf3T%R|&)n}h3K;PxVi8~-Oz-9jl>v^QD^h#UjL{McO~5r+ zGJGcq^0x|x2`6+!nCAB0C%+zKq&mJ0-h-v41R1kCqPUDbK2oGuO*=pab79*S1er z@(Vj=kUEX0M$dN%dAex{8ELQITc&MD9+tnaS(|W3K84L1mKHo6Rf7Ix=N}e5z47nu kgb$eizy8Mw0Q!Wv@Ug?C$yACMQn0pjh)dQLXFL=B4@?nqYybcN diff --git a/app/src/main/res/drawable/drawable-hdpi/ic_settings_update_black.png b/app/src/main/res/drawable/drawable-hdpi/ic_settings_update_black.png deleted file mode 100755 index 1e318eecc2817d0da257bfbfd8b96aabca214f04..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 431 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^TIJBsuwEZMZhtXJ?ObIx*K(VRbg-8F4%|1~c7*Kp%@WaM&h2M(2~V#_+`1@rJ6 zzBe;=)8@QFJ(*{P@8?#(yLp_4%}7&$bxDKB1g0PdE)PZ!$6+_uoQFHq6PVXDxF_&@ zxp(g12k9TCA6oz3Gb_rSurBKv*T%h@xHqg+%6j`&p#D}nU!TJxxd*EgCvN=~|9^L5 zJI~7*@uAAXrc4iiXZlG+Hi>ipxnA>*w@35>TkCse7OMT* z?l*(o^@qy6=&VP27TYhD+?W0!aMkUAp#jURTVkB^4=_ErJjoO6c|=HDn9Eepu;Oc! U)=K@?z!+fgboFyt=akR{04@o<&Hw-a diff --git a/app/src/main/res/drawable/drawable-mdpi/ic_settings_update_black.png b/app/src/main/res/drawable/drawable-mdpi/ic_settings_update_black.png deleted file mode 100755 index 964553137f8d2d1138c49f86c6414fe6af9bbad9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 379 zcmV->0fhdEP)Px$HAzH4R9Fe^l)Fj;Q4ogX-9ig1v9+?Ww6*ay6blPMQi<6348b~Gn&cTQ^8yxv zU~3^-i-9D@>-WPMGAxIH*-hmPe9XTyb7q#|?AB^wv4Pk?Y~a6dV4=hUHaNQrTQE%E zW^b#ExP%Y*NsRCpumUr+Wx99Z6KuG7y-*vLpmJNJ;|?^y27aO66VetxM^|cVbZbH% zY>50F93-SIcZ=SH%It(r-(UmGcijYOi<`eDrk5{m4|K4m-gjMRX`hqUJ-Kowr<~MnFE)QAcAStd=Jl-4 zvb5>P(}LUpef$202Cl diff --git a/app/src/main/res/drawable/drawable-xhdpi/ic_settings_update_black.png b/app/src/main/res/drawable/drawable-xhdpi/ic_settings_update_black.png deleted file mode 100755 index 049edf0d876c5964133ec3e7ed787e284ddf1954..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 710 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-HD>U~2GmaSW+o zd^^oHThvjcP1u)(d$AyQAU7w+v@D&_Yi=$%e>DHGtQBbzV`I4(ub}D7WVTV{;F4~I z8xbp3ENIfun)SR@Hsamf!t}|r-`sm${UEjMe9hd?XO6$SG2<+Y&jFn1pEA>9-6vKj z=eS=`+_HVnf9t30U-A-UHr{$l=@)iu-dIb)r6U-Y)Ji1fCcop7``WO_yZU*lgg?#{XF9se|6`RaYIZQU1~q2c?yLhh;B?5y&B!;*TQ z)|YMe-Fb^mrxZU6?qvTjeay0P+fg+(i&rMeXBTFBod3V+bqH6$Rlko#+h1wS|9aA< zKhE`))#FQ%@eYYsU!77C+F7%*&VPmQg2O9%>&2J(-j-V>w=m>`VZETF-@b_GpZ^w| z&ycw_*a+5(kV_od_{gK=MU2ERK zvi=+Y?dKQm|DEm%zrFMNljxNe{-4$u&)?J?GRL`lC7bor>-M!dv%_lhGbb}^9jTeg z{&i)aV0E30)t1P*lN=!jwH4P!%@s8{;c3?sX?}0p_4P*1Ii*SazHhnuYMD;?)5ACJ z2Qj~0@?ULoYeSKjW5?STruuiM+AF_A=G;9eaL`?(>6GTwj8b)z*q;W+=U&^a&hYoY zy^G?CJ3#_nLB8Q}#V_tZHhI9}Iaf{Qp-??F8bk@_U ze=J_j$#gAIzvG}E@J@Y`-cR|SrQH7MakuvcYx~=n2^%otL=t>V((ikkB%aAm*bYpX N44$rjF6*2UngDR^J5c}t diff --git a/app/src/main/res/drawable/drawable-xxhdpi/ic_settings_update_black.png b/app/src/main/res/drawable/drawable-xxhdpi/ic_settings_update_black.png deleted file mode 100755 index 10ae87baed7fa8ea0d0b78de6bb76f7a65ca5c83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1091 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-HD>VE*gr;uuoF z_;%Lm)xLo;$JduXs_L0?oaOp#BgYlH+3&fqxv;UF>Zxy-wBLcPt5aplB%=ckbF|Hu zXbX2Ws`T?1d^n-J{`~fLXWy2bi>rRW`Mq8G{ojADE`N73d+q&OuQz|YmA<|)+qtb-oso0l4VTvA6GF@Omi3=lIrTw@;f4uD zv1`Aom{tAJ+i9KjOa78;vc%uajF6(3-}X=JSr;E@Y%Gi5-RRxUzxDrU+LvP zf9D<9x&GpOVr*~d$jdVTOsC2G6Wv}sxk2~r-P71}E?3%kUac!-W zdgUUKonKs*>^)f$|Y|M-g?PbeHW6-*jseB zdodf&id@!LPx)o{Uv;>zd-;nmGn0x3)8;iTE4Jr)6vgcP#<{fL=HC0|v7Ca}ObQE& zCQo_!Hs|GI3uO8M&IeQNrWKcE2`mfls?F&pX{?y?Wi$K=qgF8=EJU z)|al%QlGS6jH^?y^~GJ+*WzN7-fx*+&&t;ucLK6UgrSE?L diff --git a/app/src/main/res/drawable/drawable-xxxhdpi/ic_settings_update_black.png b/app/src/main/res/drawable/drawable-xxxhdpi/ic_settings_update_black.png deleted file mode 100755 index 49ac8ea654e4f48096c13cdb32633705825c5981..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2029 zcmc(g{Xf$S8^^yJ+ZqRrh~?p+4doWcLt=7=bL9CXr1G@fg%+j<&tkJBloAJFiiMnt zo!irHlG!kh40jBXeaDG|X-i}>9Oth%ug~Xmy??oWxL(&UpX{H#U6mBj3IG5oxt(_L z-DAez*$3a7oRj#0JwR^wx;g>%JncmQK-9XqobpSAtduQ32tKQsbunS-|3wsJ8mDxegtUoa*Ima^3#>+cyAUe#-l9o=_vzPf1 zHBRW6>R_o&CwrZt6K4)~1v9iovUiogZ(O-pOCsEj3ezISZz3<~_`6?qqHH+Abjevl zv@LuoCp=1uD^V`WYRJ;N*p7`%f!ZF?qh2POj81M^rGHNZ45cJg|(tU-xGQ~iM^45a+( z2d@2+H_5VVyMr!JZ`+-5VPcuU8;UhJ%kZ}dux<=BiT{RUqRiy0haj$D1#Y5dT3UVP z7(?}fI>v|cew5m%o#~nzqjUGTYvxl z*fEbI5bw|YZ(JdocuPh-X5+k{ey)&bUKuubRPO$wh(|-a=89MlF_+ z&5-lVXaE(7WlU7g7Eo6xPYe~amrq6dD}B6eZ;@|=U1gaUWnviWA=KlbrBo|t(xlKg zRA*hW(#!hEl*1+FkGMmo1fQ<(;qE$2@ro|yEx_L)2o;Pm`t4Kb%~+cHO%ZAA7jUvS zR$_CF(o3hv6L&?6Gi$xkqbYKEsRdytL{N`Ya{?n{e0S2s9Qr9GMQC$X$z$3;LLBIh z#koKHv?kX5wyRo1odV+{Wyb9kRdYpD#m@SnvM5kaQ+`<_6rLeAd^SCn+Axso@ceq~ zY}MGxOeBXYtOk>~Tzd4goX(^Akn)}FYaKZfRu89mNXR{jw#xnklnm$k|r|%;)O^hPjoEh}I(Uoe`JX z`Kis8^B+;omszXhk%bR|DT}9(uCe|?=-@9cjVv)k;itBRPopyLg^k8P=T>(W#Dg0j z{@$yfaZp-&!T0Y4?tf59$vU)6;f6#&?R#MYI@lEYv9I=jl?>7OU(xqfRYAk=$`vgM z=J<{`Dme13--96?E;`n`lI6WeO6&0SGfvRdyCf=jml1PLs~)M{(IP&cs~wj)yi%SK zs^Zm8<2AI+PwjMlR&;#nC_U(fYgM+^@gDesxAALcpOiT~mOsmaRnd7XRfNn;PH88g z3$gE9D)lMI>c-wq)|)x;M{Bsu8Ho&Y^@N}6zJWIwMa8u95CdH{6Ur5in*3CIL23Fi z&n!F6WFK|$1*3(hs8hG1s>({adGt@?_5Zy6uirUU=9pDKE@>J@Gnqh%`&z%ys7q}p zy;OR9WC@tp`%<3%W{iOpg3lROhDg8W-HBRy$Ozx&^ZXdvn!3HFas{mX(1e8>*_Bbd z*19xU(3X`(_2CSf_yQ~60w|)m{d_J^*^}G|x-%h5^_xMLB0bvjxW&R?;yl61-3`Dn zH5g`FlNp9_Xxj_R=zJPIFamfou2bQ9O>TK-nE^*Xc%=Got(bQLQ(=>jwcot0eolIoU?Sdr^W?r>SUlPkLg*BH@e85fz+nCz7f}rQ1!+g_uukr zK$s14)mjOc+s`JQwxd${0hIX zKd7%e8>k#Z;gJV#u@}P(m?@EVxU)q4b~!#L!hGLtkA8~zDOHR8a{rizdsvPucn{Go T%FJM|zX7)&yIJBsuwEZMZhtXJ?ObIx*K(VRbg-8F4%|1~c7*Kp%@WaM&h2M(2~V#_+`1@rJ6 zzBe;=)8@QFJ(*{P@8?#(yLp_4%}7&$bxDKB1g0PdE)PZ!$6+_uoQFHq6PVXDxF_&@ zxp(g12k9TCA6oz3Gb_rSurBKv*T%h@xHqg+%6j`&p#D}nU!TJxxd*EgCvN=~|9^L5 zJI~7*@uAAXrc4iiXZlG+Hi>ipxnA>*w@35>TkCse7OMT* z?l*(o^@qy6=&VP27TYhD+?W0!aMkUAp#jURTVkB^4=_ErJjoO6c|=HDn9Eepu;Oc! U)=K@?z!+fgboFyt=akR{04@o<&Hw-a diff --git a/app/src/main/res/drawable/ic_settings_update_black/drawable-mdpi/ic_settings_update_black.png b/app/src/main/res/drawable/ic_settings_update_black/drawable-mdpi/ic_settings_update_black.png deleted file mode 100755 index 964553137f8d2d1138c49f86c6414fe6af9bbad9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 379 zcmV->0fhdEP)Px$HAzH4R9Fe^l)Fj;Q4ogX-9ig1v9+?Ww6*ay6blPMQi<6348b~Gn&cTQ^8yxv zU~3^-i-9D@>-WPMGAxIH*-hmPe9XTyb7q#|?AB^wv4Pk?Y~a6dV4=hUHaNQrTQE%E zW^b#ExP%Y*NsRCpumUr+Wx99Z6KuG7y-*vLpmJNJ;|?^y27aO66VetxM^|cVbZbH% zY>50F93-SIcZ=SH%It(r-(UmGcijYOi<`eDrk5{m4|K4m-gjMRX`hqUJ-Kowr<~MnFE)QAcAStd=Jl-4 zvb5>P(}LUpef$202Cl diff --git a/app/src/main/res/drawable/ic_settings_update_black/drawable-xhdpi/ic_settings_update_black.png b/app/src/main/res/drawable/ic_settings_update_black/drawable-xhdpi/ic_settings_update_black.png deleted file mode 100755 index 049edf0d876c5964133ec3e7ed787e284ddf1954..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 710 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-HD>U~2GmaSW+o zd^^oHThvjcP1u)(d$AyQAU7w+v@D&_Yi=$%e>DHGtQBbzV`I4(ub}D7WVTV{;F4~I z8xbp3ENIfun)SR@Hsamf!t}|r-`sm${UEjMe9hd?XO6$SG2<+Y&jFn1pEA>9-6vKj z=eS=`+_HVnf9t30U-A-UHr{$l=@)iu-dIb)r6U-Y)Ji1fCcop7``WO_yZU*lgg?#{XF9se|6`RaYIZQU1~q2c?yLhh;B?5y&B!;*TQ z)|YMe-Fb^mrxZU6?qvTjeay0P+fg+(i&rMeXBTFBod3V+bqH6$Rlko#+h1wS|9aA< zKhE`))#FQ%@eYYsU!77C+F7%*&VPmQg2O9%>&2J(-j-V>w=m>`VZETF-@b_GpZ^w| z&ycw_*a+5(kV_od_{gK=MU2ERK zvi=+Y?dKQm|DEm%zrFMNljxNe{-4$u&)?J?GRL`lC7bor>-M!dv%_lhGbb}^9jTeg z{&i)aV0E30)t1P*lN=!jwH4P!%@s8{;c3?sX?}0p_4P*1Ii*SazHhnuYMD;?)5ACJ z2Qj~0@?ULoYeSKjW5?STruuiM+AF_A=G;9eaL`?(>6GTwj8b)z*q;W+=U&^a&hYoY zy^G?CJ3#_nLB8Q}#V_tZHhI9}Iaf{Qp-??F8bk@_U ze=J_j$#gAIzvG}E@J@Y`-cR|SrQH7MakuvcYx~=n2^%otL=t>V((ikkB%aAm*bYpX N44$rjF6*2UngDR^J5c}t diff --git a/app/src/main/res/drawable/ic_settings_update_black/drawable-xxhdpi/ic_settings_update_black.png b/app/src/main/res/drawable/ic_settings_update_black/drawable-xxhdpi/ic_settings_update_black.png deleted file mode 100755 index 10ae87baed7fa8ea0d0b78de6bb76f7a65ca5c83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1091 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-HD>VE*gr;uuoF z_;%Lm)xLo;$JduXs_L0?oaOp#BgYlH+3&fqxv;UF>Zxy-wBLcPt5aplB%=ckbF|Hu zXbX2Ws`T?1d^n-J{`~fLXWy2bi>rRW`Mq8G{ojADE`N73d+q&OuQz|YmA<|)+qtb-oso0l4VTvA6GF@Omi3=lIrTw@;f4uD zv1`Aom{tAJ+i9KjOa78;vc%uajF6(3-}X=JSr;E@Y%Gi5-RRxUzxDrU+LvP zf9D<9x&GpOVr*~d$jdVTOsC2G6Wv}sxk2~r-P71}E?3%kUac!-W zdgUUKonKs*>^)f$|Y|M-g?PbeHW6-*jseB zdodf&id@!LPx)o{Uv;>zd-;nmGn0x3)8;iTE4Jr)6vgcP#<{fL=HC0|v7Ca}ObQE& zCQo_!Hs|GI3uO8M&IeQNrWKcE2`mfls?F&pX{?y?Wi$K=qgF8=EJU z)|al%QlGS6jH^?y^~GJ+*WzN7-fx*+&&t;ucLK6UgrSE?L diff --git a/app/src/main/res/drawable/ic_settings_update_black/drawable-xxxhdpi/ic_settings_update_black.png b/app/src/main/res/drawable/ic_settings_update_black/drawable-xxxhdpi/ic_settings_update_black.png deleted file mode 100755 index 49ac8ea654e4f48096c13cdb32633705825c5981..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2029 zcmc(g{Xf$S8^^yJ+ZqRrh~?p+4doWcLt=7=bL9CXr1G@fg%+j<&tkJBloAJFiiMnt zo!irHlG!kh40jBXeaDG|X-i}>9Oth%ug~Xmy??oWxL(&UpX{H#U6mBj3IG5oxt(_L z-DAez*$3a7oRj#0JwR^wx;g>%JncmQK-9XqobpSAtduQ32tKQsbunS-|3wsJ8mDxegtUoa*Ima^3#>+cyAUe#-l9o=_vzPf1 zHBRW6>R_o&CwrZt6K4)~1v9iovUiogZ(O-pOCsEj3ezISZz3<~_`6?qqHH+Abjevl zv@LuoCp=1uD^V`WYRJ;N*p7`%f!ZF?qh2POj81M^rGHNZ45cJg|(tU-xGQ~iM^45a+( z2d@2+H_5VVyMr!JZ`+-5VPcuU8;UhJ%kZ}dux<=BiT{RUqRiy0haj$D1#Y5dT3UVP z7(?}fI>v|cew5m%o#~nzqjUGTYvxl z*fEbI5bw|YZ(JdocuPh-X5+k{ey)&bUKuubRPO$wh(|-a=89MlF_+ z&5-lVXaE(7WlU7g7Eo6xPYe~amrq6dD}B6eZ;@|=U1gaUWnviWA=KlbrBo|t(xlKg zRA*hW(#!hEl*1+FkGMmo1fQ<(;qE$2@ro|yEx_L)2o;Pm`t4Kb%~+cHO%ZAA7jUvS zR$_CF(o3hv6L&?6Gi$xkqbYKEsRdytL{N`Ya{?n{e0S2s9Qr9GMQC$X$z$3;LLBIh z#koKHv?kX5wyRo1odV+{Wyb9kRdYpD#m@SnvM5kaQ+`<_6rLeAd^SCn+Axso@ceq~ zY}MGxOeBXYtOk>~Tzd4goX(^Akn)}FYaKZfRu89mNXR{jw#xnklnm$k|r|%;)O^hPjoEh}I(Uoe`JX z`Kis8^B+;omszXhk%bR|DT}9(uCe|?=-@9cjVv)k;itBRPopyLg^k8P=T>(W#Dg0j z{@$yfaZp-&!T0Y4?tf59$vU)6;f6#&?R#MYI@lEYv9I=jl?>7OU(xqfRYAk=$`vgM z=J<{`Dme13--96?E;`n`lI6WeO6&0SGfvRdyCf=jml1PLs~)M{(IP&cs~wj)yi%SK zs^Zm8<2AI+PwjMlR&;#nC_U(fYgM+^@gDesxAALcpOiT~mOsmaRnd7XRfNn;PH88g z3$gE9D)lMI>c-wq)|)x;M{Bsu8Ho&Y^@N}6zJWIwMa8u95CdH{6Ur5in*3CIL23Fi z&n!F6WFK|$1*3(hs8hG1s>({adGt@?_5Zy6uirUU=9pDKE@>J@Gnqh%`&z%ys7q}p zy;OR9WC@tp`%<3%W{iOpg3lROhDg8W-HBRy$Ozx&^ZXdvn!3HFas{mX(1e8>*_Bbd z*19xU(3X`(_2CSf_yQ~60w|)m{d_J^*^}G|x-%h5^_xMLB0bvjxW&R?;yl61-3`Dn zH5g`FlNp9_Xxj_R=zJPIFamfou2bQ9O>TK-nE^*Xc%=Got(bQLQ(=>jwcot0eolIoU?Sdr^W?r>SUlPkLg*BH@e85fz+nCz7f}rQ1!+g_uukr zK$s14)mjOc+s`JQwxd${0hIX zKd7%e8>k#Z;gJV#u@}P(m?@EVxU)q4b~!#L!hGLtkA8~zDOHR8a{rizdsvPucn{Go T%FJM|zX7)&y