mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 15:23:00 +00:00 
			
		
		
		
	Merge branch 'dev' into bumpSomeLibraries
This commit is contained in:
		| @@ -220,6 +220,7 @@ dependencies { | ||||
|     // https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01 | ||||
|     implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' | ||||
|     implementation 'androidx.webkit:webkit:1.4.0' | ||||
|     implementation 'androidx.work:work-runtime:2.7.1' | ||||
|     implementation 'com.google.android.material:material:1.5.0' | ||||
|  | ||||
| /** Third-party libraries **/ | ||||
|   | ||||
| @@ -381,9 +381,6 @@ | ||||
|         <service | ||||
|             android:name=".RouterActivity$FetcherService" | ||||
|             android:exported="false" /> | ||||
|         <service | ||||
|             android:name=".CheckForNewAppVersion" | ||||
|             android:exported="false" /> | ||||
|  | ||||
|         <!-- opting out of sending metrics to Google in Android System WebView --> | ||||
|         <meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" /> | ||||
|   | ||||
| @@ -1,264 +0,0 @@ | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.app.Application; | ||||
| import android.app.IntentService; | ||||
| import android.app.PendingIntent; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.content.pm.Signature; | ||||
| import android.net.Uri; | ||||
| import android.util.Log; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.core.app.NotificationCompat; | ||||
| import androidx.core.app.NotificationManagerCompat; | ||||
| import androidx.core.content.pm.PackageInfoCompat; | ||||
| import androidx.preference.PreferenceManager; | ||||
|  | ||||
| import com.grack.nanojson.JsonObject; | ||||
| import com.grack.nanojson.JsonParser; | ||||
| import com.grack.nanojson.JsonParserException; | ||||
|  | ||||
| import org.schabi.newpipe.error.ErrorInfo; | ||||
| import org.schabi.newpipe.error.ErrorUtil; | ||||
| import org.schabi.newpipe.error.UserAction; | ||||
| import org.schabi.newpipe.extractor.downloader.Response; | ||||
| import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; | ||||
|  | ||||
| import java.io.ByteArrayInputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.security.MessageDigest; | ||||
| import java.security.NoSuchAlgorithmException; | ||||
| import java.security.cert.CertificateEncodingException; | ||||
| import java.security.cert.CertificateException; | ||||
| import java.security.cert.CertificateFactory; | ||||
| import java.security.cert.X509Certificate; | ||||
| import java.util.List; | ||||
|  | ||||
| public final class CheckForNewAppVersion extends IntentService { | ||||
|     public CheckForNewAppVersion() { | ||||
|         super("CheckForNewAppVersion"); | ||||
|     } | ||||
|  | ||||
|     private static final boolean DEBUG = MainActivity.DEBUG; | ||||
|     private static final String TAG = CheckForNewAppVersion.class.getSimpleName(); | ||||
|  | ||||
|     // Public key of the certificate that is used in NewPipe release versions | ||||
|     private static final String RELEASE_CERT_PUBLIC_KEY_SHA1 | ||||
|             = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"; | ||||
|     private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json"; | ||||
|  | ||||
|     /** | ||||
|      * Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133. | ||||
|      * | ||||
|      * @param application The application | ||||
|      * @return String with the APK's SHA1 fingerprint in hexadecimal | ||||
|      */ | ||||
|     @NonNull | ||||
|     private static String getCertificateSHA1Fingerprint(@NonNull final Application application) { | ||||
|         final List<Signature> signatures; | ||||
|         try { | ||||
|             signatures = PackageInfoCompat.getSignatures(application.getPackageManager(), | ||||
|                     application.getPackageName()); | ||||
|         } catch (final PackageManager.NameNotFoundException e) { | ||||
|             ErrorUtil.createNotification(application, new ErrorInfo(e, | ||||
|                     UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info")); | ||||
|             return ""; | ||||
|         } | ||||
|         if (signatures.isEmpty()) { | ||||
|             return ""; | ||||
|         } | ||||
|  | ||||
|         final X509Certificate c; | ||||
|         try { | ||||
|             final byte[] cert = signatures.get(0).toByteArray(); | ||||
|             final InputStream input = new ByteArrayInputStream(cert); | ||||
|             final CertificateFactory cf = CertificateFactory.getInstance("X509"); | ||||
|             c = (X509Certificate) cf.generateCertificate(input); | ||||
|         } catch (final CertificateException e) { | ||||
|             ErrorUtil.createNotification(application, new ErrorInfo(e, | ||||
|                     UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error")); | ||||
|             return ""; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             final MessageDigest md = MessageDigest.getInstance("SHA1"); | ||||
|             final byte[] publicKey = md.digest(c.getEncoded()); | ||||
|             return byte2HexFormatted(publicKey); | ||||
|         } catch (NoSuchAlgorithmException | CertificateEncodingException e) { | ||||
|             ErrorUtil.createNotification(application, new ErrorInfo(e, | ||||
|                     UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key")); | ||||
|             return ""; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static String byte2HexFormatted(final byte[] arr) { | ||||
|         final StringBuilder str = new StringBuilder(arr.length * 2); | ||||
|  | ||||
|         for (int i = 0; i < arr.length; i++) { | ||||
|             String h = Integer.toHexString(arr[i]); | ||||
|             final int l = h.length(); | ||||
|             if (l == 1) { | ||||
|                 h = "0" + h; | ||||
|             } | ||||
|             if (l > 2) { | ||||
|                 h = h.substring(l - 2, l); | ||||
|             } | ||||
|             str.append(h.toUpperCase()); | ||||
|             if (i < (arr.length - 1)) { | ||||
|                 str.append(':'); | ||||
|             } | ||||
|         } | ||||
|         return str.toString(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Method to compare the current and latest available app version. | ||||
|      * If a newer version is available, we show the update notification. | ||||
|      * | ||||
|      * @param application    The application | ||||
|      * @param versionName    Name of new version | ||||
|      * @param apkLocationUrl Url with the new apk | ||||
|      * @param versionCode    Code of new version | ||||
|      */ | ||||
|     private static void compareAppVersionAndShowNotification(@NonNull final Application application, | ||||
|                                                              final String versionName, | ||||
|                                                              final String apkLocationUrl, | ||||
|                                                              final int versionCode) { | ||||
|         if (BuildConfig.VERSION_CODE >= versionCode) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // A pending intent to open the apk location url in the browser. | ||||
|         final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl)); | ||||
|         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|         final PendingIntent pendingIntent | ||||
|                 = PendingIntent.getActivity(application, 0, intent, 0); | ||||
|  | ||||
|         final String channelId = application | ||||
|                 .getString(R.string.app_update_notification_channel_id); | ||||
|         final NotificationCompat.Builder notificationBuilder | ||||
|                 = new NotificationCompat.Builder(application, channelId) | ||||
|                 .setSmallIcon(R.drawable.ic_newpipe_update) | ||||
|                 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||
|                 .setContentIntent(pendingIntent) | ||||
|                 .setAutoCancel(true) | ||||
|                 .setContentTitle(application | ||||
|                         .getString(R.string.app_update_notification_content_title)) | ||||
|                 .setContentText(application | ||||
|                         .getString(R.string.app_update_notification_content_text) | ||||
|                         + " " + versionName); | ||||
|  | ||||
|         final NotificationManagerCompat notificationManager | ||||
|                 = NotificationManagerCompat.from(application); | ||||
|         notificationManager.notify(2000, notificationBuilder.build()); | ||||
|     } | ||||
|  | ||||
|     public static boolean isReleaseApk(@NonNull final App app) { | ||||
|         return getCertificateSHA1Fingerprint(app).equals(RELEASE_CERT_PUBLIC_KEY_SHA1); | ||||
|     } | ||||
|  | ||||
|     private void checkNewVersion() throws IOException, ReCaptchaException { | ||||
|         final App app = App.getApp(); | ||||
|  | ||||
|         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); | ||||
|         final NewVersionManager manager = new NewVersionManager(); | ||||
|  | ||||
|         // Check if the current apk is a github one or not. | ||||
|         if (!isReleaseApk(app)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Check if the last request has happened a certain time ago | ||||
|         // to reduce the number of API requests. | ||||
|         final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0); | ||||
|         if (!manager.isExpired(expiry)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Make a network request to get latest NewPipe data. | ||||
|         final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL); | ||||
|         handleResponse(response, manager, prefs, app); | ||||
|     } | ||||
|  | ||||
|     private void handleResponse(@NonNull final Response response, | ||||
|                                 @NonNull final NewVersionManager manager, | ||||
|                                 @NonNull final SharedPreferences prefs, | ||||
|                                 @NonNull final App app) { | ||||
|         try { | ||||
|             // Store a timestamp which needs to be exceeded, | ||||
|             // before a new request to the API is made. | ||||
|             final long newExpiry = manager | ||||
|                     .coerceExpiry(response.getHeader("expires")); | ||||
|             prefs.edit() | ||||
|                     .putLong(app.getString(R.string.update_expiry_key), newExpiry) | ||||
|                     .apply(); | ||||
|         } catch (final Exception e) { | ||||
|             if (DEBUG) { | ||||
|                 Log.w(TAG, "Could not extract and save new expiry date", e); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Parse the json from the response. | ||||
|         try { | ||||
|  | ||||
|             final JsonObject githubStableObject = JsonParser.object() | ||||
|                     .from(response.responseBody()).getObject("flavors") | ||||
|                     .getObject("github").getObject("stable"); | ||||
|  | ||||
|             final String versionName = githubStableObject | ||||
|                     .getString("version"); | ||||
|             final int versionCode = githubStableObject | ||||
|                     .getInt("version_code"); | ||||
|             final String apkLocationUrl = githubStableObject | ||||
|                     .getString("apk"); | ||||
|  | ||||
|             compareAppVersionAndShowNotification(app, versionName, | ||||
|                     apkLocationUrl, versionCode); | ||||
|         } catch (final JsonParserException e) { | ||||
|             // Most likely something is wrong in data received from NEWPIPE_API_URL. | ||||
|             // Do not alarm user and fail silently. | ||||
|             if (DEBUG) { | ||||
|                 Log.w(TAG, "Could not get NewPipe API: invalid json", e); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Start a new service which | ||||
|      * checks if all conditions for performing a version check are met, | ||||
|      * fetches the API endpoint {@link #NEWPIPE_API_URL} containing info | ||||
|      * about the latest NewPipe version | ||||
|      * and displays a notification about ana available update. | ||||
|      * <br> | ||||
|      * Following conditions need to be met, before data is request from the server: | ||||
|      * <ul> | ||||
|      * <li> The app is signed with the correct signing key (by TeamNewPipe / schabi). | ||||
|      * If the signing key differs from the one used upstream, the update cannot be installed.</li> | ||||
|      * <li>The user enabled searching for and notifying about updates in the settings.</li> | ||||
|      * <li>The app did not recently check for updates. | ||||
|      * We do not want to make unnecessary connections and DOS our servers.</li> | ||||
|      * </ul> | ||||
|      * <b>Must not be executed</b> when the app is in background. | ||||
|      */ | ||||
|     public static void startNewVersionCheckService() { | ||||
|         final Intent intent = new Intent(App.getApp().getApplicationContext(), | ||||
|                 CheckForNewAppVersion.class); | ||||
|         App.getApp().startService(intent); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onHandleIntent(@Nullable final Intent intent) { | ||||
|         try { | ||||
|             checkNewVersion(); | ||||
|         } catch (final IOException e) { | ||||
|             Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e); | ||||
|         } catch (final ReCaptchaException e) { | ||||
|             Log.e(TAG, "ReCaptchaException should never happen here.", e); | ||||
|         } | ||||
|  | ||||
|     } | ||||
| } | ||||
| @@ -20,7 +20,6 @@ | ||||
|  | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService; | ||||
| import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; | ||||
|  | ||||
| import android.content.BroadcastReceiver; | ||||
| @@ -174,10 +173,9 @@ public class MainActivity extends AppCompatActivity { | ||||
|         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); | ||||
|  | ||||
|         if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) { | ||||
|             // Start the service which is checking all conditions | ||||
|             // Start the worker which is checking all conditions | ||||
|             // and eventually searching for a new version. | ||||
|             // The service searching for a new NewPipe version must not be started in background. | ||||
|             startNewVersionCheckService(); | ||||
|             NewVersionWorker.enqueueNewVersionCheckingWork(app); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,28 +0,0 @@ | ||||
| package org.schabi.newpipe | ||||
|  | ||||
| import java.time.Instant | ||||
| import java.time.ZonedDateTime | ||||
| import java.time.format.DateTimeFormatter | ||||
|  | ||||
| class NewVersionManager { | ||||
|  | ||||
|     fun isExpired(expiry: Long): Boolean { | ||||
|         return Instant.ofEpochSecond(expiry).isBefore(Instant.now()) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Coerce expiry date time in between 6 hours and 72 hours from now | ||||
|      * | ||||
|      * @return Epoch second of expiry date time | ||||
|      */ | ||||
|     fun coerceExpiry(expiryString: String?): Long { | ||||
|         val now = ZonedDateTime.now() | ||||
|         return expiryString?.let { | ||||
|  | ||||
|             var expiry = ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString)) | ||||
|             expiry = maxOf(expiry, now.plusHours(6)) | ||||
|             expiry = minOf(expiry, now.plusHours(72)) | ||||
|             expiry.toEpochSecond() | ||||
|         } ?: now.plusHours(6).toEpochSecond() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										163
									
								
								app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| package org.schabi.newpipe | ||||
|  | ||||
| import android.app.PendingIntent | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.util.Log | ||||
| import androidx.core.app.NotificationCompat | ||||
| import androidx.core.app.NotificationManagerCompat | ||||
| import androidx.core.content.edit | ||||
| import androidx.core.net.toUri | ||||
| import androidx.preference.PreferenceManager | ||||
| import androidx.work.OneTimeWorkRequest | ||||
| import androidx.work.WorkManager | ||||
| import androidx.work.WorkRequest | ||||
| import androidx.work.Worker | ||||
| import androidx.work.WorkerParameters | ||||
| import com.grack.nanojson.JsonParser | ||||
| import com.grack.nanojson.JsonParserException | ||||
| import org.schabi.newpipe.extractor.downloader.Response | ||||
| import org.schabi.newpipe.extractor.exceptions.ReCaptchaException | ||||
| import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry | ||||
| import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired | ||||
| import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk | ||||
| import java.io.IOException | ||||
|  | ||||
| class NewVersionWorker( | ||||
|     context: Context, | ||||
|     workerParams: WorkerParameters | ||||
| ) : Worker(context, workerParams) { | ||||
|  | ||||
|     /** | ||||
|      * Method to compare the current and latest available app version. | ||||
|      * If a newer version is available, we show the update notification. | ||||
|      * | ||||
|      * @param versionName    Name of new version | ||||
|      * @param apkLocationUrl Url with the new apk | ||||
|      * @param versionCode    Code of new version | ||||
|      */ | ||||
|     private fun compareAppVersionAndShowNotification( | ||||
|         versionName: String, | ||||
|         apkLocationUrl: String?, | ||||
|         versionCode: Int | ||||
|     ) { | ||||
|         if (BuildConfig.VERSION_CODE >= versionCode) { | ||||
|             return | ||||
|         } | ||||
|         val app = App.getApp() | ||||
|  | ||||
|         // A pending intent to open the apk location url in the browser. | ||||
|         val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri()) | ||||
|         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||||
|         val pendingIntent = PendingIntent.getActivity(app, 0, intent, 0) | ||||
|         val channelId = app.getString(R.string.app_update_notification_channel_id) | ||||
|         val notificationBuilder = NotificationCompat.Builder(app, channelId) | ||||
|             .setSmallIcon(R.drawable.ic_newpipe_update) | ||||
|             .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||
|             .setContentIntent(pendingIntent) | ||||
|             .setAutoCancel(true) | ||||
|             .setContentTitle(app.getString(R.string.app_update_notification_content_title)) | ||||
|             .setContentText( | ||||
|                 app.getString(R.string.app_update_notification_content_text) + | ||||
|                     " " + versionName | ||||
|             ) | ||||
|         val notificationManager = NotificationManagerCompat.from(app) | ||||
|         notificationManager.notify(2000, notificationBuilder.build()) | ||||
|     } | ||||
|  | ||||
|     @Throws(IOException::class, ReCaptchaException::class) | ||||
|     private fun checkNewVersion() { | ||||
|         // Check if the current apk is a github one or not. | ||||
|         if (!isReleaseApk()) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) | ||||
|         // Check if the last request has happened a certain time ago | ||||
|         // to reduce the number of API requests. | ||||
|         val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0) | ||||
|         if (!isLastUpdateCheckExpired(expiry)) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         // Make a network request to get latest NewPipe data. | ||||
|         val response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL) | ||||
|         handleResponse(response) | ||||
|     } | ||||
|  | ||||
|     private fun handleResponse(response: Response) { | ||||
|         val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) | ||||
|         try { | ||||
|             // Store a timestamp which needs to be exceeded, | ||||
|             // before a new request to the API is made. | ||||
|             val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires")) | ||||
|             prefs.edit { | ||||
|                 putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry) | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             if (DEBUG) { | ||||
|                 Log.w(TAG, "Could not extract and save new expiry date", e) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Parse the json from the response. | ||||
|         try { | ||||
|             val githubStableObject = JsonParser.`object`() | ||||
|                 .from(response.responseBody()).getObject("flavors") | ||||
|                 .getObject("github").getObject("stable") | ||||
|  | ||||
|             val versionName = githubStableObject.getString("version") | ||||
|             val versionCode = githubStableObject.getInt("version_code") | ||||
|             val apkLocationUrl = githubStableObject.getString("apk") | ||||
|             compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode) | ||||
|         } catch (e: JsonParserException) { | ||||
|             // Most likely something is wrong in data received from NEWPIPE_API_URL. | ||||
|             // Do not alarm user and fail silently. | ||||
|             if (DEBUG) { | ||||
|                 Log.w(TAG, "Could not get NewPipe API: invalid json", e) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun doWork(): Result { | ||||
|         try { | ||||
|             checkNewVersion() | ||||
|         } catch (e: IOException) { | ||||
|             Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e) | ||||
|             return Result.failure() | ||||
|         } catch (e: ReCaptchaException) { | ||||
|             Log.e(TAG, "ReCaptchaException should never happen here.", e) | ||||
|             return Result.failure() | ||||
|         } | ||||
|         return Result.success() | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         private val DEBUG = MainActivity.DEBUG | ||||
|         private val TAG = NewVersionWorker::class.java.simpleName | ||||
|         private const val NEWPIPE_API_URL = "https://newpipe.net/api/data.json" | ||||
|  | ||||
|         /** | ||||
|          * Start a new worker which | ||||
|          * checks if all conditions for performing a version check are met, | ||||
|          * fetches the API endpoint [.NEWPIPE_API_URL] containing info | ||||
|          * about the latest NewPipe version | ||||
|          * and displays a notification about ana available update. | ||||
|          * <br></br> | ||||
|          * Following conditions need to be met, before data is request from the server: | ||||
|          * | ||||
|          *  *  The app is signed with the correct signing key (by TeamNewPipe / schabi). | ||||
|          * If the signing key differs from the one used upstream, the update cannot be installed. | ||||
|          *  * The user enabled searching for and notifying about updates in the settings. | ||||
|          *  * The app did not recently check for updates. | ||||
|          * We do not want to make unnecessary connections and DOS our servers. | ||||
|          * | ||||
|          */ | ||||
|         @JvmStatic | ||||
|         fun enqueueNewVersionCheckingWork(context: Context) { | ||||
|             val workRequest: WorkRequest = | ||||
|                 OneTimeWorkRequest.Builder(NewVersionWorker::class.java).build() | ||||
|             WorkManager.getInstance(context).enqueue(workRequest) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -350,7 +350,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | ||||
|                 new AlertDialog.Builder(requireContext()) | ||||
|                         .setMessage(R.string.remove_watched_popup_warning) | ||||
|                         .setTitle(R.string.remove_watched_popup_title) | ||||
|                         .setPositiveButton(R.string.yes, | ||||
|                         .setPositiveButton(R.string.ok, | ||||
|                                 (DialogInterface d, int id) -> removeWatchedStreams(false)) | ||||
|                         .setNeutralButton( | ||||
|                                 R.string.remove_watched_popup_yes_and_partially_watched_videos, | ||||
|   | ||||
| @@ -1,5 +1,9 @@ | ||||
| package org.schabi.newpipe.player; | ||||
|  | ||||
| import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; | ||||
| import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; | ||||
|  | ||||
| import android.content.ComponentName; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| @@ -23,11 +27,9 @@ import androidx.recyclerview.widget.RecyclerView; | ||||
| import com.google.android.exoplayer2.PlaybackParameters; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
| import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | ||||
| import org.schabi.newpipe.local.dialog.PlaylistDialog; | ||||
| import org.schabi.newpipe.player.event.PlayerEventListener; | ||||
| import org.schabi.newpipe.player.helper.PlaybackParameterDialog; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| @@ -42,13 +44,6 @@ import org.schabi.newpipe.util.PermissionHelper; | ||||
| import org.schabi.newpipe.util.ServiceHelper; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu; | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; | ||||
| import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; | ||||
|  | ||||
| public final class PlayQueueActivity extends AppCompatActivity | ||||
|         implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, | ||||
|         View.OnClickListener, PlaybackParameterDialog.Callback { | ||||
| @@ -129,7 +124,7 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|                 NavigationHelper.openSettings(this); | ||||
|                 return true; | ||||
|             case R.id.action_append_playlist: | ||||
|                 appendAllToPlaylist(); | ||||
|                 player.onAddToPlaylistClicked(getSupportFragmentManager()); | ||||
|                 return true; | ||||
|             case R.id.action_playback_speed: | ||||
|                 openPlaybackParameterDialog(); | ||||
| @@ -443,24 +438,6 @@ public final class PlayQueueActivity extends AppCompatActivity | ||||
|         seeking = false; | ||||
|     } | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|     // Playlist append | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
|     private void appendAllToPlaylist() { | ||||
|         if (player != null && player.getPlayQueue() != null) { | ||||
|             openPlaylistAppendDialog(player.getPlayQueue().getStreams()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void openPlaylistAppendDialog(final List<PlayQueueItem> playQueueItems) { | ||||
|         PlaylistDialog.createCorrespondingDialog( | ||||
|                 getApplicationContext(), | ||||
|                 playQueueItems.stream().map(StreamEntity::new).collect(Collectors.toList()), | ||||
|                 dialog -> dialog.show(getSupportFragmentManager(), TAG) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|     // Binding Service Listener | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|   | ||||
| @@ -105,6 +105,7 @@ import androidx.core.graphics.Insets; | ||||
| import androidx.core.view.GestureDetectorCompat; | ||||
| import androidx.core.view.ViewCompat; | ||||
| import androidx.core.view.WindowInsetsCompat; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| import androidx.preference.PreferenceManager; | ||||
| import androidx.recyclerview.widget.ItemTouchHelper; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| @@ -138,6 +139,7 @@ import com.squareup.picasso.Target; | ||||
| import org.schabi.newpipe.DownloaderImpl; | ||||
| import org.schabi.newpipe.MainActivity; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||
| import org.schabi.newpipe.databinding.PlayerBinding; | ||||
| import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding; | ||||
| import org.schabi.newpipe.error.ErrorInfo; | ||||
| @@ -152,6 +154,7 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | ||||
| import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | ||||
| import org.schabi.newpipe.info_list.StreamSegmentAdapter; | ||||
| import org.schabi.newpipe.ktx.AnimationType; | ||||
| import org.schabi.newpipe.local.dialog.PlaylistDialog; | ||||
| import org.schabi.newpipe.local.history.HistoryRecordManager; | ||||
| import org.schabi.newpipe.player.MainPlayer.PlayerType; | ||||
| import org.schabi.newpipe.player.event.DisplayPortion; | ||||
| @@ -197,6 +200,7 @@ import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
| import java.util.Optional; | ||||
| import java.util.stream.Collectors; | ||||
| import java.util.stream.IntStream; | ||||
|  | ||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | ||||
| @@ -541,6 +545,7 @@ public final class Player implements | ||||
|         binding.segmentsButton.setOnClickListener(this); | ||||
|         binding.repeatButton.setOnClickListener(this); | ||||
|         binding.shuffleButton.setOnClickListener(this); | ||||
|         binding.addToPlaylistButton.setOnClickListener(this); | ||||
|  | ||||
|         binding.playPauseButton.setOnClickListener(this); | ||||
|         binding.playPreviousButton.setOnClickListener(this); | ||||
| @@ -2389,6 +2394,32 @@ public final class Player implements | ||||
|  | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Playlist append | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|     //region Playlist append | ||||
|  | ||||
|     public void onAddToPlaylistClicked(@NonNull final FragmentManager fragmentManager) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "onAddToPlaylistClicked() called"); | ||||
|         } | ||||
|  | ||||
|         if (getPlayQueue() != null) { | ||||
|             PlaylistDialog.createCorrespondingDialog( | ||||
|                     getContext(), | ||||
|                     getPlayQueue() | ||||
|                             .getStreams() | ||||
|                             .stream() | ||||
|                             .map(StreamEntity::new) | ||||
|                             .collect(Collectors.toList()), | ||||
|                     dialog -> dialog.show(fragmentManager, TAG) | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|     //endregion | ||||
|  | ||||
|  | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Mute / Unmute | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -3131,6 +3162,7 @@ public final class Player implements | ||||
|         binding.itemsListHeaderDuration.setVisibility(View.VISIBLE); | ||||
|         binding.shuffleButton.setVisibility(View.VISIBLE); | ||||
|         binding.repeatButton.setVisibility(View.VISIBLE); | ||||
|         binding.addToPlaylistButton.setVisibility(View.VISIBLE); | ||||
|  | ||||
|         hideControls(0, 0); | ||||
|         binding.itemsListPanel.requestFocus(); | ||||
| @@ -3168,6 +3200,7 @@ public final class Player implements | ||||
|         binding.itemsListHeaderDuration.setVisibility(View.GONE); | ||||
|         binding.shuffleButton.setVisibility(View.GONE); | ||||
|         binding.repeatButton.setVisibility(View.GONE); | ||||
|         binding.addToPlaylistButton.setVisibility(View.GONE); | ||||
|  | ||||
|         hideControls(0, 0); | ||||
|         binding.itemsListPanel.requestFocus(); | ||||
| @@ -3196,6 +3229,7 @@ public final class Player implements | ||||
|  | ||||
|         binding.shuffleButton.setVisibility(View.GONE); | ||||
|         binding.repeatButton.setVisibility(View.GONE); | ||||
|         binding.addToPlaylistButton.setVisibility(View.GONE); | ||||
|         binding.itemsListClose.setOnClickListener(view -> closeItemsList()); | ||||
|     } | ||||
|  | ||||
| @@ -3733,6 +3767,11 @@ public final class Player implements | ||||
|         } else if (v.getId() == binding.shuffleButton.getId()) { | ||||
|             onShuffleClicked(); | ||||
|             return; | ||||
|         } else if (v.getId() == binding.addToPlaylistButton.getId()) { | ||||
|             if (getParentActivity() != null) { | ||||
|                 onAddToPlaylistClicked(getParentActivity().getSupportFragmentManager()); | ||||
|             } | ||||
|             return; | ||||
|         } else if (v.getId() == binding.moreOptionsButton.getId()) { | ||||
|             onMoreOptionsClicked(); | ||||
|         } else if (v.getId() == binding.share.getId()) { | ||||
| @@ -3799,6 +3838,10 @@ public final class Player implements | ||||
|             case KeyEvent.KEYCODE_SPACE: | ||||
|                 if (isFullscreen) { | ||||
|                     playPause(); | ||||
|                     if (isPlaying()) { | ||||
|                         hideControls(0, 0); | ||||
|                     } | ||||
|                     return true; | ||||
|                 } | ||||
|                 break; | ||||
|             case KeyEvent.KEYCODE_BACK: | ||||
|   | ||||
| @@ -88,6 +88,8 @@ public class PlayerMediaSession implements MediaSessionCallback { | ||||
|     @Override | ||||
|     public void play() { | ||||
|         player.play(); | ||||
|         // hide the player controls even if the play command came from the media session | ||||
|         player.hideControls(0, 0); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -7,10 +7,9 @@ import android.view.MenuItem; | ||||
|  | ||||
| import androidx.annotation.NonNull; | ||||
|  | ||||
| import org.schabi.newpipe.App; | ||||
| import org.schabi.newpipe.CheckForNewAppVersion; | ||||
| import org.schabi.newpipe.MainActivity; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.util.ReleaseVersionUtil; | ||||
|  | ||||
| public class MainSettingsFragment extends BasePreferenceFragment { | ||||
|     public static final boolean DEBUG = MainActivity.DEBUG; | ||||
| @@ -24,7 +23,7 @@ public class MainSettingsFragment extends BasePreferenceFragment { | ||||
|         setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called | ||||
|  | ||||
|         // Check if the app is updatable | ||||
|         if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) { | ||||
|         if (!ReleaseVersionUtil.isReleaseApk()) { | ||||
|             getPreferenceScreen().removePreference( | ||||
|                     findPreference(getString(R.string.update_pref_screen_key))); | ||||
|  | ||||
|   | ||||
| @@ -191,7 +191,7 @@ public class PeertubeInstanceListFragment extends Fragment { | ||||
|                 .setTitle(R.string.restore_defaults) | ||||
|                 .setMessage(R.string.restore_defaults_confirmation) | ||||
|                 .setNegativeButton(R.string.cancel, null) | ||||
|                 .setPositiveButton(R.string.yes, (dialog, which) -> { | ||||
|                 .setPositiveButton(R.string.ok, (dialog, which) -> { | ||||
|                     sharedPreferences.edit().remove(savedInstanceListKey).apply(); | ||||
|                     selectInstance(PeertubeInstance.defaultInstance); | ||||
|                     updateInstanceList(); | ||||
|   | ||||
| @@ -23,8 +23,6 @@ import androidx.preference.PreferenceFragmentCompat; | ||||
|  | ||||
| import com.jakewharton.rxbinding4.widget.RxTextView; | ||||
|  | ||||
| import org.schabi.newpipe.App; | ||||
| import org.schabi.newpipe.CheckForNewAppVersion; | ||||
| import org.schabi.newpipe.MainActivity; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.databinding.SettingsLayoutBinding; | ||||
| @@ -37,6 +35,7 @@ import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchResultListen | ||||
| import org.schabi.newpipe.settings.preferencesearch.PreferenceSearcher; | ||||
| import org.schabi.newpipe.util.DeviceUtils; | ||||
| import org.schabi.newpipe.util.KeyboardUtil; | ||||
| import org.schabi.newpipe.util.ReleaseVersionUtil; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
| import org.schabi.newpipe.views.FocusOverlayView; | ||||
|  | ||||
| @@ -267,7 +266,7 @@ public class SettingsActivity extends AppCompatActivity implements | ||||
|      */ | ||||
|     private void ensureSearchRepresentsApplicationState() { | ||||
|         // Check if the update settings are available | ||||
|         if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) { | ||||
|         if (!ReleaseVersionUtil.isReleaseApk()) { | ||||
|             SettingsResourceRegistry.getInstance() | ||||
|                     .getEntryByPreferencesResId(R.xml.update_settings) | ||||
|                     .setSearchable(false); | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| package org.schabi.newpipe.settings; | ||||
|  | ||||
| import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService; | ||||
|  | ||||
| import android.os.Bundle; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import androidx.preference.Preference; | ||||
|  | ||||
| import org.schabi.newpipe.NewVersionWorker; | ||||
| import org.schabi.newpipe.R; | ||||
|  | ||||
| public class UpdateSettingsFragment extends BasePreferenceFragment { | ||||
| @@ -33,7 +32,7 @@ public class UpdateSettingsFragment extends BasePreferenceFragment { | ||||
|         // Reset the expire time. This is necessary to check for an update immediately. | ||||
|         defaultPreferences.edit() | ||||
|                 .putLong(getString(R.string.update_expiry_key), 0).apply(); | ||||
|         startNewVersionCheckService(); | ||||
|         NewVersionWorker.enqueueNewVersionCheckingWork(getContext()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -136,7 +136,7 @@ public class ChooseTabsFragment extends Fragment { | ||||
|                 .setTitle(R.string.restore_defaults) | ||||
|                 .setMessage(R.string.restore_defaults_confirmation) | ||||
|                 .setNegativeButton(R.string.cancel, null) | ||||
|                 .setPositiveButton(R.string.yes, (dialog, which) -> { | ||||
|                 .setPositiveButton(R.string.ok, (dialog, which) -> { | ||||
|                     tabsManager.resetTabs(); | ||||
|                     updateTabList(); | ||||
|                     selectedTabsAdapter.notifyDataSetChanged(); | ||||
|   | ||||
							
								
								
									
										116
									
								
								app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| package org.schabi.newpipe.util | ||||
|  | ||||
| import android.content.pm.PackageManager | ||||
| import android.content.pm.Signature | ||||
| import androidx.core.content.pm.PackageInfoCompat | ||||
| import org.schabi.newpipe.App | ||||
| import org.schabi.newpipe.error.ErrorInfo | ||||
| import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification | ||||
| import org.schabi.newpipe.error.UserAction | ||||
| import java.io.ByteArrayInputStream | ||||
| import java.io.InputStream | ||||
| import java.security.MessageDigest | ||||
| import java.security.NoSuchAlgorithmException | ||||
| import java.security.cert.CertificateEncodingException | ||||
| import java.security.cert.CertificateException | ||||
| import java.security.cert.CertificateFactory | ||||
| import java.security.cert.X509Certificate | ||||
| import java.time.Instant | ||||
| import java.time.ZonedDateTime | ||||
| import java.time.format.DateTimeFormatter | ||||
|  | ||||
| object ReleaseVersionUtil { | ||||
|     // Public key of the certificate that is used in NewPipe release versions | ||||
|     private const val RELEASE_CERT_PUBLIC_KEY_SHA1 = | ||||
|         "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15" | ||||
|  | ||||
|     @JvmStatic | ||||
|     fun isReleaseApk(): Boolean { | ||||
|         return certificateSHA1Fingerprint == RELEASE_CERT_PUBLIC_KEY_SHA1 | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133. | ||||
|      * | ||||
|      * @return String with the APK's SHA1 fingerprint in hexadecimal | ||||
|      */ | ||||
|     private val certificateSHA1Fingerprint: String | ||||
|         get() { | ||||
|             val app = App.getApp() | ||||
|             val signatures: List<Signature> = try { | ||||
|                 PackageInfoCompat.getSignatures(app.packageManager, app.packageName) | ||||
|             } catch (e: PackageManager.NameNotFoundException) { | ||||
|                 showRequestError(app, e, "Could not find package info") | ||||
|                 return "" | ||||
|             } | ||||
|             if (signatures.isEmpty()) { | ||||
|                 return "" | ||||
|             } | ||||
|             val x509cert = try { | ||||
|                 val cert = signatures[0].toByteArray() | ||||
|                 val input: InputStream = ByteArrayInputStream(cert) | ||||
|                 val cf = CertificateFactory.getInstance("X509") | ||||
|                 cf.generateCertificate(input) as X509Certificate | ||||
|             } catch (e: CertificateException) { | ||||
|                 showRequestError(app, e, "Certificate error") | ||||
|                 return "" | ||||
|             } | ||||
|  | ||||
|             return try { | ||||
|                 val md = MessageDigest.getInstance("SHA1") | ||||
|                 val publicKey = md.digest(x509cert.encoded) | ||||
|                 byte2HexFormatted(publicKey) | ||||
|             } catch (e: NoSuchAlgorithmException) { | ||||
|                 showRequestError(app, e, "Could not retrieve SHA1 key") | ||||
|                 "" | ||||
|             } catch (e: CertificateEncodingException) { | ||||
|                 showRequestError(app, e, "Could not retrieve SHA1 key") | ||||
|                 "" | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     private fun byte2HexFormatted(arr: ByteArray): String { | ||||
|         val str = StringBuilder(arr.size * 2) | ||||
|         for (i in arr.indices) { | ||||
|             var h = Integer.toHexString(arr[i].toInt()) | ||||
|             val l = h.length | ||||
|             if (l == 1) { | ||||
|                 h = "0$h" | ||||
|             } | ||||
|             if (l > 2) { | ||||
|                 h = h.substring(l - 2, l) | ||||
|             } | ||||
|             str.append(h.uppercase()) | ||||
|             if (i < arr.size - 1) { | ||||
|                 str.append(':') | ||||
|             } | ||||
|         } | ||||
|         return str.toString() | ||||
|     } | ||||
|  | ||||
|     private fun showRequestError(app: App, e: Exception, request: String) { | ||||
|         createNotification( | ||||
|             app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, request) | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     fun isLastUpdateCheckExpired(expiry: Long): Boolean { | ||||
|         return Instant.ofEpochSecond(expiry).isBefore(Instant.now()) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Coerce expiry date time in between 6 hours and 72 hours from now | ||||
|      * | ||||
|      * @return Epoch second of expiry date time | ||||
|      */ | ||||
|     fun coerceUpdateCheckExpiry(expiryString: String?): Long { | ||||
|         val now = ZonedDateTime.now() | ||||
|         return expiryString?.let { | ||||
|             var expiry = | ||||
|                 ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString)) | ||||
|             expiry = maxOf(expiry, now.plusHours(6)) | ||||
|             expiry = minOf(expiry, now.plusHours(72)) | ||||
|             expiry.toEpochSecond() | ||||
|         } ?: now.plusHours(6).toEpochSecond() | ||||
|     } | ||||
| } | ||||
| @@ -581,6 +581,21 @@ | ||||
|                 app:srcCompat="@drawable/ic_close" | ||||
|                 app:tint="@color/white" /> | ||||
|  | ||||
|             <androidx.appcompat.widget.AppCompatImageButton | ||||
|                 android:id="@+id/addToPlaylistButton" | ||||
|                 android:layout_width="50dp" | ||||
|                 android:layout_height="50dp" | ||||
|                 android:layout_centerVertical="true" | ||||
|                 android:layout_toLeftOf="@+id/itemsListClose" | ||||
|                 android:background="?attr/selectableItemBackgroundBorderless" | ||||
|                 android:clickable="true" | ||||
|                 android:focusable="true" | ||||
|                 android:padding="10dp" | ||||
|                 android:scaleType="fitXY" | ||||
|                 android:tint="?attr/colorAccent" | ||||
|                 app:srcCompat="@drawable/ic_playlist_add" | ||||
|                 tools:ignore="ContentDescription,RtlHardcoded" /> | ||||
|  | ||||
|             <androidx.appcompat.widget.AppCompatImageButton | ||||
|                 android:id="@+id/repeatButton" | ||||
|                 android:layout_width="50dp" | ||||
| @@ -620,7 +635,7 @@ | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_centerVertical="true" | ||||
|                 android:layout_toStartOf="@id/itemsListClose" | ||||
|                 android:layout_toStartOf="@id/addToPlaylistButton" | ||||
|                 android:layout_toEndOf="@id/shuffleButton" | ||||
|                 android:gravity="center" | ||||
|                 android:textColor="@android:color/white" /> | ||||
|   | ||||
| @@ -91,7 +91,6 @@ | ||||
|     <string name="show_age_restricted_content_title">محتوى مقيد للبالغين</string> | ||||
|     <string name="duration_live">بث مباشر</string> | ||||
|     <string name="error_report_title">تقرير عن المشكلة</string> | ||||
|     <string name="yes">نعم</string> | ||||
|     <string name="disabled">متوقف</string> | ||||
|     <string name="clear">تنظيف</string> | ||||
|     <string name="best_resolution">أفضل دقة</string> | ||||
|   | ||||
| @@ -169,7 +169,6 @@ | ||||
|     <string name="best_resolution">Ən yaxşı görüntü keyfiyyəti</string> | ||||
|     <string name="clear">Təmizlə</string> | ||||
|     <string name="disabled">Deaktiv edilib</string> | ||||
|     <string name="yes">Bəli</string> | ||||
|     <string name="artists">İfaçılar</string> | ||||
|     <string name="albums">Albomlar</string> | ||||
|     <string name="songs">Mahnılar</string> | ||||
|   | ||||
| @@ -44,7 +44,6 @@ | ||||
|     <string name="detail_dislikes_img_view_description">Tarrezmes</string> | ||||
|     <string name="default_video_format_title">Formatu de videu predetermináu</string> | ||||
|     <string name="black_theme_title">Prietu</string> | ||||
|     <string name="yes">Sí</string> | ||||
|     <string name="short_thousand">mil</string> | ||||
|     <string name="short_million">mill.</string> | ||||
|     <string name="short_billion">mil mill.</string> | ||||
|   | ||||
| @@ -112,7 +112,6 @@ | ||||
|     <string name="best_resolution">Eng yaxshi qaror</string> | ||||
|     <string name="clear">Tozalash</string> | ||||
|     <string name="disabled">Ijrochilar o\'chirib qo\'yilgan</string> | ||||
|     <string name="yes">Ha</string> | ||||
|     <string name="artists">Artistlar</string> | ||||
|     <string name="albums">Albomlar</string> | ||||
|     <string name="songs">Qo\'shiqlar</string> | ||||
|   | ||||
| @@ -28,7 +28,6 @@ | ||||
|     <string name="unsupported_url">不支持的 URL</string> | ||||
|     <string name="settings_category_appearance_title">外观</string> | ||||
|     <string name="all">全部</string> | ||||
|     <string name="yes">是</string> | ||||
|     <string name="network_error">网络错误</string> | ||||
|     <plurals name="videos"> | ||||
|         <item quantity="other">%s 个视频</item> | ||||
|   | ||||
| @@ -97,7 +97,6 @@ | ||||
|     <string name="playlists">Плэйлісты</string> | ||||
|     <string name="tracks">Дарожкі</string> | ||||
|     <string name="users">Карыстальнікі</string> | ||||
|     <string name="yes">Так</string> | ||||
|     <string name="disabled">Адключана</string> | ||||
|     <string name="clear">Ачысціць</string> | ||||
|     <string name="best_resolution">Лепшае разрозненне</string> | ||||
|   | ||||
| @@ -56,7 +56,6 @@ | ||||
|     <string name="file">ⴰⴼⴰⵢⵍⵓ</string> | ||||
|     <string name="play_all">ⵖⵔ ⵎⴰⵕⵕⴰ</string> | ||||
|     <string name="file_deleted">ⵉⵜⵜⵡⴰⴽⴽⵙ ⵓⴼⴰⵢⵍⵓ</string> | ||||
|     <string name="yes">ⵢⴰⵀ</string> | ||||
|     <string name="videos_string">ⵉⴼⵉⴷⵢⵓⵜⵏ</string> | ||||
|     <string name="all">ⵎⴰⵕⵕⴰ</string> | ||||
|     <string name="downloads_title">ⵓⴳⴳⴰⵎⵏ</string> | ||||
|   | ||||
| @@ -75,7 +75,6 @@ | ||||
|     <string name="downloads_title">Изтегляния</string> | ||||
|     <string name="error_report_title">Съобщение за грешка</string> | ||||
|     <string name="all">Всички</string> | ||||
|     <string name="yes">Да</string> | ||||
|     <string name="disabled">Забранено</string> | ||||
|     <string name="clear">Изчисти</string> | ||||
|     <string name="best_resolution">Най-добра резолюция</string> | ||||
|   | ||||
| @@ -55,7 +55,6 @@ | ||||
|     <string name="downloads_title">ডাউনলোডগুলি</string> | ||||
|     <string name="error_report_title">ত্রুটি প্রতিবেদন</string> | ||||
|     <string name="all">সবগুলি</string> | ||||
|     <string name="yes">হ্যাঁ</string> | ||||
|     <string name="disabled">নিস্ক্রীয়</string> | ||||
|     <string name="clear">পরিষ্কার</string> | ||||
|     <!-- error strings --> | ||||
|   | ||||
| @@ -53,7 +53,6 @@ | ||||
|     <string name="always">সবসময়</string> | ||||
|     <string name="clear">পরিষ্কার</string> | ||||
|     <string name="disabled">নিস্ক্রীয়</string> | ||||
|     <string name="yes">হ্যাঁ</string> | ||||
|     <string name="all">সবগুলি</string> | ||||
|     <string name="error_report_title">ত্রুটি প্রতিবেদন</string> | ||||
|     <string name="downloads_title">ডাউনলোডগুলি</string> | ||||
|   | ||||
| @@ -176,7 +176,6 @@ | ||||
|     <string name="best_resolution">সেরা রেজুলিউসন</string> | ||||
|     <string name="clear">পরিষ্কার</string> | ||||
|     <string name="disabled">নিস্ক্রীয়</string> | ||||
|     <string name="yes">হ্যাঁ</string> | ||||
|     <string name="artists">শিল্পীরা</string> | ||||
|     <string name="albums">অ্যালবাম গুলি</string> | ||||
|     <string name="songs">গান গুলি</string> | ||||
|   | ||||
| @@ -37,7 +37,6 @@ | ||||
|     <string name="downloads">Baixades</string> | ||||
|     <string name="downloads_title">Baixades</string> | ||||
|     <string name="all">Tot</string> | ||||
|     <string name="yes">Sí</string> | ||||
|     <string name="disabled">Desactivat</string> | ||||
|     <string name="clear">Neteja</string> | ||||
|     <string name="best_resolution">Millor resolució</string> | ||||
|   | ||||
| @@ -101,7 +101,6 @@ | ||||
|     <string name="title_licenses">مۆڵەتنامەی لایەنی-سێیەم</string> | ||||
|     <string name="app_license_title">مۆڵەتنامەی نیوپایپ</string> | ||||
|     <string name="show_hold_to_append_title">پیشاندانی ڕێنمایی ”داگرتن تا پاشکۆ”</string> | ||||
|     <string name="yes">بەڵێ</string> | ||||
|     <string name="msg_threads">دابەشکراوەکان</string> | ||||
|     <string name="title_most_played">زۆرترین لێدراو</string> | ||||
|     <string name="unbookmark_playlist">لادانی نیشانهكراو</string> | ||||
|   | ||||
| @@ -84,7 +84,6 @@ | ||||
|     <string name="no_available_dir">Určete prosím složku pro stahování později v nastavení</string> | ||||
|     <string name="info_labels">Co:\\nŽádost:\\nJazyk obsahu\\nZemě obsahu:\\nJazyk aplikace:\\nSlužba:\\nČas GMT:\\nBalíček:\\nVerze:\\nVerze OS:</string> | ||||
|     <string name="all">Vše</string> | ||||
|     <string name="yes">Ano</string> | ||||
|     <string name="short_thousand">tis.</string> | ||||
|     <string name="open_in_popup_mode">Otevřít ve vyskakovacím okně</string> | ||||
|     <string name="short_million">mil.</string> | ||||
|   | ||||
| @@ -108,7 +108,6 @@ | ||||
|     </plurals> | ||||
|     <string name="tracks">Numre</string> | ||||
|     <string name="users">Brugere</string> | ||||
|     <string name="yes">Ja</string> | ||||
|     <string name="disabled">Slået fra</string> | ||||
|     <string name="clear">Slet</string> | ||||
|     <string name="best_resolution">Bedste opløsning</string> | ||||
|   | ||||
| @@ -88,7 +88,6 @@ | ||||
|     <string name="black_theme_title">Schwarz</string> | ||||
|     <string name="title_activity_recaptcha">reCAPTCHA-Aufgabe</string> | ||||
|     <string name="recaptcha_request_toast">reCAPTCHA-Aufgabe angefordert</string> | ||||
|     <string name="yes">Ja</string> | ||||
|     <string name="all">Alle</string> | ||||
|     <string name="disabled">Deaktiviert</string> | ||||
|     <string name="open_in_popup_mode">Im Pop-up-Modus öffnen</string> | ||||
|   | ||||
| @@ -52,7 +52,6 @@ | ||||
|     <string name="downloads">Λήψεις</string> | ||||
|     <string name="downloads_title">Λήψεις</string> | ||||
|     <string name="all">Όλα</string> | ||||
|     <string name="yes">Ναι</string> | ||||
|     <string name="general_error">Σφάλμα</string> | ||||
|     <string name="error_snackbar_action">Αναφορά</string> | ||||
|     <string name="what_device_headline">Πληροφορίες:</string> | ||||
|   | ||||
| @@ -95,7 +95,6 @@ | ||||
|         <item quantity="one">%s filmeto</item> | ||||
|         <item quantity="other">%s filmetoj</item> | ||||
|     </plurals> | ||||
|     <string name="yes">Jes</string> | ||||
|     <string name="msg_popup_permission">Tiu permeso estas necesa por | ||||
| \nmalfermi en ŝprucfenestra modo</string> | ||||
|     <string name="popup_playing_toast">Ludante en ŝprucfenestra modo</string> | ||||
|   | ||||
| @@ -82,7 +82,6 @@ | ||||
|     <string name="info_labels">Lo sucedido:\\nPetición:\\nIdioma del contenido:\\nPaís del contenido:\\nIdioma de la aplicación:\\nServicio:\\nHora GMT:\\nPaquete:\\nVersión:\\nVersión del SO:</string> | ||||
|     <string name="black_theme_title">Negro</string> | ||||
|     <string name="all">Todo</string> | ||||
|     <string name="yes">Sí</string> | ||||
|     <string name="short_thousand">k</string> | ||||
|     <string name="short_million">M</string> | ||||
|     <string name="short_billion">MM</string> | ||||
|   | ||||
| @@ -93,7 +93,6 @@ | ||||
|     <string name="downloads_title">Allalaadimised</string> | ||||
|     <string name="error_report_title">Vea teatamine</string> | ||||
|     <string name="all">Kõik</string> | ||||
|     <string name="yes">Jah</string> | ||||
|     <string name="disabled">Keelatud</string> | ||||
|     <string name="clear">Kustuta</string> | ||||
|     <string name="best_resolution">Parim lahutus</string> | ||||
|   | ||||
| @@ -62,7 +62,6 @@ | ||||
|     <string name="downloads_title">Deskargak</string> | ||||
|     <string name="error_report_title">Errore-txostena</string> | ||||
|     <string name="all">Dena</string> | ||||
|     <string name="yes">Bai</string> | ||||
|     <string name="disabled">Desgaituta</string> | ||||
|     <string name="clear">Garbitu</string> | ||||
|     <string name="best_resolution">Bereizmen onena</string> | ||||
|   | ||||
| @@ -109,7 +109,6 @@ | ||||
|     <string name="channels">کانالها</string> | ||||
|     <string name="playlists">سیاهههای پخش</string> | ||||
|     <string name="users">کاربران</string> | ||||
|     <string name="yes">بله</string> | ||||
|     <string name="disabled">غیرفعال</string> | ||||
|     <string name="clear">پاککردن</string> | ||||
|     <string name="play_all">پخش همه</string> | ||||
|   | ||||
| @@ -73,7 +73,6 @@ | ||||
|     <string name="downloads_title">Lataukset</string> | ||||
|     <string name="error_report_title">Virheraportti</string> | ||||
|     <string name="all">Kaikki</string> | ||||
|     <string name="yes">Kyllä</string> | ||||
|     <string name="disabled">Poistettu käytöstä</string> | ||||
|     <string name="clear">Pyyhi</string> | ||||
|     <string name="best_resolution">Paras resoluutio</string> | ||||
|   | ||||
| @@ -86,7 +86,6 @@ | ||||
|     <string name="recaptcha_request_toast">Défi reCAPTCHA demandé</string> | ||||
|     <string name="open_in_popup_mode">Ouvrir en mode pop-up</string> | ||||
|     <string name="popup_playing_toast">Lecture en mode flottant</string> | ||||
|     <string name="yes">Oui</string> | ||||
|     <string name="disabled">Désactivés</string> | ||||
|     <string name="info_labels">Quoi :\\nRequest :\\nContent Language :\\nContent Country :\\nApp Language :\\nService :\\nGMT Time :\\nPackage :\\nVersion :\\nOS version :</string> | ||||
|     <string name="short_thousand">k</string> | ||||
|   | ||||
| @@ -96,7 +96,6 @@ | ||||
|     <string name="playlists">Listas de reprodución</string> | ||||
|     <string name="tracks">Pistas</string> | ||||
|     <string name="users">Usuarios</string> | ||||
|     <string name="yes">Si</string> | ||||
|     <string name="disabled">Desactivado</string> | ||||
|     <string name="clear">Limpar</string> | ||||
|     <string name="best_resolution">Mellor resolución</string> | ||||
|   | ||||
| @@ -58,7 +58,6 @@ | ||||
|     <string name="downloads_title">הורדות</string> | ||||
|     <string name="error_report_title">דוח שגיאה</string> | ||||
|     <string name="all">הכול</string> | ||||
|     <string name="yes">כן</string> | ||||
|     <string name="disabled">מושבת</string> | ||||
|     <string name="clear">ניקוי</string> | ||||
|     <string name="general_error">שגיאה</string> | ||||
|   | ||||
| @@ -90,7 +90,6 @@ | ||||
|     <string name="downloads_title">डाउनलोड</string> | ||||
|     <string name="error_report_title">त्रुटी की रिपोर्ट</string> | ||||
|     <string name="all">सारे</string> | ||||
|     <string name="yes">सहमत हूँ</string> | ||||
|     <string name="disabled">बंद करे</string> | ||||
|     <string name="clear">साफ़</string> | ||||
|     <string name="best_resolution">बेहतर विडियो की क्वालिटी</string> | ||||
|   | ||||
| @@ -71,7 +71,6 @@ | ||||
|     <string name="downloads_title">Preuzimanja</string> | ||||
|     <string name="error_report_title">Prijavi grešku</string> | ||||
|     <string name="all">Sve</string> | ||||
|     <string name="yes">Da</string> | ||||
|     <string name="disabled">Isključeno</string> | ||||
|     <string name="clear">Očisti</string> | ||||
|     <string name="best_resolution">Najbolja rezolucija</string> | ||||
|   | ||||
| @@ -111,7 +111,6 @@ | ||||
|     <string name="settings_category_debug_title">Hibaelhárítás</string> | ||||
|     <string name="popup_playing_toast">Lejátszás felugró ablakban</string> | ||||
|     <string name="all">Összes</string> | ||||
|     <string name="yes">Igen</string> | ||||
|     <string name="disabled">Letiltva</string> | ||||
|     <string name="clear">Törlés</string> | ||||
|     <string name="best_resolution">Legjobb felbontás</string> | ||||
|   | ||||
| @@ -65,7 +65,6 @@ | ||||
|     <string name="file_deleted">Ֆայլը ջնջվեց</string> | ||||
|     <string name="file">Ֆայլ</string> | ||||
|     <string name="songs">Երգեր</string> | ||||
|     <string name="yes">Այո</string> | ||||
|     <string name="enable_search_history_title">Որոնման պատմություն</string> | ||||
|     <string name="close">Փակել</string> | ||||
|     <plurals name="days"> | ||||
|   | ||||
| @@ -85,7 +85,6 @@ | ||||
|     <string name="tracks">Pistas</string> | ||||
|     <string name="users">Usatores</string> | ||||
|     <string name="events">Eventos</string> | ||||
|     <string name="yes">Si</string> | ||||
|     <string name="disabled">Disactivate</string> | ||||
|     <string name="clear">Vacuar</string> | ||||
|     <string name="best_resolution">Melior resolution</string> | ||||
|   | ||||
| @@ -86,7 +86,6 @@ | ||||
|     <string name="short_thousand">r</string> | ||||
|     <string name="short_million">J</string> | ||||
|     <string name="short_billion">T</string> | ||||
|     <string name="yes">Ya</string> | ||||
|     <string name="open_in_popup_mode">Buka dalam mode popup</string> | ||||
|     <string name="msg_popup_permission">Izin ini dibutuhkan untuk | ||||
| \nmembuka di mode sembul</string> | ||||
|   | ||||
| @@ -87,7 +87,6 @@ | ||||
|     <string name="short_million">M</string> | ||||
|     <string name="short_billion">Mrd</string> | ||||
|     <string name="recaptcha_request_toast">È richiesta la risoluzione del reCAPTCHA</string> | ||||
|     <string name="yes">Sì</string> | ||||
|     <string name="open_in_popup_mode">Apri in modalità popup</string> | ||||
|     <string name="popup_playing_toast">Riproduzione in modalità popup</string> | ||||
|     <string name="disabled">Disattivato</string> | ||||
|   | ||||
| @@ -87,7 +87,6 @@ | ||||
|     <string name="short_thousand">k</string> | ||||
|     <string name="short_million">M</string> | ||||
|     <string name="short_billion">B</string> | ||||
|     <string name="yes">はい</string> | ||||
|     <string name="open_in_popup_mode">ポップアップモードで開く</string> | ||||
|     <string name="msg_popup_permission">ポップアップモードで開くには | ||||
| \n権限の許可が必要です</string> | ||||
|   | ||||
| @@ -30,7 +30,6 @@ | ||||
|     <string name="export_to">Sifeḍ ɣer</string> | ||||
|     <string name="controls_add_to_playlist_title">Rnu ɣer</string> | ||||
|     <string name="playback_step">Amecwaṛ</string> | ||||
|     <string name="yes">Ih</string> | ||||
|     <string name="msg_running">Azdam n NewPipe</string> | ||||
|     <string name="restore_defaults">Err-d imezwar</string> | ||||
|     <string name="channel_created_by">Yerna-t %s</string> | ||||
|   | ||||
| @@ -169,7 +169,6 @@ | ||||
|     <string name="app_license">NewPipe nermalava kopîleft libre ye: Hûn dikarin li gorî kêfa xwe bikar bînin, parve bikin û baştir bikin. Bi taybetî hûn dikarin wê di bin mercên Lîsansa Giştî ya GNU ya Giştî ya ku ji hêla Weqfa Nermalava Azad ve hatî weşandin de, an guhertoya 3 ya Lîsansê, an jî (li gorî vebijarka we) guhertoyek paşîn ji nû ve belav bikin û / an biguherînin.</string> | ||||
|     <string name="clear">Zelal</string> | ||||
|     <string name="disabled">Bêmecel</string> | ||||
|     <string name="yes">Erê</string> | ||||
|     <string name="artists">Hunermend</string> | ||||
|     <string name="albums">Album</string> | ||||
|     <string name="songs">Stran</string> | ||||
|   | ||||
| @@ -102,7 +102,6 @@ | ||||
|     <string name="popup_playing_toast">팝업 모드에서 재생 중</string> | ||||
|     <string name="error_report_title">오류 보고</string> | ||||
|     <string name="all">전부</string> | ||||
|     <string name="yes">네</string> | ||||
|     <string name="disabled">해제됨</string> | ||||
|     <string name="clear">지우기</string> | ||||
|     <string name="best_resolution">최대 해상도</string> | ||||
|   | ||||
| @@ -86,7 +86,6 @@ | ||||
|     <string name="downloads_title">دابەزاندنەکان</string> | ||||
|     <string name="error_report_title">ناتوانرێ سکاڵابکرێ</string> | ||||
|     <string name="all">گشتی</string> | ||||
|     <string name="yes">بەڵێ</string> | ||||
|     <string name="disabled">ناکارایە</string> | ||||
|     <string name="clear">پاککردنەوە</string> | ||||
|     <string name="best_resolution">باشترین قەبارە</string> | ||||
|   | ||||
| @@ -58,7 +58,6 @@ | ||||
|     <string name="downloads_title">Atsisiuntimai</string> | ||||
|     <string name="error_report_title">Klaidų ataskaita</string> | ||||
|     <string name="all">Visi</string> | ||||
|     <string name="yes">Taip</string> | ||||
|     <string name="disabled">Išjungta</string> | ||||
|     <string name="clear">Išvalyti</string> | ||||
|     <string name="best_resolution">Geriausia raiška</string> | ||||
|   | ||||
| @@ -236,7 +236,6 @@ | ||||
|     <string name="best_resolution">Labākā izšķirtspēja</string> | ||||
|     <string name="clear">Notīrīt</string> | ||||
|     <string name="disabled">Atspējots</string> | ||||
|     <string name="yes">Jā</string> | ||||
|     <string name="artists">Mākslinieki</string> | ||||
|     <string name="albums">Albūmi</string> | ||||
|     <string name="songs">Dziesmas</string> | ||||
|   | ||||
| @@ -93,7 +93,6 @@ | ||||
|     <string name="downloads_title">Превземања</string> | ||||
|     <string name="error_report_title">Извештај за грешки</string> | ||||
|     <string name="all">Сите</string> | ||||
|     <string name="yes">Да</string> | ||||
|     <string name="disabled">Оневозможено</string> | ||||
|     <string name="clear">Избриши</string> | ||||
|     <string name="best_resolution">Најдобра резолуција</string> | ||||
|   | ||||
| @@ -270,7 +270,6 @@ | ||||
|     <string name="best_resolution">മികച്ച റിസല്യൂഷൻ</string> | ||||
|     <string name="clear">തെളിക്കുക</string> | ||||
|     <string name="disabled">അസാധുവാക്കപ്പെട്ടു</string> | ||||
|     <string name="yes">അതെ</string> | ||||
|     <string name="artists">കലാകാരന്മാർ</string> | ||||
|     <string name="albums">ആൽബങ്ങൾ</string> | ||||
|     <string name="songs">പാട്ടുകൾ</string> | ||||
|   | ||||
| @@ -105,7 +105,6 @@ | ||||
|     <string name="tracks">Trek</string> | ||||
|     <string name="users">Pengguna</string> | ||||
|     <string name="events">Peristiwa</string> | ||||
|     <string name="yes">Ya</string> | ||||
|     <string name="disabled">Dinyahdayakan</string> | ||||
|     <string name="clear">Bersihkan</string> | ||||
|     <string name="best_resolution">Resolusi terbaik</string> | ||||
|   | ||||
| @@ -89,7 +89,6 @@ | ||||
|     <string name="black_theme_title">Svart</string> | ||||
|     <string name="popup_playing_toast">Spiller av i oppsprettsmodus</string> | ||||
|     <string name="all">Alle</string> | ||||
|     <string name="yes">Ja</string> | ||||
|     <string name="disabled">Avskrudd</string> | ||||
|     <string name="short_thousand">k</string> | ||||
|     <string name="short_million">M</string> | ||||
|   | ||||
| @@ -111,7 +111,6 @@ | ||||
|     <string name="tracks">ट्रयाकहरु</string> | ||||
|     <string name="users">प्रयोगकर्ताहरु</string> | ||||
|     <string name="events">घटनाहरू</string> | ||||
|     <string name="yes">हो</string> | ||||
|     <string name="disabled">अक्षम</string> | ||||
|     <string name="clear">स्पष्ट</string> | ||||
|     <string name="best_resolution">सर्वश्रेष्ठ रेसोलुशन</string> | ||||
|   | ||||
| @@ -93,7 +93,6 @@ | ||||
|     <string name="downloads_title">Downloads</string> | ||||
|     <string name="error_report_title">Foutrapport</string> | ||||
|     <string name="all">Alles</string> | ||||
|     <string name="yes">Ja</string> | ||||
|     <string name="disabled">Uitgeschakeld</string> | ||||
|     <string name="clear">Wissen</string> | ||||
|     <string name="best_resolution">Beste resolutie</string> | ||||
|   | ||||
| @@ -85,7 +85,6 @@ | ||||
|     <string name="recaptcha_request_toast">reCAPTCHA-uitdaging gevraagd</string> | ||||
|     <string name="open_in_popup_mode">Openen in pop-upmodus</string> | ||||
|     <string name="all">Alles</string> | ||||
|     <string name="yes">Ja</string> | ||||
|     <string name="short_thousand">k</string> | ||||
|     <string name="short_million">M</string> | ||||
|     <string name="short_billion">B</string> | ||||
|   | ||||
| @@ -92,7 +92,6 @@ | ||||
|     <string name="downloads_title">ਡਾਊਨਲੋਡਸ</string> | ||||
|     <string name="error_report_title">Error ਰਿਪੋਰਟ</string> | ||||
|     <string name="all">ਸਾਰੇ</string> | ||||
|     <string name="yes">ਹਾਂ</string> | ||||
|     <string name="disabled">ਬੰਦ ਕੀਤਾ</string> | ||||
|     <string name="clear">ਮਿਟਾਓ</string> | ||||
|     <string name="best_resolution">ਵਧੀਆ Resolution</string> | ||||
|   | ||||
| @@ -95,7 +95,6 @@ | ||||
|     <string name="show_search_suggestions_summary">Wybierz podpowiedzi, które będą wyświetlane podczas wyszukiwania</string> | ||||
|     <string name="popup_playing_toast">Odtwarzanie w trybie okienkowym</string> | ||||
|     <string name="all">Wszystkie</string> | ||||
|     <string name="yes">Tak</string> | ||||
|     <string name="disabled">Wyłączone</string> | ||||
|     <string name="clear">Wyczyść</string> | ||||
|     <string name="short_thousand">tys.</string> | ||||
|   | ||||
| @@ -90,7 +90,6 @@ | ||||
|     <string name="default_video_format_title">Formato de vídeo padrão</string> | ||||
|     <string name="popup_playing_toast">Reproduzindo em modo popup</string> | ||||
|     <string name="all">Tudo</string> | ||||
|     <string name="yes">Sim</string> | ||||
|     <string name="disabled">Desativado</string> | ||||
|     <string name="short_thousand">k</string> | ||||
|     <string name="short_million">M</string> | ||||
|   | ||||
| @@ -395,7 +395,6 @@ | ||||
|     <string name="max_retry_msg">Tentativas máximas</string> | ||||
|     <string name="title_activity_history">Histórico</string> | ||||
|     <string name="playback_pitch">Velocidade</string> | ||||
|     <string name="yes">Sim</string> | ||||
|     <string name="error_download_resource_gone">Não é possível recuperar esta descarga</string> | ||||
|     <string name="rename_playlist">Mudar nome</string> | ||||
|     <string name="feed_group_dialog_empty_selection">Nenhuma subscrição selecionada</string> | ||||
|   | ||||
| @@ -83,7 +83,6 @@ | ||||
|     <string name="open_in_popup_mode">Abrir no modo popup</string> | ||||
|     <string name="black_theme_title">Preto</string> | ||||
|     <string name="all">Tudo</string> | ||||
|     <string name="yes">Sim</string> | ||||
|     <string name="short_thousand">k</string> | ||||
|     <string name="short_million">M</string> | ||||
|     <string name="short_billion">MM</string> | ||||
|   | ||||
| @@ -90,7 +90,6 @@ | ||||
|     <string name="black_theme_title">Negru</string> | ||||
|     <string name="popup_playing_toast">Redare în mod pop-up</string> | ||||
|     <string name="all">Toate</string> | ||||
|     <string name="yes">Da</string> | ||||
|     <string name="disabled">Dezactivat</string> | ||||
|     <string name="app_ui_crash">Aplicația/UI s-a oprit</string> | ||||
|     <string name="info_labels">Ce:\\nSolicitare:\\nLimba conținutului:\\nȚara conținutului:\\nLimba aplicației:\\nServiciu:\\nOra GMT:\\nPachet:\\nVersiune: \\nVersiune SO:</string> | ||||
|   | ||||
| @@ -86,7 +86,6 @@ | ||||
|     <string name="black_theme_title">Чёрная</string> | ||||
|     <string name="popup_remember_size_pos_title">Помнить параметры окна</string> | ||||
|     <string name="popup_playing_toast">Воспроизведение во всплывающем окне</string> | ||||
|     <string name="yes">Да</string> | ||||
|     <string name="clear">Очистить</string> | ||||
|     <string name="all">Всё</string> | ||||
|     <string name="info_labels">Что:\\nЗапрос:\\nЯзык контента:\\nСтрана контента:\\nЯзык приложения:\\nСервис:\\nВремя по Гринвичу:\\nПакет:\\nВерсия пакета:\\nВерсия ОС:</string> | ||||
|   | ||||
| @@ -415,7 +415,6 @@ | ||||
|     <string name="best_resolution">Risolutzione mègius</string> | ||||
|     <string name="clear">Isbòida</string> | ||||
|     <string name="disabled">Disabilitadu</string> | ||||
|     <string name="yes">Eja</string> | ||||
|     <string name="artists">Artista</string> | ||||
|     <string name="albums">Albums</string> | ||||
|     <string name="songs">Cantzones</string> | ||||
|   | ||||
| @@ -87,7 +87,6 @@ | ||||
|     <string name="short_million">M</string> | ||||
|     <string name="short_billion">B</string> | ||||
|     <string name="recaptcha_request_toast">Požiadavka reCAPTCHA</string> | ||||
|     <string name="yes">Áno</string> | ||||
|     <string name="open_in_popup_mode">Spustiť v okne</string> | ||||
|     <string name="msg_popup_permission">Tieto práva sú potrebné pre | ||||
| \nprehrávanie v mini okne</string> | ||||
|   | ||||
| @@ -87,7 +87,6 @@ | ||||
|     <string name="short_thousand">k</string> | ||||
|     <string name="short_million">mio</string> | ||||
|     <string name="short_billion">mrd</string> | ||||
|     <string name="yes">Da</string> | ||||
|     <string name="open_in_popup_mode">Odpri v pojavnem načinu</string> | ||||
|     <string name="msg_popup_permission">To dovoljenje je potrebno za odpiranje  | ||||
| \nv pojavnem načinu</string> | ||||
|   | ||||
| @@ -319,7 +319,6 @@ | ||||
|     <string name="duration_live">Toos</string> | ||||
|     <string name="undo">Soo celi</string> | ||||
|     <string name="disabled">Xidhan</string> | ||||
|     <string name="yes">Haa</string> | ||||
|     <string name="users">Isticmaale</string> | ||||
|     <string name="videos_string">Muuqaalo</string> | ||||
|     <string name="all">Dhammaan</string> | ||||
|   | ||||
| @@ -34,7 +34,6 @@ | ||||
|     <string name="downloads_title">Shkarkimet</string> | ||||
|     <string name="error_report_title">Raporti i gabimit</string> | ||||
|     <string name="all">Të gjitha</string> | ||||
|     <string name="yes">Po</string> | ||||
|     <string name="missions_header_pending">Në pritje</string> | ||||
|     <plurals name="feed_group_dialog_selection_count"> | ||||
|         <item quantity="one">%d i zgjedhur</item> | ||||
|   | ||||
| @@ -87,7 +87,6 @@ | ||||
|     <string name="short_thousand">хиљ</string> | ||||
|     <string name="short_million">мил</string> | ||||
|     <string name="short_billion">млрд</string> | ||||
|     <string name="yes">Да</string> | ||||
|     <string name="open_in_popup_mode">Отвори у искачућем режиму</string> | ||||
|     <string name="msg_popup_permission">Ова дозвола је потребна за | ||||
| \nотварање у искачућем режиму</string> | ||||
|   | ||||
| @@ -58,7 +58,6 @@ | ||||
|     <string name="downloads_title">Hämtningar</string> | ||||
|     <string name="error_report_title">Felrapport</string> | ||||
|     <string name="all">Alla</string> | ||||
|     <string name="yes">Ja</string> | ||||
|     <string name="disabled">Inaktiverad</string> | ||||
|     <string name="clear">Rensa</string> | ||||
|     <string name="best_resolution">Bästa upplösningen</string> | ||||
|   | ||||
| @@ -73,7 +73,6 @@ | ||||
|     <string name="all">அனைத்தும்</string> | ||||
|     <string name="playlists">ஒளிச்சரங்கள்</string> | ||||
|     <string name="users">பயனர்கள்</string> | ||||
|     <string name="yes">ஆம்</string> | ||||
|     <string name="clear">அழி</string> | ||||
|     <string name="always">எப்பொழுதும்</string> | ||||
|     <string name="just_once">ஒரு முறை</string> | ||||
|   | ||||
| @@ -52,7 +52,6 @@ | ||||
|     <string name="downloads_title">డౌన్ లోడ్</string> | ||||
|     <string name="error_report_title">లోపం నివేదిక</string> | ||||
|     <string name="all">అన్ని</string> | ||||
|     <string name="yes">అవును</string> | ||||
|     <string name="play_all">అన్నింటినీ ప్లే చేయండి</string> | ||||
|     <string name="notification_channel_name">న్యూప్యాప్ నోటిఫికేషన్</string> | ||||
|     <string name="general_error">లోపం</string> | ||||
|   | ||||
| @@ -104,7 +104,6 @@ | ||||
|     <string name="tracks">แทร็ค</string> | ||||
|     <string name="users">ผู้ใช้</string> | ||||
|     <string name="events">เหตุการณ์</string> | ||||
|     <string name="yes">ใช่</string> | ||||
|     <string name="disabled">ปิดการใช้งาน</string> | ||||
|     <string name="clear">ล้าง</string> | ||||
|     <string name="best_resolution">ความละเอียดที่ดีที่สุด</string> | ||||
|   | ||||
| @@ -86,7 +86,6 @@ | ||||
|     <string name="black_theme_title">Siyah</string> | ||||
|     <string name="popup_playing_toast">Açılır pencere kipinde oynatılıyor</string> | ||||
|     <string name="all">Tümü</string> | ||||
|     <string name="yes">Evet</string> | ||||
|     <string name="disabled">Devre dışı</string> | ||||
|     <string name="your_comment">Yorumunuz (İngilizce):</string> | ||||
|     <string name="error_details_headline">Ayrıntılar:</string> | ||||
|   | ||||
| @@ -123,7 +123,6 @@ | ||||
|     <string name="file">Afaylu</string> | ||||
|     <string name="always">Ku dwal</string> | ||||
|     <string name="clear">Sfeḍ</string> | ||||
|     <string name="yes">Yah</string> | ||||
|     <string name="artists">Inaẓuṛen</string> | ||||
|     <string name="songs">Tiɣennijin</string> | ||||
|     <string name="events">Imezza</string> | ||||
|   | ||||
| @@ -58,7 +58,6 @@ | ||||
|     <string name="downloads_title">Завантаження</string> | ||||
|     <string name="error_report_title">Звіт про помилку</string> | ||||
|     <string name="all">Усе</string> | ||||
|     <string name="yes">Так</string> | ||||
|     <string name="disabled">Вимкнено</string> | ||||
|     <string name="app_ui_crash">Збій застосунку/інтерфейсу</string> | ||||
|     <string name="your_comment">Ваш коментар (англійською):</string> | ||||
|   | ||||
| @@ -92,7 +92,6 @@ | ||||
|     <string name="downloads_title">ڈاؤن لوڈز</string> | ||||
|     <string name="error_report_title">خرابی کی اطلاع</string> | ||||
|     <string name="all">تمام</string> | ||||
|     <string name="yes">ہاں</string> | ||||
|     <string name="disabled">غیر فعال</string> | ||||
|     <string name="clear">صاف</string> | ||||
|     <string name="best_resolution">بہترین ریزولوشن</string> | ||||
|   | ||||
| @@ -56,7 +56,6 @@ | ||||
|     <string name="downloads_title">Tải xuống</string> | ||||
|     <string name="error_report_title">Báo lỗi</string> | ||||
|     <string name="all">Tất cả</string> | ||||
|     <string name="yes">Có</string> | ||||
|     <string name="disabled">Vô hiệu</string> | ||||
|     <string name="clear">Xóa</string> | ||||
|     <string name="best_resolution">Độ phân giải tốt nhất</string> | ||||
|   | ||||
| @@ -28,7 +28,6 @@ | ||||
|     <string name="unsupported_url">不支持的 URL</string> | ||||
|     <string name="settings_category_appearance_title">外观</string> | ||||
|     <string name="all">全部</string> | ||||
|     <string name="yes">是</string> | ||||
|     <string name="network_error">网络错误</string> | ||||
|     <plurals name="videos"> | ||||
|         <item quantity="other">%s 个视频</item> | ||||
|   | ||||
| @@ -86,7 +86,6 @@ | ||||
|     <string name="black_theme_title">純黑</string> | ||||
|     <string name="popup_playing_toast">以畫中畫模式播放</string> | ||||
|     <string name="all">所有</string> | ||||
|     <string name="yes">是</string> | ||||
|     <string name="app_ui_crash">App/界面閃退</string> | ||||
|     <string name="info_labels">經過:\\n請求:\\n內容語言:\\n內容國家:\\nApp 語言:\\n服務:\\nGMT 時間:\\n封裝:\\n版本:\\nOS 版本:</string> | ||||
|     <string name="short_thousand">千</string> | ||||
|   | ||||
| @@ -61,7 +61,6 @@ | ||||
|     <string name="downloads_title">下載</string> | ||||
|     <string name="error_report_title">錯誤回報</string> | ||||
|     <string name="all">全部</string> | ||||
|     <string name="yes">是的</string> | ||||
|     <string name="disabled">已停用</string> | ||||
|     <string name="clear">清除</string> | ||||
|     <string name="best_resolution">最佳解析度</string> | ||||
|   | ||||
| @@ -168,7 +168,6 @@ | ||||
|     <string name="songs">Songs</string> | ||||
|     <string name="albums">Albums</string> | ||||
|     <string name="artists">Artists</string> | ||||
|     <string name="yes">Yes</string> | ||||
|     <string name="disabled">Disabled</string> | ||||
|     <string name="clear">Clear</string> | ||||
|     <string name="best_resolution">Best resolution</string> | ||||
|   | ||||
| @@ -2,8 +2,9 @@ package org.schabi.newpipe | ||||
|  | ||||
| import org.junit.Assert.assertFalse | ||||
| import org.junit.Assert.assertTrue | ||||
| import org.junit.Before | ||||
| import org.junit.Test | ||||
| import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry | ||||
| import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired | ||||
| import java.time.Instant | ||||
| import java.time.ZoneId | ||||
| import java.time.format.DateTimeFormatter | ||||
| @@ -11,18 +12,11 @@ import kotlin.math.abs | ||||
|  | ||||
| class NewVersionManagerTest { | ||||
|  | ||||
|     private lateinit var manager: NewVersionManager | ||||
|  | ||||
|     @Before | ||||
|     fun setup() { | ||||
|         manager = NewVersionManager() | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     fun `Expiry is reached`() { | ||||
|         val oneHourEarlier = Instant.now().atZone(ZoneId.of("GMT")).minusHours(1) | ||||
|  | ||||
|         val expired = manager.isExpired(oneHourEarlier.toEpochSecond()) | ||||
|         val expired = isLastUpdateCheckExpired(oneHourEarlier.toEpochSecond()) | ||||
|  | ||||
|         assertTrue(expired) | ||||
|     } | ||||
| @@ -31,7 +25,7 @@ class NewVersionManagerTest { | ||||
|     fun `Expiry is not reached`() { | ||||
|         val oneHourLater = Instant.now().atZone(ZoneId.of("GMT")).plusHours(1) | ||||
|  | ||||
|         val expired = manager.isExpired(oneHourLater.toEpochSecond()) | ||||
|         val expired = isLastUpdateCheckExpired(oneHourLater.toEpochSecond()) | ||||
|  | ||||
|         assertFalse(expired) | ||||
|     } | ||||
| @@ -47,7 +41,7 @@ class NewVersionManagerTest { | ||||
|     fun `Expiry must be returned as is because it is inside the acceptable range of 6-72 hours`() { | ||||
|         val sixHoursLater = Instant.now().atZone(ZoneId.of("GMT")).plusHours(6) | ||||
|  | ||||
|         val coerced = manager.coerceExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(sixHoursLater)) | ||||
|         val coerced = coerceUpdateCheckExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(sixHoursLater)) | ||||
|  | ||||
|         assertNearlyEqual(sixHoursLater.toEpochSecond(), coerced) | ||||
|     } | ||||
| @@ -56,7 +50,7 @@ class NewVersionManagerTest { | ||||
|     fun `Expiry must be increased to 6 hours if below`() { | ||||
|         val tooLow = Instant.now().atZone(ZoneId.of("GMT")).plusHours(5) | ||||
|  | ||||
|         val coerced = manager.coerceExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(tooLow)) | ||||
|         val coerced = coerceUpdateCheckExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(tooLow)) | ||||
|  | ||||
|         assertNearlyEqual(tooLow.plusHours(1).toEpochSecond(), coerced) | ||||
|     } | ||||
| @@ -65,7 +59,7 @@ class NewVersionManagerTest { | ||||
|     fun `Expiry must be decreased to 72 hours if above`() { | ||||
|         val tooHigh = Instant.now().atZone(ZoneId.of("GMT")).plusHours(73) | ||||
|  | ||||
|         val coerced = manager.coerceExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(tooHigh)) | ||||
|         val coerced = coerceUpdateCheckExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(tooHigh)) | ||||
|  | ||||
|         assertNearlyEqual(tooHigh.minusHours(1).toEpochSecond(), coerced) | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 litetex
					litetex