1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-12-03 06:58:05 +00:00

Merge branch 'dev' into Refactor_VideoPlayerUi

This commit is contained in:
Isira Seneviratne
2022-11-14 08:59:03 +05:30
108 changed files with 1496 additions and 440 deletions

View File

@@ -1,6 +1,5 @@
package org.schabi.newpipe
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.util.Log
@@ -18,6 +17,7 @@ import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonParserException
import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.util.PendingIntentCompat
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
@@ -49,7 +49,7 @@ class NewVersionWorker(
// A pending intent to open the apk location url in the browser.
val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri())
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntent.getActivity(app, 0, intent, 0)
val pendingIntent = PendingIntentCompat.getActivity(app, 0, intent, 0)
val channelId = app.getString(R.string.app_update_notification_channel_id)
val notificationBuilder = NotificationCompat.Builder(app, channelId)
.setSmallIcon(R.drawable.ic_newpipe_update)

View File

@@ -48,7 +48,10 @@ abstract class FeedDAO {
ON s.uid = f.stream_id
LEFT JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
ON (
:groupId <> ${FeedGroupEntity.GROUP_ALL_ID}
AND fgs.subscription_id = f.subscription_id
)
WHERE (
:groupId = ${FeedGroupEntity.GROUP_ALL_ID}

View File

@@ -5,7 +5,6 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.view.View
import android.widget.Toast
import androidx.core.app.NotificationCompat
@@ -13,6 +12,7 @@ import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import org.schabi.newpipe.R
import org.schabi.newpipe.util.PendingIntentCompat
/**
* This class contains all of the methods that should be used to let the user know that an error has
@@ -104,11 +104,6 @@ class ErrorUtil {
*/
@JvmStatic
fun createNotification(context: Context, errorInfo: ErrorInfo) {
var pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
pendingIntentFlags = pendingIntentFlags or PendingIntent.FLAG_IMMUTABLE
}
val notificationBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(
context,
@@ -119,11 +114,11 @@ class ErrorUtil {
.setContentText(context.getString(errorInfo.messageStringId))
.setAutoCancel(true)
.setContentIntent(
PendingIntent.getActivity(
PendingIntentCompat.getActivity(
context,
0,
getErrorActivityIntent(context, errorInfo),
pendingIntentFlags
PendingIntent.FLAG_UPDATE_CURRENT
)
)

View File

@@ -248,6 +248,7 @@ public final class VideoDetailFragment
autoPlayEnabled = true; // forcefully start playing
openVideoPlayerAutoFullscreen();
}
updateOverlayPlayQueueButtonVisibility();
}
@Override
@@ -337,6 +338,8 @@ public final class VideoDetailFragment
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
updateOverlayPlayQueueButtonVisibility();
setupBrightness();
if (tabSettingsChanged) {
@@ -639,19 +642,7 @@ public final class VideoDetailFragment
? View.VISIBLE
: View.GONE
);
if (DeviceUtils.isTv(getContext())) {
// remove ripple effects from detail controls
final int transparent = ContextCompat.getColor(requireContext(),
R.color.transparent_background_color);
binding.detailControlsPlaylistAppend.setBackgroundColor(transparent);
binding.detailControlsBackground.setBackgroundColor(transparent);
binding.detailControlsPopup.setBackgroundColor(transparent);
binding.detailControlsDownload.setBackgroundColor(transparent);
binding.detailControlsShare.setBackgroundColor(transparent);
binding.detailControlsOpenInBrowser.setBackgroundColor(transparent);
binding.detailControlsPlayWithKodi.setBackgroundColor(transparent);
}
accommodateForTvAndDesktopMode();
}
@Override
@@ -1820,6 +1811,14 @@ public final class VideoDetailFragment
+ title + "], playQueue = [" + playQueue + "]");
}
// Register broadcast receiver to listen to playQueue changes
// and hide the overlayPlayQueueButton when the playQueue is empty / destroyed.
if (playQueue != null && playQueue.getBroadcastReceiver() != null) {
playQueue.getBroadcastReceiver().subscribe(
event -> updateOverlayPlayQueueButtonVisibility()
);
}
// This should be the only place where we push data to stack.
// It will allow to have live instance of PlayQueue with actual information about
// deleted/added items inside Channel/Playlist queue and makes possible to have
@@ -1926,6 +1925,7 @@ public final class VideoDetailFragment
currentInfo.getUploaderName(),
currentInfo.getThumbnailUrl());
}
updateOverlayPlayQueueButtonVisibility();
}
@Override
@@ -2106,6 +2106,30 @@ public final class VideoDetailFragment
}
}
/**
* Make changes to the UI to accommodate for better usability on bigger screens such as TVs
* or in Android's desktop mode (DeX etc).
*/
private void accommodateForTvAndDesktopMode() {
if (DeviceUtils.isTv(getContext())) {
// remove ripple effects from detail controls
final int transparent = ContextCompat.getColor(requireContext(),
R.color.transparent_background_color);
binding.detailControlsPlaylistAppend.setBackgroundColor(transparent);
binding.detailControlsBackground.setBackgroundColor(transparent);
binding.detailControlsPopup.setBackgroundColor(transparent);
binding.detailControlsDownload.setBackgroundColor(transparent);
binding.detailControlsShare.setBackgroundColor(transparent);
binding.detailControlsOpenInBrowser.setBackgroundColor(transparent);
binding.detailControlsPlayWithKodi.setBackgroundColor(transparent);
}
if (DeviceUtils.isDesktopMode(getContext())) {
// Remove the "hover" overlay (since it is visible on all mouse events and interferes
// with the video content being played)
binding.detailThumbnailRootLayout.setForeground(null);
}
}
private void checkLandscape() {
if ((!player.isPlaying() && player.getPlayQueue() != playQueue)
|| player.getPlayQueue() == null) {
@@ -2392,6 +2416,18 @@ public final class VideoDetailFragment
});
}
private void updateOverlayPlayQueueButtonVisibility() {
final boolean isPlayQueueEmpty =
player == null // no player => no play queue :)
|| player.getPlayQueue() == null
|| player.getPlayQueue().isEmpty();
if (binding != null) {
// binding is null when rotating the device...
binding.overlayPlayQueueButton.setVisibility(
isPlayQueueEmpty ? View.GONE : View.VISIBLE);
}
}
private void updateOverlayData(@Nullable final String overlayTitle,
@Nullable final String uploader,
@Nullable final String thumbnailUrl) {

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe.local.feed.notifications
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
@@ -20,6 +19,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.PendingIntentCompat
import org.schabi.newpipe.util.PicassoHelper
/**
@@ -70,16 +70,13 @@ class NotificationHelper(val context: Context) {
// open the channel page when clicking on the notification
builder.setContentIntent(
PendingIntent.getActivity(
PendingIntentCompat.getActivity(
context,
data.pseudoId,
NavigationHelper
.getChannelIntent(context, data.listInfo.serviceId, data.listInfo.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
PendingIntent.FLAG_IMMUTABLE
else
0
0
)
)

View File

@@ -19,7 +19,6 @@
package org.schabi.newpipe.local.feed.service
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
@@ -43,6 +42,7 @@ import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import org.schabi.newpipe.util.PendingIntentCompat
import java.util.concurrent.TimeUnit
class FeedLoadService : Service() {
@@ -152,12 +152,8 @@ class FeedLoadService : Service() {
private lateinit var notificationBuilder: NotificationCompat.Builder
private fun createNotification(): NotificationCompat.Builder {
val cancelActionIntent = PendingIntent.getBroadcast(
this,
NOTIFICATION_ID,
Intent(ACTION_CANCEL),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
)
val cancelActionIntent =
PendingIntentCompat.getBroadcast(this, NOTIFICATION_ID, Intent(ACTION_CANCEL), 0)
return NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)

View File

@@ -27,7 +27,7 @@ import com.xwray.groupie.viewbinding.GroupieViewHolder
import icepick.State
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
import org.schabi.newpipe.databinding.DialogTitleBinding
import org.schabi.newpipe.databinding.FeedItemCarouselBinding
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
@@ -254,7 +254,11 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java]
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let(this::handleFeedGroups) }
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) {
it?.let { (groups, listViewMode) ->
handleFeedGroups(groups, listViewMode)
}
}
setupInitialLayout()
}
@@ -276,14 +280,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
}
carouselAdapter.setOnItemLongClickListener { item, _ ->
if ((
item is FeedGroupCardItem &&
item.groupId == FeedGroupEntity.GROUP_ALL_ID
) ||
(
item is FeedGroupCardGridItem &&
item.groupId == FeedGroupEntity.GROUP_ALL_ID
)
if ((item is FeedGroupCardItem && item.groupId == GROUP_ALL_ID) ||
(item is FeedGroupCardGridItem && item.groupId == GROUP_ALL_ID)
) {
return@setOnItemLongClickListener false
}
@@ -411,17 +409,12 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
}
private fun handleFeedGroups(groups: List<Group>) {
val listViewMode = viewModel.getListViewMode()
private fun handleFeedGroups(groups: List<Group>, listViewMode: Boolean) {
if (feedGroupsCarouselState != null) {
feedGroupsCarousel.onRestoreInstanceState(feedGroupsCarouselState)
feedGroupsCarouselState = null
}
feedGroupsCarousel.listViewMode = listViewMode
feedGroupsSortMenuItem.showSortButton = groups.size > 1
feedGroupsSortMenuItem.listViewMode = listViewMode
binding.itemsList.post {
if (context == null) {
// since this part was posted to the next UI cycle, the fragment might have been
@@ -429,6 +422,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
return@post
}
feedGroupsCarousel.listViewMode = listViewMode
feedGroupsSortMenuItem.showSortButton = groups.size > 1
feedGroupsSortMenuItem.listViewMode = listViewMode
feedGroupsCarousel.notifyChanged(FeedGroupCarouselItem.PAYLOAD_UPDATE_LIST_VIEW_MODE)
feedGroupsSortMenuItem.notifyChanged(GroupsHeader.PAYLOAD_UPDATE_ICONS)
@@ -437,10 +433,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
clear()
if (listViewMode) {
add(FeedGroupAddNewItem())
add(FeedGroupCardItem(-1, getString(R.string.all), FeedGroupIcon.RSS))
add(FeedGroupCardItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.RSS))
} else {
add(FeedGroupAddNewGridItem())
add(FeedGroupCardGridItem(-1, getString(R.string.all), FeedGroupIcon.RSS))
add(FeedGroupCardGridItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.RSS))
}
addAll(groups)
}

View File

@@ -27,9 +27,9 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica
private val listViewModeFlowable = listViewMode.distinctUntilChanged()
private val mutableStateLiveData = MutableLiveData<SubscriptionState>()
private val mutableFeedGroupsLiveData = MutableLiveData<List<Group>>()
private val mutableFeedGroupsLiveData = MutableLiveData<Pair<List<Group>, Boolean>>()
val stateLiveData: LiveData<SubscriptionState> = mutableStateLiveData
val feedGroupsLiveData: LiveData<List<Group>> = mutableFeedGroupsLiveData
val feedGroupsLiveData: LiveData<Pair<List<Group>, Boolean>> = mutableFeedGroupsLiveData
private var feedGroupItemsDisposable = Flowable
.combineLatest(
@@ -39,7 +39,10 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica
)
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.map { (feedGroups, listViewMode) ->
feedGroups.map(if (listViewMode) ::FeedGroupCardItem else ::FeedGroupCardGridItem)
Pair(
feedGroups.map(if (listViewMode) ::FeedGroupCardItem else ::FeedGroupCardGridItem),
listViewMode
)
}
.subscribeOn(Schedulers.io())
.subscribe(

View File

@@ -10,7 +10,7 @@ import com.xwray.groupie.viewbinding.GroupieViewHolder
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FeedItemCarouselBinding
import org.schabi.newpipe.util.DeviceUtils
import java.lang.Integer.max
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCount
class FeedGroupCarouselItem(
private val carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>,
@@ -71,10 +71,7 @@ class FeedGroupCarouselItem(
carouselLayoutManager = if (listViewMode) {
LinearLayoutManager(context)
} else {
GridLayoutManager(
context,
max(1, viewBinding.recyclerView.width / DeviceUtils.dpToPx(112, context))
)
GridLayoutManager(context, getGridSpanCount(context, DeviceUtils.dpToPx(112, context)))
}
viewBinding.recyclerView.apply {

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe.player.notification;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.graphics.Bitmap;
@@ -22,6 +21,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PendingIntentCompat;
import java.util.List;
import java.util.Objects;
@@ -133,8 +133,8 @@ public final class NotificationUtil {
R.color.dark_background_color))
.setColorized(player.getPrefs().getBoolean(
player.getContext().getString(R.string.notification_colorize_key), true))
.setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
.setDeleteIntent(PendingIntentCompat.getBroadcast(player.getContext(),
NOTIFICATION_ID, new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
// set the initial value for the video thumbnail, updatable with updateNotificationThumbnail
setLargeIcon(builder);
@@ -151,7 +151,7 @@ public final class NotificationUtil {
}
// also update content intent, in case the user switched players
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
notificationBuilder.setContentIntent(PendingIntentCompat.getActivity(player.getContext(),
NOTIFICATION_ID, getIntentForNotification(), FLAG_UPDATE_CURRENT));
notificationBuilder.setContentTitle(player.getVideoTitle());
notificationBuilder.setContentText(player.getUploaderName());
@@ -334,7 +334,7 @@ public final class NotificationUtil {
@StringRes final int title,
final String intentAction) {
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
PendingIntentCompat.getBroadcast(player.getContext(), NOTIFICATION_ID,
new Intent(intentAction), FLAG_UPDATE_CURRENT));
}

View File

@@ -5,7 +5,7 @@ import static org.schabi.newpipe.player.notification.NotificationConstants.ACTIO
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.content.res.ColorStateList;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -21,7 +21,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.widget.TextViewCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
@@ -214,17 +214,13 @@ public class NotificationActionsPreference extends Preference {
.getRoot();
// if present set action icon with correct color
if (NotificationConstants.ACTION_ICONS[action] != 0) {
Drawable drawable = AppCompatResources.getDrawable(getContext(),
NotificationConstants.ACTION_ICONS[action]);
if (drawable != null) {
final int color = ThemeHelper.resolveColorFromAttr(getContext(),
android.R.attr.textColorPrimary);
drawable = DrawableCompat.wrap(drawable).mutate();
drawable.setTint(color);
radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null,
null, drawable, null);
}
final int iconId = NotificationConstants.ACTION_ICONS[action];
if (iconId != 0) {
radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconId, 0);
final var color = ColorStateList.valueOf(ThemeHelper
.resolveColorFromAttr(getContext(), android.R.attr.textColorPrimary));
TextViewCompat.setCompoundDrawableTintList(radioButton, color);
}
radioButton.setText(NotificationConstants.getActionName(getContext(), action));

View File

@@ -1,14 +1,17 @@
package org.schabi.newpipe.util;
import android.annotation.SuppressLint;
import android.app.UiModeManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Point;
import android.hardware.input.InputManager;
import android.os.BatteryManager;
import android.os.Build;
import android.provider.Settings;
import android.util.TypedValue;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.WindowInsets;
import android.view.WindowManager;
@@ -22,6 +25,10 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import java.lang.reflect.Method;
import static android.content.Context.INPUT_SERVICE;
public final class DeviceUtils {
private static final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv";
@@ -84,6 +91,78 @@ public final class DeviceUtils {
return DeviceUtils.isTV;
}
/**
* Checks if the device is in desktop or DeX mode. This function should only
* be invoked once on view load as it is using reflection for the DeX checks.
* @param context the context to use for services and config.
* @return true if the Android device is in desktop mode or using DeX.
*/
@SuppressWarnings("JavaReflectionMemberAccess")
public static boolean isDesktopMode(@NonNull final Context context) {
// Adapted from https://stackoverflow.com/a/64615568
// to check for all input devices that have an active cursor
final InputManager im = (InputManager) context.getSystemService(INPUT_SERVICE);
for (final int id : im.getInputDeviceIds()) {
final InputDevice inputDevice = im.getInputDevice(id);
if (inputDevice.supportsSource(InputDevice.SOURCE_BLUETOOTH_STYLUS)
|| inputDevice.supportsSource(InputDevice.SOURCE_MOUSE)
|| inputDevice.supportsSource(InputDevice.SOURCE_STYLUS)
|| inputDevice.supportsSource(InputDevice.SOURCE_TOUCHPAD)
|| inputDevice.supportsSource(InputDevice.SOURCE_TRACKBALL)) {
return true;
}
}
final UiModeManager uiModeManager =
ContextCompat.getSystemService(context, UiModeManager.class);
if (uiModeManager != null
&& uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_DESK) {
return true;
}
// DeX check for standalone and multi-window mode, from:
// https://developer.samsung.com/samsung-dex/modify-optimizing.html
try {
final Configuration config = context.getResources().getConfiguration();
final Class<?> configClass = config.getClass();
final int semDesktopModeEnabledConst =
configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass);
final int currentMode =
configClass.getField("semDesktopModeEnabled").getInt(config);
if (semDesktopModeEnabledConst == currentMode) {
return true;
}
} catch (final NoSuchFieldException | IllegalAccessException ignored) {
// Device doesn't seem to support DeX
}
@SuppressLint("WrongConstant") final Object desktopModeManager = context
.getApplicationContext()
.getSystemService("desktopmode");
if (desktopModeManager != null) {
try {
final Method getDesktopModeStateMethod = desktopModeManager.getClass()
.getDeclaredMethod("getDesktopModeState");
final Object desktopModeState = getDesktopModeStateMethod
.invoke(desktopModeManager);
final Class<?> desktopModeStateClass = desktopModeState.getClass();
final Method getEnabledMethod = desktopModeStateClass
.getDeclaredMethod("getEnabled");
final int enabledStatus = (int) getEnabledMethod.invoke(desktopModeState);
if (enabledStatus == desktopModeStateClass
.getDeclaredField("ENABLED").getInt(desktopModeStateClass)) {
return true;
}
} catch (final Exception ignored) {
// Device does not support DeX 3.0 or something went wrong when trying to determine
// if it supports this feature
}
}
return false;
}
public static boolean isTablet(@NonNull final Context context) {
final String tabletModeSetting = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.tablet_mode_key), "");

