diff --git a/app/build.gradle b/app/build.gradle
index d78b7e73087..35cdde5f457 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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.4.0'
/** Third-party libraries **/
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 28cdbf02038..f9c99819c4a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -381,9 +381,6 @@
-
diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
deleted file mode 100644
index 122660d6431..00000000000
--- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
+++ /dev/null
@@ -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 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.
- *
- * Following conditions need to be met, before data is request from the server:
- *
- * - The app is signed with the correct signing key (by TeamNewPipe / schabi).
- * If the signing key differs from the one used upstream, the update cannot be installed.
- * - The user enabled searching for and notifying about updates in the settings.
- * - The app did not recently check for updates.
- * We do not want to make unnecessary connections and DOS our servers.
- *
- * Must not be executed when the app is in background.
- */
- public static void startNewVersionCheckService() {
- final Intent intent = new Intent(App.getApp().getApplicationContext(),
- CheckForNewAppVersion.class);
- 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);
- }
-
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index 95663ea0af1..b208d84436d 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -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 @@ protected void onPostCreate(final Bundle savedInstanceState) {
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);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt b/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt
deleted file mode 100644
index 36de1ecfcbe..00000000000
--- a/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt
+++ /dev/null
@@ -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()
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
new file mode 100644
index 00000000000..060114974fd
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
@@ -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.
+ *
+ * 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)
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java
index d7fb559d632..3776d78f679 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java
@@ -7,10 +7,9 @@
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 void onCreatePreferences(final Bundle savedInstanceState, final String ro
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)));
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
index 7510bb3bcde..7078514dd98 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
@@ -23,8 +23,6 @@
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.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 @@ private void initSearch(
*/
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);
diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java
index 04bad38152a..1043e88c2e8 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java
@@ -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 @@ private void checkNewVersionNow() {
// 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
diff --git a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
new file mode 100644
index 00000000000..21a9059e289
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
@@ -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 = 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()
+ }
+}
diff --git a/app/src/test/java/org/schabi/newpipe/NewVersionManagerTest.kt b/app/src/test/java/org/schabi/newpipe/NewVersionManagerTest.kt
index d2dacc7835d..7a2d965f739 100644
--- a/app/src/test/java/org/schabi/newpipe/NewVersionManagerTest.kt
+++ b/app/src/test/java/org/schabi/newpipe/NewVersionManagerTest.kt
@@ -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)
}