From 1602befc513fb20ebe63481e4380ebd0ec34af6e Mon Sep 17 00:00:00 2001 From: TacoTheDank Date: Thu, 3 Mar 2022 13:19:06 -0500 Subject: [PATCH 1/5] Move utility methods out of CheckForNewAppVersion --- .../schabi/newpipe/CheckForNewAppVersion.java | 135 +++--------------- .../java/org/schabi/newpipe/MainActivity.java | 3 +- .../settings/MainSettingsFragment.java | 5 +- .../newpipe/settings/SettingsActivity.java | 5 +- .../newpipe/util/ReleaseVersionUtil.java | 96 +++++++++++++ 5 files changed, 121 insertions(+), 123 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.java diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java index 122660d64..ca5862333 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java @@ -1,12 +1,9 @@ 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; @@ -14,29 +11,17 @@ 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 org.schabi.newpipe.util.ReleaseVersionUtil; -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() { @@ -45,122 +30,45 @@ public final class CheckForNewAppVersion extends IntentService { 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) { + private static void compareAppVersionAndShowNotification(final String versionName, + final String apkLocationUrl, + final int versionCode) { if (BuildConfig.VERSION_CODE >= versionCode) { return; } + final App app = App.getApp(); // 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 PendingIntent pendingIntent = PendingIntent.getActivity(app, 0, intent, 0); - final String channelId = application - .getString(R.string.app_update_notification_channel_id); + final String channelId = app.getString(R.string.app_update_notification_channel_id); final NotificationCompat.Builder notificationBuilder - = new NotificationCompat.Builder(application, channelId) + = new NotificationCompat.Builder(app, 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) + .setContentTitle(app.getString(R.string.app_update_notification_content_title)) + .setContentText(app.getString(R.string.app_update_notification_content_text) + " " + versionName); final NotificationManagerCompat notificationManager - = NotificationManagerCompat.from(application); + = NotificationManagerCompat.from(app); 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(); @@ -168,7 +76,7 @@ public final class CheckForNewAppVersion extends IntentService { final NewVersionManager manager = new NewVersionManager(); // Check if the current apk is a github one or not. - if (!isReleaseApk(app)) { + if (!ReleaseVersionUtil.isReleaseApk()) { return; } @@ -181,13 +89,13 @@ public final class CheckForNewAppVersion extends IntentService { // Make a network request to get latest NewPipe data. final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL); - handleResponse(response, manager, prefs, app); + handleResponse(response, manager); } private void handleResponse(@NonNull final Response response, - @NonNull final NewVersionManager manager, - @NonNull final SharedPreferences prefs, - @NonNull final App app) { + @NonNull final NewVersionManager manager) { + final App app = App.getApp(); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); try { // Store a timestamp which needs to be exceeded, // before a new request to the API is made. @@ -209,14 +117,11 @@ public final class CheckForNewAppVersion extends IntentService { .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"); + final String versionName = githubStableObject.getString("version"); + final int versionCode = githubStableObject.getInt("version_code"); + final String apkLocationUrl = githubStableObject.getString("apk"); - compareAppVersionAndShowNotification(app, versionName, + compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode); } catch (final JsonParserException e) { // Most likely something is wrong in data received from NEWPIPE_API_URL. diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 95663ea0a..3f0305b5f 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; @@ -177,7 +176,7 @@ public class MainActivity extends AppCompatActivity { // Start the service which is checking all conditions // and eventually searching for a new version. // The service searching for a new NewPipe version must not be started in background. - startNewVersionCheckService(); + CheckForNewAppVersion.startNewVersionCheckService(); } } 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 d7fb559d6..3776d78f6 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 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))); 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 7510bb3bc..7078514dd 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 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); diff --git a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.java b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.java new file mode 100644 index 000000000..5b3cfea92 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.java @@ -0,0 +1,96 @@ +package org.schabi.newpipe.util; + +import android.content.pm.PackageManager; +import android.content.pm.Signature; + +import androidx.annotation.NonNull; +import androidx.core.content.pm.PackageInfoCompat; + +import org.schabi.newpipe.App; +import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.error.ErrorUtil; +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.util.List; + +public class ReleaseVersionUtil { + // 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"; + + public static boolean isReleaseApk() { + return getCertificateSHA1Fingerprint().equals(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 + */ + @NonNull + private static String getCertificateSHA1Fingerprint() { + final App app = App.getApp(); + final List signatures; + try { + signatures = PackageInfoCompat.getSignatures(app.getPackageManager(), + app.getPackageName()); + } catch (final PackageManager.NameNotFoundException e) { + ErrorUtil.createNotification(app, 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(app, 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(app, 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(); + } +} From 0f175de5996abfacf64b8d8cb08d267b75175822 Mon Sep 17 00:00:00 2001 From: TacoTheDank Date: Thu, 3 Mar 2022 13:21:50 -0500 Subject: [PATCH 2/5] Kotlin-ize ReleaseVersionUtil, merge with NewVersionManager --- .../schabi/newpipe/CheckForNewAppVersion.java | 12 +- .../org/schabi/newpipe/NewVersionManager.kt | 28 ----- .../newpipe/util/ReleaseVersionUtil.java | 96 -------------- .../schabi/newpipe/util/ReleaseVersionUtil.kt | 118 ++++++++++++++++++ .../schabi/newpipe/NewVersionManagerTest.kt | 20 ++- 5 files changed, 130 insertions(+), 144 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/NewVersionManager.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java index ca5862333..21673942b 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java @@ -73,7 +73,6 @@ public final class CheckForNewAppVersion extends IntentService { 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 (!ReleaseVersionUtil.isReleaseApk()) { @@ -83,24 +82,23 @@ public final class CheckForNewAppVersion extends IntentService { // 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)) { + if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) { return; } // Make a network request to get latest NewPipe data. final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL); - handleResponse(response, manager); + handleResponse(response); } - private void handleResponse(@NonNull final Response response, - @NonNull final NewVersionManager manager) { + private void handleResponse(@NonNull final Response response) { final App app = App.getApp(); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(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")); + final long newExpiry = ReleaseVersionUtil + .coerceUpdateCheckExpiry(response.getHeader("expires")); prefs.edit() .putLong(app.getString(R.string.update_expiry_key), newExpiry) .apply(); 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 36de1ecfc..000000000 --- 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/util/ReleaseVersionUtil.java b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.java deleted file mode 100644 index 5b3cfea92..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.schabi.newpipe.util; - -import android.content.pm.PackageManager; -import android.content.pm.Signature; - -import androidx.annotation.NonNull; -import androidx.core.content.pm.PackageInfoCompat; - -import org.schabi.newpipe.App; -import org.schabi.newpipe.error.ErrorInfo; -import org.schabi.newpipe.error.ErrorUtil; -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.util.List; - -public class ReleaseVersionUtil { - // 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"; - - public static boolean isReleaseApk() { - return getCertificateSHA1Fingerprint().equals(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 - */ - @NonNull - private static String getCertificateSHA1Fingerprint() { - final App app = App.getApp(); - final List signatures; - try { - signatures = PackageInfoCompat.getSignatures(app.getPackageManager(), - app.getPackageName()); - } catch (final PackageManager.NameNotFoundException e) { - ErrorUtil.createNotification(app, 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(app, 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(app, 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(); - } -} 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 000000000..4ec2a9614 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt @@ -0,0 +1,118 @@ +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) + ) + } + + @JvmStatic + 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 + */ + @JvmStatic + 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 d2dacc783..7a2d965f7 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) } From 81fef1be19cac1e4f32aa685180ee7d1448e323c Mon Sep 17 00:00:00 2001 From: TacoTheDank Date: Thu, 3 Mar 2022 13:24:12 -0500 Subject: [PATCH 3/5] Migrate CheckForNewAppVersion to JobIntentService --- app/src/main/AndroidManifest.xml | 3 ++- .../schabi/newpipe/CheckForNewAppVersion.java | 18 ++++++++---------- .../java/org/schabi/newpipe/MainActivity.java | 2 +- .../settings/UpdateSettingsFragment.java | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 28cdbf020..2f215a768 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -383,7 +383,8 @@ android:exported="false" /> + android:exported="false" + android:permission="android.permission.BIND_JOB_SERVICE" /> diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java index 21673942b..4ed4d1b1b 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java @@ -1,7 +1,7 @@ package org.schabi.newpipe; -import android.app.IntentService; import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; @@ -9,6 +9,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.app.JobIntentService; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.preference.PreferenceManager; @@ -23,14 +24,12 @@ import org.schabi.newpipe.util.ReleaseVersionUtil; import java.io.IOException; -public final class CheckForNewAppVersion extends IntentService { - public CheckForNewAppVersion() { - super("CheckForNewAppVersion"); - } +public final class CheckForNewAppVersion extends JobIntentService { private static final boolean DEBUG = MainActivity.DEBUG; private static final String TAG = CheckForNewAppVersion.class.getSimpleName(); private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json"; + private static final int JOB_ID = -17000; /** * Method to compare the current and latest available app version. @@ -147,14 +146,13 @@ public final class CheckForNewAppVersion extends IntentService { * * 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); + public static void startNewVersionCheckService(final Context context) { + enqueueWork(context, CheckForNewAppVersion.class, JOB_ID, + new Intent(context, CheckForNewAppVersion.class)); } @Override - protected void onHandleIntent(@Nullable final Intent intent) { + protected void onHandleWork(@Nullable final Intent intent) { try { checkNewVersion(); } catch (final IOException e) { diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 3f0305b5f..6f1bea376 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -176,7 +176,7 @@ public class MainActivity extends AppCompatActivity { // Start the service which is checking all conditions // and eventually searching for a new version. // The service searching for a new NewPipe version must not be started in background. - CheckForNewAppVersion.startNewVersionCheckService(); + CheckForNewAppVersion.startNewVersionCheckService(app); } } 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 04bad3815..46582cb24 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -33,7 +33,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(); + startNewVersionCheckService(getContext()); } @Override From 71f141f3f8236f2f19e558685f4391c982051cb4 Mon Sep 17 00:00:00 2001 From: TacoTheDank Date: Thu, 3 Mar 2022 13:26:57 -0500 Subject: [PATCH 4/5] Migrate CheckForNewAppVersion to Worker (and rename it) --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 4 --- .../java/org/schabi/newpipe/MainActivity.java | 5 ++- ...wAppVersion.java => NewVersionWorker.java} | 34 ++++++++++++------- .../settings/UpdateSettingsFragment.java | 5 ++- 5 files changed, 27 insertions(+), 22 deletions(-) rename app/src/main/java/org/schabi/newpipe/{CheckForNewAppVersion.java => NewVersionWorker.java} (87%) diff --git a/app/build.gradle b/app/build.gradle index d78b7e730..35cdde5f4 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 2f215a768..f9c99819c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -381,10 +381,6 @@ - diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 6f1bea376..b208d8443 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -173,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. - CheckForNewAppVersion.startNewVersionCheckService(app); + NewVersionWorker.enqueueNewVersionCheckingWork(app); } } diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.java similarity index 87% rename from app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java rename to app/src/main/java/org/schabi/newpipe/NewVersionWorker.java index 4ed4d1b1b..00405a899 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java +++ b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.java @@ -8,11 +8,14 @@ import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.JobIntentService; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; 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.JsonObject; import com.grack.nanojson.JsonParser; @@ -24,12 +27,16 @@ import org.schabi.newpipe.util.ReleaseVersionUtil; import java.io.IOException; -public final class CheckForNewAppVersion extends JobIntentService { +public final class NewVersionWorker extends Worker { private static final boolean DEBUG = MainActivity.DEBUG; - private static final String TAG = CheckForNewAppVersion.class.getSimpleName(); + private static final String TAG = NewVersionWorker.class.getSimpleName(); private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json"; - private static final int JOB_ID = -17000; + + public NewVersionWorker(@NonNull final Context context, + @NonNull final WorkerParameters workerParams) { + super(context, workerParams); + } /** * Method to compare the current and latest available app version. @@ -130,7 +137,7 @@ public final class CheckForNewAppVersion extends JobIntentService { } /** - * Start a new service which + * Start a new worker 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 @@ -144,22 +151,25 @@ public final class CheckForNewAppVersion extends JobIntentService { *
  • 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 Context context) { - enqueueWork(context, CheckForNewAppVersion.class, JOB_ID, - new Intent(context, CheckForNewAppVersion.class)); + public static void enqueueNewVersionCheckingWork(final Context context) { + final WorkRequest workRequest = + new OneTimeWorkRequest.Builder(NewVersionWorker.class).build(); + WorkManager.getInstance(context).enqueue(workRequest); } + @NonNull @Override - protected void onHandleWork(@Nullable final Intent intent) { + public Result doWork() { try { checkNewVersion(); } catch (final IOException e) { Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e); + return Result.failure(); } catch (final ReCaptchaException e) { Log.e(TAG, "ReCaptchaException should never happen here.", e); + return Result.failure(); } - + return Result.success(); } } 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 46582cb24..1043e88c2 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 @@ 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(getContext()); + NewVersionWorker.enqueueNewVersionCheckingWork(getContext()); } @Override From b8b97fa6d42e644b7294af4e5b9bf2e1dc96b341 Mon Sep 17 00:00:00 2001 From: TacoTheDank Date: Thu, 3 Mar 2022 13:34:35 -0500 Subject: [PATCH 5/5] Convert NewVersionWorker to Kotlin --- .../org/schabi/newpipe/NewVersionWorker.java | 175 ------------------ .../org/schabi/newpipe/NewVersionWorker.kt | 163 ++++++++++++++++ .../schabi/newpipe/util/ReleaseVersionUtil.kt | 2 - 3 files changed, 163 insertions(+), 177 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/NewVersionWorker.java create mode 100644 app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt diff --git a/app/src/main/java/org/schabi/newpipe/NewVersionWorker.java b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.java deleted file mode 100644 index 00405a899..000000000 --- a/app/src/main/java/org/schabi/newpipe/NewVersionWorker.java +++ /dev/null @@ -1,175 +0,0 @@ -package org.schabi.newpipe; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -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.JsonObject; -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; - -import java.io.IOException; - -public final class NewVersionWorker extends Worker { - - private static final boolean DEBUG = MainActivity.DEBUG; - private static final String TAG = NewVersionWorker.class.getSimpleName(); - private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json"; - - public NewVersionWorker(@NonNull final Context context, - @NonNull final WorkerParameters workerParams) { - super(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 static void compareAppVersionAndShowNotification(final String versionName, - final String apkLocationUrl, - final int versionCode) { - if (BuildConfig.VERSION_CODE >= versionCode) { - return; - } - - final App app = App.getApp(); - // 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(app, 0, intent, 0); - - final String channelId = app.getString(R.string.app_update_notification_channel_id); - final NotificationCompat.Builder notificationBuilder - = new 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); - - final NotificationManagerCompat notificationManager - = NotificationManagerCompat.from(app); - notificationManager.notify(2000, notificationBuilder.build()); - } - - private void checkNewVersion() throws IOException, ReCaptchaException { - final App app = App.getApp(); - - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); - - // Check if the current apk is a github one or not. - if (!ReleaseVersionUtil.isReleaseApk()) { - 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 (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) { - return; - } - - // Make a network request to get latest NewPipe data. - final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL); - handleResponse(response); - } - - private void handleResponse(@NonNull final Response response) { - final App app = App.getApp(); - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); - try { - // Store a timestamp which needs to be exceeded, - // before a new request to the API is made. - final long newExpiry = ReleaseVersionUtil - .coerceUpdateCheckExpiry(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(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 worker 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.
    • - *
    - */ - public static void enqueueNewVersionCheckingWork(final Context context) { - final WorkRequest workRequest = - new OneTimeWorkRequest.Builder(NewVersionWorker.class).build(); - WorkManager.getInstance(context).enqueue(workRequest); - } - - @NonNull - @Override - public Result doWork() { - try { - checkNewVersion(); - } catch (final IOException e) { - Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e); - return Result.failure(); - } catch (final ReCaptchaException e) { - Log.e(TAG, "ReCaptchaException should never happen here.", e); - return Result.failure(); - } - return Result.success(); - } -} 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 000000000..060114974 --- /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/util/ReleaseVersionUtil.kt b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt index 4ec2a9614..21a9059e2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt +++ b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt @@ -94,7 +94,6 @@ object ReleaseVersionUtil { ) } - @JvmStatic fun isLastUpdateCheckExpired(expiry: Long): Boolean { return Instant.ofEpochSecond(expiry).isBefore(Instant.now()) } @@ -104,7 +103,6 @@ object ReleaseVersionUtil { * * @return Epoch second of expiry date time */ - @JvmStatic fun coerceUpdateCheckExpiry(expiryString: String?): Long { val now = ZonedDateTime.now() return expiryString?.let {