Merge pull request #3178 from cool-student/notificationImprovements
Notification Improvements
| @@ -44,8 +44,9 @@ | ||||
|         </receiver> | ||||
|  | ||||
|         <service | ||||
|                 android:name=".player.MainPlayer" | ||||
|                 android:exported="false"> | ||||
|             android:name=".player.MainPlayer" | ||||
|             android:exported="false" | ||||
|             android:foregroundServiceType="mediaPlayback"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MEDIA_BUTTON" /> | ||||
|             </intent-filter> | ||||
|   | ||||
| @@ -268,7 +268,7 @@ public class RouterActivity extends AppCompatActivity { | ||||
|  | ||||
|         final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext); | ||||
|         final LinearLayout rootLayout = (LinearLayout) inflater.inflate( | ||||
|                 R.layout.preferred_player_dialog_view, null, false); | ||||
|                 R.layout.single_choice_dialog_view, null, false); | ||||
|         final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); | ||||
|  | ||||
|         final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { | ||||
|   | ||||
| @@ -180,6 +180,8 @@ public abstract class BasePlayer implements | ||||
|     @NonNull | ||||
|     protected final HistoryRecordManager recordManager; | ||||
|     @NonNull | ||||
|     protected final SharedPreferences sharedPreferences; | ||||
|     @NonNull | ||||
|     protected final CustomTrackSelector trackSelector; | ||||
|     @NonNull | ||||
|     protected final PlayerDataSource dataSource; | ||||
| @@ -211,6 +213,7 @@ public abstract class BasePlayer implements | ||||
|         setupBroadcastReceiver(intentFilter); | ||||
|  | ||||
|         this.recordManager = new HistoryRecordManager(context); | ||||
|         this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); | ||||
|  | ||||
|         this.progressUpdateReactor = new SerialDisposable(); | ||||
|         this.databaseUpdateReactor = new CompositeDisposable(); | ||||
| @@ -1239,7 +1242,15 @@ public abstract class BasePlayer implements | ||||
|             Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); | ||||
|         } | ||||
|         if (simpleExoPlayer != null) { | ||||
|             simpleExoPlayer.seekTo(positionMillis); | ||||
|             // prevent invalid positions when fast-forwarding/-rewinding | ||||
|             long normalizedPositionMillis = positionMillis; | ||||
|             if (normalizedPositionMillis < 0) { | ||||
|                 normalizedPositionMillis = 0; | ||||
|             } else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) { | ||||
|                 normalizedPositionMillis = simpleExoPlayer.getDuration(); | ||||
|             } | ||||
|  | ||||
|             simpleExoPlayer.seekTo(normalizedPositionMillis); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -19,35 +19,18 @@ | ||||
|  | ||||
| package org.schabi.newpipe.player; | ||||
|  | ||||
| import android.app.NotificationManager; | ||||
| import android.app.PendingIntent; | ||||
| import android.app.Service; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.res.Resources; | ||||
| import android.graphics.Bitmap; | ||||
| import android.os.Binder; | ||||
| import android.os.Build; | ||||
| import android.os.IBinder; | ||||
| import androidx.preference.PreferenceManager; | ||||
| import android.util.DisplayMetrics; | ||||
| import android.view.ViewGroup; | ||||
| import android.view.WindowManager; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.RequiresApi; | ||||
| import androidx.core.app.NotificationCompat; | ||||
| import android.util.Log; | ||||
| import android.view.View; | ||||
| import android.widget.RemoteViews; | ||||
| import android.view.ViewGroup; | ||||
| import android.view.WindowManager; | ||||
|  | ||||
| import com.google.android.exoplayer2.Player; | ||||
|  | ||||
| import org.schabi.newpipe.BuildConfig; | ||||
| import org.schabi.newpipe.MainActivity; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.util.BitmapUtils; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; | ||||
| @@ -64,7 +47,6 @@ public final class MainPlayer extends Service { | ||||
|  | ||||
|     private VideoPlayerImpl playerImpl; | ||||
|     private WindowManager windowManager; | ||||
|     private SharedPreferences sharedPreferences; | ||||
|  | ||||
|     private final IBinder mBinder = new MainPlayer.LocalBinder(); | ||||
|  | ||||
| @@ -78,30 +60,26 @@ public final class MainPlayer extends Service { | ||||
|     // Notification | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     static final int NOTIFICATION_ID = 123789; | ||||
|     private NotificationManager notificationManager; | ||||
|     private NotificationCompat.Builder notBuilder; | ||||
|     private RemoteViews notRemoteView; | ||||
|     private RemoteViews bigNotRemoteView; | ||||
|  | ||||
|     static final String ACTION_CLOSE = | ||||
|             "org.schabi.newpipe.player.MainPlayer.CLOSE"; | ||||
|     static final String ACTION_PLAY_PAUSE = | ||||
|             "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE"; | ||||
|     static final String ACTION_OPEN_CONTROLS = | ||||
|             "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS"; | ||||
|     static final String ACTION_REPEAT = | ||||
|             "org.schabi.newpipe.player.MainPlayer.REPEAT"; | ||||
|     static final String ACTION_PLAY_NEXT = | ||||
|             "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT"; | ||||
|     static final String ACTION_PLAY_PREVIOUS = | ||||
|             "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS"; | ||||
|     static final String ACTION_FAST_REWIND = | ||||
|             "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND"; | ||||
|     static final String ACTION_FAST_FORWARD = | ||||
|             "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD"; | ||||
|  | ||||
|     private static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource"; | ||||
|     static final String ACTION_CLOSE | ||||
|             = "org.schabi.newpipe.player.MainPlayer.CLOSE"; | ||||
|     static final String ACTION_PLAY_PAUSE | ||||
|             = "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE"; | ||||
|     static final String ACTION_OPEN_CONTROLS | ||||
|             = "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS"; | ||||
|     static final String ACTION_REPEAT | ||||
|             = "org.schabi.newpipe.player.MainPlayer.REPEAT"; | ||||
|     static final String ACTION_PLAY_NEXT | ||||
|             = "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT"; | ||||
|     static final String ACTION_PLAY_PREVIOUS | ||||
|             = "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS"; | ||||
|     static final String ACTION_FAST_REWIND | ||||
|             = "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND"; | ||||
|     static final String ACTION_FAST_FORWARD | ||||
|             = "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD"; | ||||
|     static final String ACTION_SHUFFLE | ||||
|             = "org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE"; | ||||
|     public static final String ACTION_RECREATE_NOTIFICATION | ||||
|             = "org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION"; | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Service's LifeCycle | ||||
| @@ -113,9 +91,7 @@ public final class MainPlayer extends Service { | ||||
|             Log.d(TAG, "onCreate() called"); | ||||
|         } | ||||
|         assureCorrectAppLanguage(this); | ||||
|         notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); | ||||
|         windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); | ||||
|         sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); | ||||
|  | ||||
|         ThemeHelper.setTheme(this); | ||||
|         createView(); | ||||
| @@ -143,7 +119,7 @@ public final class MainPlayer extends Service { | ||||
|  | ||||
|         if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) | ||||
|                 || intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) { | ||||
|             showNotificationAndStartForeground(); | ||||
|             NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this); | ||||
|         } | ||||
|  | ||||
|         playerImpl.handleIntent(intent); | ||||
| @@ -176,7 +152,7 @@ public final class MainPlayer extends Service { | ||||
|             // So we should hide the notification at all. | ||||
|             // When autoplay enabled such notification flashing is annoying so skip this case | ||||
|             if (!autoplayEnabled) { | ||||
|                 stopForeground(true); | ||||
|                 NotificationUtil.getInstance().cancelNotificationAndStopForeground(this); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -227,11 +203,8 @@ public final class MainPlayer extends Service { | ||||
|             playerImpl.removePopupFromView(); | ||||
|             playerImpl.destroy(); | ||||
|         } | ||||
|         if (notificationManager != null) { | ||||
|             notificationManager.cancel(NOTIFICATION_ID); | ||||
|         } | ||||
|  | ||||
|         stopForeground(true); | ||||
|         NotificationUtil.getInstance().cancelNotificationAndStopForeground(this); | ||||
|         stopSelf(); | ||||
|     } | ||||
|  | ||||
| @@ -270,206 +243,6 @@ public final class MainPlayer extends Service { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void showNotificationAndStartForeground() { | ||||
|         resetNotification(); | ||||
|         if (getBigNotRemoteView() != null) { | ||||
|             getBigNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false); | ||||
|         } | ||||
|         if (getNotRemoteView() != null) { | ||||
|             getNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false); | ||||
|         } | ||||
|         startForeground(NOTIFICATION_ID, getNotBuilder().build()); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Notification | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     void resetNotification() { | ||||
|         notBuilder = createNotification(); | ||||
|         playerImpl.timesNotificationUpdated = 0; | ||||
|     } | ||||
|  | ||||
|     private NotificationCompat.Builder createNotification() { | ||||
|         notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, | ||||
|                 R.layout.player_notification); | ||||
|         bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, | ||||
|                 R.layout.player_notification_expanded); | ||||
|  | ||||
|         setupNotification(notRemoteView); | ||||
|         setupNotification(bigNotRemoteView); | ||||
|  | ||||
|         final NotificationCompat.Builder builder = new NotificationCompat | ||||
|                 .Builder(this, getString(R.string.notification_channel_id)) | ||||
|                 .setOngoing(true) | ||||
|                 .setSmallIcon(R.drawable.ic_newpipe_triangle_white) | ||||
|                 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||
|                 .setCustomContentView(notRemoteView) | ||||
|                 .setCustomBigContentView(bigNotRemoteView); | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|             setLockScreenThumbnail(builder); | ||||
|         } | ||||
|  | ||||
|         builder.setPriority(NotificationCompat.PRIORITY_MAX); | ||||
|         return builder; | ||||
|     } | ||||
|  | ||||
|     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) | ||||
|     private void setLockScreenThumbnail(final NotificationCompat.Builder builder) { | ||||
|         final boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean( | ||||
|                 getString(R.string.enable_lock_screen_video_thumbnail_key), true); | ||||
|  | ||||
|         if (isLockScreenThumbnailEnabled) { | ||||
|             playerImpl.mediaSessionManager.setLockScreenArt( | ||||
|                     builder, | ||||
|                     getCenteredThumbnailBitmap() | ||||
|             ); | ||||
|         } else { | ||||
|             playerImpl.mediaSessionManager.clearLockScreenArt(builder); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     private Bitmap getCenteredThumbnailBitmap() { | ||||
|         final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; | ||||
|         final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; | ||||
|  | ||||
|         return BitmapUtils.centerCrop(playerImpl.getThumbnail(), screenWidth, screenHeight); | ||||
|     } | ||||
|  | ||||
|     private void setupNotification(final RemoteViews remoteViews) { | ||||
|         // Don't show anything until player is playing | ||||
|         if (playerImpl == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         remoteViews.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); | ||||
|         remoteViews.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); | ||||
|         remoteViews.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail()); | ||||
|  | ||||
|         remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause, | ||||
|                 PendingIntent.getBroadcast(this, NOTIFICATION_ID, | ||||
|                         new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); | ||||
|         remoteViews.setOnClickPendingIntent(R.id.notificationStop, | ||||
|                 PendingIntent.getBroadcast(this, NOTIFICATION_ID, | ||||
|                         new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); | ||||
|         // Starts VideoDetailFragment or opens BackgroundPlayerActivity. | ||||
|         remoteViews.setOnClickPendingIntent(R.id.notificationContent, | ||||
|                 PendingIntent.getActivity(this, NOTIFICATION_ID, | ||||
|                         getIntentForNotification(), PendingIntent.FLAG_UPDATE_CURRENT)); | ||||
|         remoteViews.setOnClickPendingIntent(R.id.notificationRepeat, | ||||
|                 PendingIntent.getBroadcast(this, NOTIFICATION_ID, | ||||
|                         new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); | ||||
|  | ||||
|  | ||||
|         if (playerImpl.playQueue != null && playerImpl.playQueue.size() > 1) { | ||||
|             remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, | ||||
|                     R.drawable.exo_controls_previous); | ||||
|             remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, | ||||
|                     R.drawable.exo_controls_next); | ||||
|             remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, | ||||
|                     PendingIntent.getBroadcast(this, NOTIFICATION_ID, | ||||
|                             new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT)); | ||||
|             remoteViews.setOnClickPendingIntent(R.id.notificationFForward, | ||||
|                     PendingIntent.getBroadcast(this, NOTIFICATION_ID, | ||||
|                             new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT)); | ||||
|         } else { | ||||
|             remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, | ||||
|                     R.drawable.exo_controls_rewind); | ||||
|             remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, | ||||
|                     R.drawable.exo_controls_fastforward); | ||||
|             remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, | ||||
|                     PendingIntent.getBroadcast(this, NOTIFICATION_ID, | ||||
|                             new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT)); | ||||
|             remoteViews.setOnClickPendingIntent(R.id.notificationFForward, | ||||
|                     PendingIntent.getBroadcast(this, NOTIFICATION_ID, | ||||
|                             new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT)); | ||||
|         } | ||||
|  | ||||
|         setRepeatModeIcon(remoteViews, playerImpl.getRepeatMode()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Updates the notification, and the play/pause button in it. | ||||
|      * Used for changes on the remoteView | ||||
|      * | ||||
|      * @param drawableId if != -1, sets the drawable with that id on the play/pause button | ||||
|      */ | ||||
|     synchronized void updateNotification(final int drawableId) { | ||||
|         /*if (DEBUG) { | ||||
|             Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); | ||||
|         }*/ | ||||
|         if (notBuilder == null) { | ||||
|             return; | ||||
|         } | ||||
|         if (drawableId != -1) { | ||||
|             if (notRemoteView != null) { | ||||
|                 notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); | ||||
|             } | ||||
|             if (bigNotRemoteView != null) { | ||||
|                 bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); | ||||
|             } | ||||
|         } | ||||
|         notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); | ||||
|         playerImpl.timesNotificationUpdated++; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) { | ||||
|         if (remoteViews == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         switch (repeatMode) { | ||||
|             case Player.REPEAT_MODE_OFF: | ||||
|                 remoteViews.setInt(R.id.notificationRepeat, | ||||
|                         SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_off); | ||||
|                 break; | ||||
|             case Player.REPEAT_MODE_ONE: | ||||
|                 remoteViews.setInt(R.id.notificationRepeat, | ||||
|                         SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_one); | ||||
|                 break; | ||||
|             case Player.REPEAT_MODE_ALL: | ||||
|                 remoteViews.setInt(R.id.notificationRepeat, | ||||
|                         SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_all); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private Intent getIntentForNotification() { | ||||
|         final Intent intent; | ||||
|         if (playerImpl.audioPlayerSelected() || playerImpl.popupPlayerSelected()) { | ||||
|             // Means we play in popup or audio only. Let's show BackgroundPlayerActivity | ||||
|             intent = NavigationHelper.getBackgroundPlayerActivityIntent(getApplicationContext()); | ||||
|         } else { | ||||
|             // We are playing in fragment. Don't open another activity just show fragment. That's it | ||||
|             intent = NavigationHelper.getPlayerIntent(this, MainActivity.class, null, true); | ||||
|             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|             intent.setAction(Intent.ACTION_MAIN); | ||||
|             intent.addCategory(Intent.CATEGORY_LAUNCHER); | ||||
|         } | ||||
|         return intent; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Getters | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     NotificationCompat.Builder getNotBuilder() { | ||||
|         return notBuilder; | ||||
|     } | ||||
|  | ||||
|     RemoteViews getBigNotRemoteView() { | ||||
|         return bigNotRemoteView; | ||||
|     } | ||||
|  | ||||
|     RemoteViews getNotRemoteView() { | ||||
|         return notRemoteView; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public class LocalBinder extends Binder { | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,165 @@ | ||||
| package org.schabi.newpipe.player; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
|  | ||||
| import androidx.annotation.DrawableRes; | ||||
| import androidx.annotation.IntDef; | ||||
| import androidx.annotation.NonNull; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.util.Localization; | ||||
|  | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| import java.util.SortedSet; | ||||
| import java.util.TreeSet; | ||||
|  | ||||
| public final class NotificationConstants { | ||||
|  | ||||
|     private NotificationConstants() { } | ||||
|  | ||||
|  | ||||
|     public static final int NOTHING = 0; | ||||
|     public static final int PREVIOUS = 1; | ||||
|     public static final int NEXT = 2; | ||||
|     public static final int REWIND = 3; | ||||
|     public static final int FORWARD = 4; | ||||
|     public static final int SMART_REWIND_PREVIOUS = 5; | ||||
|     public static final int SMART_FORWARD_NEXT = 6; | ||||
|     public static final int PLAY_PAUSE = 7; | ||||
|     public static final int PLAY_PAUSE_BUFFERING = 8; | ||||
|     public static final int REPEAT = 9; | ||||
|     public static final int SHUFFLE = 10; | ||||
|     public static final int CLOSE = 11; | ||||
|  | ||||
|     @Retention(RetentionPolicy.SOURCE) | ||||
|     @IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, | ||||
|             PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT, SHUFFLE, CLOSE}) | ||||
|     public @interface Action { } | ||||
|  | ||||
|     @DrawableRes | ||||
|     public static final int[] ACTION_ICONS = { | ||||
|             0, | ||||
|             R.drawable.exo_icon_previous, | ||||
|             R.drawable.exo_icon_next, | ||||
|             R.drawable.exo_icon_rewind, | ||||
|             R.drawable.exo_icon_fastforward, | ||||
|             R.drawable.exo_icon_previous, | ||||
|             R.drawable.exo_icon_next, | ||||
|             R.drawable.ic_pause_white_24dp, | ||||
|             R.drawable.ic_hourglass_top_white_24dp, | ||||
|             R.drawable.exo_icon_repeat_all, | ||||
|             R.drawable.exo_icon_shuffle_on, | ||||
|             R.drawable.ic_close_white_24dp, | ||||
|     }; | ||||
|  | ||||
|  | ||||
|     @Action | ||||
|     public static final int[] SLOT_DEFAULTS = { | ||||
|             SMART_REWIND_PREVIOUS, | ||||
|             PLAY_PAUSE_BUFFERING, | ||||
|             SMART_FORWARD_NEXT, | ||||
|             REPEAT, | ||||
|             CLOSE, | ||||
|     }; | ||||
|  | ||||
|     @Action | ||||
|     public static final int[][] SLOT_ALLOWED_ACTIONS = { | ||||
|             new int[] {PREVIOUS, REWIND, SMART_REWIND_PREVIOUS}, | ||||
|             new int[] {REWIND, PLAY_PAUSE, PLAY_PAUSE_BUFFERING}, | ||||
|             new int[] {NEXT, FORWARD, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING}, | ||||
|             new int[] {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS, | ||||
|                     SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE}, | ||||
|             new int[] {NOTHING, NEXT, FORWARD, SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE}, | ||||
|     }; | ||||
|  | ||||
|     public static final int[] SLOT_PREF_KEYS = { | ||||
|             R.string.notification_slot_0_key, | ||||
|             R.string.notification_slot_1_key, | ||||
|             R.string.notification_slot_2_key, | ||||
|             R.string.notification_slot_3_key, | ||||
|             R.string.notification_slot_4_key, | ||||
|     }; | ||||
|  | ||||
|  | ||||
|     public static final Integer[] SLOT_COMPACT_DEFAULTS = {0, 1, 2}; | ||||
|  | ||||
|     public static final int[] SLOT_COMPACT_PREF_KEYS = { | ||||
|             R.string.notification_slot_compact_0_key, | ||||
|             R.string.notification_slot_compact_1_key, | ||||
|             R.string.notification_slot_compact_2_key, | ||||
|     }; | ||||
|  | ||||
|  | ||||
|     public static String getActionName(@NonNull final Context context, @Action final int action) { | ||||
|         switch (action) { | ||||
|             case PREVIOUS: | ||||
|                 return context.getString(R.string.exo_controls_previous_description); | ||||
|             case NEXT: | ||||
|                 return context.getString(R.string.exo_controls_next_description); | ||||
|             case REWIND: | ||||
|                 return context.getString(R.string.exo_controls_rewind_description); | ||||
|             case FORWARD: | ||||
|                 return context.getString(R.string.exo_controls_fastforward_description); | ||||
|             case SMART_REWIND_PREVIOUS: | ||||
|                 return Localization.concatenateStrings( | ||||
|                         context.getString(R.string.exo_controls_rewind_description), | ||||
|                         context.getString(R.string.exo_controls_previous_description)); | ||||
|             case SMART_FORWARD_NEXT: | ||||
|                 return Localization.concatenateStrings( | ||||
|                         context.getString(R.string.exo_controls_fastforward_description), | ||||
|                         context.getString(R.string.exo_controls_next_description)); | ||||
|             case PLAY_PAUSE: | ||||
|                 return Localization.concatenateStrings( | ||||
|                         context.getString(R.string.exo_controls_play_description), | ||||
|                         context.getString(R.string.exo_controls_pause_description)); | ||||
|             case PLAY_PAUSE_BUFFERING: | ||||
|                 return Localization.concatenateStrings( | ||||
|                         context.getString(R.string.exo_controls_play_description), | ||||
|                         context.getString(R.string.exo_controls_pause_description), | ||||
|                         context.getString(R.string.notification_action_buffering)); | ||||
|             case REPEAT: | ||||
|                 return context.getString(R.string.notification_action_repeat); | ||||
|             case SHUFFLE: | ||||
|                 return context.getString(R.string.notification_action_shuffle); | ||||
|             case CLOSE: | ||||
|                 return context.getString(R.string.close); | ||||
|             case NOTHING: default: | ||||
|                 return context.getString(R.string.notification_action_nothing); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * @param context the context to use | ||||
|      * @param sharedPreferences the shared preferences to query values from | ||||
|      * @param slotCount remove indices >= than this value (set to {@code 5} to do nothing, or make | ||||
|      *                  it lower if there are slots with empty actions) | ||||
|      * @return a sorted list of the indices of the slots to use as compact slots | ||||
|      */ | ||||
|     public static List<Integer> getCompactSlotsFromPreferences( | ||||
|             @NonNull final Context context, | ||||
|             final SharedPreferences sharedPreferences, | ||||
|             final int slotCount) { | ||||
|         final SortedSet<Integer> compactSlots = new TreeSet<>(); | ||||
|         for (int i = 0; i < 3; i++) { | ||||
|             final int compactSlot = sharedPreferences.getInt( | ||||
|                     context.getString(SLOT_COMPACT_PREF_KEYS[i]), Integer.MAX_VALUE); | ||||
|  | ||||
|             if (compactSlot == Integer.MAX_VALUE) { | ||||
|                 // settings not yet populated, return default values | ||||
|                 return new ArrayList<>(Arrays.asList(SLOT_COMPACT_DEFAULTS)); | ||||
|             } | ||||
|  | ||||
|             // a negative value (-1) is set when the user does not want a particular compact slot | ||||
|             if (compactSlot >= 0 && compactSlot < slotCount) { | ||||
|                 compactSlots.add(compactSlot); | ||||
|             } | ||||
|         } | ||||
|         return new ArrayList<>(compactSlots); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,371 @@ | ||||
| package org.schabi.newpipe.player; | ||||
|  | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.PendingIntent; | ||||
| import android.app.Service; | ||||
| import android.content.Intent; | ||||
| import android.content.pm.ServiceInfo; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.Matrix; | ||||
| import android.os.Build; | ||||
| import android.util.Log; | ||||
|  | ||||
| import androidx.annotation.DrawableRes; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.StringRes; | ||||
| import androidx.core.app.NotificationCompat; | ||||
| import androidx.core.app.NotificationManagerCompat; | ||||
| import androidx.core.content.ContextCompat; | ||||
|  | ||||
| import org.schabi.newpipe.MainActivity; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; | ||||
| import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; | ||||
| import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; | ||||
|  | ||||
| /** | ||||
|  * This is a utility class for player notifications. | ||||
|  * | ||||
|  * @author cool-student | ||||
|  */ | ||||
| public final class NotificationUtil { | ||||
|     private static final String TAG = NotificationUtil.class.getSimpleName(); | ||||
|     private static final boolean DEBUG = BasePlayer.DEBUG; | ||||
|     private static final int NOTIFICATION_ID = 123789; | ||||
|  | ||||
|     @Nullable private static NotificationUtil instance = null; | ||||
|  | ||||
|     @NotificationConstants.Action | ||||
|     private int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone(); | ||||
|  | ||||
|     private NotificationManagerCompat notificationManager; | ||||
|     private NotificationCompat.Builder notificationBuilder; | ||||
|  | ||||
|     private NotificationUtil() { | ||||
|     } | ||||
|  | ||||
|     public static NotificationUtil getInstance() { | ||||
|         if (instance == null) { | ||||
|             instance = new NotificationUtil(); | ||||
|         } | ||||
|         return instance; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     ///////////////////////////////////////////////////// | ||||
|     // NOTIFICATION | ||||
|     ///////////////////////////////////////////////////// | ||||
|  | ||||
|     /** | ||||
|      * Creates the notification if it does not exist already and recreates it if forceRecreate is | ||||
|      * true. Updates the notification with the data in the player. | ||||
|      * @param player the player currently open, to take data from | ||||
|      * @param forceRecreate whether to force the recreation of the notification even if it already | ||||
|      *                      exists | ||||
|      */ | ||||
|     synchronized void createNotificationIfNeededAndUpdate(final VideoPlayerImpl player, | ||||
|                                                           final boolean forceRecreate) { | ||||
|         if (forceRecreate || notificationBuilder == null) { | ||||
|             notificationBuilder = createNotification(player); | ||||
|         } | ||||
|         updateNotification(player); | ||||
|         notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); | ||||
|     } | ||||
|  | ||||
|     private synchronized NotificationCompat.Builder createNotification( | ||||
|             final VideoPlayerImpl player) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "createNotification()"); | ||||
|         } | ||||
|         notificationManager = NotificationManagerCompat.from(player.context); | ||||
|         final NotificationCompat.Builder builder = new NotificationCompat.Builder(player.context, | ||||
|                 player.context.getString(R.string.notification_channel_id)); | ||||
|  | ||||
|         initializeNotificationSlots(player); | ||||
|  | ||||
|         // count the number of real slots, to make sure compact slots indices are not out of bound | ||||
|         int nonNothingSlotCount = 5; | ||||
|         if (notificationSlots[3] == NotificationConstants.NOTHING) { | ||||
|             --nonNothingSlotCount; | ||||
|         } | ||||
|         if (notificationSlots[4] == NotificationConstants.NOTHING) { | ||||
|             --nonNothingSlotCount; | ||||
|         } | ||||
|  | ||||
|         // build the compact slot indices array (need code to convert from Integer... because Java) | ||||
|         final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences( | ||||
|                 player.context, player.sharedPreferences, nonNothingSlotCount); | ||||
|         final int[] compactSlots = new int[compactSlotList.size()]; | ||||
|         for (int i = 0; i < compactSlotList.size(); i++) { | ||||
|             compactSlots[i] = compactSlotList.get(i); | ||||
|         } | ||||
|  | ||||
|         builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle() | ||||
|                     .setMediaSession(player.mediaSessionManager.getSessionToken()) | ||||
|                     .setShowActionsInCompactView(compactSlots)) | ||||
|                 .setPriority(NotificationCompat.PRIORITY_HIGH) | ||||
|                 .setSmallIcon(R.drawable.ic_newpipe_triangle_white) | ||||
|                 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||
|                 .setColor(ContextCompat.getColor(player.context, R.color.gray)) | ||||
|                 .setCategory(NotificationCompat.CATEGORY_TRANSPORT) | ||||
|                 .setDeleteIntent(PendingIntent.getBroadcast(player.context, NOTIFICATION_ID, | ||||
|                         new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT)); | ||||
|  | ||||
|         return builder; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Updates the notification builder and the button icons depending on the playback state. | ||||
|      * @param player the player currently open, to take data from | ||||
|      */ | ||||
|     private synchronized void updateNotification(final VideoPlayerImpl player) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "updateNotification()"); | ||||
|         } | ||||
|  | ||||
|         // also update content intent, in case the user switched players | ||||
|         notificationBuilder.setContentIntent(PendingIntent.getActivity(player.context, | ||||
|                 NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT)); | ||||
|         notificationBuilder.setContentTitle(player.getVideoTitle()); | ||||
|         notificationBuilder.setContentText(player.getUploaderName()); | ||||
|         notificationBuilder.setTicker(player.getVideoTitle()); | ||||
|         updateActions(notificationBuilder, player); | ||||
|         setLargeIcon(notificationBuilder, player); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     @SuppressLint("RestrictedApi") | ||||
|     boolean shouldUpdateBufferingSlot() { | ||||
|         if (notificationBuilder.mActions.size() < 3) { | ||||
|             // this should never happen, but let's make sure notification actions are populated | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         // only second and third slot could contain PLAY_PAUSE_BUFFERING, update them only if they | ||||
|         // are not already in the buffering state (the only one with a null action intent) | ||||
|         return (notificationSlots[1] == NotificationConstants.PLAY_PAUSE_BUFFERING | ||||
|                 && notificationBuilder.mActions.get(1).actionIntent != null) | ||||
|                 || (notificationSlots[2] == NotificationConstants.PLAY_PAUSE_BUFFERING | ||||
|                 && notificationBuilder.mActions.get(2).actionIntent != null); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     void createNotificationAndStartForeground(final VideoPlayerImpl player, final Service service) { | ||||
|         if (notificationBuilder == null) { | ||||
|             notificationBuilder = createNotification(player); | ||||
|         } | ||||
|         updateNotification(player); | ||||
|  | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | ||||
|             service.startForeground(NOTIFICATION_ID, notificationBuilder.build(), | ||||
|                     ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK); | ||||
|         } else { | ||||
|             service.startForeground(NOTIFICATION_ID, notificationBuilder.build()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void cancelNotificationAndStopForeground(final Service service) { | ||||
|         service.stopForeground(true); | ||||
|  | ||||
|         if (notificationManager != null) { | ||||
|             notificationManager.cancel(NOTIFICATION_ID); | ||||
|         } | ||||
|         notificationManager = null; | ||||
|         notificationBuilder = null; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     ///////////////////////////////////////////////////// | ||||
|     // ACTIONS | ||||
|     ///////////////////////////////////////////////////// | ||||
|  | ||||
|     private void initializeNotificationSlots(final VideoPlayerImpl player) { | ||||
|         for (int i = 0; i < 5; ++i) { | ||||
|             notificationSlots[i] = player.sharedPreferences.getInt( | ||||
|                     player.context.getString(NotificationConstants.SLOT_PREF_KEYS[i]), | ||||
|                     NotificationConstants.SLOT_DEFAULTS[i]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @SuppressLint("RestrictedApi") | ||||
|     private void updateActions(final NotificationCompat.Builder builder, | ||||
|                                final VideoPlayerImpl player) { | ||||
|         builder.mActions.clear(); | ||||
|         for (int i = 0; i < 5; ++i) { | ||||
|             addAction(builder, player, notificationSlots[i]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void addAction(final NotificationCompat.Builder builder, | ||||
|                            final VideoPlayerImpl player, | ||||
|                            @NotificationConstants.Action final int slot) { | ||||
|         final NotificationCompat.Action action = getAction(player, slot); | ||||
|         if (action != null) { | ||||
|             builder.addAction(action); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     private NotificationCompat.Action getAction( | ||||
|             final VideoPlayerImpl player, | ||||
|             @NotificationConstants.Action final int selectedAction) { | ||||
|         final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction]; | ||||
|         switch (selectedAction) { | ||||
|             case NotificationConstants.PREVIOUS: | ||||
|                 return getAction(player, baseActionIcon, | ||||
|                         R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS); | ||||
|  | ||||
|             case NotificationConstants.NEXT: | ||||
|                 return getAction(player, baseActionIcon, | ||||
|                         R.string.exo_controls_next_description, ACTION_PLAY_NEXT); | ||||
|  | ||||
|             case NotificationConstants.REWIND: | ||||
|                 return getAction(player, baseActionIcon, | ||||
|                         R.string.exo_controls_rewind_description, ACTION_FAST_REWIND); | ||||
|  | ||||
|             case NotificationConstants.FORWARD: | ||||
|                 return getAction(player, baseActionIcon, | ||||
|                         R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD); | ||||
|  | ||||
|             case NotificationConstants.SMART_REWIND_PREVIOUS: | ||||
|                 if (player.playQueue != null && player.playQueue.size() > 1) { | ||||
|                     return getAction(player, R.drawable.exo_notification_previous, | ||||
|                             R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS); | ||||
|                 } else { | ||||
|                     return getAction(player, R.drawable.exo_controls_rewind, | ||||
|                             R.string.exo_controls_rewind_description, ACTION_FAST_REWIND); | ||||
|                 } | ||||
|  | ||||
|             case NotificationConstants.SMART_FORWARD_NEXT: | ||||
|                 if (player.playQueue != null && player.playQueue.size() > 1) { | ||||
|                     return getAction(player, R.drawable.exo_notification_next, | ||||
|                             R.string.exo_controls_next_description, ACTION_PLAY_NEXT); | ||||
|                 } else { | ||||
|                     return getAction(player, R.drawable.exo_controls_fastforward, | ||||
|                             R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD); | ||||
|                 } | ||||
|  | ||||
|             case NotificationConstants.PLAY_PAUSE_BUFFERING: | ||||
|                 if (player.getCurrentState() == BasePlayer.STATE_PREFLIGHT | ||||
|                         || player.getCurrentState() == BasePlayer.STATE_BLOCKED | ||||
|                         || player.getCurrentState() == BasePlayer.STATE_BUFFERING) { | ||||
|                     // null intent -> show hourglass icon that does nothing when clicked | ||||
|                     return new NotificationCompat.Action(R.drawable.ic_hourglass_top_white_24dp_png, | ||||
|                             player.context.getString(R.string.notification_action_buffering), | ||||
|                             null); | ||||
|                 } | ||||
|  | ||||
|             case NotificationConstants.PLAY_PAUSE: | ||||
|                 if (player.getCurrentState() == BasePlayer.STATE_COMPLETED) { | ||||
|                     return getAction(player, R.drawable.ic_replay_white_24dp_png, | ||||
|                             R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); | ||||
|                 } else if (player.isPlaying() | ||||
|                         || player.getCurrentState() == BasePlayer.STATE_PREFLIGHT | ||||
|                         || player.getCurrentState() == BasePlayer.STATE_BLOCKED | ||||
|                         || player.getCurrentState() == BasePlayer.STATE_BUFFERING) { | ||||
|                     return getAction(player, R.drawable.exo_notification_pause, | ||||
|                             R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); | ||||
|                 } else { | ||||
|                     return getAction(player, R.drawable.exo_notification_play, | ||||
|                             R.string.exo_controls_play_description, ACTION_PLAY_PAUSE); | ||||
|                 } | ||||
|  | ||||
|             case NotificationConstants.REPEAT: | ||||
|                 if (player.getRepeatMode() == REPEAT_MODE_ALL) { | ||||
|                     return getAction(player, R.drawable.exo_media_action_repeat_all, | ||||
|                             R.string.exo_controls_repeat_all_description, ACTION_REPEAT); | ||||
|                 } else if (player.getRepeatMode() == REPEAT_MODE_ONE) { | ||||
|                     return getAction(player, R.drawable.exo_media_action_repeat_one, | ||||
|                             R.string.exo_controls_repeat_one_description, ACTION_REPEAT); | ||||
|                 } else /* player.getRepeatMode() == REPEAT_MODE_OFF */ { | ||||
|                     return getAction(player, R.drawable.exo_media_action_repeat_off, | ||||
|                             R.string.exo_controls_repeat_off_description, ACTION_REPEAT); | ||||
|                 } | ||||
|  | ||||
|             case NotificationConstants.SHUFFLE: | ||||
|                 if (player.playQueue != null && player.playQueue.isShuffled()) { | ||||
|                     return getAction(player, R.drawable.exo_controls_shuffle_on, | ||||
|                             R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE); | ||||
|                 } else { | ||||
|                     return getAction(player, R.drawable.exo_controls_shuffle_off, | ||||
|                             R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE); | ||||
|                 } | ||||
|  | ||||
|             case NotificationConstants.CLOSE: | ||||
|                 return getAction(player, R.drawable.ic_close_white_24dp_png, | ||||
|                         R.string.close, ACTION_CLOSE); | ||||
|  | ||||
|             case NotificationConstants.NOTHING: | ||||
|             default: | ||||
|                 // do nothing | ||||
|                 return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private NotificationCompat.Action getAction(final VideoPlayerImpl player, | ||||
|                                                 @DrawableRes final int drawable, | ||||
|                                                 @StringRes final int title, | ||||
|                                                 final String intentAction) { | ||||
|         return new NotificationCompat.Action(drawable, player.context.getString(title), | ||||
|                 PendingIntent.getBroadcast(player.context, NOTIFICATION_ID, | ||||
|                         new Intent(intentAction), FLAG_UPDATE_CURRENT)); | ||||
|     } | ||||
|  | ||||
|     private Intent getIntentForNotification(final VideoPlayerImpl player) { | ||||
|         if (player.audioPlayerSelected() || player.popupPlayerSelected()) { | ||||
|             // Means we play in popup or audio only. Let's show the play queue | ||||
|             return NavigationHelper.getPlayQueueActivityIntent(player.context); | ||||
|         } else { | ||||
|             // We are playing in fragment. Don't open another activity just show fragment. That's it | ||||
|             final Intent intent = NavigationHelper.getPlayerIntent( | ||||
|                     player.context, MainActivity.class, null, true); | ||||
|             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|             intent.setAction(Intent.ACTION_MAIN); | ||||
|             intent.addCategory(Intent.CATEGORY_LAUNCHER); | ||||
|             return intent; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     ///////////////////////////////////////////////////// | ||||
|     // BITMAP | ||||
|     ///////////////////////////////////////////////////// | ||||
|  | ||||
|     private void setLargeIcon(final NotificationCompat.Builder builder, | ||||
|                               final VideoPlayerImpl player) { | ||||
|         final boolean scaleImageToSquareAspectRatio = player.sharedPreferences.getBoolean( | ||||
|                 player.context.getString(R.string.scale_to_square_image_in_notifications_key), | ||||
|                 false); | ||||
|         if (scaleImageToSquareAspectRatio) { | ||||
|             builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail())); | ||||
|         } else { | ||||
|             builder.setLargeIcon(player.getThumbnail()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private Bitmap getBitmapWithSquareAspectRatio(final Bitmap bitmap) { | ||||
|         return getResizedBitmap(bitmap, bitmap.getWidth(), bitmap.getWidth()); | ||||
|     } | ||||
|  | ||||
|     private Bitmap getResizedBitmap(final Bitmap bitmap, final int newWidth, final int newHeight) { | ||||
|         final int width = bitmap.getWidth(); | ||||
|         final int height = bitmap.getHeight(); | ||||
|         final float scaleWidth = ((float) newWidth) / width; | ||||
|         final float scaleHeight = ((float) newHeight) / height; | ||||
|         final Matrix matrix = new Matrix(); | ||||
|         matrix.postScale(scaleWidth, scaleHeight); | ||||
|         return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false); | ||||
|     } | ||||
| } | ||||
| @@ -56,6 +56,7 @@ import android.widget.ProgressBar; | ||||
| import android.widget.RelativeLayout; | ||||
| import android.widget.SeekBar; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| @@ -65,6 +66,7 @@ import androidx.recyclerview.widget.RecyclerView; | ||||
| import com.google.android.exoplayer2.ExoPlaybackException; | ||||
| import com.google.android.exoplayer2.Player; | ||||
| import com.google.android.exoplayer2.SimpleExoPlayer; | ||||
| import com.google.android.exoplayer2.Timeline; | ||||
| import com.google.android.exoplayer2.source.MediaSource; | ||||
| import com.google.android.exoplayer2.text.CaptionStyleCompat; | ||||
| import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; | ||||
| @@ -110,10 +112,10 @@ import static org.schabi.newpipe.player.MainPlayer.ACTION_OPEN_CONTROLS; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; | ||||
| import static org.schabi.newpipe.player.MainPlayer.NOTIFICATION_ID; | ||||
| import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; | ||||
| import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; | ||||
| import static org.schabi.newpipe.util.AnimationUtils.animateRotation; | ||||
| import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||
| @@ -143,7 +145,6 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|             | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; | ||||
|  | ||||
|     private static final float MAX_GESTURE_LENGTH = 0.75f; | ||||
|     private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60; | ||||
|  | ||||
|     private TextView titleTextView; | ||||
|     private TextView channelTextView; | ||||
| @@ -189,7 +190,6 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|     private boolean isVerticalVideo = false; | ||||
|     private boolean fragmentIsVisible = false; | ||||
|     boolean shouldUpdateOnProgress; | ||||
|     int timesNotificationUpdated; | ||||
|  | ||||
|     private final MainPlayer service; | ||||
|     private PlayerServiceEventListener fragmentListener; | ||||
| @@ -200,9 +200,6 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|     @NonNull | ||||
|     private final AudioPlaybackResolver resolver; | ||||
|  | ||||
|     private int cachedDuration; | ||||
|     private String cachedDurationString; | ||||
|  | ||||
|     // Popup | ||||
|     private WindowManager.LayoutParams popupLayoutParams; | ||||
|     public WindowManager windowManager; | ||||
| @@ -569,29 +566,32 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|         setupScreenRotationButton(); | ||||
|     } | ||||
|  | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
|         // ExoPlayer Video Listener | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // ExoPlayer Video Listener | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     void onShuffleOrRepeatModeChanged() { | ||||
|         updatePlaybackButtons(); | ||||
|         updatePlayback(); | ||||
|         NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onRepeatModeChanged(final int i) { | ||||
|         super.onRepeatModeChanged(i); | ||||
|         updatePlaybackButtons(); | ||||
|         updatePlayback(); | ||||
|         service.resetNotification(); | ||||
|         service.updateNotification(-1); | ||||
|         onShuffleOrRepeatModeChanged(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onShuffleClicked() { | ||||
|         super.onShuffleClicked(); | ||||
|         updatePlaybackButtons(); | ||||
|         updatePlayback(); | ||||
|         onShuffleOrRepeatModeChanged(); | ||||
|  | ||||
|     } | ||||
|  | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
|         // Playback Listener | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Playback Listener | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public void onPlayerError(final ExoPlaybackException error) { | ||||
| @@ -602,6 +602,13 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onTimelineChanged(final Timeline timeline, final int reason) { | ||||
|         super.onTimelineChanged(timeline, reason); | ||||
|         // force recreate notification to ensure seek bar is shown when preparation finishes | ||||
|         NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); | ||||
|     } | ||||
|  | ||||
|     protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { | ||||
|         super.onMetadataChanged(tag); | ||||
|  | ||||
| @@ -610,8 +617,7 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|         titleTextView.setText(tag.getMetadata().getName()); | ||||
|         channelTextView.setText(tag.getMetadata().getUploaderName()); | ||||
|  | ||||
|         service.resetNotification(); | ||||
|         service.updateNotification(-1); | ||||
|         NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); | ||||
|         updateMetadata(); | ||||
|     } | ||||
|  | ||||
| @@ -634,35 +640,17 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|     public void onUpdateProgress(final int currentProgress, | ||||
|                                  final int duration, final int bufferPercent) { | ||||
|         super.onUpdateProgress(currentProgress, duration, bufferPercent); | ||||
|  | ||||
|         updateProgress(currentProgress, duration, bufferPercent); | ||||
|  | ||||
|         if (!shouldUpdateOnProgress || getCurrentState() == BasePlayer.STATE_COMPLETED | ||||
|                 || getCurrentState() == BasePlayer.STATE_PAUSED || getPlayQueue() == null) { | ||||
|             return; | ||||
|         } | ||||
|         // setMetadata only updates the metadata when any of the metadata keys are null | ||||
|         mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(), getThumbnail(), | ||||
|                 duration); | ||||
|     } | ||||
|  | ||||
|         if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) { | ||||
|             service.resetNotification(); | ||||
|         } | ||||
|  | ||||
|         if (service.getBigNotRemoteView() != null) { | ||||
|             if (cachedDuration != duration) { | ||||
|                 cachedDuration = duration; | ||||
|                 cachedDurationString = getTimeString(duration); | ||||
|             } | ||||
|             service.getBigNotRemoteView() | ||||
|                     .setProgressBar(R.id.notificationProgressBar, | ||||
|                             duration, currentProgress, false); | ||||
|             service.getBigNotRemoteView() | ||||
|                     .setTextViewText(R.id.notificationTime, | ||||
|                             getTimeString(currentProgress) + " / " + cachedDurationString); | ||||
|         } | ||||
|         if (service.getNotRemoteView() != null) { | ||||
|             service.getNotRemoteView() | ||||
|                     .setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); | ||||
|         } | ||||
|         service.updateNotification(-1); | ||||
|     @Override | ||||
|     public void onPlayQueueEdited() { | ||||
|         updatePlayback(); | ||||
|         NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -1082,8 +1070,7 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|         animatePlayButtons(false, 100); | ||||
|         getRootView().setKeepScreenOn(false); | ||||
|  | ||||
|         service.resetNotification(); | ||||
|         service.updateNotification(R.drawable.exo_controls_play); | ||||
|         NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -1091,8 +1078,9 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|         super.onBuffering(); | ||||
|         getRootView().setKeepScreenOn(true); | ||||
|  | ||||
|         service.resetNotification(); | ||||
|         service.updateNotification(R.drawable.exo_controls_play); | ||||
|         if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) { | ||||
|             NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -1110,10 +1098,7 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|         checkLandscape(); | ||||
|         getRootView().setKeepScreenOn(true); | ||||
|  | ||||
|         service.resetNotification(); | ||||
|         service.updateNotification(R.drawable.exo_controls_pause); | ||||
|  | ||||
|         service.startForeground(NOTIFICATION_ID, service.getNotBuilder().build()); | ||||
|         NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -1129,13 +1114,12 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|  | ||||
|         updateWindowFlags(IDLE_WINDOW_FLAGS); | ||||
|  | ||||
|         service.resetNotification(); | ||||
|         service.updateNotification(R.drawable.exo_controls_play); | ||||
|  | ||||
|         // Remove running notification when user don't want music (or video in popup) | ||||
|         // to be played in background | ||||
|         if (!minimizeOnPopupEnabled() && !backgroundPlaybackEnabled() && videoPlayerSelected()) { | ||||
|             service.stopForeground(true); | ||||
|             NotificationUtil.getInstance().cancelNotificationAndStopForeground(service); | ||||
|         } else { | ||||
|             NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); | ||||
|         } | ||||
|  | ||||
|         getRootView().setKeepScreenOn(false); | ||||
| @@ -1147,8 +1131,7 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|         animatePlayButtons(false, 100); | ||||
|         getRootView().setKeepScreenOn(true); | ||||
|  | ||||
|         service.resetNotification(); | ||||
|         service.updateNotification(R.drawable.exo_controls_play); | ||||
|         NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -1158,20 +1141,17 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|             playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); | ||||
|             animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); | ||||
|         }); | ||||
|         getRootView().setKeepScreenOn(false); | ||||
|  | ||||
|         getRootView().setKeepScreenOn(false); | ||||
|         updateWindowFlags(IDLE_WINDOW_FLAGS); | ||||
|  | ||||
|         service.resetNotification(); | ||||
|         service.updateNotification(R.drawable.ic_replay_white_24dp); | ||||
|  | ||||
|         NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); | ||||
|         super.onCompleted(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void destroy() { | ||||
|         super.destroy(); | ||||
|  | ||||
|         service.getContentResolver().unregisterContentObserver(settingsContentObserver); | ||||
|     } | ||||
|  | ||||
| @@ -1195,6 +1175,8 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|         intentFilter.addAction(ACTION_PLAY_NEXT); | ||||
|         intentFilter.addAction(ACTION_FAST_REWIND); | ||||
|         intentFilter.addAction(ACTION_FAST_FORWARD); | ||||
|         intentFilter.addAction(ACTION_SHUFFLE); | ||||
|         intentFilter.addAction(ACTION_RECREATE_NOTIFICATION); | ||||
|  | ||||
|         intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED); | ||||
|         intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED); | ||||
| @@ -1244,6 +1226,17 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|             case ACTION_REPEAT: | ||||
|                 onRepeatClicked(); | ||||
|                 break; | ||||
|             case ACTION_SHUFFLE: | ||||
|                 onShuffleClicked(); | ||||
|                 break; | ||||
|             case ACTION_RECREATE_NOTIFICATION: | ||||
|                 NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); | ||||
|                 break; | ||||
|             case Intent.ACTION_HEADSET_PLUG: //FIXME | ||||
|                 /*notificationManager.cancel(NOTIFICATION_ID); | ||||
|                 mediaSessionManager.dispose(); | ||||
|                 mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/ | ||||
|                 break; | ||||
|             case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED: | ||||
|                 fragmentIsVisible = true; | ||||
|                 useVideoSource(true); | ||||
| @@ -1301,7 +1294,6 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|                 } | ||||
|                 break; | ||||
|         } | ||||
|         service.resetNotification(); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -1313,10 +1305,7 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|                                   final View view, | ||||
|                                   final Bitmap loadedImage) { | ||||
|         super.onLoadingComplete(imageUri, view, loadedImage); | ||||
|         // rebuild notification here since remote view does not release bitmaps, | ||||
|         // causing memory leaks | ||||
|         service.resetNotification(); | ||||
|         service.updateNotification(-1); | ||||
|         NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -1324,20 +1313,18 @@ public class VideoPlayerImpl extends VideoPlayer | ||||
|                                 final View view, | ||||
|                                 final FailReason failReason) { | ||||
|         super.onLoadingFailed(imageUri, view, failReason); | ||||
|         service.resetNotification(); | ||||
|         service.updateNotification(-1); | ||||
|         NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onLoadingCancelled(final String imageUri, final View view) { | ||||
|         super.onLoadingCancelled(imageUri, view); | ||||
|         service.resetNotification(); | ||||
|         service.updateNotification(-1); | ||||
|         NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); | ||||
|     } | ||||
|  | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
|         // Utils | ||||
|         //////////////////////////////////////////////////////////////////////////*/ | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Utils | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private void setInitialGestureValues() { | ||||
|         if (getAudioReactor() != null) { | ||||
|   | ||||
| @@ -3,44 +3,58 @@ package org.schabi.newpipe.player.helper; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.graphics.Bitmap; | ||||
| import android.media.MediaMetadata; | ||||
| import android.os.Build; | ||||
| import android.support.v4.media.MediaMetadataCompat; | ||||
| import android.support.v4.media.session.MediaSessionCompat; | ||||
| import android.support.v4.media.session.PlaybackStateCompat; | ||||
| import android.util.Log; | ||||
| import android.view.KeyEvent; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.RequiresApi; | ||||
| import androidx.core.app.NotificationCompat; | ||||
| import androidx.media.app.NotificationCompat.MediaStyle; | ||||
| import androidx.media.session.MediaButtonReceiver; | ||||
|  | ||||
| import com.google.android.exoplayer2.Player; | ||||
| import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; | ||||
|  | ||||
| import org.schabi.newpipe.MainActivity; | ||||
| import org.schabi.newpipe.player.mediasession.MediaSessionCallback; | ||||
| import org.schabi.newpipe.player.mediasession.PlayQueueNavigator; | ||||
| import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController; | ||||
|  | ||||
| public class MediaSessionManager { | ||||
|     private static final String TAG = "MediaSessionManager"; | ||||
|     private static final String TAG = MediaSessionManager.class.getSimpleName(); | ||||
|     public static final boolean DEBUG = MainActivity.DEBUG; | ||||
|  | ||||
|     @NonNull | ||||
|     private final MediaSessionCompat mediaSession; | ||||
|     @NonNull | ||||
|     private final MediaSessionConnector sessionConnector; | ||||
|  | ||||
|     private int lastAlbumArtHashCode; | ||||
|  | ||||
|     public MediaSessionManager(@NonNull final Context context, | ||||
|                                @NonNull final Player player, | ||||
|                                @NonNull final MediaSessionCallback callback) { | ||||
|         this.mediaSession = new MediaSessionCompat(context, TAG); | ||||
|         this.mediaSession.setActive(true); | ||||
|         mediaSession = new MediaSessionCompat(context, TAG); | ||||
|         mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | ||||
|                 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); | ||||
|         mediaSession.setActive(true); | ||||
|  | ||||
|         this.sessionConnector = new MediaSessionConnector(mediaSession); | ||||
|         this.sessionConnector.setControlDispatcher(new PlayQueuePlaybackController(callback)); | ||||
|         this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); | ||||
|         this.sessionConnector.setPlayer(player); | ||||
|         mediaSession.setPlaybackState(new PlaybackStateCompat.Builder() | ||||
|                 .setState(PlaybackStateCompat.STATE_NONE, -1, 1) | ||||
|                 .setActions(PlaybackStateCompat.ACTION_SEEK_TO | ||||
|                         | PlaybackStateCompat.ACTION_PLAY | ||||
|                         | PlaybackStateCompat.ACTION_PAUSE // was play and pause now play/pause | ||||
|                         | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | ||||
|                         | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | ||||
|                         | PlaybackStateCompat.ACTION_SET_REPEAT_MODE | ||||
|                         | PlaybackStateCompat.ACTION_STOP) | ||||
|                 .build()); | ||||
|  | ||||
|         sessionConnector = new MediaSessionConnector(mediaSession); | ||||
|         sessionConnector.setControlDispatcher(new PlayQueuePlaybackController(callback)); | ||||
|         sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); | ||||
|         sessionConnector.setPlayer(player); | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
| @@ -49,46 +63,78 @@ public class MediaSessionManager { | ||||
|         return MediaButtonReceiver.handleIntent(mediaSession, intent); | ||||
|     } | ||||
|  | ||||
|     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) | ||||
|     public void setLockScreenArt(final NotificationCompat.Builder builder, | ||||
|                                  @Nullable final Bitmap thumbnailBitmap) { | ||||
|         if (thumbnailBitmap == null || !mediaSession.isActive()) { | ||||
|     public MediaSessionCompat.Token getSessionToken() { | ||||
|         return mediaSession.getSessionToken(); | ||||
|     } | ||||
|  | ||||
|     public void setMetadata(final String title, | ||||
|                             final String artist, | ||||
|                             final Bitmap albumArt, | ||||
|                             final long duration) { | ||||
|         if (albumArt == null || !mediaSession.isActive()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         mediaSession.setMetadata( | ||||
|                 new MediaMetadataCompat.Builder() | ||||
|                         .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thumbnailBitmap) | ||||
|                         .build() | ||||
|         ); | ||||
|         if (DEBUG) { | ||||
|             if (getMetadataAlbumArt() == null) { | ||||
|                 Log.d(TAG, "N_getMetadataAlbumArt: thumb == null"); | ||||
|             } | ||||
|             if (getMetadataTitle() == null) { | ||||
|                 Log.d(TAG, "N_getMetadataTitle: title == null"); | ||||
|             } | ||||
|             if (getMetadataArtist() == null) { | ||||
|                 Log.d(TAG, "N_getMetadataArtist: artist == null"); | ||||
|             } | ||||
|             if (getMetadataDuration() <= 1) { | ||||
|                 Log.d(TAG, "N_getMetadataDuration: duration <= 1; " + getMetadataDuration()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         final MediaStyle mediaStyle = new MediaStyle() | ||||
|                 .setMediaSession(mediaSession.getSessionToken()); | ||||
|         if (getMetadataAlbumArt() == null || getMetadataTitle() == null | ||||
|                 || getMetadataArtist() == null || getMetadataDuration() <= 1 | ||||
|                 || albumArt.hashCode() != lastAlbumArtHashCode) { | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, "setMetadata: N_Metadata update: t: " + title + " a: " + artist | ||||
|                         + " thumb: " + albumArt.hashCode() + " d: " + duration); | ||||
|             } | ||||
|  | ||||
|         builder.setStyle(mediaStyle); | ||||
|             mediaSession.setMetadata(new MediaMetadataCompat.Builder() | ||||
|                     .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) | ||||
|                     .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist) | ||||
|                     .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt) | ||||
|                     .putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, albumArt) | ||||
|                     .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration).build()); | ||||
|             lastAlbumArtHashCode = albumArt.hashCode(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) | ||||
|     public void clearLockScreenArt(final NotificationCompat.Builder builder) { | ||||
|         mediaSession.setMetadata( | ||||
|                 new MediaMetadataCompat.Builder() | ||||
|                         .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, null) | ||||
|                         .build() | ||||
|         ); | ||||
|     private Bitmap getMetadataAlbumArt() { | ||||
|         return mediaSession.getController().getMetadata() | ||||
|                 .getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART); | ||||
|     } | ||||
|  | ||||
|         final MediaStyle mediaStyle = new MediaStyle() | ||||
|                 .setMediaSession(mediaSession.getSessionToken()); | ||||
|     private String getMetadataTitle() { | ||||
|         return mediaSession.getController().getMetadata() | ||||
|                 .getString(MediaMetadataCompat.METADATA_KEY_TITLE); | ||||
|     } | ||||
|  | ||||
|         builder.setStyle(mediaStyle); | ||||
|     private String getMetadataArtist() { | ||||
|         return mediaSession.getController().getMetadata() | ||||
|                 .getString(MediaMetadataCompat.METADATA_KEY_ARTIST); | ||||
|     } | ||||
|  | ||||
|     private long getMetadataDuration() { | ||||
|         return mediaSession.getController().getMetadata() | ||||
|                 .getLong(MediaMetadataCompat.METADATA_KEY_DURATION); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Should be called on player destruction to prevent leakage. | ||||
|      */ | ||||
|     public void dispose() { | ||||
|         this.sessionConnector.setPlayer(null); | ||||
|         this.sessionConnector.setQueueNavigator(null); | ||||
|         this.mediaSession.setActive(false); | ||||
|         this.mediaSession.release(); | ||||
|         sessionConnector.setPlayer(null); | ||||
|         sessionConnector.setQueueNavigator(null); | ||||
|         mediaSession.setActive(false); | ||||
|         mediaSession.release(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -255,21 +255,21 @@ public class MediaSourceManager { | ||||
|  | ||||
|         // Loading and Syncing | ||||
|         switch (event.type()) { | ||||
|             case INIT: | ||||
|             case REORDER: | ||||
|             case ERROR: | ||||
|             case SELECT: | ||||
|             case INIT: case REORDER: case ERROR: case SELECT: | ||||
|                 loadImmediate(); // low frequency, critical events | ||||
|                 break; | ||||
|             case APPEND: | ||||
|             case REMOVE: | ||||
|             case MOVE: | ||||
|             case RECOVERY: | ||||
|             case APPEND: case REMOVE: case MOVE: case RECOVERY: | ||||
|             default: | ||||
|                 loadDebounced(); // high frequency or noncritical events | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         // update ui and notification | ||||
|         switch (event.type()) { | ||||
|             case APPEND: case REMOVE: case MOVE: case REORDER: | ||||
|                 playbackListener.onPlayQueueEdited(); | ||||
|         } | ||||
|  | ||||
|         if (!isPlayQueueReady()) { | ||||
|             maybeBlock(); | ||||
|             playQueue.fetch(); | ||||
|   | ||||
| @@ -69,7 +69,7 @@ public interface PlaybackListener { | ||||
|     MediaSource sourceOf(PlayQueueItem item, StreamInfo info); | ||||
|  | ||||
|     /** | ||||
|      * Called when the play queue can no longer to played or used. | ||||
|      * Called when the play queue can no longer be played or used. | ||||
|      * Currently, this means the play queue is empty and complete. | ||||
|      * Signals to the listener that it should shutdown. | ||||
|      * <p> | ||||
| @@ -77,4 +77,13 @@ public interface PlaybackListener { | ||||
|      * </p> | ||||
|      */ | ||||
|     void onPlaybackShutdown(); | ||||
|  | ||||
|     /** | ||||
|      * Called whenever the play queue was edited (items were added, deleted or moved), | ||||
|      * use this to e.g. update notification buttons or fragment ui. | ||||
|      * <p> | ||||
|      * May be called at any time. | ||||
|      * </p> | ||||
|      */ | ||||
|     void onPlayQueueEdited(); | ||||
| } | ||||
|   | ||||
| @@ -5,12 +5,12 @@ import android.os.Bundle; | ||||
| import androidx.preference.PreferenceManager; | ||||
| import android.view.View; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.ActionBar; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.preference.PreferenceFragmentCompat; | ||||
|  | ||||
| import org.schabi.newpipe.MainActivity; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { | ||||
|     protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); | ||||
| @@ -25,24 +25,16 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) { | ||||
|         super.onViewCreated(view, savedInstanceState); | ||||
|     public void onViewCreated(@NonNull final View rootView, | ||||
|                               @Nullable final Bundle savedInstanceState) { | ||||
|         super.onViewCreated(rootView, savedInstanceState); | ||||
|         setDivider(null); | ||||
|         updateTitle(); | ||||
|         ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         updateTitle(); | ||||
|     } | ||||
|  | ||||
|     private void updateTitle() { | ||||
|         if (getActivity() instanceof AppCompatActivity) { | ||||
|             final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); | ||||
|             if (actionBar != null) { | ||||
|                 actionBar.setTitle(getPreferenceScreen().getTitle()); | ||||
|             } | ||||
|         } | ||||
|         ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,270 @@ | ||||
| package org.schabi.newpipe.settings; | ||||
|  | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.graphics.PorterDuff; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.CheckBox; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.LinearLayout; | ||||
| import android.widget.RadioButton; | ||||
| import android.widget.RadioGroup; | ||||
| import android.widget.Switch; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.AlertDialog; | ||||
| import androidx.appcompat.content.res.AppCompatResources; | ||||
| import androidx.fragment.app.Fragment; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.player.MainPlayer; | ||||
| import org.schabi.newpipe.player.NotificationConstants; | ||||
| import org.schabi.newpipe.util.DeviceUtils; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
| import org.schabi.newpipe.views.FocusOverlayView; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| public class NotificationSettingsFragment extends Fragment { | ||||
|  | ||||
|     private Switch scaleSwitch; | ||||
|     private NotificationSlot[] notificationSlots; | ||||
|  | ||||
|     private SharedPreferences pref; | ||||
|     private List<Integer> compactSlots; | ||||
|     private String scaleKey; | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|     // Lifecycle | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         pref = PreferenceManager.getDefaultSharedPreferences(requireContext()); | ||||
|         scaleKey = getString(R.string.scale_to_square_image_in_notifications_key); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(@NonNull final LayoutInflater inflater, | ||||
|                              final ViewGroup container, | ||||
|                              @Nullable final Bundle savedInstanceState) { | ||||
|         return inflater.inflate(R.layout.settings_notification, container, false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onViewCreated(@NonNull final View rootView, | ||||
|                               @Nullable final Bundle savedInstanceState) { | ||||
|         super.onViewCreated(rootView, savedInstanceState); | ||||
|  | ||||
|         setupScaleSwitch(rootView); | ||||
|         setupActions(rootView); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         ThemeHelper.setTitleToAppCompatActivity(getActivity(), | ||||
|                 getString(R.string.settings_category_notification_title)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onPause() { | ||||
|         super.onPause(); | ||||
|         saveChanges(); | ||||
|         requireContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION)); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|     // Setup | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     private void setupScaleSwitch(@NonNull final View view) { | ||||
|         scaleSwitch = view.findViewById(R.id.notificationScaleSwitch); | ||||
|         scaleSwitch.setChecked(pref.getBoolean(scaleKey, false)); | ||||
|  | ||||
|         view.findViewById(R.id.notificationScaleSwitchClickableArea) | ||||
|                 .setOnClickListener(v -> scaleSwitch.toggle()); | ||||
|     } | ||||
|  | ||||
|     private void setupActions(@NonNull final View view) { | ||||
|         compactSlots = | ||||
|                 NotificationConstants.getCompactSlotsFromPreferences(requireContext(), pref, 5); | ||||
|         notificationSlots = new NotificationSlot[5]; | ||||
|         for (int i = 0; i < 5; i++) { | ||||
|             notificationSlots[i] = new NotificationSlot(i, view); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|     // Saving | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     private void saveChanges() { | ||||
|         final SharedPreferences.Editor editor = pref.edit(); | ||||
|         editor.putBoolean(scaleKey, scaleSwitch.isChecked()); | ||||
|  | ||||
|         for (int i = 0; i < 3; i++) { | ||||
|             editor.putInt(getString(NotificationConstants.SLOT_COMPACT_PREF_KEYS[i]), | ||||
|                     (i < compactSlots.size() ? compactSlots.get(i) : -1)); | ||||
|         } | ||||
|  | ||||
|         for (int i = 0; i < 5; i++) { | ||||
|             editor.putInt(getString(NotificationConstants.SLOT_PREF_KEYS[i]), | ||||
|                     notificationSlots[i].selectedAction); | ||||
|         } | ||||
|  | ||||
|         editor.apply(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|     // Notification action | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     private static final int[] SLOT_ITEMS = { | ||||
|             R.id.notificationAction0, | ||||
|             R.id.notificationAction1, | ||||
|             R.id.notificationAction2, | ||||
|             R.id.notificationAction3, | ||||
|             R.id.notificationAction4, | ||||
|     }; | ||||
|  | ||||
|     private static final int[] SLOT_TITLES = { | ||||
|             R.string.notification_action_0_title, | ||||
|             R.string.notification_action_1_title, | ||||
|             R.string.notification_action_2_title, | ||||
|             R.string.notification_action_3_title, | ||||
|             R.string.notification_action_4_title, | ||||
|     }; | ||||
|  | ||||
|     private class NotificationSlot { | ||||
|  | ||||
|         final int i; | ||||
|         @NotificationConstants.Action int selectedAction; | ||||
|  | ||||
|         ImageView icon; | ||||
|         TextView summary; | ||||
|  | ||||
|         NotificationSlot(final int actionIndex, final View parentView) { | ||||
|             this.i = actionIndex; | ||||
|  | ||||
|             final View view = parentView.findViewById(SLOT_ITEMS[i]); | ||||
|             setupSelectedAction(view); | ||||
|             setupTitle(view); | ||||
|             setupCheckbox(view); | ||||
|         } | ||||
|  | ||||
|         void setupTitle(final View view) { | ||||
|             ((TextView) view.findViewById(R.id.notificationActionTitle)) | ||||
|                     .setText(SLOT_TITLES[i]); | ||||
|             view.findViewById(R.id.notificationActionClickableArea).setOnClickListener( | ||||
|                     v -> openActionChooserDialog()); | ||||
|         } | ||||
|  | ||||
|         void setupCheckbox(final View view) { | ||||
|             final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox); | ||||
|             compactSlotCheckBox.setChecked(compactSlots.contains(i)); | ||||
|             view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener( | ||||
|                     v -> { | ||||
|                         if (compactSlotCheckBox.isChecked()) { | ||||
|                             compactSlots.remove((Integer) i); | ||||
|                         } else if (compactSlots.size() < 3) { | ||||
|                             compactSlots.add(i); | ||||
|                         } else { | ||||
|                             Toast.makeText(requireContext(), | ||||
|                                     R.string.notification_actions_at_most_three, | ||||
|                                     Toast.LENGTH_SHORT).show(); | ||||
|                             return; | ||||
|                         } | ||||
|  | ||||
|                         compactSlotCheckBox.toggle(); | ||||
|                     }); | ||||
|         } | ||||
|  | ||||
|         void setupSelectedAction(final View view) { | ||||
|             icon = view.findViewById(R.id.notificationActionIcon); | ||||
|             summary = view.findViewById(R.id.notificationActionSummary); | ||||
|             selectedAction = pref.getInt(getString(NotificationConstants.SLOT_PREF_KEYS[i]), | ||||
|                     NotificationConstants.SLOT_DEFAULTS[i]); | ||||
|             updateInfo(); | ||||
|         } | ||||
|  | ||||
|         void updateInfo() { | ||||
|             if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) { | ||||
|                 icon.setImageDrawable(null); | ||||
|             } else { | ||||
|                 icon.setImageDrawable(AppCompatResources.getDrawable(requireContext(), | ||||
|                         NotificationConstants.ACTION_ICONS[selectedAction])); | ||||
|             } | ||||
|  | ||||
|             summary.setText(NotificationConstants.getActionName(requireContext(), selectedAction)); | ||||
|         } | ||||
|  | ||||
|         void openActionChooserDialog() { | ||||
|             final LayoutInflater inflater = LayoutInflater.from(requireContext()); | ||||
|             final LinearLayout rootLayout = (LinearLayout) inflater.inflate( | ||||
|                     R.layout.single_choice_dialog_view, null, false); | ||||
|             final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); | ||||
|  | ||||
|             final AlertDialog alertDialog = new AlertDialog.Builder(requireContext()) | ||||
|                     .setTitle(SLOT_TITLES[i]) | ||||
|                     .setView(radioGroup) | ||||
|                     .setCancelable(true) | ||||
|                     .create(); | ||||
|  | ||||
|             final View.OnClickListener radioButtonsClickListener = v -> { | ||||
|                 selectedAction = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][v.getId()]; | ||||
|                 updateInfo(); | ||||
|                 alertDialog.dismiss(); | ||||
|             }; | ||||
|  | ||||
|             for (int id = 0; id < NotificationConstants.SLOT_ALLOWED_ACTIONS[i].length; ++id) { | ||||
|                 final int action = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][id]; | ||||
|                 final RadioButton radioButton | ||||
|                         = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); | ||||
|  | ||||
|                 // if present set action icon with correct color | ||||
|                 if (NotificationConstants.ACTION_ICONS[action] != 0) { | ||||
|                     final Drawable drawable = AppCompatResources.getDrawable(requireContext(), | ||||
|                             NotificationConstants.ACTION_ICONS[action]); | ||||
|                     if (drawable != null) { | ||||
|                         final int color = ThemeHelper.resolveColorFromAttr(requireContext(), | ||||
|                                 android.R.attr.textColorPrimary); | ||||
|                         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|                             drawable.setTint(color); | ||||
|                         } else { | ||||
|                             drawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN); | ||||
|                         } | ||||
|                         radioButton.setCompoundDrawablesWithIntrinsicBounds( | ||||
|                                 null, null, drawable, null); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 radioButton.setText(NotificationConstants.getActionName(requireContext(), action)); | ||||
|                 radioButton.setChecked(action == selectedAction); | ||||
|                 radioButton.setId(id); | ||||
|                 radioButton.setLayoutParams(new RadioGroup.LayoutParams( | ||||
|                         ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); | ||||
|                 radioButton.setOnClickListener(radioButtonsClickListener); | ||||
|                 radioGroup.addView(radioButton); | ||||
|             } | ||||
|             alertDialog.show(); | ||||
|  | ||||
|             if (DeviceUtils.isTv(requireContext())) { | ||||
|                 FocusOverlayView.setupFocusObserver(alertDialog); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -22,9 +22,7 @@ import android.widget.Toast; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.ActionBar; | ||||
| import androidx.appcompat.app.AlertDialog; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.appcompat.content.res.AppCompatResources; | ||||
| import androidx.appcompat.widget.AppCompatImageView; | ||||
| import androidx.fragment.app.Fragment; | ||||
| @@ -117,7 +115,8 @@ public class PeertubeInstanceListFragment extends Fragment { | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         updateTitle(); | ||||
|         ThemeHelper.setTitleToAppCompatActivity(getActivity(), | ||||
|                 getString(R.string.peertube_instance_url_title)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -176,15 +175,6 @@ public class PeertubeInstanceListFragment extends Fragment { | ||||
|         sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply(); | ||||
|     } | ||||
|  | ||||
|     private void updateTitle() { | ||||
|         if (getActivity() instanceof AppCompatActivity) { | ||||
|             final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); | ||||
|             if (actionBar != null) { | ||||
|                 actionBar.setTitle(R.string.peertube_instance_url_title); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void saveChanges() { | ||||
|         final JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances"); | ||||
|         for (final PeertubeInstance instance : instanceList) { | ||||
|   | ||||
| @@ -16,9 +16,7 @@ import android.widget.TextView; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.ActionBar; | ||||
| import androidx.appcompat.app.AlertDialog; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.appcompat.content.res.AppCompatResources; | ||||
| import androidx.appcompat.widget.AppCompatImageView; | ||||
| import androidx.fragment.app.Fragment; | ||||
| @@ -92,7 +90,8 @@ public class ChooseTabsFragment extends Fragment { | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         updateTitle(); | ||||
|         ThemeHelper.setTitleToAppCompatActivity(getActivity(), | ||||
|                 getString(R.string.main_page_content)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -137,15 +136,6 @@ public class ChooseTabsFragment extends Fragment { | ||||
|         tabList.addAll(tabsManager.getTabs()); | ||||
|     } | ||||
|  | ||||
|     private void updateTitle() { | ||||
|         if (getActivity() instanceof AppCompatActivity) { | ||||
|             final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); | ||||
|             if (actionBar != null) { | ||||
|                 actionBar.setTitle(R.string.main_page_content); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void saveChanges() { | ||||
|         tabsManager.saveTabs(tabList); | ||||
|     } | ||||
|   | ||||
| @@ -57,7 +57,7 @@ import org.schabi.newpipe.settings.SettingsActivity; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| @SuppressWarnings({"unused", "WeakerAccess"}) | ||||
| @SuppressWarnings({"unused"}) | ||||
| public final class NavigationHelper { | ||||
|     public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag"; | ||||
|     public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag"; | ||||
| @@ -69,16 +69,18 @@ public final class NavigationHelper { | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @NonNull | ||||
|     public static Intent getPlayerIntent(@NonNull final Context context, | ||||
|                                          @NonNull final Class targetClazz, | ||||
|                                          @NonNull final PlayQueue playQueue, | ||||
|                                          @Nullable final String quality, | ||||
|                                          final boolean resumePlayback) { | ||||
|     public static <T> Intent getPlayerIntent(@NonNull final Context context, | ||||
|                                              @NonNull final Class<T> targetClazz, | ||||
|                                              @Nullable final PlayQueue playQueue, | ||||
|                                              @Nullable final String quality, | ||||
|                                              final boolean resumePlayback) { | ||||
|         final Intent intent = new Intent(context, targetClazz); | ||||
|  | ||||
|         final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); | ||||
|         if (cacheKey != null) { | ||||
|             intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); | ||||
|         if (playQueue != null) { | ||||
|             final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); | ||||
|             if (cacheKey != null) { | ||||
|                 intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); | ||||
|             } | ||||
|         } | ||||
|         if (quality != null) { | ||||
|             intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); | ||||
| @@ -90,53 +92,51 @@ public final class NavigationHelper { | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public static Intent getPlayerIntent(@NonNull final Context context, | ||||
|                                          @NonNull final Class targetClazz, | ||||
|                                          @NonNull final PlayQueue playQueue, | ||||
|                                          final boolean resumePlayback) { | ||||
|     public static <T> Intent getPlayerIntent(@NonNull final Context context, | ||||
|                                              @NonNull final Class<T> targetClazz, | ||||
|                                              @Nullable final PlayQueue playQueue, | ||||
|                                              final boolean resumePlayback) { | ||||
|         return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public static Intent getPlayerEnqueueIntent(@NonNull final Context context, | ||||
|                                                 @NonNull final Class targetClazz, | ||||
|                                                 @NonNull final PlayQueue playQueue, | ||||
|                                                 final boolean selectOnAppend, | ||||
|                                                 final boolean resumePlayback) { | ||||
|     public static <T> Intent getPlayerEnqueueIntent(@NonNull final Context context, | ||||
|                                                     @NonNull final Class<T> targetClazz, | ||||
|                                                     @Nullable final PlayQueue playQueue, | ||||
|                                                     final boolean selectOnAppend, | ||||
|                                                     final boolean resumePlayback) { | ||||
|         return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) | ||||
|                 .putExtra(BasePlayer.APPEND_ONLY, true) | ||||
|                 .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); | ||||
|     } | ||||
|  | ||||
|     @NonNull | ||||
|     public static Intent getPlayerIntent(@NonNull final Context context, | ||||
|                                          @NonNull final Class targetClazz, | ||||
|                                          @NonNull final PlayQueue playQueue, | ||||
|                                          final int repeatMode, | ||||
|                                          final float playbackSpeed, | ||||
|                                          final float playbackPitch, | ||||
|                                          final boolean playbackSkipSilence, | ||||
|                                          @Nullable final String playbackQuality, | ||||
|                                          final boolean resumePlayback, | ||||
|                                          final boolean startPaused, | ||||
|                                          final boolean isMuted) { | ||||
|     public static <T> Intent getPlayerIntent(@NonNull final Context context, | ||||
|                                              @NonNull final Class<T> targetClazz, | ||||
|                                              @Nullable final PlayQueue playQueue, | ||||
|                                              final int repeatMode, | ||||
|                                              final float playbackSpeed, | ||||
|                                              final float playbackPitch, | ||||
|                                              final boolean playbackSkipSilence, | ||||
|                                              @Nullable final String playbackQuality, | ||||
|                                              final boolean resumePlayback, | ||||
|                                              final boolean startPaused, | ||||
|                                              final boolean isMuted) { | ||||
|         return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback) | ||||
|                 .putExtra(BasePlayer.REPEAT_MODE, repeatMode) | ||||
|                 .putExtra(BasePlayer.START_PAUSED, startPaused) | ||||
|                 .putExtra(BasePlayer.IS_MUTED, isMuted); | ||||
|     } | ||||
|  | ||||
|     public static void playOnMainPlayer( | ||||
|             final AppCompatActivity activity, | ||||
|             final PlayQueue queue, | ||||
|             final boolean autoPlay) { | ||||
|     public static void playOnMainPlayer(final AppCompatActivity activity, | ||||
|                                         final PlayQueue queue, | ||||
|                                         final boolean autoPlay) { | ||||
|         playOnMainPlayer(activity.getSupportFragmentManager(), queue, autoPlay); | ||||
|     } | ||||
|  | ||||
|     public static void playOnMainPlayer( | ||||
|             final FragmentManager fragmentManager, | ||||
|             final PlayQueue queue, | ||||
|             final boolean autoPlay) { | ||||
|     public static void playOnMainPlayer(final FragmentManager fragmentManager, | ||||
|                                         final PlayQueue queue, | ||||
|                                         final boolean autoPlay) { | ||||
|         final PlayQueueItem currentStream = queue.getItem(); | ||||
|         openVideoDetailFragment( | ||||
|                 fragmentManager, | ||||
| @@ -148,7 +148,7 @@ public final class NavigationHelper { | ||||
|     } | ||||
|  | ||||
|     public static void playOnMainPlayer(@NonNull final Context context, | ||||
|                                         @NonNull final PlayQueue queue, | ||||
|                                         @Nullable final PlayQueue queue, | ||||
|                                         @NonNull final StreamingService.LinkType linkType, | ||||
|                                         @NonNull final String url, | ||||
|                                         @NonNull final String title, | ||||
| @@ -553,18 +553,14 @@ public final class NavigationHelper { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public static Intent getBackgroundPlayerActivityIntent(final Context context) { | ||||
|         return getServicePlayerActivityIntent(context, BackgroundPlayerActivity.class); | ||||
|     } | ||||
|  | ||||
|     private static Intent getServicePlayerActivityIntent(final Context context, | ||||
|                                                          final Class activityClass) { | ||||
|         final Intent intent = new Intent(context, activityClass); | ||||
|     public static Intent getPlayQueueActivityIntent(final Context context) { | ||||
|         final Intent intent = new Intent(context, BackgroundPlayerActivity.class); | ||||
|         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { | ||||
|             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|         } | ||||
|         return intent; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Link handling | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|   | ||||
| @@ -19,6 +19,7 @@ | ||||
|  | ||||
| package org.schabi.newpipe.util; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.res.TypedArray; | ||||
| import androidx.preference.PreferenceManager; | ||||
| @@ -26,7 +27,10 @@ import android.util.TypedValue; | ||||
| import android.view.ContextThemeWrapper; | ||||
|  | ||||
| import androidx.annotation.AttrRes; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.StyleRes; | ||||
| import androidx.appcompat.app.ActionBar; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.core.content.ContextCompat; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| @@ -231,4 +235,20 @@ public final class ThemeHelper { | ||||
|         return PreferenceManager.getDefaultSharedPreferences(context) | ||||
|                 .getString(themeKey, defaultTheme); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets the title to the activity, if the activity is an {@link AppCompatActivity} and has an | ||||
|      * action bar. | ||||
|      * @param activity the activity to set the title of | ||||
|      * @param title the title to set to the activity | ||||
|      */ | ||||
|     public static void setTitleToAppCompatActivity(@Nullable final Activity activity, | ||||
|                                                    final CharSequence title) { | ||||
|         if (activity instanceof AppCompatActivity) { | ||||
|             final ActionBar actionBar = ((AppCompatActivity) activity).getSupportActionBar(); | ||||
|             if (actionBar != null) { | ||||
|                 actionBar.setTitle(title); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| Before Width: | Height: | Size: 221 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_close_white_24dp_png.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 415 B | 
| After Width: | Height: | Size: 302 B | 
| Before Width: | Height: | Size: 675 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_replay_white_24dp_png.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 621 B | 
| Before Width: | Height: | Size: 175 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_close_white_24dp_png.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 285 B | 
| After Width: | Height: | Size: 246 B | 
| Before Width: | Height: | Size: 457 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_replay_white_24dp_png.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 411 B | 
| Before Width: | Height: | Size: 257 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_close_white_24dp_png.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 602 B | 
| After Width: | Height: | Size: 413 B | 
| Before Width: | Height: | Size: 908 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_replay_white_24dp_png.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 837 B | 
| Before Width: | Height: | Size: 347 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_close_white_24dp_png.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.0 KiB | 
| After Width: | Height: | Size: 614 B | 
| Before Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_replay_white_24dp_png.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 436 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp_png.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.0 KiB | 
| After Width: | Height: | Size: 777 B | 
| Before Width: | Height: | Size: 1.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxxhdpi/ic_replay_white_24dp_png.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.0 KiB | 
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_close_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:width="24dp" | ||||
|         android:height="24dp" | ||||
|         android:viewportWidth="24.0" | ||||
|         android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFFFF" | ||||
|         android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> | ||||
| </vector> | ||||
| @@ -0,0 +1,9 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="24dp" | ||||
|     android:height="24dp" | ||||
|     android:viewportWidth="24" | ||||
|     android:viewportHeight="24"> | ||||
|   <path | ||||
|       android:pathData="M6,2l0.01,6L10,12l-3.99,4.01L6,22h12v-6l-4,-4l4,-3.99V2H6zM16,16.5V20H8v-3.5l4,-4L16,16.5z" | ||||
|       android:fillColor="#ffffff"/> | ||||
| </vector> | ||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_replay_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:width="24dp" | ||||
|         android:height="24dp" | ||||
|         android:viewportWidth="24.0" | ||||
|         android:viewportHeight="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFFFF" | ||||
|         android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z"/> | ||||
| </vector> | ||||
| @@ -465,21 +465,20 @@ | ||||
|             android:layout_height="60dp" | ||||
|             android:id="@+id/playQueueControl"> | ||||
|  | ||||
|             <ImageButton | ||||
|             <androidx.appcompat.widget.AppCompatImageButton | ||||
|                 android:id="@+id/playQueueClose" | ||||
|                 android:layout_width="50dp" | ||||
|                 android:layout_height="50dp" | ||||
|                 android:layout_centerVertical="true" | ||||
|                 android:layout_alignParentEnd="true" | ||||
|                 android:layout_centerVertical="true" | ||||
|                 android:layout_marginEnd="40dp" | ||||
|                 android:padding="10dp" | ||||
|                 android:clickable="true" | ||||
|                 android:focusable="true" | ||||
|                 android:scaleType="fitXY" | ||||
|                 android:tint="?attr/colorAccent" | ||||
|                 android:src="@drawable/ic_close_white_24dp" | ||||
|                 android:background="?android:selectableItemBackground" | ||||
|                 tools:ignore="ContentDescription"/> | ||||
|                 android:clickable="true" | ||||
|                 android:contentDescription="@string/close" | ||||
|                 android:focusable="true" | ||||
|                 android:padding="10dp" | ||||
|                 android:scaleType="fitXY" | ||||
|                 app:srcCompat="@drawable/ic_close_white_24dp" /> | ||||
|  | ||||
|             <ImageButton | ||||
|                 android:id="@+id/repeatButton" | ||||
|   | ||||
| @@ -10,10 +10,8 @@ | ||||
|     android:maxLines="2" | ||||
|     android:minHeight="?attr/listPreferredItemHeightSmall" | ||||
|     android:paddingEnd="?attr/listPreferredItemPaddingRight" | ||||
|     android:paddingLeft="?attr/listPreferredItemPaddingLeft" | ||||
|     android:paddingRight="?attr/listPreferredItemPaddingRight" | ||||
|     android:paddingStart="?attr/listPreferredItemPaddingLeft" | ||||
|     android:background="?attr/checked_selector" | ||||
|     android:textColor="?attr/textColorAlertDialogListItem" | ||||
|     tools:drawableLeft="?attr/ic_play" | ||||
|     tools:drawableLeft="?attr/ic_play_arrow" | ||||
|     tools:text="Lorem ipsum dolor sit amet" /> | ||||
|   | ||||
| @@ -77,234 +77,234 @@ | ||||
|                 android:paddingEnd="@dimen/player_main_controls_padding" | ||||
|                 android:baselineAligned="false"> | ||||
|  | ||||
|             <LinearLayout | ||||
|                 android:id="@+id/primaryControls" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:minHeight="45dp" | ||||
|                 android:baselineAligned="false" | ||||
|                 android:gravity="top" | ||||
|                 tools:ignore="RtlHardcoded"> | ||||
|                 <LinearLayout | ||||
|                     android:id="@+id/primaryControls" | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:minHeight="45dp" | ||||
|                     android:baselineAligned="false" | ||||
|                     android:gravity="top" | ||||
|                     tools:ignore="RtlHardcoded"> | ||||
|  | ||||
|                 <ImageButton | ||||
|                     android:id="@+id/playerCloseButton" | ||||
|                     android:layout_width="36dp" | ||||
|                     android:layout_height="36dp" | ||||
|                     android:padding="@dimen/player_main_buttons_padding" | ||||
|                     android:layout_marginEnd="8dp" | ||||
|                     android:clickable="true" | ||||
|                     android:focusable="true" | ||||
|                     android:scaleType="fitXY" | ||||
|                     android:src="?attr/ic_close" | ||||
|                     android:background="?attr/selectableItemBackgroundBorderless" | ||||
|                     tools:ignore="ContentDescription,RtlHardcoded" | ||||
|                     android:visibility="gone" /> | ||||
|                     <androidx.appcompat.widget.AppCompatImageButton | ||||
|                         android:id="@+id/playerCloseButton" | ||||
|                         android:layout_width="36dp" | ||||
|                         android:layout_height="36dp" | ||||
|                         android:padding="@dimen/player_main_buttons_padding" | ||||
|                         android:layout_marginEnd="8dp" | ||||
|                         android:clickable="true" | ||||
|                         android:focusable="true" | ||||
|                         android:scaleType="fitXY" | ||||
|                         app:srcCompat="?attr/ic_close" | ||||
|                         android:background="?attr/selectableItemBackgroundBorderless" | ||||
|                         tools:ignore="ContentDescription,RtlHardcoded" | ||||
|                         android:visibility="gone" /> | ||||
|  | ||||
|                     <LinearLayout | ||||
|                         android:id="@+id/metadataView" | ||||
|                         android:layout_width="0dp" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:gravity="top" | ||||
|                         android:orientation="vertical" | ||||
|                         android:layout_marginTop="6dp" | ||||
|                         android:layout_marginRight="8dp" | ||||
|                         tools:ignore="RtlHardcoded" | ||||
|                         android:layout_weight="1"> | ||||
|  | ||||
|                         <TextView | ||||
|                             android:id="@+id/titleTextView" | ||||
|                             android:layout_width="match_parent" | ||||
|                             android:layout_height="wrap_content" | ||||
|                             android:ellipsize="marquee" | ||||
|                             android:fadingEdge="horizontal" | ||||
|                             android:marqueeRepeatLimit="marquee_forever" | ||||
|                             android:scrollHorizontally="true" | ||||
|                             android:singleLine="true" | ||||
|                             android:textColor="@android:color/white" | ||||
|                             android:textSize="15sp" | ||||
|                             android:textStyle="bold" | ||||
|                             android:clickable="true" | ||||
|                             android:focusable="true" | ||||
|                             tools:ignore="RtlHardcoded" | ||||
|                             tools:text="The Video Title LONG very LONG"/> | ||||
|  | ||||
|                         <TextView | ||||
|                             android:id="@+id/channelTextView" | ||||
|                             android:layout_width="match_parent" | ||||
|                             android:layout_height="wrap_content" | ||||
|                             android:ellipsize="marquee" | ||||
|                             android:fadingEdge="horizontal" | ||||
|                             android:marqueeRepeatLimit="marquee_forever" | ||||
|                             android:scrollHorizontally="true" | ||||
|                             android:singleLine="true" | ||||
|                             android:textColor="@android:color/white" | ||||
|                             android:textSize="12sp" | ||||
|                             android:clickable="true" | ||||
|                             android:focusable="true" | ||||
|                             tools:text="The Video Artist  LONG very LONG very Long"/> | ||||
|                     </LinearLayout> | ||||
|  | ||||
|                     <TextView | ||||
|                         android:id="@+id/qualityTextView" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="35dp" | ||||
|                         android:minWidth="0dp" | ||||
|                         android:padding="@dimen/player_main_buttons_padding" | ||||
|                         android:layout_marginEnd="8dp" | ||||
|                         android:gravity="center" | ||||
|                         android:text="720p" | ||||
|                         android:textColor="@android:color/white" | ||||
|                         android:textStyle="bold" | ||||
|                         android:background="?attr/selectableItemBackground" | ||||
|                         android:visibility="visible" | ||||
|                         tools:ignore="HardcodedText,RtlHardcoded"/> | ||||
|  | ||||
|                     <TextView | ||||
|                         android:id="@+id/playbackSpeed" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="35dp" | ||||
|                         android:padding="@dimen/player_main_buttons_padding" | ||||
|                         android:layout_marginEnd="8dp" | ||||
|                         android:gravity="center" | ||||
|                         android:minWidth="0dp" | ||||
|                         android:textColor="@android:color/white" | ||||
|                         android:textStyle="bold" | ||||
|                         android:background="?attr/selectableItemBackground" | ||||
|                         tools:ignore="RtlHardcoded,RtlSymmetry" | ||||
|                         tools:text="1x"/> | ||||
|  | ||||
|                     <androidx.appcompat.widget.AppCompatImageButton | ||||
|                         android:id="@+id/queueButton" | ||||
|                         android:layout_width="35dp" | ||||
|                         android:layout_height="35dp" | ||||
|                         android:paddingTop="5dp" | ||||
|                         android:paddingStart="3dp" | ||||
|                         android:paddingEnd="3dp" | ||||
|                         android:paddingBottom="3dp" | ||||
|                         android:layout_marginEnd="8dp" | ||||
|                         android:clickable="true" | ||||
|                         android:focusable="true" | ||||
|                         android:scaleType="fitCenter" | ||||
|                         app:srcCompat="@drawable/ic_list_white_24dp" | ||||
|                         android:background="?attr/selectableItemBackground" | ||||
|                         tools:ignore="ContentDescription,RtlHardcoded" | ||||
|                         android:visibility="gone"/> | ||||
|  | ||||
|                     <androidx.appcompat.widget.AppCompatImageButton | ||||
|                         android:id="@+id/moreOptionsButton" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:padding="@dimen/player_main_buttons_padding" | ||||
|                         android:clickable="true" | ||||
|                         android:focusable="true" | ||||
|                         android:scaleType="fitXY" | ||||
|                         app:srcCompat="@drawable/ic_expand_more_white_24dp" | ||||
|                         android:background="?attr/selectableItemBackgroundBorderless" | ||||
|                         tools:ignore="ContentDescription,RtlHardcoded"/> | ||||
|  | ||||
|                 </LinearLayout> | ||||
|  | ||||
|                 <LinearLayout | ||||
|                     android:id="@+id/metadataView" | ||||
|                     android:layout_width="0dp" | ||||
|                     android:id="@+id/secondaryControls" | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:gravity="top" | ||||
|                     android:orientation="vertical" | ||||
|                     android:layout_marginTop="6dp" | ||||
|                     android:layout_marginRight="8dp" | ||||
|                     android:visibility="invisible" | ||||
|                     tools:ignore="RtlHardcoded" | ||||
|                     android:layout_weight="1"> | ||||
|                     tools:visibility="visible"> | ||||
|  | ||||
|                     <TextView | ||||
|                         android:id="@+id/titleTextView" | ||||
|                         android:layout_width="match_parent" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:ellipsize="marquee" | ||||
|                         android:fadingEdge="horizontal" | ||||
|                         android:marqueeRepeatLimit="marquee_forever" | ||||
|                         android:scrollHorizontally="true" | ||||
|                         android:singleLine="true" | ||||
|                         android:id="@+id/resizeTextView" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="35dp" | ||||
|                         android:padding="@dimen/player_main_buttons_padding" | ||||
|                         android:layout_marginEnd="8dp" | ||||
|                         android:gravity="center" | ||||
|                         android:minWidth="50dp" | ||||
|                         android:textColor="@android:color/white" | ||||
|                         android:textSize="15sp" | ||||
|                         android:textStyle="bold" | ||||
|                         android:clickable="true" | ||||
|                         android:focusable="true" | ||||
|                         tools:ignore="RtlHardcoded" | ||||
|                         tools:text="The Video Title LONG very LONG"/> | ||||
|                         android:background="?attr/selectableItemBackground" | ||||
|                         tools:ignore="HardcodedText,RtlHardcoded" | ||||
|                         tools:text="FIT"/> | ||||
|  | ||||
|                     <TextView | ||||
|                         android:id="@+id/channelTextView" | ||||
|                         android:layout_width="match_parent" | ||||
|                         android:id="@+id/captionTextView" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:ellipsize="marquee" | ||||
|                         android:fadingEdge="horizontal" | ||||
|                         android:marqueeRepeatLimit="marquee_forever" | ||||
|                         android:scrollHorizontally="true" | ||||
|                         android:singleLine="true" | ||||
|                         android:padding="@dimen/player_main_buttons_padding" | ||||
|                         android:layout_marginEnd="8dp" | ||||
|                         android:gravity="center|left" | ||||
|                         android:minHeight="35dp" | ||||
|                         android:maxWidth="100dp" | ||||
|                         android:lines="1" | ||||
|                         android:ellipsize="end" | ||||
|                         android:minWidth="50dp" | ||||
|                         android:textColor="@android:color/white" | ||||
|                         android:textSize="12sp" | ||||
|                         android:textStyle="bold" | ||||
|                         android:background="?attr/selectableItemBackground" | ||||
|                         tools:ignore="RelativeOverlap,RtlHardcoded" | ||||
|                         tools:text="English"/> | ||||
|  | ||||
|                     <Space | ||||
|                         android:id="@+id/spaceBeforeButton" | ||||
|                         android:layout_width="0dp" | ||||
|                         android:layout_height="0dp" | ||||
|                         android:layout_weight="3"/> | ||||
|  | ||||
|                     <androidx.appcompat.widget.AppCompatImageButton | ||||
|                         android:id="@+id/playWithKodi" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="35dp" | ||||
|                         android:padding="@dimen/player_main_buttons_padding" | ||||
|                         android:layout_marginEnd="8dp" | ||||
|                         android:clickable="true" | ||||
|                         android:focusable="true" | ||||
|                         tools:text="The Video Artist  LONG very LONG very Long"/> | ||||
|                 </LinearLayout> | ||||
|                         android:scaleType="fitXY" | ||||
|                         app:srcCompat="@drawable/ic_cast_white_24dp" | ||||
|                         android:background="?attr/selectableItemBackground" | ||||
|                         android:contentDescription="@string/play_with_kodi_title" | ||||
|                         tools:ignore="RtlHardcoded"/> | ||||
|  | ||||
|                 <TextView | ||||
|                     android:id="@+id/qualityTextView" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="35dp" | ||||
|                     android:minWidth="0dp" | ||||
|                     android:padding="@dimen/player_main_buttons_padding" | ||||
|                     android:layout_marginEnd="8dp" | ||||
|                     android:gravity="center" | ||||
|                     android:text="720p" | ||||
|                     android:textColor="@android:color/white" | ||||
|                     android:textStyle="bold" | ||||
|                     android:background="?attr/selectableItemBackground" | ||||
|                     android:visibility="visible" | ||||
|                     tools:ignore="HardcodedText,RtlHardcoded"/> | ||||
|                     <androidx.appcompat.widget.AppCompatImageButton | ||||
|                         android:id="@+id/openInBrowser" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="35dp" | ||||
|                         android:padding="@dimen/player_main_buttons_padding" | ||||
|                         android:layout_marginEnd="8dp" | ||||
|                         android:clickable="true" | ||||
|                         android:focusable="true" | ||||
|                         android:scaleType="fitXY" | ||||
|                         app:srcCompat="@drawable/ic_language_white_24dp" | ||||
|                         android:background="?attr/selectableItemBackground" | ||||
|                         android:contentDescription="@string/open_in_browser" | ||||
|                         tools:ignore="RtlHardcoded"/> | ||||
|  | ||||
|                 <TextView | ||||
|                     android:id="@+id/playbackSpeed" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="35dp" | ||||
|                     android:padding="@dimen/player_main_buttons_padding" | ||||
|                     android:layout_marginEnd="8dp" | ||||
|                     android:gravity="center" | ||||
|                     android:minWidth="0dp" | ||||
|                     android:textColor="@android:color/white" | ||||
|                     android:textStyle="bold" | ||||
|                     android:background="?attr/selectableItemBackground" | ||||
|                     tools:ignore="RtlHardcoded,RtlSymmetry" | ||||
|                     tools:text="1x"/> | ||||
|                     <androidx.appcompat.widget.AppCompatImageButton | ||||
|                         android:id="@+id/share" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="35dp" | ||||
|                         android:padding="@dimen/player_main_buttons_padding" | ||||
|                         android:layout_marginEnd="8dp" | ||||
|                         android:clickable="true" | ||||
|                         android:focusable="true" | ||||
|                         android:scaleType="fitXY" | ||||
|                         app:srcCompat="@drawable/ic_share_white_24dp" | ||||
|                         android:background="?attr/selectableItemBackground" | ||||
|                         android:contentDescription="@string/share" | ||||
|                         tools:ignore="RtlHardcoded"/> | ||||
|  | ||||
|                 <androidx.appcompat.widget.AppCompatImageButton | ||||
|                     android:id="@+id/queueButton" | ||||
|                     android:layout_width="35dp" | ||||
|                     android:layout_height="35dp" | ||||
|                     android:paddingTop="5dp" | ||||
|                     android:paddingStart="3dp" | ||||
|                     android:paddingEnd="3dp" | ||||
|                     android:paddingBottom="3dp" | ||||
|                     android:layout_marginEnd="8dp" | ||||
|                     android:clickable="true" | ||||
|                     android:focusable="true" | ||||
|                     android:scaleType="fitCenter" | ||||
|                     app:srcCompat="@drawable/ic_list_white_24dp" | ||||
|                     android:background="?attr/selectableItemBackground" | ||||
|                     tools:ignore="ContentDescription,RtlHardcoded" | ||||
|                     android:visibility="gone"/> | ||||
|  | ||||
|                 <androidx.appcompat.widget.AppCompatImageButton | ||||
|                     android:id="@+id/moreOptionsButton" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:padding="@dimen/player_main_buttons_padding" | ||||
|                     android:clickable="true" | ||||
|                     android:focusable="true" | ||||
|                     android:scaleType="fitXY" | ||||
|                     app:srcCompat="@drawable/ic_expand_more_white_24dp" | ||||
|                     android:background="?attr/selectableItemBackgroundBorderless" | ||||
|                     tools:ignore="ContentDescription,RtlHardcoded"/> | ||||
|  | ||||
|                 </LinearLayout> | ||||
|  | ||||
|             <LinearLayout | ||||
|                 android:id="@+id/secondaryControls" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:gravity="top" | ||||
|                 android:visibility="invisible" | ||||
|                 tools:ignore="RtlHardcoded" | ||||
|                 tools:visibility="visible"> | ||||
|  | ||||
|                 <TextView | ||||
|                     android:id="@+id/resizeTextView" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="35dp" | ||||
|                     android:padding="@dimen/player_main_buttons_padding" | ||||
|                     android:layout_marginEnd="8dp" | ||||
|                     android:gravity="center" | ||||
|                     android:minWidth="50dp" | ||||
|                     android:textColor="@android:color/white" | ||||
|                     android:textStyle="bold" | ||||
|                     android:background="?attr/selectableItemBackground" | ||||
|                     tools:ignore="HardcodedText,RtlHardcoded" | ||||
|                     tools:text="FIT"/> | ||||
|  | ||||
|                 <TextView | ||||
|                     android:id="@+id/captionTextView" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:padding="@dimen/player_main_buttons_padding" | ||||
|                     android:layout_marginEnd="8dp" | ||||
|                     android:gravity="center|left" | ||||
|                     android:minHeight="35dp" | ||||
|                     android:maxWidth="100dp" | ||||
|                     android:lines="1" | ||||
|                     android:ellipsize="end" | ||||
|                     android:minWidth="50dp" | ||||
|                     android:textColor="@android:color/white" | ||||
|                     android:textStyle="bold" | ||||
|                     android:background="?attr/selectableItemBackground" | ||||
|                     tools:ignore="RelativeOverlap,RtlHardcoded" | ||||
|                     tools:text="English"/> | ||||
|  | ||||
|                 <Space | ||||
|                     android:id="@+id/spaceBeforeButton" | ||||
|                     android:layout_width="0dp" | ||||
|                     android:layout_height="0dp" | ||||
|                     android:layout_weight="3"/> | ||||
|  | ||||
|                 <androidx.appcompat.widget.AppCompatImageButton | ||||
|                     android:id="@+id/playWithKodi" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="35dp" | ||||
|                     android:padding="@dimen/player_main_buttons_padding" | ||||
|                     android:layout_marginEnd="8dp" | ||||
|                     android:clickable="true" | ||||
|                     android:focusable="true" | ||||
|                     android:scaleType="fitXY" | ||||
|                     app:srcCompat="@drawable/ic_cast_white_24dp" | ||||
|                     android:background="?attr/selectableItemBackground" | ||||
|                     android:contentDescription="@string/play_with_kodi_title" | ||||
|                     tools:ignore="RtlHardcoded"/> | ||||
|  | ||||
|                 <androidx.appcompat.widget.AppCompatImageButton | ||||
|                     android:id="@+id/openInBrowser" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="35dp" | ||||
|                     android:padding="@dimen/player_main_buttons_padding" | ||||
|                     android:layout_marginEnd="8dp" | ||||
|                     android:clickable="true" | ||||
|                     android:focusable="true" | ||||
|                     android:scaleType="fitXY" | ||||
|                     app:srcCompat="@drawable/ic_language_white_24dp" | ||||
|                     android:background="?attr/selectableItemBackground" | ||||
|                     android:contentDescription="@string/open_in_browser" | ||||
|                     tools:ignore="RtlHardcoded"/> | ||||
|  | ||||
|                 <androidx.appcompat.widget.AppCompatImageButton | ||||
|                     android:id="@+id/share" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="35dp" | ||||
|                     android:padding="@dimen/player_main_buttons_padding" | ||||
|                     android:layout_marginEnd="8dp" | ||||
|                     android:clickable="true" | ||||
|                     android:focusable="true" | ||||
|                     android:scaleType="fitXY" | ||||
|                     app:srcCompat="@drawable/ic_share_white_24dp" | ||||
|                     android:background="?attr/selectableItemBackground" | ||||
|                     android:contentDescription="@string/share" | ||||
|                     tools:ignore="RtlHardcoded"/> | ||||
|  | ||||
|                 <androidx.appcompat.widget.AppCompatImageButton | ||||
|                     android:id="@+id/switchMute" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="37dp" | ||||
|                     android:padding="@dimen/player_main_buttons_padding" | ||||
|                     android:clickable="true" | ||||
|                     android:focusable="true" | ||||
|                     android:scaleType="fitXY" | ||||
|                     app:srcCompat="@drawable/ic_volume_off_white_24dp" | ||||
|                     android:background="?attr/selectableItemBackground" | ||||
|                     android:contentDescription="@string/mute" | ||||
|                     tools:ignore="RtlHardcoded" /> | ||||
|                     <androidx.appcompat.widget.AppCompatImageButton | ||||
|                         android:id="@+id/switchMute" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="37dp" | ||||
|                         android:padding="@dimen/player_main_buttons_padding" | ||||
|                         android:clickable="true" | ||||
|                         android:focusable="true" | ||||
|                         android:scaleType="fitXY" | ||||
|                         app:srcCompat="@drawable/ic_volume_off_white_24dp" | ||||
|                         android:background="?attr/selectableItemBackground" | ||||
|                         android:contentDescription="@string/mute" | ||||
|                         tools:ignore="RtlHardcoded" /> | ||||
|  | ||||
|                 </LinearLayout> | ||||
|  | ||||
| @@ -407,42 +407,42 @@ | ||||
|             android:orientation="horizontal" | ||||
|             android:weightSum="5.5"> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatImageButton | ||||
|             android:id="@+id/playPreviousButton" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="40dp" | ||||
|             android:layout_weight="1" | ||||
|             android:layout_marginEnd="10dp" | ||||
|             android:clickable="true" | ||||
|             android:focusable="true" | ||||
|             android:background="?attr/selectableItemBackgroundBorderless" | ||||
|             android:scaleType="fitCenter" | ||||
|             app:srcCompat="@drawable/ic_previous_white_24dp" | ||||
|             tools:ignore="ContentDescription"/> | ||||
|             <androidx.appcompat.widget.AppCompatImageButton | ||||
|                 android:id="@+id/playPreviousButton" | ||||
|                 android:layout_width="0dp" | ||||
|                 android:layout_height="40dp" | ||||
|                 android:layout_weight="1" | ||||
|                 android:layout_marginEnd="10dp" | ||||
|                 android:clickable="true" | ||||
|                 android:focusable="true" | ||||
|                 android:background="?attr/selectableItemBackgroundBorderless" | ||||
|                 android:scaleType="fitCenter" | ||||
|                 app:srcCompat="@drawable/ic_previous_white_24dp" | ||||
|                 tools:ignore="ContentDescription"/> | ||||
|  | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatImageButton | ||||
|             android:id="@+id/playPauseButton" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="60dp" | ||||
|             android:layout_weight="1" | ||||
|             android:background="?attr/selectableItemBackgroundBorderless" | ||||
|             android:scaleType="fitCenter" | ||||
|             app:srcCompat="@drawable/ic_pause_white_24dp" | ||||
|             tools:ignore="ContentDescription"/> | ||||
|             <androidx.appcompat.widget.AppCompatImageButton | ||||
|                 android:id="@+id/playPauseButton" | ||||
|                 android:layout_width="0dp" | ||||
|                 android:layout_height="60dp" | ||||
|                 android:layout_weight="1" | ||||
|                 android:background="?attr/selectableItemBackgroundBorderless" | ||||
|                 android:scaleType="fitCenter" | ||||
|                 app:srcCompat="@drawable/ic_pause_white_24dp" | ||||
|                 tools:ignore="ContentDescription"/> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatImageButton | ||||
|             android:id="@+id/playNextButton" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="40dp" | ||||
|             android:layout_weight="1" | ||||
|             android:layout_marginStart="10dp" | ||||
|             android:clickable="true" | ||||
|             android:focusable="true" | ||||
|             android:background="?attr/selectableItemBackgroundBorderless" | ||||
|             android:scaleType="fitCenter" | ||||
|             app:srcCompat="@drawable/ic_next_white_24dp" | ||||
|             tools:ignore="ContentDescription"/> | ||||
|             <androidx.appcompat.widget.AppCompatImageButton | ||||
|                 android:id="@+id/playNextButton" | ||||
|                 android:layout_width="0dp" | ||||
|                 android:layout_height="40dp" | ||||
|                 android:layout_weight="1" | ||||
|                 android:layout_marginStart="10dp" | ||||
|                 android:clickable="true" | ||||
|                 android:focusable="true" | ||||
|                 android:background="?attr/selectableItemBackgroundBorderless" | ||||
|                 android:scaleType="fitCenter" | ||||
|                 app:srcCompat="@drawable/ic_next_white_24dp" | ||||
|                 tools:ignore="ContentDescription"/> | ||||
|  | ||||
|         </LinearLayout> | ||||
|  | ||||
| @@ -461,21 +461,20 @@ | ||||
|             android:layout_height="60dp" | ||||
|             android:id="@+id/playQueueControl"> | ||||
|  | ||||
|             <ImageButton | ||||
|             <androidx.appcompat.widget.AppCompatImageButton | ||||
|                 android:id="@+id/playQueueClose" | ||||
|                 android:layout_width="50dp" | ||||
|                 android:layout_height="50dp" | ||||
|                 android:layout_centerVertical="true" | ||||
|                 android:layout_alignParentEnd="true" | ||||
|                 android:layout_centerVertical="true" | ||||
|                 android:layout_marginEnd="40dp" | ||||
|                 android:padding="10dp" | ||||
|                 android:clickable="true" | ||||
|                 android:focusable="true" | ||||
|                 android:scaleType="fitXY" | ||||
|                 android:tint="?attr/colorAccent" | ||||
|                 android:src="@drawable/ic_close_white_24dp" | ||||
|                 android:background="?android:selectableItemBackground" | ||||
|                 tools:ignore="ContentDescription"/> | ||||
|                 android:clickable="true" | ||||
|                 android:contentDescription="@string/close" | ||||
|                 android:focusable="true" | ||||
|                 android:padding="10dp" | ||||
|                 android:scaleType="fitXY" | ||||
|                 app:srcCompat="@drawable/ic_close_white_24dp" /> | ||||
|  | ||||
|             <ImageButton | ||||
|                 android:id="@+id/repeatButton" | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     android:layout_width="match_parent" | ||||
|              android:layout_height="64dp" | ||||
|              xmlns:tools="http://schemas.android.com/tools"> | ||||
|     android:layout_height="64dp" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:id="@+id/notificationContent" | ||||
| @@ -107,8 +107,8 @@ | ||||
|             android:focusable="true" | ||||
|             android:padding="5dp" | ||||
|             android:scaleType="fitCenter" | ||||
|             android:src="@drawable/ic_close_white_24dp" | ||||
|             tools:ignore="ContentDescription,RtlHardcoded"/> | ||||
|             app:srcCompat="@drawable/ic_close_white_24dp" | ||||
|             tools:ignore="ContentDescription,RtlHardcoded" /> | ||||
|     </LinearLayout> | ||||
|  | ||||
|     <ProgressBar | ||||
| @@ -121,4 +121,4 @@ | ||||
|         android:progressDrawable="@drawable/custom_progress_bar" | ||||
|         tools:ignore="RtlHardcoded" | ||||
|         tools:progress="52"/> | ||||
| </FrameLayout> | ||||
| </FrameLayout> | ||||
|   | ||||
| @@ -29,8 +29,8 @@ | ||||
|         android:focusable="true" | ||||
|         android:padding="8dp" | ||||
|         android:scaleType="fitCenter" | ||||
|         android:src="@drawable/ic_close_white_24dp" | ||||
|         tools:ignore="ContentDescription,RtlHardcoded"/> | ||||
|         app:srcCompat="@drawable/ic_close_white_24dp" | ||||
|         tools:ignore="ContentDescription,RtlHardcoded" /> | ||||
|  | ||||
|  | ||||
|     <LinearLayout | ||||
| @@ -162,4 +162,4 @@ | ||||
|             android:src="@drawable/exo_controls_next" | ||||
|             tools:ignore="ContentDescription"/> | ||||
|     </RelativeLayout> | ||||
| </RelativeLayout> | ||||
| </RelativeLayout> | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <FrameLayout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
| @@ -11,8 +10,8 @@ | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_gravity="bottom|center_horizontal" | ||||
|         android:layout_marginBottom="24dp" | ||||
|         app:srcCompat="@drawable/ic_close_white_24dp" | ||||
|         app:backgroundTint="@color/light_youtube_primary_color" | ||||
|         app:borderWidth="0dp" | ||||
|         app:fabSize="normal"/> | ||||
| </FrameLayout> | ||||
|         app:fabSize="normal" | ||||
|         app:srcCompat="@drawable/ic_close_white_24dp" /> | ||||
| </FrameLayout> | ||||
|   | ||||
| @@ -12,7 +12,7 @@ | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginLeft="12dp" | ||||
|         android:layout_alignBaseline="@+id/autoplay_switch" | ||||
|         android:text="@string/next_video_title" | ||||
|         android:text="@string/exo_controls_next_description" | ||||
|         android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|         android:textSize="12sp" | ||||
|         tools:ignore="RtlHardcoded" /> | ||||
|   | ||||
							
								
								
									
										135
									
								
								app/src/main/res/layout/settings_notification.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,135 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
|  | ||||
|     <androidx.constraintlayout.widget.ConstraintLayout | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_width="match_parent"> | ||||
|  | ||||
|         <Switch | ||||
|             android:id="@+id/notificationScaleSwitch" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginEnd="16dp" | ||||
|             android:clickable="false" | ||||
|             android:focusable="false" | ||||
|             app:layout_constraintBottom_toBottomOf="@+id/textView2" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintTop_toTopOf="@+id/textView" /> | ||||
|  | ||||
|         <TextView | ||||
|             android:id="@+id/textView" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginStart="16dp" | ||||
|             android:layout_marginTop="16dp" | ||||
|             android:layout_marginEnd="16dp" | ||||
|             android:maxLines="1" | ||||
|             android:text="@string/notification_scale_to_square_image_title" | ||||
|             android:textAppearance="?android:attr/textAppearanceLarge" | ||||
|             android:textSize="14sp" | ||||
|             app:layout_constraintEnd_toStartOf="@+id/notificationScaleSwitch" | ||||
|             app:layout_constraintHorizontal_bias="0.0" | ||||
|             app:layout_constraintStart_toStartOf="parent" | ||||
|             app:layout_constraintTop_toTopOf="parent" /> | ||||
|  | ||||
|         <TextView | ||||
|             android:id="@+id/textView2" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginStart="16dp" | ||||
|             android:layout_marginEnd="16dp" | ||||
|             android:text="@string/notification_scale_to_square_image_summary" | ||||
|             app:layout_constraintEnd_toStartOf="@+id/notificationScaleSwitch" | ||||
|             app:layout_constraintStart_toStartOf="parent" | ||||
|             app:layout_constraintTop_toBottomOf="@+id/textView" /> | ||||
|  | ||||
|         <View | ||||
|             android:id="@+id/notificationScaleSwitchClickableArea" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="0dp" | ||||
|             android:clickable="true" | ||||
|             android:focusable="true" | ||||
|             app:layout_constraintBottom_toTopOf="@+id/divider" | ||||
|             app:layout_constraintTop_toTopOf="parent" | ||||
|             android:background="?android:selectableItemBackground" /> | ||||
|  | ||||
|         <View | ||||
|             android:id="@+id/divider" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="1dp" | ||||
|             android:layout_marginStart="32dp" | ||||
|             android:layout_marginTop="16dp" | ||||
|             android:layout_marginEnd="32dp" | ||||
|             android:background="?android:attr/listDivider" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintStart_toStartOf="parent" | ||||
|             app:layout_constraintTop_toBottomOf="@+id/textView2" /> | ||||
|  | ||||
|         <TextView | ||||
|             android:id="@+id/textView4" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginStart="16dp" | ||||
|             android:layout_marginTop="16dp" | ||||
|             android:layout_marginEnd="16dp" | ||||
|             android:clickable="false" | ||||
|             android:focusable="false" | ||||
|             android:gravity="center" | ||||
|             android:lines="4" | ||||
|             android:text="@string/notification_actions_summary" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintHorizontal_bias="0.0" | ||||
|             app:layout_constraintStart_toStartOf="parent" | ||||
|             app:layout_constraintTop_toBottomOf="@+id/divider" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/notificationAction0" | ||||
|             layout="@layout/settings_notification_action" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginTop="8dp" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintStart_toStartOf="parent" | ||||
|             app:layout_constraintTop_toBottomOf="@+id/textView4" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/notificationAction1" | ||||
|             layout="@layout/settings_notification_action" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="wrap_content" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintStart_toStartOf="parent" | ||||
|             app:layout_constraintTop_toBottomOf="@+id/notificationAction0" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/notificationAction2" | ||||
|             layout="@layout/settings_notification_action" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="wrap_content" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintStart_toStartOf="parent" | ||||
|             app:layout_constraintTop_toBottomOf="@+id/notificationAction1" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/notificationAction3" | ||||
|             layout="@layout/settings_notification_action" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="wrap_content" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintStart_toStartOf="parent" | ||||
|             app:layout_constraintTop_toBottomOf="@+id/notificationAction2" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/notificationAction4" | ||||
|             layout="@layout/settings_notification_action" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="wrap_content" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintStart_toStartOf="parent" | ||||
|             app:layout_constraintTop_toBottomOf="@+id/notificationAction3" /> | ||||
|  | ||||
|     </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| </ScrollView> | ||||
							
								
								
									
										88
									
								
								app/src/main/res/layout/settings_notification_action.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,88 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:background="?android:selectableItemBackground"> | ||||
|  | ||||
|     <androidx.appcompat.widget.AppCompatImageView | ||||
|         android:id="@+id/notificationActionIcon" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="48dp" | ||||
|         android:layout_marginStart="12dp" | ||||
|         android:layout_marginTop="8dp" | ||||
|         android:layout_marginBottom="8dp" | ||||
|         app:tint="?android:textColorPrimary" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintDimensionRatio="H,1:1" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         tools:ignore="ContentDescription" | ||||
|         tools:src="@drawable/ic_previous_white_24dp" /> | ||||
|  | ||||
|     <TextView | ||||
|         android:id="@+id/notificationActionTitle" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginStart="8dp" | ||||
|         android:ellipsize="end" | ||||
|         android:maxLines="1" | ||||
|         android:textAppearance="?android:attr/textAppearanceLarge" | ||||
|         android:textSize="14sp" | ||||
|         app:layout_constraintBottom_toTopOf="@+id/notificationActionSummary" | ||||
|         app:layout_constraintEnd_toEndOf="@id/notificationActionClickableArea" | ||||
|         app:layout_constraintHorizontal_bias="0.0" | ||||
|         app:layout_constraintStart_toEndOf="@+id/notificationActionIcon" | ||||
|         app:layout_constraintTop_toTopOf="@+id/notificationActionIcon" | ||||
|         app:layout_constraintVertical_chainStyle="packed" | ||||
|         tools:text="@string/notification_action_1_title" /> | ||||
|  | ||||
|     <TextView | ||||
|         android:id="@+id/notificationActionSummary" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:ellipsize="end" | ||||
|         android:maxLines="1" | ||||
|         app:layout_constraintBottom_toBottomOf="@+id/notificationActionIcon" | ||||
|         app:layout_constraintEnd_toEndOf="@+id/notificationActionClickableArea" | ||||
|         app:layout_constraintHorizontal_bias="0.0" | ||||
|         app:layout_constraintStart_toStartOf="@+id/notificationActionTitle" | ||||
|         app:layout_constraintTop_toBottomOf="@+id/notificationActionTitle" | ||||
|         tools:text="@string/notification_action_play_pause_buffering_value" /> | ||||
|  | ||||
|     <View | ||||
|         android:id="@+id/notificationActionClickableArea" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="0dp" | ||||
|         android:background="?android:selectableItemBackground" | ||||
|         android:clickable="true" | ||||
|         android:focusable="true" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintEnd_toStartOf="@id/notificationActionCheckBoxClickableArea" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" /> | ||||
|  | ||||
|     <CheckBox | ||||
|         android:id="@+id/notificationActionCheckBox" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:clickable="false" | ||||
|         android:focusable="false" | ||||
|         app:layout_constraintBottom_toBottomOf="@+id/notificationActionCheckBoxClickableArea" | ||||
|         app:layout_constraintEnd_toEndOf="@+id/notificationActionCheckBoxClickableArea" | ||||
|         app:layout_constraintStart_toStartOf="@+id/notificationActionCheckBoxClickableArea" | ||||
|         app:layout_constraintTop_toTopOf="parent" /> | ||||
|  | ||||
|     <View | ||||
|         android:id="@+id/notificationActionCheckBoxClickableArea" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="0dp" | ||||
|         android:clickable="true" | ||||
|         android:focusable="true" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintDimensionRatio="H,1:1" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" /> | ||||
|  | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| @@ -47,7 +47,7 @@ | ||||
|             android:contentDescription="@string/search" | ||||
|             android:scaleType="fitCenter" | ||||
|             app:srcCompat="?attr/ic_close" | ||||
|             tools:ignore="RtlHardcoded"/> | ||||
|             tools:ignore="RtlHardcoded" /> | ||||
|     </FrameLayout> | ||||
|  | ||||
| </FrameLayout> | ||||
| </FrameLayout> | ||||
|   | ||||
| @@ -114,6 +114,19 @@ | ||||
|         <item>144p</item> | ||||
|     </string-array> | ||||
|  | ||||
|     <string name="settings_notifications_compact_view_key" translatable="false">notifications_compact_view</string> | ||||
|     <string name="settings_notifications_compact_view_default_value" translatable="false">0,1,2</string> | ||||
|     <string name="scale_to_square_image_in_notifications_key" translatable="false">scale_to_square_image_in_notifications</string> | ||||
|  | ||||
|     <string name="notification_slot_0_key" translatable="false">notification_slot_0_key</string> | ||||
|     <string name="notification_slot_1_key" translatable="false">notification_slot_1_key</string> | ||||
|     <string name="notification_slot_2_key" translatable="false">notification_slot_2_key</string> | ||||
|     <string name="notification_slot_3_key" translatable="false">notification_slot_3_key</string> | ||||
|     <string name="notification_slot_4_key" translatable="false">notification_slot_4_key</string> | ||||
|  | ||||
|     <string name="notification_slot_compact_0_key" translatable="false">notification_slot_compact_0_key</string> | ||||
|     <string name="notification_slot_compact_1_key" translatable="false">notification_slot_compact_1_key</string> | ||||
|     <string name="notification_slot_compact_2_key" translatable="false">notification_slot_compact_2_key</string> | ||||
|  | ||||
|     <string name="video_mp4_key" translatable="false">video_mp4</string> | ||||
|     <string name="video_webm_key" translatable="false">video_webm</string> | ||||
| @@ -199,7 +212,6 @@ | ||||
|     <string name="playback_skip_silence_key" translatable="false">playback_skip_silence_key</string> | ||||
|  | ||||
|     <string name="app_language_key" translatable="false">app_language_key</string> | ||||
|     <string name="enable_lock_screen_video_thumbnail_key" translatable="false">enable_lock_screen_video_thumbnail</string> | ||||
|  | ||||
|     <string name="feed_update_threshold_key" translatable="false">feed_update_threshold_key</string> | ||||
|     <string name="feed_update_threshold_default_value" translatable="false">300</string> | ||||
|   | ||||
| @@ -57,9 +57,23 @@ | ||||
|     <string name="kore_not_found">Install missing Kore app?</string> | ||||
|     <string name="kore_package" translatable="false">org.xbmc.kore</string> | ||||
|     <string name="show_play_with_kodi_title">Show \"Play with Kodi\" option</string> | ||||
|     <string name="enable_lock_screen_video_thumbnail_title">Lock screen video thumbnail</string> | ||||
|     <string name="show_play_with_kodi_summary">Display an option to play a video via Kodi media center</string> | ||||
|     <string name="enable_lock_screen_video_thumbnail_summary">A video thumbnail is shown on the lock screen when using the background player</string> | ||||
|  | ||||
|     <string name="notification_scale_to_square_image_title">Scale thumbnail to 1:1 aspect ratio</string> | ||||
|     <string name="notification_scale_to_square_image_summary">Scale the video thumbnail shown in the notification from 16:9 to 1:1 aspect ratio (may introduce distortions)</string> | ||||
|     <string name="notification_action_0_title">First action button</string> | ||||
|     <string name="notification_action_1_title">Second action button</string> | ||||
|     <string name="notification_action_2_title">Third action button</string> | ||||
|     <string name="notification_action_3_title">Fourth action button</string> | ||||
|     <string name="notification_action_4_title">Fifth action button</string> | ||||
|     <string name="notification_actions_summary">Edit each notification action below by tapping on it.\nSelect up to three of them to be shown in the compact notification by using the checkboxes on the right.</string> | ||||
|     <string name="notification_actions_at_most_three">You can select at most three actions to show in the compact notification!</string> | ||||
|  | ||||
|     <string name="notification_action_repeat">Repeat</string> | ||||
|     <string name="notification_action_shuffle">Shuffle</string> | ||||
|     <string name="notification_action_buffering">Buffering</string> | ||||
|     <string name="notification_action_nothing">Nothing</string> | ||||
|  | ||||
|     <string name="play_audio">Audio</string> | ||||
|     <string name="default_audio_format_title">Default audio format</string> | ||||
|     <string name="default_video_format_title">Default video format</string> | ||||
| @@ -106,7 +120,6 @@ | ||||
|     <string name="resume_on_audio_focus_gain_title">Resume playing</string> | ||||
|     <string name="resume_on_audio_focus_gain_summary">Continue playing after interruptions (e.g. phonecalls)</string> | ||||
|     <string name="download_dialog_title">Download</string> | ||||
|     <string name="next_video_title">Next</string> | ||||
|     <string name="autoplay_title">Autoplay</string> | ||||
|     <string name="show_next_and_similar_title">Show \'Next\' and \'Similar\' videos</string> | ||||
|     <string name="show_hold_to_append_title">Show \"Hold to append\" tip</string> | ||||
| @@ -134,12 +147,12 @@ | ||||
|     <string name="settings_category_other_title">Other</string> | ||||
|     <string name="settings_category_debug_title">Debug</string> | ||||
|     <string name="settings_category_updates_title">Updates</string> | ||||
|     <string name="settings_category_notification_title">Notification</string> | ||||
|     <string name="background_player_playing_toast">Playing in background</string> | ||||
|     <string name="popup_playing_toast">Playing in popup mode</string> | ||||
|     <string name="background_player_append">Queued on background player</string> | ||||
|     <string name="popup_playing_append">Queued on popup player</string> | ||||
|     <string name="c3s_url" translatable="false">https://www.c3s.cc/</string> | ||||
|     <string name="play_btn_text">Play</string> | ||||
|     <string name="content">Content</string> | ||||
|     <string name="show_age_restricted_content_title">Age restricted content</string> | ||||
|     <string name="video_is_age_restricted">Show age restricted video. Future changes are possible from the settings.</string> | ||||
|   | ||||
| @@ -1,38 +1,37 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <PreferenceScreen | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     android:title="@string/settings_category_appearance_title"> | ||||
|  | ||||
|     <ListPreference | ||||
|         app:iconSpaceReserved="false" | ||||
|         android:defaultValue="@string/default_theme_value" | ||||
|         android:entries="@array/theme_description_list" | ||||
|         android:entryValues="@array/theme_values_list" | ||||
|         android:key="@string/theme_key" | ||||
|         android:summary="%s" | ||||
|         android:title="@string/theme_title"/> | ||||
|         android:title="@string/theme_title" | ||||
|         app:iconSpaceReserved="false" /> | ||||
|  | ||||
|     <SwitchPreferenceCompat | ||||
|         app:iconSpaceReserved="false" | ||||
|         android:defaultValue="true" | ||||
|         android:key="@string/show_hold_to_append_key" | ||||
|         android:summary="@string/show_hold_to_append_summary" | ||||
|         android:title="@string/show_hold_to_append_title" | ||||
|         android:summary="@string/show_hold_to_append_summary"/> | ||||
|         app:iconSpaceReserved="false" /> | ||||
|  | ||||
|     <ListPreference | ||||
|         app:iconSpaceReserved="false" | ||||
|         android:defaultValue="@string/list_view_mode_value" | ||||
|         android:entries="@array/list_view_mode_description" | ||||
|         android:entryValues="@array/list_view_mode_values" | ||||
|         android:key="@string/list_view_mode_key" | ||||
|         android:summary="%s" | ||||
|         android:title="@string/list_view_mode"/> | ||||
|         android:title="@string/list_view_mode" | ||||
|         app:iconSpaceReserved="false" /> | ||||
|  | ||||
|     <Preference | ||||
|         app:iconSpaceReserved="false" | ||||
|         android:key="@string/caption_settings_key" | ||||
|         android:summary="@string/caption_setting_description" | ||||
|         android:title="@string/caption_setting_title" | ||||
|         android:summary="@string/caption_setting_description"/> | ||||
|         app:iconSpaceReserved="false" /> | ||||
|  | ||||
| </PreferenceScreen> | ||||
|   | ||||
| @@ -35,6 +35,12 @@ | ||||
|         android:icon="?attr/ic_language" | ||||
|         android:title="@string/content"/> | ||||
|  | ||||
|     <PreferenceScreen | ||||
|         app:iconSpaceReserved="false" | ||||
|         android:fragment="org.schabi.newpipe.settings.NotificationSettingsFragment" | ||||
|         android:icon="?attr/ic_play_arrow" | ||||
|         android:title="@string/settings_category_notification_title"/> | ||||
|  | ||||
|     <PreferenceScreen | ||||
|         app:iconSpaceReserved="false" | ||||
|         android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment" | ||||
|   | ||||
| @@ -81,13 +81,6 @@ | ||||
|             android:summary="@string/show_play_with_kodi_summary" | ||||
|             android:title="@string/show_play_with_kodi_title"/> | ||||
|  | ||||
|         <SwitchPreferenceCompat | ||||
|             app:iconSpaceReserved="false" | ||||
|             android:defaultValue="true" | ||||
|             android:key="@string/enable_lock_screen_video_thumbnail_key" | ||||
|             android:summary="@string/enable_lock_screen_video_thumbnail_summary" | ||||
|             android:title="@string/enable_lock_screen_video_thumbnail_title"/> | ||||
|  | ||||
|     </PreferenceCategory> | ||||
|  | ||||
|     <PreferenceCategory | ||||
|   | ||||
 Tobias Groza
					Tobias Groza