View File

@@ -56,7 +56,6 @@ import java.util.stream.Collectors;
*/
public final class Localization {
public static final String DOT_SEPARATOR = "";
private static PrettyTime prettyTime;
@@ -76,16 +75,8 @@ public final class Localization {
public static org.schabi.newpipe.extractor.localization.Localization getPreferredLocalization(
final Context context) {
final String contentLanguage = PreferenceManager
.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.content_language_key),
context.getString(R.string.default_localization_key));
if (contentLanguage.equals(context.getString(R.string.default_localization_key))) {
return org.schabi.newpipe.extractor.localization.Localization
.fromLocale(Locale.getDefault());
}
return org.schabi.newpipe.extractor.localization.Localization
.fromLocalizationCode(contentLanguage);
.fromLocale(getPreferredLocale(context));
}
public static ContentCountry getPreferredContentCountry(final Context context) {
@@ -99,22 +90,11 @@ public final class Localization {
}
public static Locale getPreferredLocale(final Context context) {
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
return getLocaleFromPrefs(context, R.string.content_language_key);
}
final String languageCode = sp.getString(context.getString(R.string.content_language_key),
context.getString(R.string.default_localization_key));
try {
if (languageCode.length() == 2) {
return new Locale(languageCode);
} else if (languageCode.contains("_")) {
final String country = languageCode.substring(languageCode.indexOf("_"));
return new Locale(languageCode.substring(0, 2), country);
}
} catch (final Exception ignored) {
}
return Locale.getDefault();
public static Locale getAppLocale(final Context context) {
return getLocaleFromPrefs(context, R.string.app_language_key);
}
public static String localizeNumber(final Context context, final long number) {
@@ -183,13 +163,13 @@ public final class Localization {
final double value = (double) count;
if (count >= 1000000000) {
return localizeNumber(context, round(value / 1000000000, 1))
return localizeNumber(context, round(value / 1000000000))
+ context.getString(R.string.short_billion);
} else if (count >= 1000000) {
return localizeNumber(context, round(value / 1000000, 1))
return localizeNumber(context, round(value / 1000000))
+ context.getString(R.string.short_million);
} else if (count >= 1000) {
return localizeNumber(context, round(value / 1000, 1))
return localizeNumber(context, round(value / 1000))
+ context.getString(R.string.short_thousand);
} else {
return localizeNumber(context, value);
@@ -226,21 +206,6 @@ public final class Localization {
deletedCount, shortCount(context, deletedCount));
}
private static String getQuantity(final Context context, @PluralsRes final int pluralId,
@StringRes final int zeroCaseStringId, final long count,
final String formattedCount) {
if (count == 0) {
return context.getString(zeroCaseStringId);
}
// As we use the already formatted count
// is not the responsibility of this method handle long numbers
// (it probably will fall in the "other" category,
// or some language have some specific rule... then we have to change it)
final int safeCount = (int) MathUtils.clamp(count, Integer.MIN_VALUE, Integer.MAX_VALUE);
return context.getResources().getQuantityString(pluralId, safeCount, formattedCount);
}
public static String getDurationString(final long duration) {
final String output;
@@ -314,37 +279,42 @@ public final class Localization {
return prettyTime.formatUnrounded(offsetDateTime);
}
private static void changeAppLanguage(final Locale loc, final Resources res) {
public static void assureCorrectAppLanguage(final Context c) {
final Resources res = c.getResources();
final DisplayMetrics dm = res.getDisplayMetrics();
final Configuration conf = res.getConfiguration();
conf.setLocale(loc);
conf.setLocale(getAppLocale(c));
res.updateConfiguration(conf, dm);
}
public static Locale getAppLocale(final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String lang = prefs.getString(context.getString(R.string.app_language_key), "en");
final Locale loc;
if (lang.equals(context.getString(R.string.default_localization_key))) {
loc = Locale.getDefault();
} else if (lang.matches(".*-.*")) {
//to differentiate different versions of the language
//for example, pt (portuguese in Portugal) and pt-br (portuguese in Brazil)
final String[] localisation = lang.split("-");
lang = localisation[0];
final String country = localisation[1];
loc = new Locale(lang, country);
private static Locale getLocaleFromPrefs(final Context context, @StringRes final int prefKey) {
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
final String defaultKey = context.getString(R.string.default_localization_key);
final String languageCode = sp.getString(context.getString(prefKey), defaultKey);
if (languageCode.equals(defaultKey)) {
return Locale.getDefault();
} else {
loc = new Locale(lang);
return Locale.forLanguageTag(languageCode);
}
return loc;
}
public static void assureCorrectAppLanguage(final Context c) {
changeAppLanguage(getAppLocale(c), c.getResources());
private static double round(final double value) {
return new BigDecimal(value).setScale(1, RoundingMode.HALF_UP).doubleValue();
}
private static double round(final double value, final int places) {
return new BigDecimal(value).setScale(places, RoundingMode.HALF_UP).doubleValue();
private static String getQuantity(final Context context, @PluralsRes final int pluralId,
@StringRes final int zeroCaseStringId, final long count,
final String formattedCount) {
if (count == 0) {
return context.getString(zeroCaseStringId);
}
// As we use the already formatted count
// is not the responsibility of this method handle long numbers
// (it probably will fall in the "other" category,
// or some language have some specific rule... then we have to change it)
final int safeCount = (int) MathUtils.clamp(count, Integer.MIN_VALUE, Integer.MAX_VALUE);
return context.getResources().getQuantityString(pluralId, safeCount, formattedCount);
}
}

View File

@@ -0,0 +1,69 @@
package org.schabi.newpipe.util;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.annotation.NonNull;
public final class PendingIntentCompat {
private PendingIntentCompat() {
}
private static int addImmutableFlag(final int flags) {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
? flags | PendingIntent.FLAG_IMMUTABLE : flags;
}
/**
* Creates a {@link PendingIntent} to start an activity. It is immutable on API level 23 and
* greater.
*
* @param context The context in which the activity should be started.
* @param requestCode The request code
* @param intent The Intent of the activity to be launched.
* @param flags The flags for the intent.
* @return The pending intent.
* @see PendingIntent#getActivity(Context, int, Intent, int)
*/
@NonNull
public static PendingIntent getActivity(@NonNull final Context context, final int requestCode,
@NonNull final Intent intent, final int flags) {
return PendingIntent.getActivity(context, requestCode, intent, addImmutableFlag(flags));
}
/**
* Creates a {@link PendingIntent} to start a service. It is immutable on API level 23 and
* greater.
*
* @param context The context in which the service should be started.
* @param requestCode The request code
* @param intent The Intent of the service to be launched.
* @param flags The flags for the intent.
* @return The pending intent.
* @see PendingIntent#getService(Context, int, Intent, int)
*/
@NonNull
public static PendingIntent getService(@NonNull final Context context, final int requestCode,
@NonNull final Intent intent, final int flags) {
return PendingIntent.getService(context, requestCode, intent, addImmutableFlag(flags));
}
/**
* Creates a {@link PendingIntent} to perform a broadcast. It is immutable on API level 23 and
* greater.
*
* @param context The context in which the broadcast should be performed.
* @param requestCode The request code
* @param intent The Intent to be broadcast.
* @param flags The flags for the intent.
* @return The pending intent.
* @see PendingIntent#getBroadcast(Context, int, Intent, int)
*/
@NonNull
public static PendingIntent getBroadcast(@NonNull final Context context, final int requestCode,
@NonNull final Intent intent, final int flags) {
return PendingIntent.getBroadcast(context, requestCode, intent, addImmutableFlag(flags));
}
}

View File

@@ -47,6 +47,7 @@ import us.shandian.giga.get.MissionRecoveryInfo;
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.PendingIntentCompat;
import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManager.NetworkState;
@@ -142,7 +143,7 @@ public class DownloadManagerService extends Service {
Intent openDownloadListIntent = new Intent(this, DownloadActivity.class)
.setAction(Intent.ACTION_MAIN);
mOpenDownloadList = PendingIntent.getActivity(this, 0,
mOpenDownloadList = PendingIntentCompat.getActivity(this, 0,
openDownloadListIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
@@ -484,7 +485,8 @@ public class DownloadManagerService extends Service {
private PendingIntent makePendingIntent(String action) {
Intent intent = new Intent(this, DownloadManagerService.class).setAction(action);
return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
return PendingIntentCompat.getService(this, intent.hashCode(), intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
private void manageLock(boolean acquire) {