Kotlin-ize ReleaseVersionUtil, merge with NewVersionManager
This commit is contained in:
parent
1602befc51
commit
0f175de599
5 changed files with 130 additions and 144 deletions
|
@ -73,7 +73,6 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||||
final App app = App.getApp();
|
final App app = App.getApp();
|
||||||
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||||
final NewVersionManager manager = new NewVersionManager();
|
|
||||||
|
|
||||||
// Check if the current apk is a github one or not.
|
// Check if the current apk is a github one or not.
|
||||||
if (!ReleaseVersionUtil.isReleaseApk()) {
|
if (!ReleaseVersionUtil.isReleaseApk()) {
|
||||||
|
@ -83,24 +82,23 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||||
// Check if the last request has happened a certain time ago
|
// Check if the last request has happened a certain time ago
|
||||||
// to reduce the number of API requests.
|
// to reduce the number of API requests.
|
||||||
final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
|
final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
|
||||||
if (!manager.isExpired(expiry)) {
|
if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a network request to get latest NewPipe data.
|
// Make a network request to get latest NewPipe data.
|
||||||
final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
|
final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
|
||||||
handleResponse(response, manager);
|
handleResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleResponse(@NonNull final Response response,
|
private void handleResponse(@NonNull final Response response) {
|
||||||
@NonNull final NewVersionManager manager) {
|
|
||||||
final App app = App.getApp();
|
final App app = App.getApp();
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||||
try {
|
try {
|
||||||
// Store a timestamp which needs to be exceeded,
|
// Store a timestamp which needs to be exceeded,
|
||||||
// before a new request to the API is made.
|
// before a new request to the API is made.
|
||||||
final long newExpiry = manager
|
final long newExpiry = ReleaseVersionUtil
|
||||||
.coerceExpiry(response.getHeader("expires"));
|
.coerceUpdateCheckExpiry(response.getHeader("expires"));
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
|
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
|
||||||
.apply();
|
.apply();
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<Signature> 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();
|
|
||||||
}
|
|
||||||
}
|
|
118
app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
Normal file
118
app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
Normal file
|
@ -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<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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,9 @@ package org.schabi.newpipe
|
||||||
|
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
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.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
@ -11,18 +12,11 @@ import kotlin.math.abs
|
||||||
|
|
||||||
class NewVersionManagerTest {
|
class NewVersionManagerTest {
|
||||||
|
|
||||||
private lateinit var manager: NewVersionManager
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
manager = NewVersionManager()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Expiry is reached`() {
|
fun `Expiry is reached`() {
|
||||||
val oneHourEarlier = Instant.now().atZone(ZoneId.of("GMT")).minusHours(1)
|
val oneHourEarlier = Instant.now().atZone(ZoneId.of("GMT")).minusHours(1)
|
||||||
|
|
||||||
val expired = manager.isExpired(oneHourEarlier.toEpochSecond())
|
val expired = isLastUpdateCheckExpired(oneHourEarlier.toEpochSecond())
|
||||||
|
|
||||||
assertTrue(expired)
|
assertTrue(expired)
|
||||||
}
|
}
|
||||||
|
@ -31,7 +25,7 @@ class NewVersionManagerTest {
|
||||||
fun `Expiry is not reached`() {
|
fun `Expiry is not reached`() {
|
||||||
val oneHourLater = Instant.now().atZone(ZoneId.of("GMT")).plusHours(1)
|
val oneHourLater = Instant.now().atZone(ZoneId.of("GMT")).plusHours(1)
|
||||||
|
|
||||||
val expired = manager.isExpired(oneHourLater.toEpochSecond())
|
val expired = isLastUpdateCheckExpired(oneHourLater.toEpochSecond())
|
||||||
|
|
||||||
assertFalse(expired)
|
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`() {
|
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 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)
|
assertNearlyEqual(sixHoursLater.toEpochSecond(), coerced)
|
||||||
}
|
}
|
||||||
|
@ -56,7 +50,7 @@ class NewVersionManagerTest {
|
||||||
fun `Expiry must be increased to 6 hours if below`() {
|
fun `Expiry must be increased to 6 hours if below`() {
|
||||||
val tooLow = Instant.now().atZone(ZoneId.of("GMT")).plusHours(5)
|
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)
|
assertNearlyEqual(tooLow.plusHours(1).toEpochSecond(), coerced)
|
||||||
}
|
}
|
||||||
|
@ -65,7 +59,7 @@ class NewVersionManagerTest {
|
||||||
fun `Expiry must be decreased to 72 hours if above`() {
|
fun `Expiry must be decreased to 72 hours if above`() {
|
||||||
val tooHigh = Instant.now().atZone(ZoneId.of("GMT")).plusHours(73)
|
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)
|
assertNearlyEqual(tooHigh.minusHours(1).toEpochSecond(), coerced)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue