diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..42fa51d7dd588995368fbd0de96e3e5a1af851bf --- /dev/null +++ b/Android.bp @@ -0,0 +1,34 @@ +android_app { + name: "Updater", + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + resource_dirs: ["res"], + + static_libs: [ + "com.google.android.material_material", + "androidx.core_core", + "androidx.core_core-ktx", + "androidx.appcompat_appcompat", + "androidx.cardview_cardview", + "androidx.preference_preference", + "androidx.recyclerview_recyclerview", + ], + + platform_apis: true, + privileged: true, + certificate: "platform", + optimize: { + proguard_flags_files: ["proguard.flags"], + }, + + required: ["privapp_whitelist_org.lineageos.updater.xml"], +} + +prebuilt_etc { + name: "privapp_whitelist_org.lineageos.updater.xml", + src: "privapp_whitelist_org.lineageos.updater.xml", + sub_dir: "permissions", +} diff --git a/Android.mk b/Android.mk index 12b6fae6d78f9ce4025c12759eea4f3a706fb340..c8896687ae448faf3e7bd790bbb1db6b58ba238f 100644 --- a/Android.mk +++ b/Android.mk @@ -1,43 +1,5 @@ LOCAL_PATH:= $(call my-dir) -include $(CLEAR_VARS) - -LOCAL_MODULE_TAGS := optional - -LOCAL_SRC_FILES := $(call all-java-files-under, src) - -LOCAL_USE_AAPT2 := true - -LOCAL_STATIC_ANDROID_LIBRARIES := \ - com.google.android.material_material \ - androidx.core_core \ - androidx.appcompat_appcompat \ - androidx.cardview_cardview \ - androidx.preference_preference \ - androidx.recyclerview_recyclerview \ - -LOCAL_RESOURCE_DIR := \ - $(LOCAL_PATH)/res - -LOCAL_PACKAGE_NAME := Updater -LOCAL_PRIVATE_PLATFORM_APIS := true -LOCAL_PRIVILEGED_MODULE := true -LOCAL_CERTIFICATE := platform -LOCAL_PROGUARD_FLAG_FILES := proguard.flags - -LOCAL_REQUIRED_MODULES := privapp_whitelist_org.lineageos.updater.xml - -include $(BUILD_PACKAGE) - -include $(CLEAR_VARS) -LOCAL_MODULE := privapp_whitelist_org.lineageos.updater.xml -LOCAL_MODULE_CLASS := ETC -LOCAL_MODULE_TAGS := optional -LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/permissions -LOCAL_SRC_FILES := $(LOCAL_MODULE) -include $(BUILD_PREBUILT) - - include $(CLEAR_VARS) LOCAL_MODULE := UpdaterStudio LOCAL_MODULE_CLASS := FAKE diff --git a/build.gradle b/build.gradle index c3a61f8f918c9f35ae4ec827a583843a06dd9d70..04864013ba58aed869f0b4f8d17d3bfc3059ff0d 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.1.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10" } } @@ -19,6 +20,7 @@ def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) apply plugin: 'com.android.application' +apply plugin: 'org.jetbrains.kotlin.android' android { compileSdkVersion 30 @@ -58,9 +60,10 @@ android { dependencies { compileOnly fileTree(dir: 'system_libs/', include: ['*.jar']) - implementation 'com.google.android.material:material:1.4.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'com.google.android.material:material:1.4.0' } diff --git a/src/org/lineageos/updater/UpdatesCheckReceiver.java b/src/org/lineageos/updater/UpdatesCheckReceiver.java index 11745c9a22e3acef7bd9aae48c957d41bb0aff9f..ab6162a7699f6848ea9409e456311dc4de2a7a95 100644 --- a/src/org/lineageos/updater/UpdatesCheckReceiver.java +++ b/src/org/lineageos/updater/UpdatesCheckReceiver.java @@ -32,6 +32,7 @@ import androidx.core.app.NotificationCompat; import androidx.preference.PreferenceManager; import org.json.JSONException; +import org.lineageos.updater.controller.UpdaterService; import org.lineageos.updater.download.DownloadClient; import org.lineageos.updater.misc.Constants; import org.lineageos.updater.misc.JsonValidator; @@ -54,6 +55,10 @@ public class UpdatesCheckReceiver extends BroadcastReceiver { @Override public void onReceive(final Context context, Intent intent) { + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = preferences.edit(); + if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { // Check if the current value is empty or null and set anon hash. String eLicenseID = Settings.Secure.getString(context.getContentResolver(), @@ -67,6 +72,10 @@ public class UpdatesCheckReceiver extends BroadcastReceiver { } Utils.cleanupDownloadsDir(context); + + // Reset resume or update check failed on reboot + editor.putBoolean(Constants.AUTO_UPDATE_CHECK_FAILED, false).apply(); + editor.putString(Constants.RESUME_DOWNLOAD_ID, "").apply(); } final File json = Utils.getCachedUpdateList(context); @@ -74,9 +83,6 @@ public class UpdatesCheckReceiver extends BroadcastReceiver { Log.i(TAG, "Removing cached json file due validation failure"); } - final SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(context); - if (!Utils.isUpdateCheckEnabled(context)) { return; } @@ -87,9 +93,14 @@ public class UpdatesCheckReceiver extends BroadcastReceiver { } if (!Utils.isNetworkAvailable(context)) { - Log.d(TAG, "Network not available, scheduling new check"); - scheduleUpdatesCheck(context); + if (!UpdaterService.isNetworkCallBackActive()) { + editor.putBoolean(Constants.AUTO_UPDATE_CHECK_FAILED, true).apply(); + UpdaterService.setupNetworkCallback(true); + } return; + } else if (UpdaterService.isNetworkCallBackActive()) { + editor.putBoolean(Constants.AUTO_UPDATE_CHECK_FAILED, false).apply(); + UpdaterService.setupNetworkCallback(false); } final File jsonNew = new File(json.getAbsolutePath() + UUID.randomUUID()); @@ -120,9 +131,7 @@ public class UpdatesCheckReceiver extends BroadcastReceiver { //noinspection ResultOfMethodCallIgnored jsonNew.renameTo(json); long currentMillis = System.currentTimeMillis(); - preferences.edit() - .putLong(Constants.PREF_LAST_UPDATE_CHECK, currentMillis) - .apply(); + editor.putLong(Constants.PREF_LAST_UPDATE_CHECK, currentMillis).apply(); // In case we set a one-shot check because of a previous failure cancelUpdatesCheck(context); } catch (IOException | JSONException e) { diff --git a/src/org/lineageos/updater/controller/UpdaterController.java b/src/org/lineageos/updater/controller/UpdaterController.java index 5d7d51a7182ea00d82086fb04542d5cbfb1c1944..307f6ffa4ef1009d102bf18fd877a202998e236c 100644 --- a/src/org/lineageos/updater/controller/UpdaterController.java +++ b/src/org/lineageos/updater/controller/UpdaterController.java @@ -66,7 +66,7 @@ public class UpdaterController { private int mActiveDownloads = 0; private final Set mVerifyingUpdates = new HashSet<>(); - protected static synchronized UpdaterController getInstance(Context context) { + public static synchronized UpdaterController getInstance(Context context) { if (sUpdaterController == null) { sUpdaterController = new UpdaterController(context); } diff --git a/src/org/lineageos/updater/controller/UpdaterService.java b/src/org/lineageos/updater/controller/UpdaterService.java index ba4baafdadd0529fdf43635fd657d9ecc9f182a1..5aca766f0d294ee705e16e8764aa5766aec32b35 100644 --- a/src/org/lineageos/updater/controller/UpdaterService.java +++ b/src/org/lineageos/updater/controller/UpdaterService.java @@ -25,6 +25,9 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; @@ -39,6 +42,7 @@ import androidx.preference.PreferenceManager; import org.lineageos.updater.R; import org.lineageos.updater.UpdaterReceiver; import org.lineageos.updater.UpdatesActivity; +import org.lineageos.updater.misc.ConnectionStateMonitor; import org.lineageos.updater.misc.Constants; import org.lineageos.updater.misc.StringGenerator; import org.lineageos.updater.misc.Utils; @@ -71,6 +75,7 @@ public class UpdaterService extends Service { private static final int NOTIFICATION_ID = 10; private final IBinder mBinder = new LocalBinder(); + private static boolean isNetworkCallBackActive = false; private boolean mHasClients; private BroadcastReceiver mBroadcastReceiver; @@ -80,11 +85,21 @@ public class UpdaterService extends Service { private UpdaterController mUpdaterController; + private static final NetworkRequest networkRequest = new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(); + private static ConnectivityManager.NetworkCallback mConnectionStateMonitor; + private static ConnectivityManager mConnectivityManager; + @Override public void onCreate() { super.onCreate(); mUpdaterController = UpdaterController.getInstance(this); + mConnectionStateMonitor = new ConnectionStateMonitor().getInstance(this); + mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); mNotificationManager = getSystemService(NotificationManager.class); NotificationChannel notificationChannel = new NotificationChannel( @@ -246,7 +261,8 @@ public class UpdaterService extends Service { private void tryStopSelf() { if (!mHasClients && !mUpdaterController.hasActiveDownloads() && - !mUpdaterController.isInstallingUpdate() && !areNotificationsActive()) { + !mUpdaterController.isInstallingUpdate() && !isNetworkCallBackActive() + && !areNotificationsActive()) { Log.d(TAG, "Service no longer needed, stopping"); stopSelf(); } @@ -268,6 +284,8 @@ public class UpdaterService extends Service { } private void handleUpdateStatusChange(UpdateInfo update) { + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = pref.edit(); switch (update.getStatus()) { case DELETED: { stopForeground(STOP_FOREGROUND_DETACH); @@ -299,6 +317,10 @@ public class UpdaterService extends Service { mNotificationBuilder.addAction(android.R.drawable.ic_media_pause, getString(R.string.pause_button), getPausePendingIntent(update.getDownloadId())); + if (isNetworkCallBackActive) { + editor.putString(Constants.RESUME_DOWNLOAD_ID, "").apply(); + setupNetworkCallback(false); + } mNotificationBuilder.setTicker(text); mNotificationBuilder.setOngoing(true); mNotificationBuilder.setAutoCancel(false); @@ -317,6 +339,10 @@ public class UpdaterService extends Service { mNotificationBuilder.addAction(android.R.drawable.ic_media_play, getString(R.string.resume_button), getResumePendingIntent(update.getDownloadId())); + if (isNetworkCallBackActive) { + editor.putString(Constants.RESUME_DOWNLOAD_ID, "").apply(); + setupNetworkCallback(false); + } mNotificationBuilder.setTicker(text); mNotificationBuilder.setOngoing(false); mNotificationBuilder.setAutoCancel(false); @@ -341,6 +367,10 @@ public class UpdaterService extends Service { mNotificationBuilder.setOngoing(false); mNotificationBuilder.setAutoCancel(false); mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); + if (!Utils.isNetworkAvailable(this)) { + editor.putString(Constants.RESUME_DOWNLOAD_ID, update.getDownloadId()).apply(); + setupNetworkCallback(true); + } tryStopSelf(); break; } @@ -455,7 +485,6 @@ public class UpdaterService extends Service { mNotificationBuilder.setAutoCancel(true); mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); boolean deleteUpdate = pref.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, true); if (deleteUpdate) { mUpdaterController.deleteUpdate(update.getDownloadId()); @@ -505,6 +534,8 @@ public class UpdaterService extends Service { } } + public static boolean isNetworkCallBackActive() { return isNetworkCallBackActive; } + private void handleDownloadProgressChange(UpdateInfo update) { int progress = update.getProgress(); mNotificationBuilder.setProgress(100, progress, false); @@ -545,6 +576,25 @@ public class UpdaterService extends Service { mNotificationBuilder.setContentTitle(buildInfo); } + public static void setupNetworkCallback(boolean shouldEnable) { + if (mConnectivityManager == null || mConnectionStateMonitor == null) { + Log.e(TAG, "Unable to set network callback"); + return; + } + if (shouldEnable) { + mConnectivityManager.registerNetworkCallback(networkRequest, mConnectionStateMonitor); + } else { + try { + mConnectivityManager.unregisterNetworkCallback(mConnectionStateMonitor); + } catch (IllegalArgumentException | NullPointerException e) { + Log.e(TAG, "Network callback was not registered"); + } + } + + isNetworkCallBackActive = shouldEnable; + Log.d(TAG, "Network callback enabled: " + shouldEnable); + } + private PendingIntent getResumePendingIntent(String downloadId) { final Intent intent = new Intent(this, UpdaterService.class); intent.setAction(ACTION_DOWNLOAD_CONTROL); diff --git a/src/org/lineageos/updater/misc/ConnectionStateMonitor.kt b/src/org/lineageos/updater/misc/ConnectionStateMonitor.kt new file mode 100644 index 0000000000000000000000000000000000000000..edb1b51835791a21231055187856c8fe3693b8b9 --- /dev/null +++ b/src/org/lineageos/updater/misc/ConnectionStateMonitor.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 MURENA SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lineageos.updater.misc + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.net.ConnectivityManager +import android.net.Network +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.preference.PreferenceManager +import org.lineageos.updater.UpdatesCheckReceiver +import org.lineageos.updater.controller.UpdaterController + +class ConnectionStateMonitor { + companion object { + private var instance: ConnectivityManager.NetworkCallback? = null + } + + fun getInstance(context: Context): ConnectivityManager.NetworkCallback { + if (instance == null) { + instance = networkCallback(context) + } + + return instance!! + } + + /** + * API callbacks to determine which status we currently in + * we need the below callbacks: + * - onAvailable: device connected to a network of course + * - onLost: when the connection completely lost + */ + private fun networkCallback(context: Context) = object: ConnectivityManager.NetworkCallback() { + private val tag = "ConnectionStateMonitor" + private val pref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + private val updaterController: UpdaterController = UpdaterController.getInstance(context) + + private fun checkForUpdatesOrResume() { + val downloadId: String = pref.getString(Constants.RESUME_DOWNLOAD_ID, "")!! + if (downloadId.isNotEmpty()) { + Handler(Looper.getMainLooper()).postDelayed({ + updaterController.resumeDownload( + downloadId + ) + }, 2000L) // 2 seconds + } + if (pref.getBoolean(Constants.AUTO_UPDATE_CHECK_FAILED, false)) { + Handler(Looper.getMainLooper()).postDelayed({ + val broadcastIntent = Intent() + broadcastIntent.setClassName(context, UpdatesCheckReceiver::class.java.name) + context.sendBroadcast(broadcastIntent) + }, 10000L) // 10 seconds + } + + } + + override fun onAvailable(network: Network) { + Log.d(tag, "Network available") + checkForUpdatesOrResume() + } + + override fun onLost(network: Network) { + Log.d(tag, "Network not available") + checkForUpdatesOrResume() + } + } +} diff --git a/src/org/lineageos/updater/misc/Constants.java b/src/org/lineageos/updater/misc/Constants.java index 8602f56ae435ff0673c99e0ad03f4d3317d3f2d4..812e70562d56b507734a28c3d8820686612c97cf 100644 --- a/src/org/lineageos/updater/misc/Constants.java +++ b/src/org/lineageos/updater/misc/Constants.java @@ -60,4 +60,6 @@ public final class Constants { public static final String UPDATE_RECOVERY_PROPERTY = "persist.vendor.recovery_update"; public static final String HAS_SEEN_INFO_DIALOG = "has_seen_info_dialog"; + public static final String RESUME_DOWNLOAD_ID = "resume_download_id"; + public static final String AUTO_UPDATE_CHECK_FAILED = "auto_update_check_failed"; }