diff --git a/.gitignore b/.gitignore index 5c6962be1..40e7d2c03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,15 @@ -.gitignore -.gradle -/local.properties +.gradle/ +local.properties .DS_Store -/build -/captures -/app/app.iml -/.idea -/*.iml +build/ +captures/ +.idea/ +*.iml *~ .weblate *.class +**/debug/ +**/release/ # vscode / eclipse files *.classpath diff --git a/app/.gitignore b/app/.gitignore deleted file mode 100644 index 53edac5e4..000000000 --- a/app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.gitignore -/build -*.iml diff --git a/app/build.gradle b/app/build.gradle index c7176c4bc..488bfa3ba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,8 +17,8 @@ android { resValue "string", "app_name", "NewPipe" minSdkVersion 19 targetSdkVersion 29 - versionCode 978 - versionName "0.21.12" + versionCode 979 + versionName "0.21.13" multiDexEnabled true @@ -107,7 +107,7 @@ ext { icepickVersion = '3.2.0' exoPlayerVersion = '2.12.3' googleAutoServiceVersion = '1.0' - groupieVersion = '2.9.0' + groupieVersion = '2.10.0' markwonVersion = '4.6.2' leakCanaryVersion = '2.5' @@ -190,7 +190,7 @@ dependencies { // name and the commit hash with the commit hash of the (pushed) commit you want to test // This works thanks to JitPack: https://jitpack.io/ implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' - implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.11' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:4f60225ddc' /** Checkstyle **/ checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" @@ -209,14 +209,17 @@ dependencies { implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}" implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' - implementation 'androidx.media:media:1.4.2' + implementation 'androidx.media:media:1.4.3' implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.preference:preference:1.1.1' - implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation "androidx.room:room-runtime:${androidxRoomVersion}" implementation "androidx.room:room-rxjava3:${androidxRoomVersion}" kapt "androidx.room:room-compiler:${androidxRoomVersion}" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + // Newer version specified to prevent accessibility regressions with RecyclerView, see: + // https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01 + implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.webkit:webkit:1.4.0' implementation 'com.google.android.material:material:1.2.1' implementation "androidx.work:work-runtime-ktx:${workVersion}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d92d0b5bf..6a2700596 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -256,6 +256,21 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java index 433c155c2..639443377 100644 --- a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java +++ b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java @@ -51,8 +51,12 @@ import java.util.ArrayList; *
  • {@link #saveState()}
  • *
  • {@link #restoreState(Parcelable, ClassLoader)}
  • * + * + * @deprecated Switch to {@link androidx.viewpager2.widget.ViewPager2} and use + * {@link androidx.viewpager2.adapter.FragmentStateAdapter} instead. */ @SuppressWarnings("deprecation") +@Deprecated public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter { private static final String TAG = "FragmentStatePagerAdapt"; private static final boolean DEBUG = false; @@ -86,9 +90,10 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt private final int mBehavior; private FragmentTransaction mCurTransaction = null; - private final ArrayList mSavedState = new ArrayList(); - private final ArrayList mFragments = new ArrayList(); + private final ArrayList mSavedState = new ArrayList<>(); + private final ArrayList mFragments = new ArrayList<>(); private Fragment mCurrentPrimaryItem = null; + private boolean mExecutingFinishUpdate; /** * Constructor for {@link FragmentStatePagerAdapterMenuWorkaround} @@ -208,7 +213,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt mFragments.set(position, null); mCurTransaction.remove(fragment); - if (fragment == mCurrentPrimaryItem) { + if (fragment.equals(mCurrentPrimaryItem)) { mCurrentPrimaryItem = null; } } @@ -247,7 +252,19 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt @Override public void finishUpdate(@NonNull final ViewGroup container) { if (mCurTransaction != null) { - mCurTransaction.commitNowAllowingStateLoss(); + // We drop any transactions that attempt to be committed + // from a re-entrant call to finishUpdate(). We need to + // do this as a workaround for Robolectric running measure/layout + // calls inline rather than allowing them to be posted + // as they would on a real device. + if (!mExecutingFinishUpdate) { + try { + mExecutingFinishUpdate = true; + mCurTransaction.commitNowAllowingStateLoss(); + } finally { + mExecutingFinishUpdate = false; + } + } mCurTransaction = null; } } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 3ec23fd62..3601e5c61 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -262,4 +262,5 @@ public class App extends MultiDexApplication { protected boolean isDisposedRxExceptionsReported() { return false; } + } diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java index 0be427648..16ddb8376 100644 --- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -21,7 +21,6 @@ public abstract class BaseFragment extends Fragment { //These values are used for controlling fragments when they are part of the frontpage @State protected boolean useAsFrontPage = false; - private boolean mIsVisibleToUser = false; public void useAsFrontPage(final boolean value) { useAsFrontPage = value; @@ -85,12 +84,6 @@ public abstract class BaseFragment extends Fragment { AppWatcher.INSTANCE.getObjectWatcher().watch(this); } - @Override - public void setUserVisibleHint(final boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - mIsVisibleToUser = isVisibleToUser; - } - /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ @@ -109,8 +102,7 @@ public abstract class BaseFragment extends Fragment { if (DEBUG) { Log.d(TAG, "setTitle() called with: title = [" + title + "]"); } - if ((!useAsFrontPage || mIsVisibleToUser) - && (activity != null && activity.getSupportActionBar() != null)) { + if (!useAsFrontPage && activity != null && activity.getSupportActionBar() != null) { activity.getSupportActionBar().setDisplayShowTitleEnabled(true); activity.getSupportActionBar().setTitle(title); } diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java index 64874cd93..9e43394ac 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java @@ -7,7 +7,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.Signature; -import android.net.ConnectivityManager; import android.net.Uri; import android.util.Log; @@ -15,7 +14,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; -import androidx.core.content.ContextCompat; import androidx.core.content.pm.PackageInfoCompat; import androidx.preference.PreferenceManager; @@ -48,7 +46,8 @@ public final class CheckForNewAppVersion extends IntentService { private static final boolean DEBUG = MainActivity.DEBUG; private static final String TAG = CheckForNewAppVersion.class.getSimpleName(); - private static final String GITHUB_APK_SHA1 + // Public key of the certificate that is used in NewPipe release versions + private static final String RELEASE_CERT_PUBLIC_KEY_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 NEWPIPE_API_URL = "https://newpipe.net/api/data.json"; @@ -129,44 +128,37 @@ public final class CheckForNewAppVersion extends IntentService { final String versionName, final String apkLocationUrl, final int versionCode) { - final int notificationId = 2000; - - if (BuildConfig.VERSION_CODE < versionCode) { - // A pending intent to open the apk location url in the browser. - final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final PendingIntent pendingIntent - = PendingIntent.getActivity(application, 0, intent, 0); - - final String channelId = application - .getString(R.string.app_update_notification_channel_id); - final NotificationCompat.Builder notificationBuilder - = new NotificationCompat.Builder(application, channelId) - .setSmallIcon(R.drawable.ic_newpipe_update) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentIntent(pendingIntent) - .setAutoCancel(true) - .setContentTitle(application - .getString(R.string.app_update_notification_content_title)) - .setContentText(application - .getString(R.string.app_update_notification_content_text) - + " " + versionName); - - final NotificationManagerCompat notificationManager - = NotificationManagerCompat.from(application); - notificationManager.notify(notificationId, notificationBuilder.build()); + if (BuildConfig.VERSION_CODE >= versionCode) { + return; } + + // A pending intent to open the apk location url in the browser. + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final PendingIntent pendingIntent + = PendingIntent.getActivity(application, 0, intent, 0); + + final String channelId = application + .getString(R.string.app_update_notification_channel_id); + final NotificationCompat.Builder notificationBuilder + = new NotificationCompat.Builder(application, channelId) + .setSmallIcon(R.drawable.ic_newpipe_update) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setContentTitle(application + .getString(R.string.app_update_notification_content_title)) + .setContentText(application + .getString(R.string.app_update_notification_content_text) + + " " + versionName); + + final NotificationManagerCompat notificationManager + = NotificationManagerCompat.from(application); + notificationManager.notify(2000, notificationBuilder.build()); } - private static boolean isConnected(@NonNull final App app) { - final ConnectivityManager connectivityManager = - ContextCompat.getSystemService(app, ConnectivityManager.class); - return connectivityManager != null && connectivityManager.getActiveNetworkInfo() != null - && connectivityManager.getActiveNetworkInfo().isConnected(); - } - - public static boolean isGithubApk(@NonNull final App app) { - return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1); + public static boolean isReleaseApk(@NonNull final App app) { + return getCertificateSHA1Fingerprint(app).equals(RELEASE_CERT_PUBLIC_KEY_SHA1); } private void checkNewVersion() throws IOException, ReCaptchaException { @@ -175,9 +167,8 @@ public final class CheckForNewAppVersion extends IntentService { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); final NewVersionManager manager = new NewVersionManager(); - // Check if user has enabled/disabled update checking - // and if the current apk is a github one or not. - if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) { + // Check if the current apk is a github one or not. + if (!isReleaseApk(app)) { return; } @@ -213,6 +204,7 @@ public final class CheckForNewAppVersion extends IntentService { // Parse the json from the response. try { + final JsonObject githubStableObject = JsonParser.object() .from(response.responseBody()).getObject("flavors") .getObject("github").getObject("stable"); @@ -235,6 +227,23 @@ public final class CheckForNewAppVersion extends IntentService { } } + /** + * Start a new service which + * checks if all conditions for performing a version check are met, + * fetches the API endpoint {@link #NEWPIPE_API_URL} containing info + * about the latest NewPipe version + * and displays a notification about ana available update. + *
    + * Following conditions need to be met, before data is request from the server: + *
      + *
    • The app is signed with the correct signing key (by TeamNewPipe / schabi). + * If the signing key differs from the one used upstream, the update cannot be installed.
    • + *
    • The user enabled searching for and notifying about updates in the settings.
    • + *
    • The app did not recently check for updates. + * We do not want to make unnecessary connections and DOS our servers.
    • + *
    + * Must not be executed when the app is in background. + */ public static void startNewVersionCheckService() { final Intent intent = new Intent(App.getApp().getApplicationContext(), CheckForNewAppVersion.class); diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 46c256f76..dda0dd0e4 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -165,13 +165,25 @@ public class MainActivity extends AppCompatActivity { } openMiniPlayerUponPlayerStarted(); - // Check for new version - startNewVersionCheckService(); - // shedule worker for checking for new streans and creating corresponding notifications NotificationWorker.schedule(this); } + @Override + protected void onPostCreate(final Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + + final App app = App.getApp(); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); + + if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) { + // Start the service which is checking all conditions + // and eventually searching for a new version. + // The service searching for a new NewPipe version must not be started in background. + startNewVersionCheckService(); + } + } + private void setupDrawer() throws ExtractionException { addDrawerMenuForCurrentService(); diff --git a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java index ef24937fe..fcad0b612 100644 --- a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java +++ b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java @@ -18,6 +18,9 @@ import org.schabi.newpipe.util.NavigationHelper; import java.util.Collections; public final class QueueItemMenuUtil { + private QueueItemMenuUtil() { + } + public static void openPopupMenu(final PlayQueue playQueue, final PlayQueueItem item, final View view, @@ -57,6 +60,13 @@ public final class QueueItemMenuUtil { ); return true; + case R.id.menu_item_channel_details: + // An intent must be used here. + // Opening with FragmentManager transactions is not working, + // as PlayQueueActivity doesn't use fragments. + NavigationHelper.openChannelFragmentUsingIntent(context, item.getServiceId(), + item.getUploaderUrl(), item.getUploader()); + return true; case R.id.menu_item_share: shareText(context, item.getTitle(), item.getUrl(), item.getThumbnailUrl()); @@ -67,6 +77,4 @@ public final class QueueItemMenuUtil { popupMenu.show(); } - - private QueueItemMenuUtil() { } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java index 0cccfa4fe..fe4eef37a 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java @@ -20,8 +20,8 @@ public class BlankFragment extends BaseFragment { } @Override - public void setUserVisibleHint(final boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); + public void onResume() { + super.onResume(); setTitle("NewPipe"); // leave this inline. Will make it harder for copy cats. // If you are a Copy cat FUCK YOU. 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 bda16849a..5552cf73f 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 @@ -1181,7 +1181,7 @@ public final class VideoDetailFragment addVideoPlayerView(); final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(), - MainPlayer.class, queue, autoPlayEnabled); + MainPlayer.class, queue, true, autoPlayEnabled); ContextCompat.startForegroundService(activity, playerIntent); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index c30b6fc05..037eb8f94 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -143,7 +143,7 @@ public abstract class BaseListFragment extends BaseStateFragment final View focusedItem = itemsList.getFocusedChild(); final RecyclerView.ViewHolder itemHolder = itemsList.findContainingViewHolder(focusedItem); - return itemHolder.getAdapterPosition(); + return itemHolder.getBindingAdapterPosition(); } catch (final NullPointerException e) { return -1; } 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 fe0122ea5..5ab54f98a 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 @@ -103,11 +103,9 @@ public class ChannelFragment extends BaseListInfoFragment } @Override - public void setUserVisibleHint(final boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - if (activity != null - && useAsFrontPage - && isVisibleToUser) { + public void onResume() { + super.onResume(); + if (activity != null && useAsFrontPage) { setTitle(currentInfo != null ? currentInfo.getName() : name); } } 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 f37f487bf..c25f18e8b 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 @@ -99,9 +99,12 @@ public class KioskFragment extends BaseListInfoFragment { } @Override - public void setUserVisibleHint(final boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - if (useAsFrontPage && isVisibleToUser && activity != null) { + public void onResume() { + super.onResume(); + if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) { + reloadContent(); + } + if (useAsFrontPage && activity != null) { try { setTitle(kioskTranslatedName); } catch (final Exception e) { @@ -117,15 +120,6 @@ public class KioskFragment extends BaseListInfoFragment { return inflater.inflate(R.layout.fragment_kiosk, container, false); } - @Override - public void onResume() { - super.onResume(); - - if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) { - reloadContent(); - } - } - /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ 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 7de212383..d4d73f74f 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 @@ -1088,7 +1088,7 @@ public class SearchFragment extends BaseListFragment { defaultPreferences.edit() .putBoolean(getString(R.string.update_app_key), (boolean) checkForUpdates).apply(); - if ((boolean) checkForUpdates) { - // Search for updates immediately when update checks are enabled. - // Reset the expire time. This is necessary to check for an update immediately. - defaultPreferences.edit() - .putLong(getString(R.string.update_expiry_key), 0).apply(); - startNewVersionCheckService(); - } + if ((boolean) checkForUpdates) { + checkNewVersionNow(); + } return true; }; + private final Preference.OnPreferenceClickListener manualUpdateClick + = preference -> { + Toast.makeText(getContext(), R.string.checking_updates_toast, Toast.LENGTH_SHORT).show(); + checkNewVersionNow(); + return true; + }; + + private void checkNewVersionNow() { + // Search for updates immediately when update checks are enabled. + // Reset the expire time. This is necessary to check for an update immediately. + defaultPreferences.edit() + .putLong(getString(R.string.update_expiry_key), 0).apply(); + startNewVersionCheckService(); + } + @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResource(R.xml.update_settings); - final String updateToggleKey = getString(R.string.update_app_key); - findPreference(updateToggleKey).setOnPreferenceChangeListener(updatePreferenceChange); + findPreference(getString(R.string.update_app_key)) + .setOnPreferenceChangeListener(updatePreferenceChange); + findPreference(getString(R.string.manual_update_key)) + .setOnPreferenceClickListener(manualUpdateClick); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java index 6e50765ba..c9eb42fca 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java @@ -299,8 +299,8 @@ public class ChooseTabsFragment extends Fragment { return false; } - final int sourceIndex = source.getAdapterPosition(); - final int targetIndex = target.getAdapterPosition(); + final int sourceIndex = source.getBindingAdapterPosition(); + final int targetIndex = target.getBindingAdapterPosition(); selectedTabsAdapter.swapItems(sourceIndex, targetIndex); return true; } @@ -318,7 +318,7 @@ public class ChooseTabsFragment extends Fragment { @Override public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, final int swipeDir) { - final int position = viewHolder.getAdapterPosition(); + final int position = viewHolder.getBindingAdapterPosition(); tabList.remove(position); selectedTabsAdapter.notifyItemRemoved(position); 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 f70002409..bc6cdca8d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -77,7 +77,8 @@ public final class NavigationHelper { @NonNull public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, - @Nullable final PlayQueue playQueue) { + @Nullable final PlayQueue playQueue, + final boolean resumePlayback) { final Intent intent = new Intent(context, targetClazz); if (playQueue != null) { @@ -87,6 +88,7 @@ public final class NavigationHelper { } } intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal()); + intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback); return intent; } @@ -95,8 +97,9 @@ public final class NavigationHelper { public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, @Nullable final PlayQueue playQueue, + final boolean resumePlayback, final boolean playWhenReady) { - return getPlayerIntent(context, targetClazz, playQueue) + return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) .putExtra(Player.PLAY_WHEN_READY, playWhenReady); } @@ -104,7 +107,14 @@ public final class NavigationHelper { public static Intent getPlayerEnqueueIntent(@NonNull final Context context, @NonNull final Class targetClazz, @Nullable final PlayQueue playQueue) { - return getPlayerIntent(context, targetClazz, playQueue) + // when enqueueing `resumePlayback` is always `false` since: + // - if there is a video already playing, the value of `resumePlayback` just doesn't make + // any difference. + // - if there is nothing already playing, it is useful for the enqueue action to have a + // slightly different behaviour than the normal play action: the latter resumes playback, + // the former doesn't. (note that enqueue can be triggered when nothing is playing only + // by long pressing the video detail fragment, playlist or channel controls + return getPlayerIntent(context, targetClazz, playQueue, false) .putExtra(Player.ENQUEUE, true); } @@ -112,7 +122,8 @@ public final class NavigationHelper { public static Intent getPlayerEnqueueNextIntent(@NonNull final Context context, @NonNull final Class targetClazz, @Nullable final PlayQueue playQueue) { - return getPlayerIntent(context, targetClazz, playQueue) + // see comment in `getPlayerEnqueueIntent` as to why `resumePlayback` is false + return getPlayerIntent(context, targetClazz, playQueue, false) .putExtra(Player.ENQUEUE_NEXT, true); } @@ -495,6 +506,27 @@ public final class NavigationHelper { context.startActivity(intent); } + /** + * Opens {@link ChannelFragment}. + * Use this instead of {@link #openChannelFragment(FragmentManager, int, String, String)} + * when no fragments are used / no FragmentManager is available. + * @param context + * @param serviceId + * @param url + * @param title + */ + public static void openChannelFragmentUsingIntent(final Context context, + final int serviceId, + final String url, + @NonNull final String title) { + final Intent intent = getOpenIntent(context, url, serviceId, + StreamingService.LinkType.CHANNEL); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Constants.KEY_TITLE, title); + + context.startActivity(intent); + } + public static void openMainActivity(final Context context) { final Intent mIntent = new Intent(context, MainActivity.class); mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/app/src/main/java/org/schabi/newpipe/util/NewPipeTextViewHelper.java b/app/src/main/java/org/schabi/newpipe/util/NewPipeTextViewHelper.java new file mode 100644 index 000000000..cf1a9a03a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/NewPipeTextViewHelper.java @@ -0,0 +1,61 @@ +package org.schabi.newpipe.util; + +import android.content.Context; +import android.text.Selection; +import android.text.Spannable; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.schabi.newpipe.util.external_communication.ShareUtils; +import org.schabi.newpipe.views.NewPipeEditText; +import org.schabi.newpipe.views.NewPipeTextView; + +public final class NewPipeTextViewHelper { + private NewPipeTextViewHelper() { + } + + /** + * Share the selected text of {@link NewPipeTextView NewPipeTextViews} and + * {@link NewPipeEditText NewPipeEditTexts} with + * {@link ShareUtils#shareText(Context, String, String)}. + * + *

    + * This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when + * using the {@code Share} command of the popup menu which appears when selecting text. + *

    + * + * @param textView the {@link TextView} on which sharing the selected text. It should be a + * {@link NewPipeTextView} or a {@link NewPipeEditText} (even if + * {@link TextView standard TextViews} are supported). + */ + public static void shareSelectedTextWithShareUtils(@NonNull final TextView textView) { + final CharSequence textViewText = textView.getText(); + shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText)); + if (textViewText instanceof Spannable) { + Selection.setSelection((Spannable) textViewText, textView.getSelectionEnd()); + } + } + + @Nullable + private static CharSequence getSelectedText(@NonNull final TextView textView, + @Nullable final CharSequence text) { + if (!textView.hasSelection() || text == null) { + return null; + } + + final int start = textView.getSelectionStart(); + final int end = textView.getSelectionEnd(); + return String.valueOf(start > end ? text.subSequence(end, start) + : text.subSequence(start, end)); + } + + private static void shareSelectedTextIfNotNullAndNotEmpty( + @NonNull final TextView textView, + @Nullable final CharSequence selectedText) { + if (selectedText != null && selectedText.length() != 0) { + ShareUtils.shareText(textView.getContext(), "", selectedText.toString()); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/NewPipeEditText.java b/app/src/main/java/org/schabi/newpipe/views/NewPipeEditText.java new file mode 100644 index 000000000..2adc28d0e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeEditText.java @@ -0,0 +1,45 @@ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatEditText; + +import org.schabi.newpipe.util.NewPipeTextViewHelper; +import org.schabi.newpipe.util.external_communication.ShareUtils; + +/** + * An {@link AppCompatEditText} which uses {@link ShareUtils#shareText(Context, String, String)} + * when sharing selected text by using the {@code Share} command of the floating actions. + *

    + * This allows NewPipe to show Android share sheet instead of EMUI share sheet when sharing text + * from {@link AppCompatEditText} on EMUI devices. + *

    + */ +public class NewPipeEditText extends AppCompatEditText { + + public NewPipeEditText(@NonNull final Context context) { + super(context); + } + + public NewPipeEditText(@NonNull final Context context, @Nullable final AttributeSet attrs) { + super(context, attrs); + } + + public NewPipeEditText(@NonNull final Context context, + @Nullable final AttributeSet attrs, + final int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public boolean onTextContextMenuItem(final int id) { + if (id == android.R.id.shareText) { + NewPipeTextViewHelper.shareSelectedTextWithShareUtils(this); + return true; + } + return super.onTextContextMenuItem(id); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/NewPipeTextView.java b/app/src/main/java/org/schabi/newpipe/views/NewPipeTextView.java new file mode 100644 index 000000000..8fdac32db --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeTextView.java @@ -0,0 +1,45 @@ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; + +import org.schabi.newpipe.util.NewPipeTextViewHelper; +import org.schabi.newpipe.util.external_communication.ShareUtils; + +/** + * An {@link AppCompatTextView} which uses {@link ShareUtils#shareText(Context, String, String)} + * when sharing selected text by using the {@code Share} command of the floating actions. + *

    + * This allows NewPipe to show Android share sheet instead of EMUI share sheet when sharing text + * from {@link AppCompatTextView} on EMUI devices. + *

    + */ +public class NewPipeTextView extends AppCompatTextView { + + public NewPipeTextView(@NonNull final Context context) { + super(context); + } + + public NewPipeTextView(@NonNull final Context context, @Nullable final AttributeSet attrs) { + super(context, attrs); + } + + public NewPipeTextView(@NonNull final Context context, + @Nullable final AttributeSet attrs, + final int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public boolean onTextContextMenuItem(final int id) { + if (id == android.R.id.shareText) { + NewPipeTextViewHelper.shareSelectedTextWithShareUtils(this); + return true; + } + return super.onTextContextMenuItem(id); + } +} diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index 4b79d92f6..c2359552e 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -60,7 +60,7 @@ android:padding="8dp" tools:ignore="RtlHardcoded,RtlSymmetry"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -82,7 +82,7 @@ - - - - - - - - + android:layout_above="@+id/playback_controls"> - + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:orientation="vertical" + android:padding="8dp" + tools:ignore="RtlHardcoded,RtlSymmetry"> - + + + + + - + android:layout_centerInParent="true" + android:background="#c0000000" + android:paddingLeft="30dp" + android:paddingTop="5dp" + android:paddingRight="30dp" + android:paddingBottom="5dp" + android:textColor="@android:color/white" + android:textSize="22sp" + android:textStyle="bold" + android:visibility="gone" + tools:ignore="RtlHardcoded" + tools:text="1:06:29" + tools:visibility="visible" /> + + android:paddingRight="12dp"> - - - + diff --git a/app/src/main/res/layout/channel_header.xml b/app/src/main/res/layout/channel_header.xml index aebb5d613..9366faf2c 100644 --- a/app/src/main/res/layout/channel_header.xml +++ b/app/src/main/res/layout/channel_header.xml @@ -49,7 +49,7 @@ tools:visibility="visible" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -85,14 +85,14 @@ android:layout_gravity="end" android:text="@string/give_back" /> - - @@ -105,14 +105,14 @@ android:layout_gravity="end" android:text="@string/open_in_browser" /> - - diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index 873f3c884..9e2257539 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -30,7 +30,7 @@ android:visibility="gone" tools:visibility="visible"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> - - - - --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + tools:listitem="@layout/select_channel_item" /> - - - + tools:listitem="@layout/select_kiosk_item" /> diff --git a/app/src/main/res/layout/select_kiosk_item.xml b/app/src/main/res/layout/select_kiosk_item.xml index 6cd04ae34..680767bba 100644 --- a/app/src/main/res/layout/select_kiosk_item.xml +++ b/app/src/main/res/layout/select_kiosk_item.xml @@ -22,7 +22,7 @@ app:tint="@color/contrastColor" tools:ignore="RtlHardcoded" /> - - - - diff --git a/app/src/main/res/layout/settings_category_header_title.xml b/app/src/main/res/layout/settings_category_header_title.xml index 679b9048c..c7d6920b0 100644 --- a/app/src/main/res/layout/settings_category_header_title.xml +++ b/app/src/main/res/layout/settings_category_header_title.xml @@ -1,5 +1,5 @@ - - - - - - - - - - + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index daf999abf..0f37ff1f0 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -384,6 +384,7 @@ update_app_key + manual_update_key update_pref_screen_key update_expiry_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 710425217..e67016142 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -538,6 +538,8 @@ Updates Show a notification to prompt app update when a new version is available + Check for updates + Manually check for new versions Minimize on app switch Action when switching to other app from main video player — %s @@ -568,6 +570,7 @@ recovering Queue Action denied by the system + Checking for updates… Download failed diff --git a/app/src/main/res/xml/update_settings.xml b/app/src/main/res/xml/update_settings.xml index adaa47352..ef121ec4e 100644 --- a/app/src/main/res/xml/update_settings.xml +++ b/app/src/main/res/xml/update_settings.xml @@ -12,4 +12,11 @@ app:singleLineTitle="false" app:iconSpaceReserved="false" /> + + diff --git a/build.gradle b/build.gradle index 4617611a8..1bcddd7cc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.5.30' + ext.kotlin_version = '1.5.31' repositories { google() mavenCentral() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.android.tools.build:gradle:7.0.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/fastlane/metadata/android/en-US/changelogs/979.txt b/fastlane/metadata/android/en-US/changelogs/979.txt new file mode 100644 index 000000000..520d92993 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/979.txt @@ -0,0 +1,2 @@ +- Fixed resuming playback +- Improvements to ensure that the service which determines if NewPipe should check for a new version checks is not started in background \ No newline at end of file