diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 4b505f00e..07cb9f66c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,6 +15,8 @@ Do not report crashes in the GitHub issue tracker. NewPipe has an automated cras * If you are an Android/Java developer, you are always welcome to fix/implement an issue/a feature yourself. PRs welcome! * 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. ## Bug Fixing * If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to tnp@newpipe.schabi.org to let me know that you intend to help. We'll send you further instructions. You may, on request, register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information. diff --git a/app/build.gradle b/app/build.gradle index dd7c8ff1b..a5ff67bee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.schabi.newpipe" minSdkVersion 15 targetSdkVersion 27 - versionCode 62 - versionName "0.13.3" + versionCode 64 + versionName "0.13.5" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -54,7 +54,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:0501a2f543' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:bf1c771' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index fbe5ab23f..20596c6eb 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -42,3 +42,9 @@ -dontwarn javax.annotation.** # A resource is loaded with a relative path so the package of this class must be preserved. -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + !static !transient ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); +} diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java index 962ec8a36..0450290d2 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java @@ -128,47 +128,31 @@ public class AboutActivity extends AppCompatActivity { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_about, container, false); + Context context = this.getContext(); + TextView version = rootView.findViewById(R.id.app_version); version.setText(BuildConfig.VERSION_NAME); View githubLink = rootView.findViewById(R.id.github_link); - githubLink.setOnClickListener(new OnGithubLinkClickListener()); + githubLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.github_url), context)); View donationLink = rootView.findViewById(R.id.donation_link); - donationLink.setOnClickListener(new OnDonationLinkClickListener()); + donationLink.setOnClickListener(v -> openWebsite(context.getString(R.string.donation_url), context)); View websiteLink = rootView.findViewById(R.id.website_link); - websiteLink.setOnClickListener(new OnWebsiteLinkClickListener()); + websiteLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.website_url), context)); + + View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link); + privacyPolicyLink.setOnClickListener(v -> openWebsite(context.getString(R.string.privacy_policy_url), context)); return rootView; } - private static class OnGithubLinkClickListener implements View.OnClickListener { - @Override - public void onClick(final View view) { - final Context context = view.getContext(); - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.github_url))); - context.startActivity(intent); - } + private void openWebsite(String url, Context context) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + context.startActivity(intent); } - private static class OnDonationLinkClickListener implements View.OnClickListener { - @Override - public void onClick(final View view) { - final Context context = view.getContext(); - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.donation_url))); - context.startActivity(intent); - } - } - - private static class OnWebsiteLinkClickListener implements View.OnClickListener { - @Override - public void onClick(final View view) { - final Context context = view.getContext(); - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.website_url))); - context.startActivity(intent); - } - } } diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java index e4436322a..701e18cbf 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java @@ -42,7 +42,7 @@ public class LicenseFragmentHelper extends AsyncTask { } @Override - protected void onPostExecute(Integer result){ + protected void onPostExecute(Integer result) { Activity activity = getActivity(); if (activity == null) { return; 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 b64bf35cb..68c0a11ff 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 @@ -54,7 +54,7 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java index c7f97cee0..5dfdcd655 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java @@ -77,8 +77,7 @@ public class KioskFragment extends BaseListInfoFragment { UrlIdHandler kioskTypeUrlIdHandler = service.getKioskList() .getUrlIdHandlerByType(kioskId); instance.setInitialData(serviceId, - kioskTypeUrlIdHandler.setId(kioskId).getUrl(), - kioskId); + kioskTypeUrlIdHandler.getUrl(kioskId), kioskId); instance.kioskId = kioskId; return instance; } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 94e0654e2..4c3d70421 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -271,6 +271,7 @@ public abstract class BasePlayer implements if (audioReactor != null) audioReactor.dispose(); if (playbackManager != null) playbackManager.dispose(); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); + if (mediaSessionManager != null) mediaSessionManager.dispose(); if (playQueueAdapter != null) { playQueueAdapter.unsetSelectedListener(); 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 502e5b802..0dea47e56 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -119,6 +119,10 @@ public final class MainVideoPlayer extends AppCompatActivity if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); setVolumeControlStream(AudioManager.STREAM_MUSIC); + WindowManager.LayoutParams lp = getWindow().getAttributes(); + lp.screenBrightness = PlayerHelper.getScreenBrightness(getApplicationContext()); + getWindow().setAttributes(lp); + hideSystemUi(); setContentView(R.layout.activity_main_player); playerImpl = new VideoPlayerImpl(this); @@ -205,6 +209,9 @@ public final class MainVideoPlayer extends AppCompatActivity if (DEBUG) Log.d(TAG, "onStop() called"); super.onStop(); playerImpl.destroy(); + + PlayerHelper.setScreenBrightness(getApplicationContext(), + getWindow().getAttributes().screenBrightness); } /*////////////////////////////////////////////////////////////////////////// @@ -647,7 +654,7 @@ public final class MainVideoPlayer extends AppCompatActivity @Override protected int getOverrideResolutionIndex(final List sortedVideos, final String playbackQuality) { - return ListHelper.getDefaultResolutionIndex(context, sortedVideos, playbackQuality); + return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); } /*////////////////////////////////////////////////////////////////////////// @@ -884,7 +891,9 @@ public final class MainVideoPlayer extends AppCompatActivity private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext()); private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f; - private float currentBrightness = .5f; + private float currentBrightness = getWindow().getAttributes().screenBrightness > 0 + ? getWindow().getAttributes().screenBrightness + : 0.5f; private int currentVolume, maxVolume = playerImpl.getAudioReactor().getMaxVolume(); private final float stepsVolume = 15, stepVolume = (float) Math.ceil(maxVolume / stepsVolume), minVolume = 0; 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 1dcf4a89d..8107345a1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -517,7 +517,7 @@ public final class PopupVideoPlayer extends Service { @Override protected int getOverrideResolutionIndex(final List sortedVideos, final String playbackQuality) { - return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos, playbackQuality); + return ListHelper.getPopupResolutionIndex(context, sortedVideos, playbackQuality); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java index 2611705a8..b174ed3ed 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -11,7 +11,6 @@ import android.view.KeyEvent; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; -import org.schabi.newpipe.player.mediasession.DummyPlaybackPreparer; import org.schabi.newpipe.player.mediasession.MediaSessionCallback; import org.schabi.newpipe.player.mediasession.PlayQueueNavigator; import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController; @@ -26,10 +25,12 @@ public class MediaSessionManager { @NonNull final Player player, @NonNull final MediaSessionCallback callback) { this.mediaSession = new MediaSessionCompat(context, TAG); + this.mediaSession.setActive(true); + this.sessionConnector = new MediaSessionConnector(mediaSession, new PlayQueuePlaybackController(callback)); this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); - this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer()); + this.sessionConnector.setPlayer(player, null); } @Nullable @@ -37,4 +38,11 @@ public class MediaSessionManager { public KeyEvent handleMediaButtonIntent(final Intent intent) { return MediaButtonReceiver.handleIntent(mediaSession, intent); } + + public void dispose() { + this.sessionConnector.setPlayer(null, null); + this.sessionConnector.setQueueNavigator(null); + this.mediaSession.setActive(false); + this.mediaSession.release(); + } } 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 4ae8eec2a..dbe0e9f46 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 @@ -9,9 +9,9 @@ import android.support.annotation.Nullable; import android.view.accessibility.CaptioningManager; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.MimeTypes; @@ -37,6 +37,7 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.concurrent.TimeUnit; import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL; import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; @@ -260,6 +261,16 @@ public class PlayerHelper { return captioningManager.getFontScale(); } + + public static float getScreenBrightness(@NonNull final Context context) { + //a value of less than 0, the default, means to use the preferred screen brightness + return getScreenBrightness(context, -1); + } + + public static void setScreenBrightness(@NonNull final Context context, final float setScreenBrightness) { + setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis()); + } + //////////////////////////////////////////////////////////////////////////// // Private helpers //////////////////////////////////////////////////////////////////////////// @@ -292,4 +303,23 @@ public class PlayerHelper { private static boolean isAutoQueueEnabled(@NonNull final Context context, final boolean b) { return getPreferences(context).getBoolean(context.getString(R.string.auto_queue_key), b); } + + private static void setScreenBrightness(@NonNull final Context context, final float screenBrightness, final long timestamp) { + SharedPreferences.Editor editor = getPreferences(context).edit(); + editor.putFloat(context.getString(R.string.screen_brightness_key), screenBrightness); + editor.putLong(context.getString(R.string.screen_brightness_timestamp_key), timestamp); + editor.apply(); + } + + private static float getScreenBrightness(@NonNull final Context context, final float screenBrightness) { + SharedPreferences sp = getPreferences(context); + long timestamp = sp.getLong(context.getString(R.string.screen_brightness_timestamp_key), 0); + // hypothesis: 4h covers a viewing block, eg evening. External lightning conditions will change in the next + // viewing block so we fall back to the default brightness + if ((System.currentTimeMillis() - timestamp) > TimeUnit.HOURS.toMillis(4)) { + return screenBrightness; + } else { + return sp.getFloat(context.getString(R.string.screen_brightness_key), screenBrightness); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java deleted file mode 100644 index 431a90d8a..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.schabi.newpipe.player.mediasession; - -import android.net.Uri; -import android.os.Bundle; -import android.os.ResultReceiver; - -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; - -public class DummyPlaybackPreparer implements MediaSessionConnector.PlaybackPreparer { - @Override - public long getSupportedPrepareActions() { - return 0; - } - - @Override - public void onPrepare() { - - } - - @Override - public void onPrepareFromMediaId(String mediaId, Bundle extras) { - - } - - @Override - public void onPrepareFromSearch(String query, Bundle extras) { - - } - - @Override - public void onPrepareFromUri(Uri uri, Bundle extras) { - - } - - @Override - public String[] getCommands() { - return new String[0]; - } - - @Override - public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { - - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java index a1a57a87d..498fb4a88 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java @@ -13,5 +13,4 @@ public interface MediaSessionCallback { void onPlay(); void onPause(); - void onSetShuffle(final boolean isShuffled); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java index 2aa41bd63..a460a1653 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java @@ -1,7 +1,5 @@ package org.schabi.newpipe.player.mediasession; -import android.support.v4.media.session.PlaybackStateCompat; - import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ext.mediasession.DefaultPlaybackController; @@ -22,10 +20,4 @@ public class PlayQueuePlaybackController extends DefaultPlaybackController { public void onPause(Player player) { callback.onPause(); } - - @Override - public void onSetShuffleMode(Player player, int shuffleMode) { - callback.onSetShuffle(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL - || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP); - } } 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 6cce4a764..3365828d1 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 @@ -69,9 +69,4 @@ public class BasePlayerMediaSession implements MediaSessionCallback { public void onPause() { player.onPause(); } - - @Override - public void onSetShuffle(boolean isShuffled) { - player.onShuffleModeEnabledChanged(isShuffled); - } } diff --git a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java index 1b60c596f..3ad08c3ec 100644 --- a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java @@ -1,7 +1,9 @@ package org.schabi.newpipe.report; import android.app.Activity; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.graphics.Color; import android.net.Uri; @@ -33,10 +35,8 @@ import org.json.JSONArray; import org.json.JSONObject; import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.BuildConfig; -import org.schabi.newpipe.Downloader; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.util.ThemeHelper; import java.io.PrintWriter; @@ -44,9 +44,9 @@ import java.io.StringWriter; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; -import java.util.Locale; import java.util.TimeZone; import java.util.Vector; +import java.util.concurrent.atomic.AtomicBoolean; /* * Created by Christian Schabesberger on 24.10.15. @@ -210,12 +210,31 @@ public class ErrorActivity extends AppCompatActivity { currentTimeStamp = getCurrentTimeStamp(); reportButton.setOnClickListener((View v) -> { - Intent i = new Intent(Intent.ACTION_SENDTO); - i.setData(Uri.parse("mailto:" + ERROR_EMAIL_ADDRESS)) - .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT) - .putExtra(Intent.EXTRA_TEXT, buildJson()); + Context context = this; + new AlertDialog.Builder(context) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.privacy_policy_title) + .setMessage(R.string.start_accept_privacy_policy) + .setCancelable(false) + .setNeutralButton(R.string.read_privacy_policy, (dialog, which) -> { + Intent webIntent = new Intent(Intent.ACTION_VIEW, + Uri.parse(context.getString(R.string.privacy_policy_url)) + ); + context.startActivity(webIntent); + }) + .setPositiveButton(R.string.accept, (dialog, which) -> { + Intent i = new Intent(Intent.ACTION_SENDTO); + i.setData(Uri.parse("mailto:" + ERROR_EMAIL_ADDRESS)) + .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT) + .putExtra(Intent.EXTRA_TEXT, buildJson()); + + startActivity(Intent.createChooser(i, "Send Email")); + }) + .setNegativeButton(R.string.decline, (dialog, which) -> { + // do nothing + }) + .show(); - startActivity(Intent.createChooser(i, "Send Email")); }); // normal bugreport 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 09e2740c9..1897589c6 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -39,7 +39,7 @@ import org.schabi.newpipe.extractor.kiosk.KioskInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.search.SearchEngine; import org.schabi.newpipe.extractor.search.SearchResult; -import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index 79fd1e496..4f607b581 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.util; import android.content.Context; import android.content.SharedPreferences; +import android.net.ConnectivityManager; import android.preference.PreferenceManager; import android.support.annotation.StringRes; @@ -13,56 +14,38 @@ import org.schabi.newpipe.extractor.stream.VideoStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; @SuppressWarnings("WeakerAccess") public final class ListHelper { + // Video format in order of quality. 0=lowest quality, n=highest quality + private static final List VIDEO_FORMAT_QUALITY_RANKING = + Arrays.asList(MediaFormat.v3GPP, MediaFormat.WEBM, MediaFormat.MPEG_4); + + // Audio format in order of quality. 0=lowest quality, n=highest quality + private static final List AUDIO_FORMAT_QUALITY_RANKING = + Arrays.asList(MediaFormat.MP3, MediaFormat.WEBMA, MediaFormat.M4A); + // Audio format in order of efficiency. 0=most efficient, n=least efficient + private static final List AUDIO_FORMAT_EFFICIENCY_RANKING = + Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3); + private static final List HIGH_RESOLUTION_LIST = Arrays.asList("1440p", "2160p", "1440p60", "2160p60"); - /** - * Return the index of the default stream in the list, based on the parameters - * defaultResolution and defaultFormat - * - * @return index of the default resolution&format - */ - public static int getDefaultResolutionIndex(String defaultResolution, String bestResolutionKey, MediaFormat defaultFormat, List videoStreams) { - if (videoStreams == null || videoStreams.isEmpty()) return -1; - - sortStreamList(videoStreams, false); - if (defaultResolution.equals(bestResolutionKey)) { - return 0; - } - - int defaultStreamIndex = getDefaultStreamIndex(defaultResolution, defaultFormat, videoStreams); - if (defaultStreamIndex == -1 && defaultResolution.contains("p60")) { - defaultStreamIndex = getDefaultStreamIndex(defaultResolution.replace("p60", "p"), defaultFormat, videoStreams); - } - - // this is actually an error, - // but maybe there is really no stream fitting to the default value. - if (defaultStreamIndex == -1) return 0; - - return defaultStreamIndex; - } - /** * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) */ public static int getDefaultResolutionIndex(Context context, List videoStreams) { - SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context); - if (defaultPreferences == null) return 0; - - String defaultResolution = defaultPreferences.getString(context.getString(R.string.default_resolution_key), context.getString(R.string.default_resolution_value)); - return getDefaultResolutionIndex(context, videoStreams, defaultResolution); + String defaultResolution = computeDefaultResolution(context, + R.string.default_resolution_key, R.string.default_resolution_value); + return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); } /** * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) */ - public static int getDefaultResolutionIndex(Context context, List videoStreams, String defaultResolution) { + public static int getResolutionIndex(Context context, List videoStreams, String defaultResolution) { return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); } @@ -70,69 +53,29 @@ public final class ListHelper { * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) */ public static int getPopupDefaultResolutionIndex(Context context, List videoStreams) { - SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context); - if (defaultPreferences == null) return 0; - - String defaultResolution = defaultPreferences.getString(context.getString(R.string.default_popup_resolution_key), context.getString(R.string.default_popup_resolution_value)); - return getPopupDefaultResolutionIndex(context, videoStreams, defaultResolution); + String defaultResolution = computeDefaultResolution(context, + R.string.default_popup_resolution_key, R.string.default_popup_resolution_value); + return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); } /** * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) */ - public static int getPopupDefaultResolutionIndex(Context context, List videoStreams, String defaultResolution) { + public static int getPopupResolutionIndex(Context context, List videoStreams, String defaultResolution) { return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); } public static int getDefaultAudioFormat(Context context, List audioStreams) { - MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_audio_format_key, R.string.default_audio_format_value); - return getHighestQualityAudioIndex(defaultFormat, audioStreams); - } + MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_audio_format_key, + R.string.default_audio_format_value); - public static int getHighestQualityAudioIndex(List audioStreams) { - if (audioStreams == null || audioStreams.isEmpty()) return -1; - - int highestQualityIndex = 0; - if (audioStreams.size() > 1) for (int i = 1; i < audioStreams.size(); i++) { - AudioStream audioStream = audioStreams.get(i); - if (audioStream.getAverageBitrate() >= audioStreams.get(highestQualityIndex).getAverageBitrate()) highestQualityIndex = i; + // If the user has chosen to limit resolution to conserve mobile data + // usage then we should also limit our audio usage. + if (isLimitingDataUsage(context)) { + return getMostCompactAudioIndex(defaultFormat, audioStreams); + } else { + return getHighestQualityAudioIndex(defaultFormat, audioStreams); } - return highestQualityIndex; - } - - /** - * Get the audio from the list with the highest bitrate - * - * @param audioStreams list the audio streams - * @return audio with highest average bitrate - */ - public static AudioStream getHighestQualityAudio(List audioStreams) { - if (audioStreams == null || audioStreams.isEmpty()) return null; - - return audioStreams.get(getHighestQualityAudioIndex(audioStreams)); - } - - /** - * Get the audio from the list with the highest bitrate - * - * @param audioStreams list the audio streams - * @return index of the audio with the highest average bitrate of the default format - */ - public static int getHighestQualityAudioIndex(MediaFormat defaultFormat, List audioStreams) { - if (audioStreams == null || audioStreams.isEmpty() || defaultFormat == null) return -1; - - int highestQualityIndex = -1; - for (int i = 0; i < audioStreams.size(); i++) { - AudioStream audioStream = audioStreams.get(i); - if (highestQualityIndex == -1 && audioStream.getFormat() == defaultFormat) highestQualityIndex = i; - - if (highestQualityIndex != -1 && audioStream.getFormat() == defaultFormat - && audioStream.getAverageBitrate() > audioStreams.get(highestQualityIndex).getAverageBitrate()) { - highestQualityIndex = i; - } - } - if (highestQualityIndex == -1) highestQualityIndex = getHighestQualityAudioIndex(audioStreams); - return highestQualityIndex; } /** @@ -154,6 +97,50 @@ public final class ListHelper { return getSortedStreamVideosList(defaultFormat, showHigherResolutions, videoStreams, videoOnlyStreams, ascendingOrder); } + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private static String computeDefaultResolution(Context context, int key, int value) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + + // Load the prefered resolution otherwise the best available + String resolution = preferences != null + ? preferences.getString(context.getString(key), context.getString(value)) + : context.getString(R.string.best_resolution_key); + + String maxResolution = getResolutionLimit(context); + if (maxResolution != null && compareVideoStreamResolution(maxResolution, resolution) < 1){ + resolution = maxResolution; + } + return resolution; + } + + /** + * Return the index of the default stream in the list, based on the parameters + * defaultResolution and defaultFormat + * + * @return index of the default resolution&format + */ + static int getDefaultResolutionIndex(String defaultResolution, String bestResolutionKey, + MediaFormat defaultFormat, List videoStreams) { + if (videoStreams == null || videoStreams.isEmpty()) return -1; + + sortStreamList(videoStreams, false); + if (defaultResolution.equals(bestResolutionKey)) { + return 0; + } + + int defaultStreamIndex = getVideoStreamIndex(defaultResolution, defaultFormat, videoStreams); + + // this is actually an error, + // but maybe there is really no stream fitting to the default value. + if (defaultStreamIndex == -1) { + return 0; + } + return defaultStreamIndex; + } + /** * Join the two lists of video streams (video_only and normal videos), and sort them according with default format * chosen by the user @@ -165,7 +152,7 @@ public final class ListHelper { * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest @return the sorted list * @return the sorted list */ - public static List getSortedStreamVideosList(MediaFormat defaultFormat, boolean showHigherResolutions, List videoStreams, List videoOnlyStreams, boolean ascendingOrder) { + static List getSortedStreamVideosList(MediaFormat defaultFormat, boolean showHigherResolutions, List videoStreams, List videoOnlyStreams, boolean ascendingOrder) { ArrayList retList = new ArrayList<>(); HashMap hashMap = new HashMap<>(); @@ -215,36 +202,138 @@ public final class ListHelper { * @param videoStreams list that the sorting will be applied * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest */ - public static void sortStreamList(List videoStreams, final boolean ascendingOrder) { - Collections.sort(videoStreams, new Comparator() { - @Override - public int compare(VideoStream o1, VideoStream o2) { - int res1 = Integer.parseInt(o1.getResolution().replace("0p60", "1").replaceAll("[^\\d.]", "")); - int res2 = Integer.parseInt(o2.getResolution().replace("0p60", "1").replaceAll("[^\\d.]", "")); - - return ascendingOrder ? res1 - res2 : res2 - res1; - } + private static void sortStreamList(List videoStreams, final boolean ascendingOrder) { + Collections.sort(videoStreams, (o1, o2) -> { + int result = compareVideoStreamResolution(o1, o2, VIDEO_FORMAT_QUALITY_RANKING); + return result == 0 ? 0 : (ascendingOrder ? result : -result); }); } - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ + /** + * Get the audio from the list with the highest quality. Format will be ignored if it yields + * no results. + * + * @param audioStreams list the audio streams + * @return index of the audio with the highest average bitrate of the default format + */ + static int getHighestQualityAudioIndex(MediaFormat format, List audioStreams) { + int result = -1; + if (audioStreams != null) { + while(result == -1) { + AudioStream prevStream = null; + for (int idx = 0; idx < audioStreams.size(); idx++) { + AudioStream stream = audioStreams.get(idx); + if ((format == null || stream.getFormat() == format) && + (prevStream == null || compareAudioStreamBitrate(prevStream, stream, + AUDIO_FORMAT_QUALITY_RANKING) < 0)) { + prevStream = stream; + result = idx; + } + } + if (result == -1 && format == null) { + break; + } + format = null; + } + } + return result; + } - private static int getDefaultStreamIndex(String defaultResolution, MediaFormat defaultFormat, List videoStreams) { - int defaultStreamIndex = -1; - for (int i = 0; i < videoStreams.size(); i++) { - VideoStream stream = videoStreams.get(i); - if (defaultStreamIndex == -1 && stream.getResolution().equals(defaultResolution)) defaultStreamIndex = i; + /** + * Get the audio from the list with the lowest bitrate and efficient format. Format will be + * ignored if it yields no results. + * + * @param format The target format type or null if it doesn't matter + * @param audioStreams list the audio streams + * @return index of the audio stream that can produce the most compact results or -1 if not found. + */ + static int getMostCompactAudioIndex(MediaFormat format, List audioStreams) { + int result = -1; + if (audioStreams != null) { + while(result == -1) { + AudioStream prevStream = null; + for (int idx = 0; idx < audioStreams.size(); idx++) { + AudioStream stream = audioStreams.get(idx); + if ((format == null || stream.getFormat() == format) && + (prevStream == null || compareAudioStreamBitrate(prevStream, stream, + AUDIO_FORMAT_EFFICIENCY_RANKING) > 0)) { + prevStream = stream; + result = idx; + } + } + if (result == -1 && format == null) { + break; + } + format = null; + } + } + return result; + } - if (stream.getFormat() == defaultFormat && stream.getResolution().equals(defaultResolution)) { - return i; + /** + * Locates a possible match for the given resolution and format in the provided list. + * In this order: + * 1. Find a format and resolution match + * 2. Find a format and resolution match and ignore the refresh + * 3. Find a resolution match + * 4. Find a resolution match and ignore the refresh + * 5. Find a resolution just below the requested resolution and ignore the refresh + * 6. Give up + */ + static int getVideoStreamIndex(String targetResolution, MediaFormat targetFormat, + List videoStreams) { + int fullMatchIndex = -1; + int fullMatchNoRefreshIndex = -1; + int resMatchOnlyIndex = -1; + int resMatchOnlyNoRefreshIndex = -1; + int lowerResMatchNoRefreshIndex = -1; + String targetResolutionNoRefresh = targetResolution.replaceAll("p\\d+$", "p"); + + for (int idx = 0; idx < videoStreams.size(); idx++) { + MediaFormat format = targetFormat == null ? null : videoStreams.get(idx).getFormat(); + String resolution = videoStreams.get(idx).getResolution(); + String resolutionNoRefresh = resolution.replaceAll("p\\d+$", "p"); + + if (format == targetFormat && resolution.equals(targetResolution)) { + fullMatchIndex = idx; + } + + if (format == targetFormat && resolutionNoRefresh.equals(targetResolutionNoRefresh)) { + fullMatchNoRefreshIndex = idx; + } + + if (resMatchOnlyIndex == -1 && resolution.equals(targetResolution)) { + resMatchOnlyIndex = idx; + } + + if (resMatchOnlyNoRefreshIndex == -1 && resolutionNoRefresh.equals(targetResolutionNoRefresh)) { + resMatchOnlyNoRefreshIndex = idx; + } + + if (lowerResMatchNoRefreshIndex == -1 && compareVideoStreamResolution(resolutionNoRefresh, targetResolutionNoRefresh) < 0) { + lowerResMatchNoRefreshIndex = idx; } } - return defaultStreamIndex; + if (fullMatchIndex != -1) { + return fullMatchIndex; + } + if (fullMatchNoRefreshIndex != -1) { + return fullMatchNoRefreshIndex; + } + if (resMatchOnlyIndex != -1) { + return resMatchOnlyIndex; + } + if (resMatchOnlyNoRefreshIndex != -1) { + return resMatchOnlyNoRefreshIndex; + } + return lowerResMatchNoRefreshIndex; } + /** + * Fetches the desired resolution or returns the default if it is not found. The resolution + * will be reduced if video chocking is active. + */ private static int getDefaultResolutionWithDefaultFormat(Context context, String defaultResolution, List videoStreams) { MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_video_format_key, R.string.default_video_format_value); return getDefaultResolutionIndex(defaultResolution, context.getString(R.string.best_resolution_key), defaultFormat, videoStreams); @@ -280,4 +369,85 @@ public final class ListHelper { } return format; } + + // Compares the quality of two audio streams + private static int compareAudioStreamBitrate(AudioStream streamA, AudioStream streamB, + List formatRanking) { + if (streamA == null) { + return -1; + } + if (streamB == null) { + return 1; + } + if (streamA.getAverageBitrate() < streamB.getAverageBitrate()) { + return -1; + } + if (streamA.getAverageBitrate() > streamB.getAverageBitrate()) { + return 1; + } + + // Same bitrate and format + return formatRanking.indexOf(streamA.getFormat()) - formatRanking.indexOf(streamB.getFormat()); + } + + private static int compareVideoStreamResolution(String r1, String r2) { + int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1") + .replaceAll("[^\\d.]", "")); + int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1") + .replaceAll("[^\\d.]", "")); + return res1 - res2; + } + + // Compares the quality of two video streams. + private static int compareVideoStreamResolution(VideoStream streamA, VideoStream streamB, + List formatRanking) { + if (streamA == null) { + return -1; + } + if (streamB == null) { + return 1; + } + + int resComp = compareVideoStreamResolution(streamA.getResolution(), streamB.getResolution()); + if (resComp != 0) { + return resComp; + } + + // Same bitrate and format + return formatRanking.indexOf(streamA.getFormat()) - formatRanking.indexOf(streamB.getFormat()); + } + + + + private static boolean isLimitingDataUsage(Context context) { + return getResolutionLimit(context) != null; + } + + /** + * The maximum resolution allowed + * @param context App context + * @return maximum resolution allowed or null if there is no maximum + */ + private static String getResolutionLimit(Context context) { + String resolutionLimit = null; + if (!isWifiActive(context)) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + String defValue = context.getString(R.string.limit_data_usage_none_key); + String value = preferences.getString( + context.getString(R.string.limit_mobile_data_usage_key), defValue); + resolutionLimit = value.equals(defValue) ? null : value; + } + return resolutionLimit; + } + + /** + * Are we connected to wifi? + * @param context App context + * @return True if connected to wifi + */ + private static boolean isWifiActive(Context context) + { + ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + return manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI; + } } 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 c48e5ffbc..f02eaae28 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -5,6 +5,7 @@ import android.os.Looper; import android.util.Log; import java.io.File; +import java.io.ObjectInputStream; import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -312,6 +313,13 @@ public class DownloadMission implements Serializable { } } + private void readObject(ObjectInputStream inputStream) + throws java.io.IOException, ClassNotFoundException + { + inputStream.defaultReadObject(); + mListeners = new ArrayList<>(); + } + private void deleteThisFromFile() { new File(getMetaFilename()).delete(); } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java index 37dc64a2c..b53f8aea9 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -2,7 +2,6 @@ package us.shandian.giga.get; import android.util.Log; -import java.io.BufferedInputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; @@ -104,11 +103,11 @@ public class DownloadRunnable implements Runnable { RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw"); f.seek(start); - BufferedInputStream ipt = new BufferedInputStream(conn.getInputStream()); - byte[] buf = new byte[512]; + java.io.InputStream ipt = conn.getInputStream(); + byte[] buf = new byte[64*1024]; while (start < end && mMission.running) { - int len = ipt.read(buf, 0, 512); + int len = ipt.read(buf, 0, buf.length); if (len == -1) { break; diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index 2d44e8c15..de9a16a1b 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -12,6 +12,7 @@ import android.widget.Toast; import org.schabi.newpipe.R; import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -59,17 +60,17 @@ public class Utility { ObjectOutputStream objectOutputStream = null; try { - objectOutputStream = new ObjectOutputStream(new FileOutputStream(fileName)); + objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(fileName))); objectOutputStream.writeObject(serializable); } catch (Exception e) { //nothing to do - } - - if(objectOutputStream != null) { - try { - objectOutputStream.close(); - } catch (Exception e) { - //nothing to do + } finally { + if(objectOutputStream != null) { + try { + objectOutputStream.close(); + } catch (Exception e) { + //nothing to do + } } } } diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index 6e1db563a..320f72ab9 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -111,5 +111,26 @@ android:layout_gravity="end" android:text="@string/open_in_browser" /> + + + + +