mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-23 08:30:44 +00:00
Merge pull request #7975 from TacoTheDank/updateCheckerRewrite
Migrate app update checker to AndroidX Work
This commit is contained in:
commit
b607a09125
@ -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 **/
|
||||
|
@ -381,9 +381,6 @@
|
||||
<service
|
||||
android:name=".RouterActivity$FetcherService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".CheckForNewAppVersion"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- opting out of sending metrics to Google in Android System WebView -->
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
|
||||
|
@ -1,264 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.IntentService;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.pm.PackageInfoCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
public final class CheckForNewAppVersion extends IntentService {
|
||||
public CheckForNewAppVersion() {
|
||||
super("CheckForNewAppVersion");
|
||||
}
|
||||
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
|
||||
|
||||
// Public key of the certificate that is used in NewPipe release versions
|
||||
private static final String RELEASE_CERT_PUBLIC_KEY_SHA1
|
||||
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json";
|
||||
|
||||
/**
|
||||
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||
*
|
||||
* @param application The application
|
||||
* @return String with the APK's SHA1 fingerprint in hexadecimal
|
||||
*/
|
||||
@NonNull
|
||||
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
|
||||
final List<Signature> signatures;
|
||||
try {
|
||||
signatures = PackageInfoCompat.getSignatures(application.getPackageManager(),
|
||||
application.getPackageName());
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
ErrorUtil.createNotification(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
|
||||
return "";
|
||||
}
|
||||
if (signatures.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final X509Certificate c;
|
||||
try {
|
||||
final byte[] cert = signatures.get(0).toByteArray();
|
||||
final InputStream input = new ByteArrayInputStream(cert);
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
} catch (final CertificateException e) {
|
||||
ErrorUtil.createNotification(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error"));
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
final MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
final byte[] publicKey = md.digest(c.getEncoded());
|
||||
return byte2HexFormatted(publicKey);
|
||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||
ErrorUtil.createNotification(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key"));
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String byte2HexFormatted(final byte[] arr) {
|
||||
final StringBuilder str = new StringBuilder(arr.length * 2);
|
||||
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
String h = Integer.toHexString(arr[i]);
|
||||
final int l = h.length();
|
||||
if (l == 1) {
|
||||
h = "0" + h;
|
||||
}
|
||||
if (l > 2) {
|
||||
h = h.substring(l - 2, l);
|
||||
}
|
||||
str.append(h.toUpperCase());
|
||||
if (i < (arr.length - 1)) {
|
||||
str.append(':');
|
||||
}
|
||||
}
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to compare the current and latest available app version.
|
||||
* If a newer version is available, we show the update notification.
|
||||
*
|
||||
* @param application The application
|
||||
* @param versionName Name of new version
|
||||
* @param apkLocationUrl Url with the new apk
|
||||
* @param versionCode Code of new version
|
||||
*/
|
||||
private static void compareAppVersionAndShowNotification(@NonNull final Application application,
|
||||
final String versionName,
|
||||
final String apkLocationUrl,
|
||||
final int versionCode) {
|
||||
if (BuildConfig.VERSION_CODE >= versionCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
final PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(application, 0, intent, 0);
|
||||
|
||||
final String channelId = application
|
||||
.getString(R.string.app_update_notification_channel_id);
|
||||
final NotificationCompat.Builder notificationBuilder
|
||||
= new NotificationCompat.Builder(application, channelId)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(application
|
||||
.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(application
|
||||
.getString(R.string.app_update_notification_content_text)
|
||||
+ " " + versionName);
|
||||
|
||||
final NotificationManagerCompat notificationManager
|
||||
= NotificationManagerCompat.from(application);
|
||||
notificationManager.notify(2000, notificationBuilder.build());
|
||||
}
|
||||
|
||||
public static boolean isReleaseApk(@NonNull final App app) {
|
||||
return getCertificateSHA1Fingerprint(app).equals(RELEASE_CERT_PUBLIC_KEY_SHA1);
|
||||
}
|
||||
|
||||
private void checkNewVersion() throws IOException, ReCaptchaException {
|
||||
final App app = App.getApp();
|
||||
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
final NewVersionManager manager = new NewVersionManager();
|
||||
|
||||
// Check if the current apk is a github one or not.
|
||||
if (!isReleaseApk(app)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the last request has happened a certain time ago
|
||||
// to reduce the number of API requests.
|
||||
final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
|
||||
if (!manager.isExpired(expiry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
|
||||
handleResponse(response, manager, prefs, app);
|
||||
}
|
||||
|
||||
private void handleResponse(@NonNull final Response response,
|
||||
@NonNull final NewVersionManager manager,
|
||||
@NonNull final SharedPreferences prefs,
|
||||
@NonNull final App app) {
|
||||
try {
|
||||
// Store a timestamp which needs to be exceeded,
|
||||
// before a new request to the API is made.
|
||||
final long newExpiry = manager
|
||||
.coerceExpiry(response.getHeader("expires"));
|
||||
prefs.edit()
|
||||
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
|
||||
.apply();
|
||||
} catch (final Exception e) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not extract and save new expiry date", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable");
|
||||
|
||||
final String versionName = githubStableObject
|
||||
.getString("version");
|
||||
final int versionCode = githubStableObject
|
||||
.getInt("version_code");
|
||||
final String apkLocationUrl = githubStableObject
|
||||
.getString("apk");
|
||||
|
||||
compareAppVersionAndShowNotification(app, versionName,
|
||||
apkLocationUrl, versionCode);
|
||||
} catch (final JsonParserException e) {
|
||||
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
||||
// Do not alarm user and fail silently.
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new service which
|
||||
* checks if all conditions for performing a version check are met,
|
||||
* fetches the API endpoint {@link #NEWPIPE_API_URL} containing info
|
||||
* about the latest NewPipe version
|
||||
* and displays a notification about ana available update.
|
||||
* <br>
|
||||
* Following conditions need to be met, before data is request from the server:
|
||||
* <ul>
|
||||
* <li> The app is signed with the correct signing key (by TeamNewPipe / schabi).
|
||||
* If the signing key differs from the one used upstream, the update cannot be installed.</li>
|
||||
* <li>The user enabled searching for and notifying about updates in the settings.</li>
|
||||
* <li>The app did not recently check for updates.
|
||||
* We do not want to make unnecessary connections and DOS our servers.</li>
|
||||
* </ul>
|
||||
* <b>Must not be executed</b> when the app is in background.
|
||||
*/
|
||||
public static void startNewVersionCheckService() {
|
||||
final Intent intent = new Intent(App.getApp().getApplicationContext(),
|
||||
CheckForNewAppVersion.class);
|
||||
App.getApp().startService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(@Nullable final Intent intent) {
|
||||
try {
|
||||
checkNewVersion();
|
||||
} catch (final IOException e) {
|
||||
Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e);
|
||||
} catch (final ReCaptchaException e) {
|
||||
Log.e(TAG, "ReCaptchaException should never happen here.", e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -20,7 +20,6 @@
|
||||
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
@ -174,10 +173,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
|
||||
// Start the service which is checking all conditions
|
||||
// Start the worker which is checking all conditions
|
||||
// and eventually searching for a new version.
|
||||
// The service searching for a new NewPipe version must not be started in background.
|
||||
startNewVersionCheckService();
|
||||
NewVersionWorker.enqueueNewVersionCheckingWork(app);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,28 +0,0 @@
|
||||
package org.schabi.newpipe
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class NewVersionManager {
|
||||
|
||||
fun isExpired(expiry: Long): Boolean {
|
||||
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce expiry date time in between 6 hours and 72 hours from now
|
||||
*
|
||||
* @return Epoch second of expiry date time
|
||||
*/
|
||||
fun coerceExpiry(expiryString: String?): Long {
|
||||
val now = ZonedDateTime.now()
|
||||
return expiryString?.let {
|
||||
|
||||
var expiry = ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString))
|
||||
expiry = maxOf(expiry, now.plusHours(6))
|
||||
expiry = minOf(expiry, now.plusHours(72))
|
||||
expiry.toEpochSecond()
|
||||
} ?: now.plusHours(6).toEpochSecond()
|
||||
}
|
||||
}
|
163
app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
Normal file
163
app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
Normal file
@ -0,0 +1,163 @@
|
||||
package org.schabi.newpipe
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkRequest
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.grack.nanojson.JsonParser
|
||||
import com.grack.nanojson.JsonParserException
|
||||
import org.schabi.newpipe.extractor.downloader.Response
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
|
||||
import java.io.IOException
|
||||
|
||||
class NewVersionWorker(
|
||||
context: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : Worker(context, workerParams) {
|
||||
|
||||
/**
|
||||
* Method to compare the current and latest available app version.
|
||||
* If a newer version is available, we show the update notification.
|
||||
*
|
||||
* @param versionName Name of new version
|
||||
* @param apkLocationUrl Url with the new apk
|
||||
* @param versionCode Code of new version
|
||||
*/
|
||||
private fun compareAppVersionAndShowNotification(
|
||||
versionName: String,
|
||||
apkLocationUrl: String?,
|
||||
versionCode: Int
|
||||
) {
|
||||
if (BuildConfig.VERSION_CODE >= versionCode) {
|
||||
return
|
||||
}
|
||||
val app = App.getApp()
|
||||
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri())
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
val pendingIntent = PendingIntent.getActivity(app, 0, intent, 0)
|
||||
val channelId = app.getString(R.string.app_update_notification_channel_id)
|
||||
val notificationBuilder = NotificationCompat.Builder(app, channelId)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(app.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(
|
||||
app.getString(R.string.app_update_notification_content_text) +
|
||||
" " + versionName
|
||||
)
|
||||
val notificationManager = NotificationManagerCompat.from(app)
|
||||
notificationManager.notify(2000, notificationBuilder.build())
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ReCaptchaException::class)
|
||||
private fun checkNewVersion() {
|
||||
// Check if the current apk is a github one or not.
|
||||
if (!isReleaseApk()) {
|
||||
return
|
||||
}
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
// Check if the last request has happened a certain time ago
|
||||
// to reduce the number of API requests.
|
||||
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
||||
if (!isLastUpdateCheckExpired(expiry)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
val response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL)
|
||||
handleResponse(response)
|
||||
}
|
||||
|
||||
private fun handleResponse(response: Response) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
try {
|
||||
// Store a timestamp which needs to be exceeded,
|
||||
// before a new request to the API is made.
|
||||
val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
|
||||
prefs.edit {
|
||||
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not extract and save new expiry date", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
val githubStableObject = JsonParser.`object`()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable")
|
||||
|
||||
val versionName = githubStableObject.getString("version")
|
||||
val versionCode = githubStableObject.getInt("version_code")
|
||||
val apkLocationUrl = githubStableObject.getString("apk")
|
||||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
|
||||
} catch (e: JsonParserException) {
|
||||
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
||||
// Do not alarm user and fail silently.
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: invalid json", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun doWork(): Result {
|
||||
try {
|
||||
checkNewVersion()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e)
|
||||
return Result.failure()
|
||||
} catch (e: ReCaptchaException) {
|
||||
Log.e(TAG, "ReCaptchaException should never happen here.", e)
|
||||
return Result.failure()
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEBUG = MainActivity.DEBUG
|
||||
private val TAG = NewVersionWorker::class.java.simpleName
|
||||
private const val NEWPIPE_API_URL = "https://newpipe.net/api/data.json"
|
||||
|
||||
/**
|
||||
* Start a new worker which
|
||||
* checks if all conditions for performing a version check are met,
|
||||
* fetches the API endpoint [.NEWPIPE_API_URL] containing info
|
||||
* about the latest NewPipe version
|
||||
* and displays a notification about ana available update.
|
||||
* <br></br>
|
||||
* Following conditions need to be met, before data is request from the server:
|
||||
*
|
||||
* * The app is signed with the correct signing key (by TeamNewPipe / schabi).
|
||||
* If the signing key differs from the one used upstream, the update cannot be installed.
|
||||
* * The user enabled searching for and notifying about updates in the settings.
|
||||
* * The app did not recently check for updates.
|
||||
* We do not want to make unnecessary connections and DOS our servers.
|
||||
*
|
||||
*/
|
||||
@JvmStatic
|
||||
fun enqueueNewVersionCheckingWork(context: Context) {
|
||||
val workRequest: WorkRequest =
|
||||
OneTimeWorkRequest.Builder(NewVersionWorker::class.java).build()
|
||||
WorkManager.getInstance(context).enqueue(workRequest)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)));
|
||||
|
||||
|
@ -23,8 +23,6 @@ import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.jakewharton.rxbinding4.widget.RxTextView;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.CheckForNewAppVersion;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.SettingsLayoutBinding;
|
||||
@ -37,6 +35,7 @@ import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchResultListen
|
||||
import org.schabi.newpipe.settings.preferencesearch.PreferenceSearcher;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.KeyboardUtil;
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
@ -267,7 +266,7 @@ public class SettingsActivity extends AppCompatActivity implements
|
||||
*/
|
||||
private void ensureSearchRepresentsApplicationState() {
|
||||
// Check if the update settings are available
|
||||
if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) {
|
||||
if (!ReleaseVersionUtil.isReleaseApk()) {
|
||||
SettingsResourceRegistry.getInstance()
|
||||
.getEntryByPreferencesResId(R.xml.update_settings)
|
||||
.setSearchable(false);
|
||||
|
@ -1,12 +1,11 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.NewVersionWorker;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class UpdateSettingsFragment extends BasePreferenceFragment {
|
||||
@ -33,7 +32,7 @@ public class UpdateSettingsFragment extends BasePreferenceFragment {
|
||||
// Reset the expire time. This is necessary to check for an update immediately.
|
||||
defaultPreferences.edit()
|
||||
.putLong(getString(R.string.update_expiry_key), 0).apply();
|
||||
startNewVersionCheckService();
|
||||
NewVersionWorker.enqueueNewVersionCheckingWork(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
116
app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
Normal file
116
app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt
Normal file
@ -0,0 +1,116 @@
|
||||
package org.schabi.newpipe.util
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.Signature
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import org.schabi.newpipe.App
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.cert.CertificateEncodingException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
object ReleaseVersionUtil {
|
||||
// Public key of the certificate that is used in NewPipe release versions
|
||||
private const val RELEASE_CERT_PUBLIC_KEY_SHA1 =
|
||||
"B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"
|
||||
|
||||
@JvmStatic
|
||||
fun isReleaseApk(): Boolean {
|
||||
return certificateSHA1Fingerprint == RELEASE_CERT_PUBLIC_KEY_SHA1
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||
*
|
||||
* @return String with the APK's SHA1 fingerprint in hexadecimal
|
||||
*/
|
||||
private val certificateSHA1Fingerprint: String
|
||||
get() {
|
||||
val app = App.getApp()
|
||||
val signatures: List<Signature> = try {
|
||||
PackageInfoCompat.getSignatures(app.packageManager, app.packageName)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
showRequestError(app, e, "Could not find package info")
|
||||
return ""
|
||||
}
|
||||
if (signatures.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
val x509cert = try {
|
||||
val cert = signatures[0].toByteArray()
|
||||
val input: InputStream = ByteArrayInputStream(cert)
|
||||
val cf = CertificateFactory.getInstance("X509")
|
||||
cf.generateCertificate(input) as X509Certificate
|
||||
} catch (e: CertificateException) {
|
||||
showRequestError(app, e, "Certificate error")
|
||||
return ""
|
||||
}
|
||||
|
||||
return try {
|
||||
val md = MessageDigest.getInstance("SHA1")
|
||||
val publicKey = md.digest(x509cert.encoded)
|
||||
byte2HexFormatted(publicKey)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
showRequestError(app, e, "Could not retrieve SHA1 key")
|
||||
""
|
||||
} catch (e: CertificateEncodingException) {
|
||||
showRequestError(app, e, "Could not retrieve SHA1 key")
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun byte2HexFormatted(arr: ByteArray): String {
|
||||
val str = StringBuilder(arr.size * 2)
|
||||
for (i in arr.indices) {
|
||||
var h = Integer.toHexString(arr[i].toInt())
|
||||
val l = h.length
|
||||
if (l == 1) {
|
||||
h = "0$h"
|
||||
}
|
||||
if (l > 2) {
|
||||
h = h.substring(l - 2, l)
|
||||
}
|
||||
str.append(h.uppercase())
|
||||
if (i < arr.size - 1) {
|
||||
str.append(':')
|
||||
}
|
||||
}
|
||||
return str.toString()
|
||||
}
|
||||
|
||||
private fun showRequestError(app: App, e: Exception, request: String) {
|
||||
createNotification(
|
||||
app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, request)
|
||||
)
|
||||
}
|
||||
|
||||
fun isLastUpdateCheckExpired(expiry: Long): Boolean {
|
||||
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce expiry date time in between 6 hours and 72 hours from now
|
||||
*
|
||||
* @return Epoch second of expiry date time
|
||||
*/
|
||||
fun coerceUpdateCheckExpiry(expiryString: String?): Long {
|
||||
val now = ZonedDateTime.now()
|
||||
return expiryString?.let {
|
||||
var expiry =
|
||||
ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString))
|
||||
expiry = maxOf(expiry, now.plusHours(6))
|
||||
expiry = minOf(expiry, now.plusHours(72))
|
||||
expiry.toEpochSecond()
|
||||
} ?: now.plusHours(6).toEpochSecond()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user