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/README.md b/README.md index 15ba3d04b..6c1aa3d4b 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,13 @@


-

ScreenshotsDescriptionFeaturesContributionDonateLicense

+

ScreenshotsDescriptionFeaturesUpdatesContributionDonateLicense

WebsiteBlogPress


-WARNING: PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS. +WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY. + +PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS. ## Screenshots @@ -73,6 +75,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. 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. + +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 +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! diff --git a/app/build.gradle b/app/build.gradle index f88a43ffc..41c397d64 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,18 +8,20 @@ android { applicationId "org.schabi.newpipe" minSdkVersion 19 targetSdkVersion 28 - versionCode 69 - versionName "0.14.2" + versionCode 720 + versionName "0.16.0" 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 debuggable true @@ -33,6 +35,7 @@ android { // but continue the build even when errors are found: abortOnError false } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -54,7 +57,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.yausername:NewPipeExtractor:b1a77fa' + implementation 'com.github.yausername:NewPipeExtractor:c220700' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' diff --git a/app/src/androidTest/java/org/schabi/newpipe/ApplicationTest.java b/app/src/androidTest/java/org/schabi/newpipe/ApplicationTest.java deleted file mode 100644 index d5b0b7087..000000000 --- a/app/src/androidTest/java/org/schabi/newpipe/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.schabi.newpipe; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1bc205f33..4cd8e83f9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,12 +35,6 @@ - - @@ -119,7 +113,6 @@ - + + + + + + + + + + + + + @@ -210,6 +216,29 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 314c95c8d..3ac2d5014 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; @@ -65,6 +66,7 @@ import io.reactivex.plugins.RxJavaPlugins; public class App extends Application { protected static final String TAG = App.class.toString(); private RefWatcher refWatcher; + private static App app; @SuppressWarnings("unchecked") private static final Class[] @@ -88,6 +90,8 @@ public class App extends Application { } refWatcher = installLeakCanary(); + app = this; + // Initialize settings first because others inits can use its values SettingsActivity.initSettings(this); @@ -100,6 +104,9 @@ public class App extends Application { ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50)); configureRxJavaErrorHandler(); + + // Check for new version + new CheckForNewAppVersionTask().execute(); } protected Downloader getDownloader() { @@ -211,6 +218,31 @@ public class App extends Application { NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mNotificationManager.createNotificationChannel(mChannel); + + setUpUpdateNotificationChannel(importance); + } + + /** + * Set up notification channel for app update. + * @param importance + */ + @TargetApi(Build.VERSION_CODES.O) + private void setUpUpdateNotificationChannel(int importance) { + + final String appUpdateId + = getString(R.string.app_update_notification_channel_id); + final CharSequence appUpdateName + = getString(R.string.app_update_notification_channel_name); + final String appUpdateDescription + = getString(R.string.app_update_notification_channel_description); + + NotificationChannel appUpdateChannel + = new NotificationChannel(appUpdateId, appUpdateName, importance); + appUpdateChannel.setDescription(appUpdateDescription); + + NotificationManager appUpdateNotificationManager + = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + appUpdateNotificationManager.createNotificationChannel(appUpdateChannel); } @Nullable @@ -226,4 +258,8 @@ public class App extends Application { protected boolean isDisposedRxExceptionsReported() { return false; } + + public static App getApp() { + return app; + } } diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java new file mode 100644 index 000000000..6a6d1b9c2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -0,0 +1,242 @@ +package org.schabi.newpipe; + +import android.app.Application; +import android.app.PendingIntent; +import android.content.Context; +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.ConnectivityManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; + +import org.json.JSONException; +import org.json.JSONObject; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.concurrent.TimeUnit; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** + * AsyncTask to check if there is a newer version of the NewPipe github apk available or not. + * If there is a newer version we show a notification, informing the user. On tapping + * the notification, the user will be directed to the download link. + */ +public class CheckForNewAppVersionTask extends AsyncTask { + + private static final Application app = App.getApp(); + private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"; + private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json"; + private static final int timeoutPeriod = 30; + + private SharedPreferences mPrefs; + private OkHttpClient client; + + @Override + protected void onPreExecute() { + + mPrefs = PreferenceManager.getDefaultSharedPreferences(app); + + // Check if user has enabled/ disabled update checking + // and if the current apk is a github one or not. + if (!mPrefs.getBoolean(app.getString(R.string.update_app_key), true) + || !isGithubApk()) { + this.cancel(true); + } + } + + @Override + protected String doInBackground(Void... voids) { + + if(isCancelled() || !isConnected()) return null; + + // Make a network request to get latest NewPipe data. + if (client == null) { + + client = new OkHttpClient + .Builder() + .readTimeout(timeoutPeriod, TimeUnit.SECONDS) + .build(); + } + + Request request = new Request.Builder() + .url(newPipeApiUrl) + .build(); + + try { + Response response = client.newCall(request).execute(); + return response.body().string(); + } catch (IOException ex) { + ErrorActivity.reportError(app, ex, null, null, + ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", + "app update API fail", R.string.app_ui_crash)); + } + + return null; + } + + @Override + protected void onPostExecute(String response) { + + // Parse the json from the response. + if (response != null) { + + try { + JSONObject mainObject = new JSONObject(response); + JSONObject flavoursObject = mainObject.getJSONObject("flavors"); + JSONObject githubObject = flavoursObject.getJSONObject("github"); + JSONObject githubStableObject = githubObject.getJSONObject("stable"); + + String versionName = githubStableObject.getString("version"); + String versionCode = githubStableObject.getString("version_code"); + String apkLocationUrl = githubStableObject.getString("apk"); + + compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode); + + } catch (JSONException ex) { + ErrorActivity.reportError(app, ex, null, null, + ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", + "could not parse app update JSON data", R.string.app_ui_crash)); + } + } + } + + /** + * Method to compare the current and latest available app version. + * If a newer version is available, we show the update notification. + * @param versionName + * @param apkLocationUrl + */ + private void compareAppVersionAndShowNotification(String versionName, + String apkLocationUrl, + String versionCode) { + + int NOTIFICATION_ID = 2000; + + if (BuildConfig.VERSION_CODE < Integer.valueOf(versionCode)) { + + // A pending intent to open the apk location url in the browser. + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl)); + PendingIntent pendingIntent + = PendingIntent.getActivity(app, 0, intent, 0); + + NotificationCompat.Builder notificationBuilder = new NotificationCompat + .Builder(app, app.getString(R.string.app_update_notification_channel_id)) + .setSmallIcon(R.drawable.ic_newpipe_update) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setContentTitle(app.getString(R.string.app_update_notification_content_title)) + .setContentText(app.getString(R.string.app_update_notification_content_text) + + " " + versionName); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(app); + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } + } + + /** + * Method to get the apk's SHA1 key. + * https://stackoverflow.com/questions/9293019/get-certificate-fingerprint-from-android-app#22506133 + */ + private static String getCertificateSHA1Fingerprint() { + + PackageManager pm = app.getPackageManager(); + String packageName = app.getPackageName(); + int flags = PackageManager.GET_SIGNATURES; + PackageInfo packageInfo = null; + + try { + packageInfo = pm.getPackageInfo(packageName, flags); + } catch (PackageManager.NameNotFoundException ex) { + ErrorActivity.reportError(app, ex, null, null, + ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", + "Could not find package info", R.string.app_ui_crash)); + } + + Signature[] signatures = packageInfo.signatures; + byte[] cert = signatures[0].toByteArray(); + InputStream input = new ByteArrayInputStream(cert); + + CertificateFactory cf = null; + X509Certificate c = null; + + try { + cf = CertificateFactory.getInstance("X509"); + c = (X509Certificate) cf.generateCertificate(input); + } catch (CertificateException ex) { + ErrorActivity.reportError(app, ex, null, null, + ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", + "Certificate error", R.string.app_ui_crash)); + } + + String hexString = null; + + try { + MessageDigest md = MessageDigest.getInstance("SHA1"); + byte[] publicKey = md.digest(c.getEncoded()); + hexString = byte2HexFormatted(publicKey); + } catch (NoSuchAlgorithmException ex1) { + ErrorActivity.reportError(app, ex1, null, null, + ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", + "Could not retrieve SHA1 key", R.string.app_ui_crash)); + } catch (CertificateEncodingException ex2) { + ErrorActivity.reportError(app, ex2, null, null, + ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", + "Could not retrieve SHA1 key", R.string.app_ui_crash)); + } + + return hexString; + } + + private static String byte2HexFormatted(byte[] arr) { + + StringBuilder str = new StringBuilder(arr.length * 2); + + for (int i = 0; i < arr.length; i++) { + String h = Integer.toHexString(arr[i]); + int l = h.length(); + if (l == 1) h = "0" + h; + if (l > 2) h = h.substring(l - 2, l); + str.append(h.toUpperCase()); + if (i < (arr.length - 1)) str.append(':'); + } + return str.toString(); + } + + public static boolean isGithubApk() { + + return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1); + } + + private boolean isConnected() { + + ConnectivityManager cm = + (ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE); + return cm.getActiveNetworkInfo() != null + && cm.getActiveNetworkInfo().isConnected(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 729bb2bd4..cdb948436 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -50,7 +50,6 @@ import android.widget.ImageView; import android.widget.TextView; import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.fragments.BackPressable; @@ -303,6 +302,7 @@ public class MainActivity extends AppCompatActivity { drawerItems.getMenu() .add(R.id.menu_services_group, s.getServiceId(), ORDER, title) .setIcon(ServiceHelper.getIcon(s.getServiceId())); + } drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); } @@ -367,6 +367,7 @@ public class MainActivity extends AppCompatActivity { String selectedServiceName = NewPipe.getService( ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName(); headerServiceView.setText(selectedServiceName); + headerServiceView.post(() -> headerServiceView.setSelected(true)); } catch (Exception e) { ErrorActivity.reportUiError(this, e); } diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index b8941670f..f040dc8b4 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -36,12 +36,12 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -81,10 +81,13 @@ public class RouterActivity extends AppCompatActivity { protected int selectedPreviously = -1; protected String currentUrl; + protected boolean internalRoute = false; protected final CompositeDisposable disposables = new CompositeDisposable(); private boolean selectionIsDownload = false; + public static final String internalRouteKey = "internalRoute"; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -94,11 +97,13 @@ public class RouterActivity extends AppCompatActivity { currentUrl = getUrl(getIntent()); if (TextUtils.isEmpty(currentUrl)) { - Toast.makeText(this, R.string.invalid_url_toast, Toast.LENGTH_LONG).show(); + handleText(); finish(); } } + internalRoute = getIntent().getBooleanExtra(internalRouteKey, false); + setTheme(ThemeHelper.isLightThemeSelected(this) ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); } @@ -112,7 +117,7 @@ public class RouterActivity extends AppCompatActivity { @Override protected void onStart() { super.onStart(); - + handleUrl(currentUrl); } @@ -353,6 +358,15 @@ public class RouterActivity extends AppCompatActivity { positiveButton.setEnabled(state); } + private void handleText(){ + String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT); + int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0); + Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + NavigationHelper.openSearch(getThemeWrapperContext(),serviceId,searchString); + } + private void handleChoice(final String selectedChoiceKey) { final List validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list)); if (validChoicesList.contains(selectedChoiceKey)) { @@ -383,8 +397,10 @@ public class RouterActivity extends AppCompatActivity { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(intent -> { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + if(!internalRoute){ + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + } startActivity(intent); finish(); 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 4f98f7f28..ec6d42b29 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -457,7 +457,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck break; case R.id.subtitle_button: stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); - location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video go together + location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together kind = 's'; break; default: @@ -477,7 +477,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck final String finalFileName = fileName; DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> { - // should be safe run the following code without "getActivity().runOnUiThread()" if (listed) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.download_dialog_title) @@ -511,11 +510,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (secondaryStream != null) { secondaryStreamUrl = secondaryStream.getStream().getUrl(); - psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; + psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; psArgs = null; long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream); - // set nearLength, only, if both sizes are fetched or known. this probably does not work on weak internet connections + // set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) { nearLength = secondaryStream.getSizeInBytes() + videoSize; } 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/fragments/EmptyFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java new file mode 100644 index 000000000..0666667d6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java @@ -0,0 +1,17 @@ +package org.schabi.newpipe.fragments; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.schabi.newpipe.BaseFragment; +import org.schabi.newpipe.R; + +public class EmptyFragment extends BaseFragment { + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_empty, container, false); + } +} 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 c007789e5..8c87c1875 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 @@ -52,6 +52,7 @@ import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; @@ -64,6 +65,7 @@ import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.fragments.EmptyFragment; import org.schabi.newpipe.fragments.list.comments.CommentsFragment; import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment; import org.schabi.newpipe.info_list.InfoItemDialog; @@ -100,6 +102,7 @@ import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; import static org.schabi.newpipe.util.AnimationUtils.animateView; public class VideoDetailFragment @@ -177,6 +180,7 @@ public class VideoDetailFragment private static final String COMMENTS_TAB_TAG = "COMMENTS"; private static final String RELATED_TAB_TAG = "NEXT VIDEO"; + private static final String EMPTY_TAB_TAG = "EMPTY TAB"; private AppBarLayout appBarLayout; private ViewPager viewPager; @@ -365,7 +369,8 @@ public class VideoDetailFragment } break; case R.id.detail_controls_download: - if (PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { + if (PermissionHelper.checkStoragePermissions(activity, + PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { this.openDownloadDialog(); } break; @@ -623,13 +628,13 @@ public class VideoDetailFragment switch (id) { case R.id.menu_item_share: { if (currentInfo != null) { - shareUrl(currentInfo.getName(), currentInfo.getUrl()); + shareUrl(currentInfo.getName(), currentInfo.getOriginalUrl()); } return true; } case R.id.menu_item_openInBrowser: { if (currentInfo != null) { - openUrlInBrowser(currentInfo.getUrl()); + openUrlInBrowser(currentInfo.getOriginalUrl()); } return true; } @@ -667,10 +672,16 @@ public class VideoDetailFragment boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(activity.getString(R.string.use_external_video_player_key), false); - sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false); + sortedVideoStreams = ListHelper.getSortedStreamVideosList( + activity, + info.getVideoStreams(), + info.getVideoOnlyStreams(), + false); selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams); - final StreamItemAdapter 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() { @@ -695,17 +706,17 @@ public class VideoDetailFragment */ protected final LinkedList stack = new LinkedList<>(); - public void clearHistory() { - stack.clear(); - } - public void pushToStack(int serviceId, String videoUrl, String name) { if (DEBUG) { - Log.d(TAG, "pushToStack() called with: serviceId = [" + serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "]"); + Log.d(TAG, "pushToStack() called with: serviceId = [" + + serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "]"); } - if (stack.size() > 0 && stack.peek().getServiceId() == serviceId && stack.peek().getUrl().equals(videoUrl)) { - Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" + serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]"); + if (stack.size() > 0 + && stack.peek().getServiceId() == serviceId + && stack.peek().getUrl().equals(videoUrl)) { + Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" + + serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]"); return; } else { Log.d(TAG, "pushToStack() wasn't equal"); @@ -736,7 +747,11 @@ public class VideoDetailFragment // Get stack item from the new top StackItem peek = stack.peek(); - selectAndLoadVideo(peek.getServiceId(), peek.getUrl(), !TextUtils.isEmpty(peek.getTitle()) ? peek.getTitle() : ""); + selectAndLoadVideo(peek.getServiceId(), + peek.getUrl(), + !TextUtils.isEmpty(peek.getTitle()) + ? peek.getTitle() + : ""); return true; } @@ -756,10 +771,10 @@ public class VideoDetailFragment } public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) { - if (DEBUG) - Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + info + "], scrollToTop = [" + scrollToTop + "]"); + if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + + info + "], scrollToTop = [" + scrollToTop + "]"); - setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName()); + setInitialData(info.getServiceId(), info.getUrl(), info.getName()); pushToStack(serviceId, url, name); showLoading(); initTabs(); @@ -811,6 +826,10 @@ public class VideoDetailFragment pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG); } + if(pageAdapter.getCount() == 0){ + pageAdapter.addFragment(new EmptyFragment(), EMPTY_TAB_TAG); + } + pageAdapter.notifyDataSetUpdate(); if(pageAdapter.getCount() < 2){ @@ -822,7 +841,10 @@ public class VideoDetailFragment private boolean shouldShowComments() { try { - return showComments && NewPipe.getService(serviceId).isCommentsSupported(); + return showComments && NewPipe.getService(serviceId) + .getServiceInfo() + .getMediaCapabilities() + .contains(COMMENTS); } catch (ExtractionException e) { return false; } @@ -1038,7 +1060,7 @@ public class VideoDetailFragment } } - pushToStack(serviceId, url, name); + //pushToStack(serviceId, url, name); animateView(thumbnailPlayButton, true, 200); videoTitleTextView.setText(name); @@ -1089,11 +1111,13 @@ public class VideoDetailFragment if (info.getDuration() > 0) { detailDurationView.setText(Localization.getDurationString(info.getDuration())); - detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.duration_background_color)); + detailDurationView.setBackgroundColor( + ContextCompat.getColor(activity, R.color.duration_background_color)); animateView(detailDurationView, true, 100); } else if (info.getStreamType() == StreamType.LIVE_STREAM) { detailDurationView.setText(R.string.duration_live); - detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.live_duration_background_color)); + detailDurationView.setBackgroundColor( + ContextCompat.getColor(activity, R.color.live_duration_background_color)); animateView(detailDurationView, true, 100); } else { detailDurationView.setVisibility(View.GONE); @@ -1150,20 +1174,28 @@ public class VideoDetailFragment public void openDownloadDialog() { - try { - DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); - downloadDialog.setVideoStreams(sortedVideoStreams); - downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); - downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); - downloadDialog.setSubtitleStreams(currentInfo.getSubtitles()); + try { + DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); + downloadDialog.setVideoStreams(sortedVideoStreams); + downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); + downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); + downloadDialog.setSubtitleStreams(currentInfo.getSubtitles()); - downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); - } catch (Exception e) { - Toast.makeText(activity, - R.string.could_not_setup_download_menu, - Toast.LENGTH_LONG).show(); - e.printStackTrace(); - } + downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); + } catch (Exception e) { + ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, + ServiceList.all() + .get(currentInfo + .getServiceId()) + .getServiceInfo() + .getName(), "", + R.string.could_not_setup_download_menu); + + ErrorActivity.reportError(getActivity(), + e, + getActivity().getClass(), + getActivity().findViewById(android.R.id.content), info); + } } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 6a3b3eb50..d9bfe842f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -31,6 +31,7 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; @@ -233,10 +234,10 @@ public class ChannelFragment extends BaseListInfoFragment { openRssFeed(); break; case R.id.menu_item_openInBrowser: - openUrlInBrowser(url); + openUrlInBrowser(currentInfo.getOriginalUrl()); break; case R.id.menu_item_share: - shareUrl(name, url); + shareUrl(name, currentInfo.getOriginalUrl()); break; default: return super.onOptionsItemSelected(item); @@ -487,12 +488,16 @@ public class ChannelFragment extends BaseListInfoFragment { protected boolean onError(Throwable exception) { if (super.onError(exception)) return true; - int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, - UserAction.REQUESTED_CHANNEL, - NewPipe.getNameOfService(serviceId), - url, - errorId); + if(exception instanceof ContentNotAvailableException){ + showError(getString(R.string.content_not_available), false); + }else{ + int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; + onUnrecoverableError(exception, + UserAction.REQUESTED_CHANNEL, + NewPipe.getNameOfService(serviceId), + url, + errorId); + } return true; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 0019a3819..d552b4e66 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -305,6 +305,16 @@ public class PlaylistFragment extends BaseListInfoFragment { NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); headerBackgroundButton.setOnClickListener(view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + + headerPopupButton.setOnLongClickListener(view -> { + NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue()); + return true; + }); + + headerBackgroundButton.setOnLongClickListener(view -> { + NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue()); + return true; + }); } private PlayQueue getPlayQueue() { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 2833abb8d..a3b01f251 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -12,6 +12,7 @@ import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.TooltipCompat; +import android.support.v7.widget.helper.ItemTouchHelper; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; @@ -39,15 +40,15 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.search.SearchExtractor; import org.schabi.newpipe.extractor.search.SearchInfo; +import org.schabi.newpipe.util.FireTvUtils; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; -import org.schabi.newpipe.util.LayoutManagerSmoothScroller; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ServiceHelper; @@ -72,8 +73,8 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; +import static android.support.v7.widget.helper.ItemTouchHelper.Callback.makeMovementFlags; import static java.util.Arrays.asList; - import static org.schabi.newpipe.util.AnimationUtils.animateView; public class SearchFragment @@ -104,8 +105,13 @@ public class SearchFragment // this three represet the current search query @State protected String searchString; + + /** + * No content filter should add like contentfilter = all + * be aware of this when implementing an extractor. + */ @State - protected String[] contentFilter; + protected String[] contentFilter = new String[0]; @State protected String sortFilter; @@ -292,7 +298,23 @@ public class SearchFragment suggestionsPanel = rootView.findViewById(R.id.suggestions_panel); suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list); suggestionsRecyclerView.setAdapter(suggestionListAdapter); - suggestionsRecyclerView.setLayoutManager(new LayoutManagerSmoothScroller(activity)); + new ItemTouchHelper(new ItemTouchHelper.Callback() { + @Override + public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + return getSuggestionMovementFlags(recyclerView, viewHolder); + } + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder viewHolder1) { + return false; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) { + onSuggestionItemSwiped(viewHolder, i); + } + }).attachToRecyclerView(suggestionsRecyclerView); searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container); searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text); @@ -335,7 +357,7 @@ public class SearchFragment || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) { search(!TextUtils.isEmpty(searchString) ? searchString - : searchEditText.getText().toString(), new String[0], ""); + : searchEditText.getText().toString(), this.contentFilter, ""); } else { if (searchEditText != null) { searchEditText.setText(""); @@ -449,6 +471,9 @@ public class SearchFragment if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { showSuggestionsPanel(); } + if(FireTvUtils.isFireTv()){ + showKeyboardSearch(); + } }); searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> { @@ -499,7 +524,9 @@ public class SearchFragment if (DEBUG) { Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]"); } - if (event != null + if(actionId == EditorInfo.IME_ACTION_PREVIOUS){ + hideKeyboardSearch(); + } else if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { search(searchEditText.getText().toString(), new String[0], ""); @@ -541,7 +568,7 @@ public class SearchFragment if (searchEditText.requestFocus()) { InputMethodManager imm = (InputMethodManager) activity.getSystemService( Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT); + imm.showSoftInput(searchEditText, InputMethodManager.SHOW_FORCED); } } @@ -551,8 +578,7 @@ public class SearchFragment InputMethodManager imm = (InputMethodManager) activity.getSystemService( Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), - InputMethodManager.HIDE_NOT_ALWAYS); + imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN); searchEditText.clearFocus(); } @@ -736,6 +762,7 @@ public class SearchFragment @Override protected void loadMoreItems() { + if(nextPageUrl == null || nextPageUrl.isEmpty()) return; isLoading.set(true); showListFooter(true); if (searchDisposable != null) searchDisposable.dispose(); @@ -890,4 +917,28 @@ public class SearchFragment return true; } + + /*////////////////////////////////////////////////////////////////////////// + // Suggestion item touch helper + //////////////////////////////////////////////////////////////////////////*/ + + public int getSuggestionMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + final int position = viewHolder.getAdapterPosition(); + final SuggestionItem item = suggestionListAdapter.getItem(position); + return item.fromHistory ? makeMovementFlags(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0; + } + + public void onSuggestionItemSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) { + final int position = viewHolder.getAdapterPosition(); + final String query = suggestionListAdapter.getItem(position).query; + final Disposable onDelete = historyRecordManager.deleteSearchHistory(query) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + howManyDeleted -> suggestionPublisher + .onNext(searchEditText.getText().toString()), + throwable -> showSnackBarError(throwable, + UserAction.DELETE_FROM_HISTORY, "none", + "Deleting item failed", R.string.general_error)); + disposables.add(onDelete); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java index 7b5f72c53..3f4e9af0b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java @@ -75,7 +75,7 @@ public class SuggestionListAdapter extends RecyclerView.Adapter dialog.dismiss())) + .setPositiveButton(R.string.delete, ((dialog, which) -> { + final Disposable onDelete = recordManager.deleteWholeStreamHistory() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + howManyDeleted -> Toast.makeText(getContext(), + R.string.view_history_deleted, + Toast.LENGTH_SHORT).show(), + throwable -> ErrorActivity.reportError(getContext(), + throwable, + SettingsActivity.class, null, + ErrorActivity.ErrorInfo.make( + UserAction.DELETE_FROM_HISTORY, + "none", + "Delete view history", + R.string.general_error))); + + final Disposable onClearOrphans = recordManager.removeOrphanedRecords() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + howManyDeleted -> {}, + throwable -> ErrorActivity.reportError(getContext(), + throwable, + SettingsActivity.class, null, + ErrorActivity.ErrorInfo.make( + UserAction.DELETE_FROM_HISTORY, + "none", + "Delete search history", + R.string.general_error))); + disposables.add(onClearOrphans); + disposables.add(onDelete); + })) + .create() + .show(); + break; + default: + return super.onOptionsItemSelected(item); + } + return true; + } + /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle - Loading /////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java index f9e2d9583..7e80264e6 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java @@ -147,11 +147,16 @@ public class SubscriptionService { } private boolean isSubscriptionUpToDate(final ChannelInfo info, final SubscriptionEntity entity) { - return info.getUrl().equals(entity.getUrl()) && + return equalsAndNotNull(info.getUrl(), entity.getUrl()) && info.getServiceId() == entity.getServiceId() && info.getName().equals(entity.getName()) && - info.getAvatarUrl().equals(entity.getAvatarUrl()) && - info.getDescription().equals(entity.getDescription()) && + equalsAndNotNull(info.getAvatarUrl(), entity.getAvatarUrl()) && + equalsAndNotNull(info.getDescription(), entity.getDescription()) && info.getSubscriberCount() == entity.getSubscriberCount(); } + + private boolean equalsAndNotNull(final Object o1, final Object o2) { + return (o1 != null && o2 != null) + && o1.equals(o2); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/AudioServiceLeakFix.java b/app/src/main/java/org/schabi/newpipe/player/AudioServiceLeakFix.java new file mode 100644 index 000000000..9f0c849f5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/AudioServiceLeakFix.java @@ -0,0 +1,30 @@ +package org.schabi.newpipe.player; + +import android.content.Context; +import android.content.ContextWrapper; + +/** + * Fixes a leak caused by AudioManager using an Activity context. + * Tracked at https://android-review.googlesource.com/#/c/140481/1 and + * https://github.com/square/leakcanary/issues/205 + * Source: + * https://gist.github.com/jankovd/891d96f476f7a9ce24e2 + */ +public class AudioServiceLeakFix extends ContextWrapper { + + AudioServiceLeakFix(Context base) { + super(base); + } + + public static ContextWrapper preventLeakOf(Context base) { + return new AudioServiceLeakFix(base); + } + + @Override + public Object getSystemService(String name) { + if (Context.AUDIO_SERVICE.equals(name)) { + return getApplicationContext().getSystemService(name); + } + return super.getSystemService(name); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 91faa1014..3989581fd 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -130,6 +130,11 @@ public final class BackgroundPlayer extends Service { onClose(); } + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); + } + @Override public IBinder onBind(Intent intent) { return mBinder; @@ -270,6 +275,8 @@ public final class BackgroundPlayer extends Service { protected class BasePlayerImpl extends BasePlayer { @NonNull final private AudioPlaybackResolver resolver; + private int cachedDuration; + private String cachedDurationString; BasePlayerImpl(Context context) { super(context); @@ -344,10 +351,14 @@ public final class BackgroundPlayer extends Service { if (!shouldUpdateOnProgress) return; resetNotification(); - if(Build.VERSION.SDK_INT >= 26 /*Oreo*/) updateNotificationThumbnail(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) updateNotificationThumbnail(); if (bigNotRemoteView != null) { + if(cachedDuration != duration) { + cachedDuration = duration; + cachedDurationString = getTimeString(duration); + } bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); - bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration)); + bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + cachedDurationString); } if (notRemoteView != null) { notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); 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 f4fea5165..5dd931b54 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -241,6 +241,11 @@ public final class MainVideoPlayer extends AppCompatActivity isBackPressed = false; } + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase)); + } + /*////////////////////////////////////////////////////////////////////////// // State Saving //////////////////////////////////////////////////////////////////////////*/ 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 f5c731ed9..7578c444c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -181,6 +181,11 @@ public final class PopupVideoPlayer extends Service { closePopup(); } + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); + } + @Override public IBinder onBind(Intent intent) { return mBinder; @@ -626,6 +631,7 @@ public final class PopupVideoPlayer extends Service { @Override public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { super.onLoadingComplete(imageUri, view, loadedImage); + if (playerImpl == null) return; // rebuild notification here since remote view does not release bitmaps, // causing memory leaks resetNotification(); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 46d20c7e1..f148aed27 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -131,7 +131,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, private void onAudioFocusLossCanDuck() { Log.d(TAG, "onAudioFocusLossCanDuck() called"); // Set the volume to 1/10 on ducking - animateAudio(player.getVolume(), DUCK_AUDIO_TO); + player.setVolume(DUCK_AUDIO_TO); } private void animateAudio(final float from, final float to) { 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 e1960247e..7752295d7 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 @@ -70,10 +70,10 @@ public class PlayerHelper { //////////////////////////////////////////////////////////////////////////// public static String getTimeString(int milliSeconds) { - long seconds = (milliSeconds % 60000L) / 1000L; - long minutes = (milliSeconds % 3600000L) / 60000L; - long hours = (milliSeconds % 86400000L) / 3600000L; - long days = (milliSeconds % (86400000L * 7L)) / 86400000L; + int seconds = (milliSeconds % 60000) / 1000; + int minutes = (milliSeconds % 3600000) / 60000; + int hours = (milliSeconds % 86400000) / 3600000; + int days = (milliSeconds % (86400000 * 7)) / 86400000; stringBuilder.setLength(0); return days > 0 ? stringFormatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString() diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java index 00604b236..657fe1327 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java @@ -1,7 +1,9 @@ package org.schabi.newpipe.player.playback; import android.net.Uri; +import android.os.Bundle; import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.MediaMetadataCompat; import org.schabi.newpipe.player.BasePlayer; import org.schabi.newpipe.player.mediasession.MediaSessionCallback; @@ -54,6 +56,12 @@ public class BasePlayerMediaSession implements MediaSessionCallback { .setTitle(item.getTitle()) .setSubtitle(item.getUploader()); + // set additional metadata for A2DP/AVRCP + Bundle additionalMetadata = new Bundle(); + additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader()); + additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration()); + descriptionBuilder.setExtras(additionalMetadata); + final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl()); if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri); 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 2dcf4a6ec..7abebc49e 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 @@ -94,15 +94,17 @@ public class VideoPlaybackResolver implements PlaybackResolver { // Below are auxiliary media sources // Create subtitle sources - for (final SubtitlesStream subtitle : info.getSubtitles()) { - final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat()); - if (mimeType == null) continue; + if(info.getSubtitles() != null) { + for (final SubtitlesStream subtitle : info.getSubtitles()) { + final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat()); + if (mimeType == null) continue; - final Format textFormat = Format.createTextSampleFormat(null, mimeType, - SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); - final MediaSource textSource = dataSource.getSampleMediaSourceFactory() - .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); - mediaSources.add(textSource); + final Format textFormat = Format.createTextSampleFormat(null, mimeType, + SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); + final MediaSource textSource = dataSource.getSampleMediaSourceFactory() + .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); + mediaSources.add(textSource); + } } if (mediaSources.size() == 1) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 5388d1821..da6d372f4 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -9,6 +9,7 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v7.preference.EditTextPreference; import android.support.v7.preference.Preference; import android.util.Log; import android.widget.Toast; @@ -16,14 +17,15 @@ import android.widget.Toast; import com.nononsenseapps.filepicker.Utils; import com.nostra13.universalimageloader.core.ImageLoader; +import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.utils.Localization; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.FilePickerActivityHelper; +import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ZipHelper; import java.io.BufferedOutputStream; @@ -42,6 +44,9 @@ import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; public class ContentSettingsFragment extends BasePreferenceFragment { @@ -58,6 +63,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment { private String thumbnailLoadToggleKey; + private CompositeDisposable disposables = new CompositeDisposable(); + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -135,30 +142,39 @@ public class ContentSettingsFragment extends BasePreferenceFragment { peerTubeInstance.setSummary(sharedPreferences.getString(getString(R.string.peertube_instance_url_key), ServiceList.PeerTube.getBaseUrl())); peerTubeInstance.setOnPreferenceChangeListener((Preference p, Object newInstance) -> { + EditTextPreference pEt = (EditTextPreference) p; String url = (String) newInstance; - if(!url.startsWith("https://")){ + if (!url.startsWith("https://")) { Toast.makeText(getActivity(), "instance url should start with https://", Toast.LENGTH_SHORT).show(); return false; - }else{ - boolean shouldUpdate = Single.fromCallable(() -> { + } else { + pEt.setSummary("fetching instance details.."); + Disposable disposable = Single.fromCallable(() -> { ServiceList.PeerTube.setInstance(url); return true; }).subscribeOn(Schedulers.io()) - .onErrorReturnItem(false) - .blockingGet(); - - if (shouldUpdate) { - p.setSummary(url); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(getString(R.string.peertube_instance_name_key), ServiceList.PeerTube.getServiceInfo().getName()).apply(); - editor.putString(getString(R.string.current_service_key), ServiceList.PeerTube.getServiceInfo().getName()).apply(); - editor.putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply(); - }else{ - Toast.makeText(getActivity(), "unable to update instance", - Toast.LENGTH_SHORT).show(); - } - return shouldUpdate; + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + if (result) { + pEt.setSummary(url); + pEt.setText(url); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(App.getApp().getString(R.string.peertube_instance_name_key), ServiceList.PeerTube.getServiceInfo().getName()).apply(); + editor.putString(App.getApp().getString(R.string.current_service_key), ServiceList.PeerTube.getServiceInfo().getName()).apply(); + NavigationHelper.openMainActivity(App.getApp()); + } else { + pEt.setSummary(ServiceList.PeerTube.getBaseUrl()); + Toast.makeText(getActivity(), "unable to update instance", + Toast.LENGTH_SHORT).show(); + } + }, error -> { + pEt.setSummary(ServiceList.PeerTube.getBaseUrl()); + Toast.makeText(getActivity(), "unable to update instance", + Toast.LENGTH_SHORT).show(); + }); + disposables.add(disposable); + return false; } }); } @@ -218,7 +234,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); - }finally { + } finally { try { if (output != null) { output.flush(); @@ -242,7 +258,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment { } finally { try { zipFile.close(); - } catch (Exception ignored){} + } catch (Exception ignored) { + } } try { @@ -265,7 +282,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { } //If settings file exist, ask if it should be imported. - if(ZipHelper.extractFileFromZip(filePath, newpipe_settings.getPath(), "newpipe.settings")) { + if (ZipHelper.extractFileFromZip(filePath, newpipe_settings.getPath(), "newpipe.settings")) { AlertDialog.Builder alert = new AlertDialog.Builder(getContext()); alert.setTitle(R.string.import_settings); @@ -320,7 +337,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); - }finally { + } finally { try { if (input != null) { input.close(); @@ -343,4 +360,5 @@ public class ContentSettingsFragment extends BasePreferenceFragment { ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); } + } 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 new file mode 100644 index 000000000..76d887dd1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -0,0 +1,33 @@ +package org.schabi.newpipe.settings; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.preference.Preference; + +import org.schabi.newpipe.CheckForNewAppVersionTask; +import org.schabi.newpipe.R; + +public class UpdateSettingsFragment extends BasePreferenceFragment { + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String updateToggleKey = getString(R.string.update_app_key); + findPreference(updateToggleKey).setOnPreferenceChangeListener(updatePreferenceChange); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.update_settings); + } + + private Preference.OnPreferenceChangeListener updatePreferenceChange + = (preference, newValue) -> { + + defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), + (boolean) newValue).apply(); + + return true; + }; +} diff --git a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java new file mode 100644 index 000000000..570b5f8b2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java @@ -0,0 +1,128 @@ +package org.schabi.newpipe.util; + +import android.content.Context; +import android.text.Layout; +import android.text.Selection; +import android.text.Spannable; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.text.style.URLSpan; +import android.view.MotionEvent; +import android.view.View; +import android.widget.TextView; + +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.SinglePlayQueue; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +public class CommentTextOnTouchListener implements View.OnTouchListener { + + public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener(); + + private static final Pattern timestampPattern = Pattern.compile(".*&t=(\\d+)"); + + @Override + public boolean onTouch(View v, MotionEvent event) { + if(!(v instanceof TextView)){ + return false; + } + TextView widget = (TextView) v; + Object text = widget.getText(); + if (text instanceof Spanned) { + Spannable buffer = (Spannable) text; + + int action = event.getAction(); + + if (action == MotionEvent.ACTION_UP + || action == MotionEvent.ACTION_DOWN) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + x -= widget.getTotalPaddingLeft(); + y -= widget.getTotalPaddingTop(); + + x += widget.getScrollX(); + y += widget.getScrollY(); + + Layout layout = widget.getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + + ClickableSpan[] link = buffer.getSpans(off, off, + ClickableSpan.class); + + if (link.length != 0) { + if (action == MotionEvent.ACTION_UP) { + boolean handled = false; + if(link[0] instanceof URLSpan){ + handled = handleUrl(v.getContext(), (URLSpan) link[0]); + } + if(!handled) link[0].onClick(widget); + } else if (action == MotionEvent.ACTION_DOWN) { + Selection.setSelection(buffer, + buffer.getSpanStart(link[0]), + buffer.getSpanEnd(link[0])); + } + return true; + } + } + + } + + return false; + } + + private boolean handleUrl(Context context, URLSpan urlSpan) { + String url = urlSpan.getURL(); + StreamingService service; + StreamingService.LinkType linkType; + try { + service = NewPipe.getServiceByUrl(url); + linkType = service.getLinkTypeByUrl(url); + } catch (ExtractionException e) { + return false; + } + if(linkType == StreamingService.LinkType.NONE){ + return false; + } + Matcher matcher = timestampPattern.matcher(url); + if(linkType == StreamingService.LinkType.STREAM && matcher.matches()){ + int seconds = Integer.parseInt(matcher.group(1)); + return playOnPopup(context, url, service, seconds); + }else{ + NavigationHelper.openRouterActivity(context, url); + return true; + } + } + + private boolean playOnPopup(Context context, String url, StreamingService service, int seconds) { + LinkHandlerFactory factory = service.getStreamLHFactory(); + String cleanUrl = null; + try { + cleanUrl = factory.getUrl(factory.getId(url)); + } catch (ParsingException e) { + return false; + } + Single single = ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false); + single.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(info -> { + PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info); + ((StreamInfo) info).setStartPosition(seconds); + NavigationHelper.enqueueOnPopupPlayer(context, playQueue, true); + }); + return true; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 3f6d82b9f..0f1c39473 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -32,6 +32,7 @@ import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.SuggestionExtractor; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; @@ -47,6 +48,7 @@ import org.schabi.newpipe.report.UserAction; import java.io.IOException; import java.io.InterruptedIOException; +import java.util.Collections; import java.util.List; import io.reactivex.Maybe; @@ -96,10 +98,13 @@ public final class ExtractorHelper { public static Single> suggestionsFor(final int serviceId, final String query) { checkServiceId(serviceId); - return Single.fromCallable(() -> - NewPipe.getService(serviceId) - .getSuggestionExtractor() - .suggestionList(query)); + return Single.fromCallable(() -> { + SuggestionExtractor extractor = NewPipe.getService(serviceId) + .getSuggestionExtractor(); + return extractor != null + ? extractor.suggestionList(query) + : Collections.emptyList(); + }); } public static Single getStreamInfo(final int serviceId, diff --git a/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java b/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java new file mode 100644 index 000000000..69666463e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java @@ -0,0 +1,10 @@ +package org.schabi.newpipe.util; + +import org.schabi.newpipe.App; + +public class FireTvUtils { + public static boolean isFireTv(){ + final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv"; + return App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java index 61bd2323d..1f32383d0 100644 --- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java +++ b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java @@ -31,6 +31,8 @@ public class KioskTranslator { return c.getString(R.string.top_50); case "New & hot": return c.getString(R.string.new_and_hot); + case "conferences": + return c.getString(R.string.conferences); default: return kioskId; } @@ -48,6 +50,8 @@ public class KioskTranslator { return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local); case "Recently added": return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent); + case "conferences": + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); default: return 0; } 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 4b93600ce..98ae3a88a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -21,6 +21,7 @@ import com.nostra13.universalimageloader.core.ImageLoader; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.RouterActivity; import org.schabi.newpipe.about.AboutActivity; import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.extractor.NewPipe; @@ -34,11 +35,11 @@ import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.comments.CommentsFragment; -import org.schabi.newpipe.local.bookmark.BookmarkFragment; -import org.schabi.newpipe.local.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; +import org.schabi.newpipe.local.bookmark.BookmarkFragment; +import org.schabi.newpipe.local.feed.FeedFragment; import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; @@ -422,6 +423,13 @@ public class NavigationHelper { context.startActivity(mIntent); } + public static void openRouterActivity(Context context, String url) { + Intent mIntent = new Intent(context, RouterActivity.class); + mIntent.setData(Uri.parse(url)); + mIntent.putExtra(RouterActivity.internalRouteKey, true); + context.startActivity(mIntent); + } + public static void openAbout(Context context) { Intent intent = new Intent(context, AboutActivity.class); context.startActivity(intent); 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/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index ab2d7c6b9..582f3590b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -11,9 +11,7 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; -import java.io.IOException; import java.util.concurrent.TimeUnit; import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; @@ -30,6 +28,8 @@ public class ServiceHelper { return R.drawable.place_holder_cloud; case 2: return R.drawable.place_holder_peertube; + case 3: + return R.drawable.place_holder_gadse; default: return R.drawable.place_holder_circle; } @@ -43,6 +43,8 @@ public class ServiceHelper { case "playlists": return c.getString(R.string.playlists); case "tracks": return c.getString(R.string.tracks); case "users": return c.getString(R.string.users); + case "conferences" : return c.getString(R.string.conferences); + case "events" : return c.getString(R.string.events); default: return filter; } } @@ -134,7 +136,7 @@ public class ServiceHelper { } public static void initService(Context context, int serviceId) { - if(serviceId == 2){ + if(serviceId == ServiceList.PeerTube.getServiceId()){ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); String peerTubeInstanceUrl = sharedPreferences.getString(context.getString(R.string.peertube_instance_url_key), ServiceList.PeerTube.getBaseUrl()); String peerTubeInstanceName = sharedPreferences.getString(context.getString(R.string.peertube_instance_name_key), ServiceList.PeerTube.getServiceInfo().getName()); 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 eb106f91d..49a7125ed 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -28,7 +28,8 @@ import io.reactivex.schedulers.Schedulers; import us.shandian.giga.util.Utility; /** - * A list adapter for a list of {@link Stream streams}, currently supporting {@link VideoStream} and {@link AudioStream}. + * A list adapter for a list of {@link Stream streams}, + * currently supporting {@link VideoStream}, {@link AudioStream} and {@link SubtitlesStream} */ public class StreamItemAdapter extends BaseAdapter { private final Context context; @@ -110,7 +111,10 @@ public class StreamItemAdapter extends BaseA } } } else if (stream instanceof AudioStream) { - qualityString = ((AudioStream) stream).getAverageBitrate() + "kbps"; + AudioStream audioStream = ((AudioStream) stream); + qualityString = audioStream.getAverageBitrate() > 0 + ? audioStream.getAverageBitrate() + "kbps" + : audioStream.getFormat().getName(); } else if (stream instanceof SubtitlesStream) { qualityString = ((SubtitlesStream) stream).getDisplayLanguageName(); if (((SubtitlesStream) stream).isAutoGenerated()) { @@ -154,8 +158,10 @@ public class StreamItemAdapter extends BaseA private final long[] streamSizes; private final String unknownSize; - public StreamSizeWrapper(List streamsList, Context context) { - this.streamsList = streamsList; + public StreamSizeWrapper(List sL, Context context) { + this.streamsList = sL != null + ? sL + : Collections.emptyList(); this.streamSizes = new long[streamsList.size()]; this.unknownSize = context == null ? "--.-" : context.getString(R.string.unknown_content); diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index b8fba6ce5..13996eaf2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -30,6 +30,7 @@ import android.view.ContextThemeWrapper; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; @@ -136,15 +137,16 @@ public class ThemeHelper { else if (selectedTheme.equals(blackTheme)) themeName = "BlackTheme"; else if (selectedTheme.equals(darkTheme)) themeName = "DarkTheme"; - switch (serviceId) { - case 2: - //service name for peertube depends on the instance - themeName += ".PeerTube"; - break; - default: - themeName += "." + service.getServiceInfo().getName(); + if(serviceId == ServiceList.PeerTube.getServiceId()){ + //service name for peertube depends on the instance + themeName += ".PeerTube"; + }else{ + themeName += "." + service.getServiceInfo().getName(); } - int resourceId = context.getResources().getIdentifier(themeName, "style", context.getPackageName()); + + int resourceId = context + .getResources() + .getIdentifier(themeName, "style", context.getPackageName()); if (resourceId > 0) { return resourceId; 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 c25d517f1..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,16 +413,30 @@ public class DownloadMission extends Mission { * Start downloading with multiple threads. */ public void start() { - if (running || current >= urls.length) return; + if (running || isFinished()) return; // ensure that the previous state is completely paused. joinForThread(init); - for (Thread thread : threads) joinForThread(thread); + if (threads != null) + for (Thread thread : threads) joinForThread(thread); enqueued = false; running = true; errCode = ERROR_NOTHING; + if (current >= urls.length && postprocessingName != null) { + runAsync(1, () -> { + if (doPostprocessing()) { + running = false; + deleteThisFromFile(); + + notify(DownloadManagerService.MESSAGE_FINISHED); + } + }); + + return; + } + if (blocks < 0) { initializer(); return; @@ -420,7 +444,7 @@ public class DownloadMission extends Mission { init = null; - if (threads.length < 1) { + if (threads == null || threads.length < 1) { threads = new Thread[currentThreadCount]; } @@ -444,18 +468,18 @@ public class DownloadMission extends Mission { public synchronized void pause() { if (!running) return; - running = false; - recovered = true; - enqueued = false; - - if (postprocessingRunning) { + if (isPsRunning()) { if (DEBUG) { Log.w(TAG, "pause during post-processing is not applicable."); } return; } - if (init != null && init.isAlive()) { + running = false; + recovered = true; + enqueued = false; + + if (init != null && Thread.currentThread() != init && init.isAlive()) { init.interrupt(); synchronized (blockState) { resetState(); @@ -532,13 +556,36 @@ public class DownloadMission extends Mission { mWritingToFile = false; } + /** + * Indicates if the download if fully finished + * + * @return true, otherwise, false + */ public boolean isFinished() { - return current >= urls.length && postprocessingName == null; + return current >= urls.length && (postprocessingName == null || postprocessingState == 2); + } + + /** + * Indicates if the download file is corrupt due a failed post-processing + * + * @return {@code true} if this mission is unrecoverable + */ + public boolean isPsFailed() { + return postprocessingName != null && errCode == DownloadMission.ERROR_POSTPROCESSING && postprocessingThis; + } + + /** + * Indicates if a post-processing algorithm is running + * + * @return true, otherwise, false + */ + public boolean isPsRunning() { + return postprocessingName != null && postprocessingState == 1; } public long getLength() { long calculated; - if (postprocessingRunning) { + if (postprocessingState == 1) { calculated = length; } else { calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; @@ -550,16 +597,19 @@ public class DownloadMission extends Mission { } private boolean doPostprocessing() { - if (postprocessingName == null) return true; + if (postprocessingName == null || postprocessingState == 2) return true; + + notifyPostProcessing(1); + notifyProgress(0); + + Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name); + + Exception exception = null; try { - notifyPostProcessing(true); - notifyProgress(0); - - Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name); - - Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, this); - algorithm.run(); + Postprocessing + .getAlgorithm(postprocessingName, this) + .run(); } catch (Exception err) { StringBuilder args = new StringBuilder(" "); if (postprocessingArgs != null) { @@ -571,15 +621,21 @@ public class DownloadMission extends Mission { } Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err); - notifyError(ERROR_POSTPROCESSING_FAILED, err); - return false; + if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING; + + exception = err; } finally { - notifyPostProcessing(false); + notifyPostProcessing(errCode == ERROR_NOTHING ? 2 : 0); } - if (errCode != ERROR_NOTHING) notify(DownloadManagerService.MESSAGE_ERROR); + if (errCode != ERROR_NOTHING) { + if (exception == null) exception = errObject; + notifyError(ERROR_POSTPROCESSING, exception); - return errCode == ERROR_NOTHING; + return false; + } + + return true; } private boolean deleteThisFromFile() { 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 df5f9e429..4a35aa166 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 @@ -50,6 +50,7 @@ import us.shandian.giga.ui.common.Deleter; import us.shandian.giga.ui.common.ProgressDrawable; import us.shandian.giga.util.Utility; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST; @@ -59,7 +60,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE; import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION; import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED; -import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_FAILED; +import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING; import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; @@ -67,7 +68,8 @@ import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; public class MissionAdapter extends Adapter { 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"); @@ -158,7 +160,7 @@ public class MissionAdapter extends Adapter { str = R.string.missions_header_pending; } else { str = R.string.missions_header_finished; - setClearButtonVisibility(true); + if (mClear != null) mClear.setVisible(true); } ((ViewHolderHeader) view).header.setText(str); @@ -178,7 +180,7 @@ public class MissionAdapter extends Adapter { if (h.item.mission instanceof DownloadMission) { DownloadMission mission = (DownloadMission) item.mission; String length = Utility.formatBytes(mission.getLength()); - if (mission.running && !mission.postprocessingRunning) length += " --.- kB/s"; + if (mission.running && !mission.isPsRunning()) length += " --.- kB/s"; h.size.setText(length); h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause); @@ -238,11 +240,10 @@ public class MissionAdapter extends Adapter { } if (hasError) { - if (Float.isNaN(progress) || Float.isInfinite(progress)) - h.progress.setProgress(1f); + h.progress.setProgress(isNotFinite(progress) ? 1f : progress); h.status.setText(R.string.msg_error); - } else if (Float.isNaN(progress) || Float.isInfinite(progress)) { - h.status.setText(UNDEFINED_SPEED); + } else if (isNotFinite(progress)) { + h.status.setText(UNDEFINED_PROGRESS); } else { h.status.setText(String.format("%.2f%%", progress * 100)); h.progress.setProgress(progress); @@ -251,11 +252,11 @@ public class MissionAdapter extends Adapter { long length = mission.getLength(); int state; - if (mission.errCode == ERROR_POSTPROCESSING_FAILED) { + if (mission.isPsFailed()) { state = 0; } else if (!mission.running) { state = mission.enqueued ? 1 : 2; - } else if (mission.postprocessingRunning) { + } else if (mission.isPsRunning()) { state = 3; } else { state = 0; @@ -322,6 +323,9 @@ public class MissionAdapter extends Adapter { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); } + if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); + } //mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); Log.v(TAG, "Starting intent: " + intent); if (intent.resolveActivity(mContext.getPackageManager()) != null) { @@ -406,7 +410,7 @@ public class MissionAdapter extends Adapter { case ERROR_CONNECT_HOST: str.append(mContext.getString(R.string.error_connect_host)); break; - case ERROR_POSTPROCESSING_FAILED: + case ERROR_POSTPROCESSING: str.append(mContext.getString(R.string.error_postprocessing_failed)); case ERROR_UNKNOWN_EXCEPTION: break; @@ -437,7 +441,6 @@ public class MissionAdapter extends Adapter { public void clearFinishedDownloads() { mDownloadManager.forgetFinishedDownloads(); applyChanges(); - setClearButtonVisibility(false); } private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) { @@ -447,6 +450,7 @@ public class MissionAdapter extends Adapter { if (mission != null) { switch (id) { case R.id.start: + h.status.setText(UNDEFINED_PROGRESS); h.state = -1; h.size.setText(Utility.formatBytes(mission.getLength())); mDownloadManager.resumeMission(mission); @@ -506,11 +510,7 @@ public class MissionAdapter extends Adapter { mIterator.end(); checkEmptyMessageVisibility(); - - if (mIterator.getOldListSize() > 0) { - int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1); - setClearButtonVisibility(lastItemType == DownloadManager.SPECIAL_FINISHED); - } + if (mClear != null) mClear.setVisible(mIterator.hasFinishedMissions()); } public void forceUpdate() { @@ -529,17 +529,10 @@ public class MissionAdapter extends Adapter { } public void setClearButton(MenuItem clearButton) { - if (mClear == null) { - int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1); - clearButton.setVisible(lastItemType == DownloadManager.SPECIAL_FINISHED); - } + if (mClear == null) clearButton.setVisible(mIterator.hasFinishedMissions()); mClear = clearButton; } - private void setClearButtonVisibility(boolean flag) { - mClear.setVisible(flag); - } - private void checkEmptyMessageVisibility() { int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE; if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag); @@ -596,6 +589,10 @@ public class MissionAdapter extends Adapter { } } + private boolean isNotFinite(Float value) { + return Float.isNaN(value) || Float.isInfinite(value); + } + class ViewHolderItem extends RecyclerView.ViewHolder { DownloadManager.MissionItem item; @@ -667,7 +664,7 @@ public class MissionAdapter extends Adapter { DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null; if (mission != null) { - if (!mission.postprocessingRunning) { + if (!mission.isPsRunning()) { if (mission.running) { pause.setVisible(true); } else { @@ -678,8 +675,10 @@ public class MissionAdapter extends Adapter { 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 aa9c497f1..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 @@ -20,6 +20,7 @@ import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.ThemeHelper; import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; @@ -40,7 +41,7 @@ public class MissionsFragment extends Fragment { private MissionAdapter mAdapter; private GridLayoutManager mGridManager; private LinearLayoutManager mLinearManager; - private Context mActivity; + private Context mContext; private DMBinder mBinder; private Bundle mBundle; @@ -53,7 +54,7 @@ public class MissionsFragment extends Fragment { mBinder = (DownloadManagerService.DMBinder) binder; mBinder.clearDownloadNotifications(); - mAdapter = new MissionAdapter(mActivity, mBinder.getDownloadManager(), mClear, mEmpty); + mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mClear, mEmpty); mAdapter.deleterLoad(mBundle, getView()); mBundle = null; @@ -79,17 +80,17 @@ public class MissionsFragment extends Fragment { mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); mLinear = mPrefs.getBoolean("linear", false); - mActivity = getActivity(); + //mContext = getActivity().getApplicationContext(); mBundle = savedInstanceState; // Bind the service - mActivity.bindService(new Intent(mActivity, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE); + mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE); // Views mEmpty = v.findViewById(R.id.list_empty_view); mList = v.findViewById(R.id.mission_recycler); - // Init + // Init layouts managers mGridManager = new GridLayoutManager(getActivity(), SPAN_SIZE); mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override @@ -103,7 +104,6 @@ public class MissionsFragment extends Fragment { } } }); - mLinearManager = new LinearLayoutManager(getActivity()); setHasOptionsMenu(true); @@ -115,13 +115,13 @@ public class MissionsFragment extends Fragment { * Added in API level 23. */ @Override - public void onAttach(Context activity) { - super.onAttach(activity); + public void onAttach(Context context) { + super.onAttach(context); // Bug: in api< 23 this is never called // so mActivity=null - // so app crashes with nullpointer exception - mActivity = activity; + // so app crashes with null-pointer exception + mContext = context; } /** @@ -132,7 +132,7 @@ public class MissionsFragment extends Fragment { public void onAttach(Activity activity) { super.onAttach(activity); - mActivity = activity; + mContext = activity.getApplicationContext(); } @@ -143,7 +143,7 @@ public class MissionsFragment extends Fragment { mBinder.removeMissionEventListener(mAdapter.getMessenger()); mBinder.enableNotifications(true); - mActivity.unbindService(mConnection); + mContext.unbindService(mConnection); mAdapter.deleterDispose(null); mBinder = null; @@ -189,7 +189,15 @@ public class MissionsFragment extends Fragment { mList.setAdapter(mAdapter); if (mSwitch != null) { - mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list); + boolean isLight = ThemeHelper.isLightThemeSelected(mContext); + int icon; + + if (mLinear) + icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp; + else + icon = isLight ? R.drawable.ic_grid_black_24dp : R.drawable.ic_grid_white_24dp; + + mSwitch.setIcon(icon); mSwitch.setTitle(mLinear ? R.string.grid : R.string.list); mPrefs.edit().putBoolean("linear", mLinear).apply(); } 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 26fa36c07..000000000 Binary files a/app/src/main/res/drawable-hdpi/grid.png and /dev/null differ 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 000000000..ba8820363 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_delete_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png new file mode 100644 index 000000000..be2850b3d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png differ 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 000000000..2db18582c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_grid_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_grid_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_grid_white_24dp.png new file mode 100644 index 000000000..5cc4722f6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_grid_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_list_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_list_black_24dp.png new file mode 100644 index 000000000..cc7b7a091 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_list_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_list_white_24dp.png new file mode 100644 index 000000000..5e6e304e3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_list_white_24dp.png differ 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 000000000..cbf336a1f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_newpipe_update.png differ 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 000000000..cdd51d35f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_settings_update_black.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_settings_update_white.png b/app/src/main/res/drawable-hdpi/ic_settings_update_white.png new file mode 100755 index 000000000..544a85c9d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_settings_update_white.png differ 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 16da863e2..000000000 Binary files a/app/src/main/res/drawable-hdpi/list.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldrtl-mdpi/ic_settings_update_black.png b/app/src/main/res/drawable-ldrtl-mdpi/ic_settings_update_black.png new file mode 100755 index 000000000..d85ec5975 Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-mdpi/ic_settings_update_black.png differ diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/grid.png b/app/src/main/res/drawable-ldrtl-xhdpi/grid.png deleted file mode 100644 index 94bb67f93..000000000 Binary files a/app/src/main/res/drawable-ldrtl-xhdpi/grid.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/list.png b/app/src/main/res/drawable-ldrtl-xhdpi/list.png deleted file mode 100644 index 905e17c8a..000000000 Binary files a/app/src/main/res/drawable-ldrtl-xhdpi/list.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_delete_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_delete_black_24dp.png new file mode 100644 index 000000000..65bc6817d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_delete_black_24dp.png differ 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 000000000..8f10392ca Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_grid_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_grid_black_24dp.png new file mode 100644 index 000000000..0f878e4ed Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_grid_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_grid_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_grid_white_24dp.png new file mode 100644 index 000000000..0096c9f11 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_grid_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_list_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_list_black_24dp.png new file mode 100644 index 000000000..6fa4e5034 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_list_black_24dp.png differ 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 000000000..f0d3f5f7c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_newpipe_update.png b/app/src/main/res/drawable-mdpi/ic_newpipe_update.png new file mode 100755 index 000000000..8ab23eb6a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_newpipe_update.png differ 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 000000000..964553137 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_settings_update_black.png differ 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 000000000..cf4642f97 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_settings_update_white.png differ diff --git a/app/src/main/res/drawable-nodpi/place_holder_gadse.png b/app/src/main/res/drawable-nodpi/place_holder_gadse.png new file mode 100644 index 000000000..f11dd57e5 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/place_holder_gadse.png differ diff --git a/app/src/main/res/drawable-v23/splash_background.xml b/app/src/main/res/drawable-v23/splash_background.xml new file mode 100644 index 000000000..a67fbc4a6 --- /dev/null +++ b/app/src/main/res/drawable-v23/splash_background.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png new file mode 100644 index 000000000..f080aa9e8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png differ 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 000000000..a1b828bf9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_grid_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_grid_black_24dp.png new file mode 100644 index 000000000..bcefc5221 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_grid_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_grid_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_grid_white_24dp.png new file mode 100644 index 000000000..745cb6cb8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_grid_white_24dp.png differ 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 000000000..0f7327fad Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_list_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png new file mode 100644 index 000000000..1de314a57 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png differ 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 000000000..5ee02aaa9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_newpipe_update.png differ 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 000000000..0304e6fd1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_settings_update_black.png differ 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 000000000..9c71b13f7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_settings_update_white.png differ 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 db7497981..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/grid.png and /dev/null differ 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 000000000..4cb4c08e2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png differ 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 000000000..bd11b4c66 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_grid_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_grid_black_24dp.png new file mode 100644 index 000000000..31cd3dc83 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_grid_black_24dp.png differ 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 000000000..ebe059481 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_grid_white_24dp.png differ 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 000000000..52d0c00a1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_list_black_24dp.png differ 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 000000000..e5f698298 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png b/app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png new file mode 100755 index 000000000..22f0e99d1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png differ 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 000000000..7316dbc88 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_settings_update_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_update_white.png b/app/src/main/res/drawable-xxhdpi/ic_settings_update_white.png new file mode 100755 index 000000000..07b1f712d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_settings_update_white.png differ 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 fbb7c1072..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/list.png and /dev/null differ 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 000000000..ab07ea2ae Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png differ 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 000000000..a8358eb71 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png differ 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 000000000..fe78d853e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_grid_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_grid_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_grid_white_24dp.png new file mode 100644 index 000000000..d52610ec8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_grid_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_list_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_list_black_24dp.png new file mode 100644 index 000000000..b165df44d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_list_black_24dp.png differ 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 000000000..053a1a0da Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png differ 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 000000000..1f44c1aaf Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_update_black.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_update_black.png new file mode 100755 index 000000000..8186c6f5f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_settings_update_black.png differ 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 000000000..8b5e6fa38 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_settings_update_white.png differ diff --git a/app/src/main/res/drawable/splash_background.xml b/app/src/main/res/drawable/splash_background.xml new file mode 100644 index 000000000..5b805cffa --- /dev/null +++ b/app/src/main/res/drawable/splash_background.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/splash_forground.xml b/app/src/main/res/drawable/splash_forground.xml new file mode 100644 index 000000000..cfb650758 --- /dev/null +++ b/app/src/main/res/drawable/splash_forground.xml @@ -0,0 +1,10 @@ + + + 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-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 2df22ab48..8cdc2f307 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -179,6 +179,7 @@ android:id="@+id/detail_content_root_hiding" android:layout_width="match_parent" android:layout_height="match_parent" + android:paddingBottom="10dp" android:layout_below="@+id/detail_title_root_layout" android:orientation="vertical" android:visibility="gone" @@ -492,6 +493,7 @@ android:id="@+id/relatedStreamsLayout" android:layout_width="0dp" android:layout_height="match_parent" + android:layout_marginTop="10dp" android:layout_weight="3"> diff --git a/app/src/main/res/layout-v21/drawer_header.xml b/app/src/main/res/layout-v21/drawer_header.xml index 4474ee4ed..574d88083 100644 --- a/app/src/main/res/layout-v21/drawer_header.xml +++ b/app/src/main/res/layout-v21/drawer_header.xml @@ -46,15 +46,22 @@ + android:textStyle="italic" + android:ellipsize="marquee" + android:fadingEdge="horizontal" + android:marqueeRepeatLimit="marquee_forever" + android:scrollHorizontally="true" + android:singleLine="true" /> - - - - - - - - - - - - - - - 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/layout/activity_play_video.xml b/app/src/main/res/layout/activity_play_video.xml deleted file mode 100644 index bc37184a4..000000000 --- a/app/src/main/res/layout/activity_play_video.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - -