diff --git a/.gitignore b/.gitignore index 0361113a9e3b292daecf4ce79eb9a9a85b70706c..5fab5acc6e4be1eb1d3fa7bdb50dc63042e4160b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,7 @@ .DS_Store /build /captures -/system_libs/*.jar /keystore.properties .externalNativeBuild .cxx -*.jks local.properties diff --git a/app/Android.bp b/app/Android.bp index 4f79f13217074fa00374ab24df4386901ec0148c..14cce603550e611d1ed17d6963d80ff66d259da4 100644 --- a/app/Android.bp +++ b/app/Android.bp @@ -10,8 +10,8 @@ android_app { // Include SettingsLib and its dependencies defaults: ["SettingsLibDefaults"], - srcs: ["src/main/java/**/*.java"], - resource_dirs: ["src/main/res"], + srcs: ["src/main/java/**/*.java", "src/main/java/**/*.kt"], + resource_dirs: ["src/main/res", "src/main/res_aosp"], manifest: "src/main/AndroidManifest.xml", platform_apis: true, @@ -33,6 +33,7 @@ android_app { "androidx.preference_preference", "androidx.recyclerview_recyclerview", "com.google.android.material_material", + "elib", ], optimize: { diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b339d044838ba507bde7b507e529f3ca72ea2ee0..7faf37fa60b406d1b512e8e1279b13b535e34d9a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,15 +14,15 @@ apply { buildscript { repositories { - maven("https://raw.githubusercontent.com/lineage-next/gradle-generatebp/v1.2/.m2") + maven("https://raw.githubusercontent.com/lineage-next/gradle-generatebp/main/.m2") } dependencies { - classpath("org.lineageos:gradle-generatebp:+") + classpath("org.lineageos:gradle-generatebp:1.23") } } -val keystorePropertiesFile = rootProject.file("keystore.properties") +val keystorePropertiesFile = rootProject.file("platform.keystore") val keystoreProperties = Properties().apply { if (keystorePropertiesFile.exists()) { load(keystorePropertiesFile.inputStream()) @@ -30,12 +30,18 @@ val keystoreProperties = Properties().apply { } android { - compileSdk = 33 + compileSdk = 35 + + sourceSets { + getByName("main") { + res.srcDirs("src/main/res", "src/main/res_studio") + } + } defaultConfig { applicationId = "org.lineageos.updater" - minSdk = 32 - targetSdk = 33 + minSdk = 35 + targetSdk = 35 versionCode = 1 versionName = "1.0" } @@ -51,22 +57,25 @@ android { ) } getByName("debug") { - // Append .dev to package name so we won't conflict with AOSP build. - applicationIdSuffix = ".dev" + signingConfig = signingConfigs.getByName("debug") } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } kotlinOptions { - jvmTarget = "11" + jvmTarget = "21" + } + + buildFeatures { + buildConfig = true } signingConfigs { - create("release") { + getByName("debug") { (keystoreProperties["keyAlias"] as String?)?.let { keyAlias = it } @@ -87,14 +96,16 @@ android { dependencies { compileOnly(fileTree(mapOf("dir" to "../system_libs", "include" to listOf("*.jar")))) - implementation("androidx.core:core-ktx:1.9.0") - implementation("androidx.appcompat:appcompat:1.6.1") + implementation("androidx.core:core-ktx:1.16.0") + implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.cardview:cardview:1.0.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0") implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0") - implementation("androidx.preference:preference:1.2.0") - implementation("androidx.recyclerview:recyclerview:1.2.1") - implementation("com.google.android.material:material:1.9.0-alpha01") + implementation("androidx.preference:preference-ktx:1.2.1") + implementation("androidx.recyclerview:recyclerview:1.4.0") + implementation("com.google.android.material:material:1.12.0") + + implementation("foundation.e:elib:0.0.1-alpha11") } configure { diff --git a/app/privapp_whitelist_org.lineageos.updater.xml b/app/privapp_whitelist_org.lineageos.updater.xml index d88ad9cb1bdc20bdbf3a9a4f82c89b00f50dba89..6361985ba8bf6185be934610761d68c8f8941860 100644 --- a/app/privapp_whitelist_org.lineageos.updater.xml +++ b/app/privapp_whitelist_org.lineageos.updater.xml @@ -16,9 +16,11 @@ --> + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5ecd007e30602aab1fa661ec33a1b58fcd6ffc01..dd6c47962d38a10db9b8b4d4a9ffe8ce995a8be5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,8 +11,11 @@ + + + "10"; + case Build.VERSION_CODES.R -> "11"; + case Build.VERSION_CODES.S, + Build.VERSION_CODES.S_V2 -> "12"; + case Build.VERSION_CODES.TIRAMISU -> "13"; + case Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> "14"; + case Build.VERSION_CODES.VANILLA_ICE_CREAM -> "15"; + default -> Build.VERSION.RELEASE; + }; + } + + private String getFromMetadata(File file, String key) { try { final String metadataContent = readZippedFile(file, METADATA_PATH); final String[] lines = metadataContent.split("\n"); for (String line : lines) { - if (!line.startsWith(METADATA_TIMESTAMP_KEY)) { + if (!line.startsWith(key)) { continue; } - final String timeStampStr = line.replace(METADATA_TIMESTAMP_KEY, ""); - return Long.parseLong(timeStampStr); + return line.replace(key, ""); } } catch (IOException e) { Log.e(TAG, "Failed to read date from local update zip package", e); } catch (NumberFormatException e) { - Log.e(TAG, "Failed to parse timestamp number from zip metadata file", e); + Log.e(TAG, "Failed to parse from zip metadata file", e); } - Log.e(TAG, "Couldn't find timestamp in zip file, falling back to $now"); - return System.currentTimeMillis(); + Log.e(TAG, "Couldn't find metadata in zip file, falling back to 0"); + return null; } private String readZippedFile(File file, String path) throws IOException { diff --git a/app/src/main/java/org/lineageos/updater/UpdaterReceiver.java b/app/src/main/java/org/lineageos/updater/UpdaterReceiver.java index 1fb2c5a04fec93f6938e5c000b246c8dcfc3403d..c8857db00b933d74ddf6c8a301b18cef2005e974 100644 --- a/app/src/main/java/org/lineageos/updater/UpdaterReceiver.java +++ b/app/src/main/java/org/lineageos/updater/UpdaterReceiver.java @@ -31,6 +31,7 @@ import androidx.preference.PreferenceManager; import org.lineageos.updater.misc.BuildInfoUtils; import org.lineageos.updater.misc.Constants; import org.lineageos.updater.misc.StringGenerator; +import org.lineageos.updater.misc.Utils; import java.text.DateFormat; @@ -60,7 +61,7 @@ public class UpdaterReceiver extends BroadcastReceiver { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); String buildDate = StringGenerator.getDateLocalizedUTC(context, DateFormat.MEDIUM, preferences.getLong(Constants.PREF_INSTALL_NEW_TIMESTAMP, 0)); - String buildInfo = context.getString(R.string.list_build_version_date, + String buildInfo = context.getString(R.string.e_list_build_version_date, BuildInfoUtils.getBuildVersion(), buildDate); Intent notificationIntent = new Intent(context, UpdatesActivity.class); @@ -87,11 +88,13 @@ public class UpdaterReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (ACTION_INSTALL_REBOOT.equals(intent.getAction())) { + Utils.removeInstalledUpdate(context); PowerManager pm = context.getSystemService(PowerManager.class); pm.reboot(null); } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); pref.edit().remove(Constants.PREF_NEEDS_REBOOT_ID).apply(); + Utils.removeInstalledUpdate(context); if (shouldShowUpdateFailedNotification(context)) { pref.edit().putBoolean(Constants.PREF_INSTALL_NOTIFIED, true).apply(); diff --git a/app/src/main/java/org/lineageos/updater/UpdatesActivity.java b/app/src/main/java/org/lineageos/updater/UpdatesActivity.java index 5fab3f0cc0d5a3f2cbbf12d7e73216e4d7febb47..14171caffc3f451ce6ab36e6504bc7a8542674ae 100644 --- a/app/src/main/java/org/lineageos/updater/UpdatesActivity.java +++ b/app/src/main/java/org/lineageos/updater/UpdatesActivity.java @@ -42,6 +42,7 @@ import android.view.View; import android.view.animation.Animation; import android.view.animation.LinearInterpolator; import android.view.animation.RotateAnimation; +import android.widget.ArrayAdapter; import android.widget.RelativeLayout; import android.widget.Spinner; import android.widget.TextView; @@ -54,6 +55,7 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.SwitchCompat; import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; import androidx.core.view.WindowInsetsCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.preference.PreferenceManager; @@ -79,6 +81,7 @@ import org.lineageos.updater.model.UpdateInfo; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -116,6 +119,10 @@ public class UpdatesActivity extends UpdatesListActivity implements UpdateImport super.onCreate(savedInstanceState); setContentView(R.layout.activity_updates); + if (UpdaterService.isDeviceRebooted()) { + Utils.removeInstalledUpdate(this); + } + mUpdateImporter = new UpdateImporter(this, this); UiModeManager uiModeManager = getSystemService(UiModeManager.class); @@ -184,8 +191,8 @@ public class UpdatesActivity extends UpdatesListActivity implements UpdateImport } TextView headerTitle = findViewById(R.id.header_title); - headerTitle.setText(getString(R.string.header_title_text, - BuildInfoUtils.getBuildVersion())); + headerTitle.setText(getString(R.string.e_header_title_text, + BuildInfoUtils.getDisplayVersion())); updateLastCheckedString(); @@ -401,10 +408,12 @@ public class UpdatesActivity extends UpdatesListActivity implements UpdateImport List sortedUpdates = controller.getUpdates(); if (sortedUpdates.isEmpty()) { findViewById(R.id.no_new_updates_view).setVisibility(View.VISIBLE); - findViewById(R.id.recycler_view).setVisibility(View.GONE); + findViewById(R.id.content).setVisibility(View.GONE); + findViewById(R.id.available_update_header).setVisibility(View.GONE); } else { findViewById(R.id.no_new_updates_view).setVisibility(View.GONE); - findViewById(R.id.recycler_view).setVisibility(View.VISIBLE); + findViewById(R.id.content).setVisibility(View.VISIBLE); + findViewById(R.id.available_update_header).setVisibility(View.VISIBLE); sortedUpdates.sort((u1, u2) -> Long.compare(u2.getTimestamp(), u1.getTimestamp())); for (UpdateInfo update : sortedUpdates) { updateIds.add(update.getDownloadId()); @@ -436,7 +445,7 @@ public class UpdatesActivity extends UpdatesListActivity implements UpdateImport preferences.edit().putLong(Constants.PREF_LAST_UPDATE_CHECK, millis).apply(); updateLastCheckedString(); if (json.exists() && Utils.isUpdateCheckEnabled(this) && - Utils.checkForNewUpdates(json, jsonNew)) { + Utils.checkForNewUpdates(jsonNew, this)) { UpdatesCheckReceiver.updateRepeatingUpdatesCheck(this); } // In case we set a one-shot check because of a previous failure @@ -550,7 +559,8 @@ public class UpdatesActivity extends UpdatesListActivity implements UpdateImport @Override public void showSnackbar(int stringId, int duration) { - Snackbar.make(findViewById(R.id.main_container), stringId, duration).show(); + Snackbar.make(findViewById(R.id.main_container), stringId, duration).setTextColor( + ContextCompat.getColor(this, foundation.e.elib.R.color.e_background)).show(); } private void refreshAnimationStart() { @@ -564,7 +574,8 @@ public class UpdatesActivity extends UpdatesListActivity implements UpdateImport mRefreshIconView.setEnabled(false); } } else { - findViewById(R.id.recycler_view).setVisibility(View.GONE); + findViewById(R.id.content).setVisibility(View.GONE); + findViewById(R.id.available_update_header).setVisibility(View.GONE); findViewById(R.id.no_new_updates_view).setVisibility(View.GONE); findViewById(R.id.refresh_progress).setVisibility(View.VISIBLE); } @@ -578,11 +589,13 @@ public class UpdatesActivity extends UpdatesListActivity implements UpdateImport } } else { findViewById(R.id.refresh_progress).setVisibility(View.GONE); - if (mAdapter.getItemCount() > 0) { - findViewById(R.id.recycler_view).setVisibility(View.VISIBLE); - } else { - findViewById(R.id.no_new_updates_view).setVisibility(View.VISIBLE); - } + } + + if (mAdapter.getItemCount() > 0) { + findViewById(R.id.content).setVisibility(View.VISIBLE); + findViewById(R.id.available_update_header).setVisibility(View.VISIBLE); + } else { + findViewById(R.id.no_new_updates_view).setVisibility(View.VISIBLE); } } @@ -591,6 +604,7 @@ public class UpdatesActivity extends UpdatesListActivity implements UpdateImport View view = LayoutInflater.from(this).inflate(R.layout.preferences_dialog, null); Spinner autoCheckInterval = view.findViewById(R.id.preferences_auto_updates_check_interval); SwitchCompat autoDelete = view.findViewById(R.id.preferences_auto_delete_updates); + SwitchCompat allUpdates = view.findViewById(R.id.preferences_all_updates); SwitchCompat meteredNetworkWarning = view.findViewById( R.id.preferences_metered_network_warning); SwitchCompat abPerfMode = view.findViewById(R.id.preferences_ab_perf_mode); @@ -601,11 +615,31 @@ public class UpdatesActivity extends UpdatesListActivity implements UpdateImport } SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + List intervalList = new ArrayList<>(Arrays.asList(getResources().getStringArray( + R.array.menu_auto_updates_check_interval_entries))); + + if (Utils.isDevModeOn(this)) { + // Add additional intervals while enabling developer options is on + intervalList.addAll(Arrays.asList(getResources().getStringArray( + R.array.menu_auto_updates_check_interval_entries_dev))); + } else if (Utils.getUpdateCheckSetting(this) > 3) { + prefs.edit().putInt(Constants.PREF_AUTO_UPDATES_CHECK_INTERVAL, + Constants.AUTO_UPDATES_CHECK_INTERVAL_DAILY).apply(); + } + + ArrayAdapter adapter = new ArrayAdapter<>(this, + android.R.layout.simple_list_item_1, intervalList); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + autoCheckInterval.setAdapter(adapter); autoCheckInterval.setSelection(Utils.getUpdateCheckSetting(this)); - autoDelete.setChecked(prefs.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, false)); + + autoDelete.setChecked(prefs.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, true)); + allUpdates.setChecked(prefs.getBoolean(Constants.PREF_ALL_UPDATES, false)); meteredNetworkWarning.setChecked(prefs.getBoolean(Constants.PREF_METERED_NETWORK_WARNING, prefs.getBoolean(Constants.PREF_MOBILE_DATA_WARNING, true))); - abPerfMode.setChecked(prefs.getBoolean(Constants.PREF_AB_PERF_MODE, false)); + abPerfMode.setChecked(prefs.getBoolean(Constants.PREF_AB_PERF_MODE, + Utils.defaultABPerfMode(this))); if (getResources().getBoolean(R.bool.config_hideRecoveryUpdate)) { // Hide the update feature if explicitly requested. @@ -643,6 +677,7 @@ public class UpdatesActivity extends UpdatesListActivity implements UpdateImport .putInt(Constants.PREF_AUTO_UPDATES_CHECK_INTERVAL, autoCheckInterval.getSelectedItemPosition()) .putBoolean(Constants.PREF_AUTO_DELETE_UPDATES, autoDelete.isChecked()) + .putBoolean(Constants.PREF_ALL_UPDATES, allUpdates.isChecked()) .putBoolean(Constants.PREF_METERED_NETWORK_WARNING, meteredNetworkWarning.isChecked()) .putBoolean(Constants.PREF_AB_PERF_MODE, abPerfMode.isChecked()) @@ -670,7 +705,7 @@ public class UpdatesActivity extends UpdatesListActivity implements UpdateImport private void maybeShowWelcomeMessage() { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean alreadySeen = preferences.getBoolean(Constants.HAS_SEEN_WELCOME_MESSAGE, false); + boolean alreadySeen = preferences.getBoolean(Constants.HAS_SEEN_WELCOME_MESSAGE, true); if (alreadySeen) { return; } diff --git a/app/src/main/java/org/lineageos/updater/UpdatesCheckReceiver.java b/app/src/main/java/org/lineageos/updater/UpdatesCheckReceiver.java index 9f454239338717f4a93dedfd3e7354745ce13f53..3b91d3207483628214c32b5199dc3c2521b67124 100644 --- a/app/src/main/java/org/lineageos/updater/UpdatesCheckReceiver.java +++ b/app/src/main/java/org/lineageos/updater/UpdatesCheckReceiver.java @@ -24,14 +24,17 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.SystemClock; +import android.provider.Settings; import android.util.Log; 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; import org.lineageos.updater.misc.Utils; import java.io.File; @@ -51,29 +54,51 @@ 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 anonHash = Settings.Secure.getString(context.getContentResolver(), + Constants.OTA_ANON_HASH); + if (anonHash == null || anonHash.isEmpty()) { + Settings.Secure.putString(context.getContentResolver(), + Constants.OTA_ANON_HASH, Utils.generateRandomID()); + } + Utils.cleanupDownloadsDir(context); + Utils.removeLocalUpdate(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 SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(context); + final File json = Utils.getCachedUpdateList(context); + if (json.exists() && !JsonValidator.validateJsonFile(json) && json.delete()) { + Log.i(TAG, "Removing cached json file due validation failure"); + } if (!Utils.isUpdateCheckEnabled(context)) { return; } - if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { - // Set a repeating alarm on boot to check for new updates once per day - scheduleRepeatingUpdatesCheck(context); - } + // Exact alarms does not support repeating, so update to make it + // work like repeating alarms. To check for update at the exact time. + updateRepeatingUpdatesCheck(context); 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 json = Utils.getCachedUpdateList(context); final File jsonNew = new File(json.getAbsolutePath() + UUID.randomUUID()); String url = Utils.getServerURL(context); DownloadClient.DownloadCallback callback = new DownloadClient.DownloadCallback() { @@ -90,16 +115,19 @@ public class UpdatesCheckReceiver extends BroadcastReceiver { @Override public void onSuccess() { try { - if (json.exists() && Utils.checkForNewUpdates(json, jsonNew)) { + if (!JsonValidator.validateJsonFile(jsonNew)) { + Log.i(TAG, "Could not parse list, scheduling new check"); + scheduleUpdatesCheck(context); + return; + } + if (json.exists() && Utils.checkForNewUpdates(jsonNew, context)) { showNotification(context); updateRepeatingUpdatesCheck(context); } //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) { @@ -160,12 +188,10 @@ public class UpdatesCheckReceiver extends BroadcastReceiver { PendingIntent updateCheckIntent = getRepeatingUpdatesCheckIntent(context); AlarmManager alarmMgr = context.getSystemService(AlarmManager.class); - alarmMgr.setRepeating(AlarmManager.RTC, System.currentTimeMillis() + - Utils.getUpdateCheckInterval(context), Utils.getUpdateCheckInterval(context), - updateCheckIntent); + long nextCheck = System.currentTimeMillis() + Utils.getUpdateCheckInterval(context); + alarmMgr.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextCheck, updateCheckIntent); - Date nextCheckDate = new Date(System.currentTimeMillis() + - Utils.getUpdateCheckInterval(context)); + Date nextCheckDate = new Date(nextCheck); Log.d(TAG, "Setting automatic updates check: " + nextCheckDate); } @@ -181,14 +207,14 @@ public class UpdatesCheckReceiver extends BroadcastReceiver { } public static void scheduleUpdatesCheck(Context context) { - long millisToNextCheck = AlarmManager.INTERVAL_HOUR * 2; + long millisToNextCheck = AlarmManager.INTERVAL_FIFTEEN_MINUTES; PendingIntent updateCheckIntent = getUpdatesCheckIntent(context); AlarmManager alarmMgr = context.getSystemService(AlarmManager.class); - alarmMgr.set(AlarmManager.ELAPSED_REALTIME, - SystemClock.elapsedRealtime() + millisToNextCheck, - updateCheckIntent); + long nextCheck = SystemClock.elapsedRealtime() + millisToNextCheck; + alarmMgr.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME, + nextCheck, updateCheckIntent); - Date nextCheckDate = new Date(System.currentTimeMillis() + millisToNextCheck); + Date nextCheckDate = new Date(nextCheck); Log.d(TAG, "Setting one-shot updates check: " + nextCheckDate); } diff --git a/app/src/main/java/org/lineageos/updater/UpdatesDbHelper.java b/app/src/main/java/org/lineageos/updater/UpdatesDbHelper.java index 6c11a60c40289968ac5935cefbbc3f75708fdacf..bf78b829f5009b85ef26692c85389fdaf154e088 100644 --- a/app/src/main/java/org/lineageos/updater/UpdatesDbHelper.java +++ b/app/src/main/java/org/lineageos/updater/UpdatesDbHelper.java @@ -30,7 +30,7 @@ import java.util.List; public class UpdatesDbHelper extends SQLiteOpenHelper { - public static final int DATABASE_VERSION = 1; + public static final int DATABASE_VERSION = 3; public static final String DATABASE_NAME = "updates.db"; public static class UpdateEntry implements BaseColumns { @@ -41,7 +41,9 @@ public class UpdatesDbHelper extends SQLiteOpenHelper { public static final String COLUMN_NAME_TIMESTAMP = "timestamp"; public static final String COLUMN_NAME_TYPE = "type"; public static final String COLUMN_NAME_VERSION = "version"; + public static final String COLUMN_NAME_DISPLAY_VERSION = "display_version"; public static final String COLUMN_NAME_SIZE = "size"; + public static final String COLUMN_NAME_ANDROID_VERSION = "android_version"; } private static final String SQL_CREATE_ENTRIES = @@ -53,7 +55,9 @@ public class UpdatesDbHelper extends SQLiteOpenHelper { UpdateEntry.COLUMN_NAME_TIMESTAMP + " INTEGER," + UpdateEntry.COLUMN_NAME_TYPE + " TEXT," + UpdateEntry.COLUMN_NAME_VERSION + " TEXT," + - UpdateEntry.COLUMN_NAME_SIZE + " INTEGER)"; + UpdateEntry.COLUMN_NAME_DISPLAY_VERSION + " TEXT," + + UpdateEntry.COLUMN_NAME_SIZE + " INTEGER," + + UpdateEntry.COLUMN_NAME_ANDROID_VERSION + " TEXT)"; private static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + UpdateEntry.TABLE_NAME; @@ -89,7 +93,9 @@ public class UpdatesDbHelper extends SQLiteOpenHelper { values.put(UpdateEntry.COLUMN_NAME_TIMESTAMP, update.getTimestamp()); values.put(UpdateEntry.COLUMN_NAME_TYPE, update.getType()); values.put(UpdateEntry.COLUMN_NAME_VERSION, update.getVersion()); + values.put(UpdateEntry.COLUMN_NAME_DISPLAY_VERSION, update.getDisplayVersion()); values.put(UpdateEntry.COLUMN_NAME_SIZE, update.getFileSize()); + values.put(UpdateEntry.COLUMN_NAME_ANDROID_VERSION, update.getAndroidVersion()); } public void removeUpdate(String downloadId) { @@ -125,8 +131,10 @@ public class UpdatesDbHelper extends SQLiteOpenHelper { UpdateEntry.COLUMN_NAME_TIMESTAMP, UpdateEntry.COLUMN_NAME_TYPE, UpdateEntry.COLUMN_NAME_VERSION, + UpdateEntry.COLUMN_NAME_DISPLAY_VERSION, UpdateEntry.COLUMN_NAME_STATUS, UpdateEntry.COLUMN_NAME_SIZE, + UpdateEntry.COLUMN_NAME_ANDROID_VERSION, }; String sort = UpdateEntry.COLUMN_NAME_TIMESTAMP + " DESC"; Cursor cursor = db.query(UpdateEntry.TABLE_NAME, projection, selection, selectionArgs, @@ -146,10 +154,14 @@ public class UpdatesDbHelper extends SQLiteOpenHelper { update.setType(cursor.getString(index)); index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_VERSION); update.setVersion(cursor.getString(index)); + index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_DISPLAY_VERSION); + update.setDisplayVersion(cursor.getString(index)); index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_STATUS); update.setPersistentStatus(cursor.getInt(index)); index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_SIZE); update.setFileSize(cursor.getLong(index)); + index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_ANDROID_VERSION); + update.setAndroidVersion(cursor.getString(index)); updates.add(update); } cursor.close(); diff --git a/app/src/main/java/org/lineageos/updater/UpdatesListAdapter.java b/app/src/main/java/org/lineageos/updater/UpdatesListAdapter.java index 72045cac10f807f6b0f50c0d0fda974b9c04b71e..6fb9e4af987d3494fa370d2577979f040628b52c 100644 --- a/app/src/main/java/org/lineageos/updater/UpdatesListAdapter.java +++ b/app/src/main/java/org/lineageos/updater/UpdatesListAdapter.java @@ -16,10 +16,9 @@ package org.lineageos.updater; import android.content.Intent; -import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Resources; -import android.os.BatteryManager; +import android.os.Build; import android.os.PowerManager; import android.text.SpannableString; import android.text.format.Formatter; @@ -57,10 +56,12 @@ import org.lineageos.updater.misc.Utils; import org.lineageos.updater.model.UpdateInfo; import org.lineageos.updater.model.UpdateStatus; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.text.DateFormat; +import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; @@ -70,10 +71,6 @@ public class UpdatesListAdapter extends RecyclerView.Adapter mDownloadIds; @@ -98,6 +95,7 @@ public class UpdatesListAdapter extends RecyclerView.Adapter 0) { + requiredSpace -= updateFile.length(); + } + + if (availableFreeSpace < requiredSpace) { + // Not enough space to download file + double spaceNeeded = (requiredSpace - availableFreeSpace) / (1024.0 * 1024.0); + // Ignore if needed space is below 1 MB, like 0.25 + // We only show integer part to the user + if (spaceNeeded >= 1) { + String message = resources.getString(R.string.e_dialog_free_space_low_message_pct, + new DecimalFormat("# MB").format(spaceNeeded)); + return new AlertDialog.Builder(mActivity) + .setTitle(R.string.e_dialog_free_space_low_title) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null); + } + } + + return null; + } + private void startDownloadWithWarning(final String downloadId) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mActivity); boolean warn = preferences.getBoolean(Constants.PREF_METERED_NETWORK_WARNING, true); @@ -337,7 +373,8 @@ public class UpdatesListAdapter extends RecyclerView.Adapter { if (checkbox.isChecked()) { preferences.edit() @@ -356,9 +393,18 @@ public class UpdatesListAdapter extends RecyclerView.Adapter startDownloadWithWarning(downloadId) : null; + clickListener = enabled ? view -> { + AlertDialog.Builder freeSpaceDialog = getSpaceDialog( + mUpdaterController.getUpdate(downloadId)); + if (freeSpaceDialog != null) { + freeSpaceDialog.show(); + } else { + startDownloadWithWarning(downloadId); + } + } : null; break; case PAUSE: button.setText(R.string.action_pause); @@ -374,7 +420,12 @@ public class UpdatesListAdapter extends RecyclerView.Adapter { if (canInstall) { - mUpdaterController.resumeDownload(downloadId); + AlertDialog.Builder freeSpaceDialog = getSpaceDialog(update); + if (freeSpaceDialog != null) { + freeSpaceDialog.show(); + } else { + mUpdaterController.resumeDownload(downloadId); + } } else { mActivity.showSnackbar(R.string.snack_update_not_installable, Snackbar.LENGTH_LONG); @@ -390,7 +441,10 @@ public class UpdatesListAdapter extends RecyclerView.Adapter { if (canInstall) { AlertDialog.Builder installDialog = getInstallDialog(downloadId); - if (installDialog != null) { + AlertDialog.Builder freeSpaceDialog = getSpaceDialog(update); + if (freeSpaceDialog != null) { + freeSpaceDialog.show(); + } else if (installDialog != null) { installDialog.show(); } } else { @@ -463,8 +517,8 @@ public class UpdatesListAdapter extends RecyclerView.Adapter { Utils.triggerUpdate(mActivity, downloadId); @@ -522,7 +608,7 @@ public class UpdatesListAdapter extends RecyclerView.Adapter= required; - } - private static boolean isScratchMounted() { try (Stream lines = Files.lines(Path.of("/proc/mounts"))) { return lines.anyMatch(x -> x.split(" ")[1].equals("/mnt/scratch")); diff --git a/app/src/main/java/org/lineageos/updater/controller/ABUpdateInstaller.java b/app/src/main/java/org/lineageos/updater/controller/ABUpdateInstaller.java index d2dcdf1720ab9f36860a28690209b613128434b6..276affa56f92dc6aceb61d08a8079d398ac0faf4 100644 --- a/app/src/main/java/org/lineageos/updater/controller/ABUpdateInstaller.java +++ b/app/src/main/java/org/lineageos/updater/controller/ABUpdateInstaller.java @@ -209,7 +209,7 @@ class ABUpdateInstaller { } boolean enableABPerfMode = PreferenceManager.getDefaultSharedPreferences(mContext) - .getBoolean(Constants.PREF_AB_PERF_MODE, false); + .getBoolean(Constants.PREF_AB_PERF_MODE, Utils.defaultABPerfMode(mContext)); mUpdateEngine.setPerformanceMode(enableABPerfMode); String zipFileUri = "file://" + file.getAbsolutePath(); diff --git a/app/src/main/java/org/lineageos/updater/controller/UpdaterService.java b/app/src/main/java/org/lineageos/updater/controller/UpdaterService.java index 8b8b43f659b66ca98e54de9d16bcaa48486a93e2..129b0cc4d82bd918b1227bdb250bcffcddaabe09 100644 --- a/app/src/main/java/org/lineageos/updater/controller/UpdaterService.java +++ b/app/src/main/java/org/lineageos/updater/controller/UpdaterService.java @@ -15,6 +15,10 @@ */ package org.lineageos.updater.controller; +import static android.os.SystemUpdateManager.STATUS_IN_PROGRESS; +import static android.os.SystemUpdateManager.STATUS_WAITING_DOWNLOAD; +import static android.os.SystemUpdateManager.STATUS_WAITING_REBOOT; + import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; @@ -25,9 +29,13 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.ServiceInfo; +import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; +import android.service.notification.StatusBarNotification; import android.text.format.Formatter; import android.util.Log; @@ -38,6 +46,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; @@ -48,6 +57,7 @@ import org.lineageos.updater.model.UpdateStatus; import java.io.IOException; import java.text.DateFormat; import java.text.NumberFormat; +import java.util.Arrays; public class UpdaterService extends Service { @@ -71,6 +81,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 +91,23 @@ 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; + + private static boolean mDeviceRebooted = true; + @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 +269,12 @@ public class UpdaterService extends Service { return mUpdaterController; } + public static boolean isDeviceRebooted() { + return mDeviceRebooted; + } + private void tryStopSelf() { + if (isNetworkCallBackActive || areNotificationsActive()) return; if (!mHasClients && !mUpdaterController.hasActiveDownloads() && !mUpdaterController.isInstallingUpdate()) { Log.d(TAG, "Service no longer needed, stopping"); @@ -254,9 +282,21 @@ public class UpdaterService extends Service { } } + private boolean areNotificationsActive() { + StatusBarNotification[] notifications = mNotificationManager.getActiveNotifications(); + return notifications != null && Arrays.stream(notifications) + .anyMatch(notification -> notification.getId() == NOTIFICATION_ID || + notification.getPackageName().equals(getPackageName())); + } + private void handleUpdateStatusChange(UpdateInfo update) { + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = pref.edit(); + mNotificationBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT); + mDeviceRebooted = false; switch (update.getStatus()) { case DELETED: { + notifySystemUpdaterService(STATUS_WAITING_DOWNLOAD, update); stopForeground(STOP_FOREGROUND_DETACH); mNotificationBuilder.setOngoing(false); mNotificationManager.cancel(NOTIFICATION_ID); @@ -264,6 +304,7 @@ public class UpdaterService extends Service { break; } case STARTING: { + notifySystemUpdaterService(STATUS_IN_PROGRESS, update); mNotificationBuilder.mActions.clear(); mNotificationBuilder.setProgress(0, 0, true); mNotificationStyle.setSummaryText(null); @@ -280,6 +321,7 @@ public class UpdaterService extends Service { break; } case DOWNLOADING: { + notifySystemUpdaterService(STATUS_IN_PROGRESS, update); String text = getString(R.string.downloading_notification); mNotificationStyle.bigText(text); mNotificationBuilder.setStyle(mNotificationStyle); @@ -287,6 +329,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); @@ -294,6 +340,7 @@ public class UpdaterService extends Service { break; } case PAUSED: { + notifySystemUpdaterService(STATUS_WAITING_DOWNLOAD, update); stopForeground(STOP_FOREGROUND_DETACH); // In case we pause before the first progress update mNotificationBuilder.setProgress(100, update.getProgress(), false); @@ -305,6 +352,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); @@ -313,6 +364,7 @@ public class UpdaterService extends Service { break; } case PAUSED_ERROR: { + notifySystemUpdaterService(STATUS_WAITING_DOWNLOAD, update); stopForeground(STOP_FOREGROUND_DETACH); int progress = update.getProgress(); // In case we pause before the first progress update @@ -329,10 +381,15 @@ 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; } case VERIFYING: { + notifySystemUpdaterService(STATUS_IN_PROGRESS, update); mNotificationBuilder.setProgress(0, 0, true); mNotificationStyle.setSummaryText(null); mNotificationBuilder.setStyle(mNotificationStyle); @@ -345,20 +402,56 @@ public class UpdaterService extends Service { break; } case VERIFIED: { + notifySystemUpdaterService(STATUS_IN_PROGRESS, update); stopForeground(STOP_FOREGROUND_DETACH); mNotificationBuilder.setStyle(null); mNotificationBuilder.setSmallIcon(R.drawable.ic_system_update); mNotificationBuilder.setProgress(0, 0, false); - String text = getString(R.string.download_completed_notification); + String text = getString(R.string.e_download_completed_notification); + + boolean hasRequiredSpace = Utils.availableFreeSpace() > (update.getFileSize() * 2); + + if (!Utils.canInstall(update) || !Utils.isBatteryLevelOk(this) + || !hasRequiredSpace) { + /* Show notification if any of the below condition didn't met. */ + text = getString(R.string.blocked_update_dialog_title) + ". "; + if (!Utils.isBatteryLevelOk(this)) { + text = text + getString(R.string.dialog_battery_low_title); + } else if (!hasRequiredSpace) { + text = text + getString(R.string.e_dialog_free_space_low_title); + } else if (!Utils.canInstall(update)) { + text = text + getString(R.string.verification_failed_notification); + } + } else if (!Utils.isABDevice()) { + /* Add action to reboot and install for Non-A/B devices. */ + mNotificationBuilder.mActions.clear(); + mNotificationBuilder.addAction(R.drawable.ic_system_update, + getString(R.string.e_reboot_install), + getInstallationPendingIntent(update.getDownloadId())); + } + mNotificationBuilder.setContentText(text); mNotificationBuilder.setTicker(text); mNotificationBuilder.setOngoing(false); mNotificationBuilder.setAutoCancel(true); mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); - tryStopSelf(); + + /* Make sure these conditions are met before auto install + - Can install package (Is a newer build or downgrade allowed) + - Battery level (Above 30% if discharging or 20% if charging) + - Free space to install (Double the size of the ota zip) + */ + if (Utils.isABDevice() && Utils.canInstall(update) + && Utils.isBatteryLevelOk(this) && hasRequiredSpace) { + Utils.triggerUpdate(this, update.getDownloadId()); + } else { + tryStopSelf(); + } + break; } case VERIFICATION_FAILED: { + notifySystemUpdaterService(STATUS_WAITING_DOWNLOAD, update); stopForeground(STOP_FOREGROUND_DETACH); mNotificationBuilder.setStyle(null); mNotificationBuilder.setSmallIcon(android.R.drawable.stat_sys_warning); @@ -373,6 +466,7 @@ public class UpdaterService extends Service { break; } case INSTALLING: { + notifySystemUpdaterService(STATUS_IN_PROGRESS, update); mNotificationBuilder.mActions.clear(); mNotificationBuilder.setStyle(mNotificationStyle); mNotificationBuilder.setSmallIcon(R.drawable.ic_system_update); @@ -396,6 +490,7 @@ public class UpdaterService extends Service { break; } case INSTALLED: { + notifySystemUpdaterService(STATUS_WAITING_REBOOT, update); stopForeground(STOP_FOREGROUND_DETACH); mNotificationBuilder.mActions.clear(); mNotificationBuilder.setStyle(null); @@ -407,22 +502,24 @@ public class UpdaterService extends Service { getString(R.string.reboot), getRebootPendingIntent()); mNotificationBuilder.setTicker(text); - mNotificationBuilder.setOngoing(false); - mNotificationBuilder.setAutoCancel(true); + mNotificationBuilder.setOngoing(true); + mNotificationBuilder.setAutoCancel(false); + mNotificationBuilder.setPriority(NotificationCompat.PRIORITY_MAX); mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); - boolean deleteUpdate = pref.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, false); + boolean deleteUpdate = pref.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, true); boolean isLocal = Update.LOCAL_ID.equals(update.getDownloadId()); // Always delete local updates if (deleteUpdate || isLocal) { - mUpdaterController.deleteUpdate(update.getDownloadId()); + pref.edit().putString(Constants.PREF_NEEDS_DELETE_ID, + update.getDownloadId()).apply(); } tryStopSelf(); break; } case INSTALLATION_FAILED: { + notifySystemUpdaterService(STATUS_WAITING_DOWNLOAD, update); stopForeground(STOP_FOREGROUND_DETACH); mNotificationBuilder.setStyle(null); mNotificationBuilder.setSmallIcon(android.R.drawable.stat_sys_warning); @@ -437,11 +534,13 @@ public class UpdaterService extends Service { break; } case INSTALLATION_CANCELLED: { + notifySystemUpdaterService(STATUS_WAITING_DOWNLOAD, update); stopForeground(true); tryStopSelf(); break; } case INSTALLATION_SUSPENDED: { + notifySystemUpdaterService(STATUS_WAITING_DOWNLOAD, update); stopForeground(STOP_FOREGROUND_DETACH); // In case we pause before the first progress update mNotificationBuilder.setProgress(100, update.getProgress(), false); @@ -463,6 +562,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); @@ -497,12 +598,31 @@ public class UpdaterService extends Service { private void setNotificationTitle(UpdateInfo update) { String buildDate = StringGenerator.getDateLocalizedUTC(this, DateFormat.MEDIUM, update.getTimestamp()); - String buildInfo = getString(R.string.list_build_version_date, + String buildInfo = getString(R.string.e_list_build_version_date, update.getVersion(), buildDate); mNotificationStyle.setBigContentTitle(buildInfo); 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); @@ -535,10 +655,22 @@ public class UpdaterService extends Service { PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } + private PendingIntent getInstallationPendingIntent(String downloadId) { + final Intent intent = new Intent(this, UpdaterService.class); + intent.setAction(ACTION_INSTALL_UPDATE); + intent.putExtra(UpdaterService.EXTRA_DOWNLOAD_ID, downloadId); + return PendingIntent.getService(this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } + private PendingIntent getResumeInstallationPendingIntent() { final Intent intent = new Intent(this, UpdaterService.class); intent.setAction(ACTION_INSTALL_RESUME); return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } + + private void notifySystemUpdaterService(int status, UpdateInfo update) { + Utils.updateSystemUpdaterService(this, status, update.getVersion()); + } } diff --git a/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java b/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java index 63067616dd91625788706d4a954cce303a103801..3c494fc9ec8f8dae018d614a32d95fb8d93a0f95 100644 --- a/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java +++ b/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java @@ -16,6 +16,8 @@ package org.lineageos.updater.download; import android.os.SystemClock; +import android.os.SystemProperties; + import android.util.Log; import java.io.File; @@ -32,6 +34,8 @@ import java.util.PriorityQueue; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.lineageos.updater.misc.Constants; + public class HttpURLConnectionClient implements DownloadClient { private final static String TAG = "HttpURLConnectionClient"; @@ -58,6 +62,11 @@ public class HttpURLConnectionClient implements DownloadClient { DownloadClient.DownloadCallback callback, boolean useDuplicateLinks) throws IOException { mClient = (HttpURLConnection) new URL(url).openConnection(); + + String defaultUserAgent = mClient.getRequestProperty("User-Agent"); + String newUserAgent = defaultUserAgent + " eOS v" + SystemProperties.get(Constants.PROP_BUILD_VERSION); + mClient.setRequestProperty("User-Agent", newUserAgent); + mDestination = destination; mProgressListener = progressListener; mCallback = callback; diff --git a/app/src/main/java/org/lineageos/updater/misc/BuildInfoUtils.java b/app/src/main/java/org/lineageos/updater/misc/BuildInfoUtils.java index 4b90a45e545419a51c727fd23581071fba205f54..6a647faf2517460c8cd0084a87e285dfddb68b1d 100644 --- a/app/src/main/java/org/lineageos/updater/misc/BuildInfoUtils.java +++ b/app/src/main/java/org/lineageos/updater/misc/BuildInfoUtils.java @@ -29,4 +29,8 @@ public final class BuildInfoUtils { public static String getBuildVersion() { return SystemProperties.get(Constants.PROP_BUILD_VERSION); } + + public static String getDisplayVersion() { + return SystemProperties.get(Constants.PROP_BUILD_DISPLAY_VERSION); + } } diff --git a/app/src/main/java/org/lineageos/updater/misc/ConnectionStateMonitor.kt b/app/src/main/java/org/lineageos/updater/misc/ConnectionStateMonitor.kt new file mode 100644 index 0000000000000000000000000000000000000000..edb1b51835791a21231055187856c8fe3693b8b9 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/org/lineageos/updater/misc/Constants.java b/app/src/main/java/org/lineageos/updater/misc/Constants.java index dfff5af027413a329ec7890245f45d6f4016ab89..80bae99de8adaef0dbf53717c283b9543c0c0921 100644 --- a/app/src/main/java/org/lineageos/updater/misc/Constants.java +++ b/app/src/main/java/org/lineageos/updater/misc/Constants.java @@ -23,18 +23,29 @@ public final class Constants { public static final String AB_PAYLOAD_BIN_PATH = "payload.bin"; public static final String AB_PAYLOAD_PROPERTIES_PATH = "payload_properties.txt"; + public static final String OTA_ANON_HASH = "ota_anon_hash"; + public static final String RESUME_DOWNLOAD_ID = "resume_download_id"; + public static final String AUTO_UPDATE_CHECK_FAILED = "auto_update_check_failed"; + public static final int AUTO_UPDATES_CHECK_INTERVAL_NEVER = 0; public static final int AUTO_UPDATES_CHECK_INTERVAL_DAILY = 1; public static final int AUTO_UPDATES_CHECK_INTERVAL_WEEKLY = 2; public static final int AUTO_UPDATES_CHECK_INTERVAL_MONTHLY = 3; + public static final int AUTO_UPDATES_CHECK_INTERVAL_5_MINUTES = 4; + public static final int AUTO_UPDATES_CHECK_INTERVAL_10_MINUTES = 5; + public static final int AUTO_UPDATES_CHECK_INTERVAL_30_MINUTES = 6; public static final String PREF_LAST_UPDATE_CHECK = "last_update_check"; public static final String PREF_AUTO_UPDATES_CHECK_INTERVAL = "auto_updates_check_interval"; public static final String PREF_AUTO_DELETE_UPDATES = "auto_delete_updates"; - public static final String PREF_AB_PERF_MODE = "ab_perf_mode"; + public static final String PREF_ALL_UPDATES = "all_updates"; + public static final String PREF_AB_PERF_MODE_OLD = "ab_perf_mode"; + public static final String PREF_AB_PERF_MODE = "e_ab_perf_mode"; public static final String PREF_METERED_NETWORK_WARNING = "pref_metered_network_warning"; public static final String PREF_MOBILE_DATA_WARNING = "pref_mobile_data_warning"; public static final String PREF_NEEDS_REBOOT_ID = "needs_reboot_id"; + public static final String PREF_NEEDS_DELETE_ID = "needs_delete_id"; + public static final String PREF_NETWORK_CALLBACK_ACTIVE = "pref_network_callback_active"; public static final String UNCRYPT_FILE_EXT = ".uncrypt"; @@ -42,12 +53,14 @@ public final class Constants { public static final String PROP_ALLOW_MAJOR_UPGRADES = "lineage.updater.allow_major_upgrades"; public static final String PROP_BUILD_DATE = "ro.build.date.utc"; public static final String PROP_BUILD_VERSION = "ro.lineage.build.version"; + public static final String PROP_BUILD_DISPLAY_VERSION = "ro.lineage.display.version"; public static final String PROP_BUILD_VERSION_INCREMENTAL = "ro.build.version.incremental"; public static final String PROP_DEVICE = "ro.lineage.device"; public static final String PROP_NEXT_DEVICE = "ro.updater.next_device"; public static final String PROP_RELEASE_TYPE = "ro.lineage.releasetype"; public static final String PROP_UPDATER_ALLOW_DOWNGRADING = "lineage.updater.allow_downgrading"; public static final String PROP_UPDATER_URI = "lineage.updater.uri"; + public static final String PROP_VERSION = "ro.lineage.version"; public static final String PREF_INSTALL_OLD_TIMESTAMP = "install_old_timestamp"; public static final String PREF_INSTALL_NEW_TIMESTAMP = "install_new_timestamp"; diff --git a/app/src/main/java/org/lineageos/updater/misc/JsonValidator.java b/app/src/main/java/org/lineageos/updater/misc/JsonValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..a1401c8e37c60de94f67c4e688bc2225088c081e --- /dev/null +++ b/app/src/main/java/org/lineageos/updater/misc/JsonValidator.java @@ -0,0 +1,166 @@ +package org.lineageos.updater.misc; + +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class JsonValidator { + + private static final String TAG = "JsonValidator" ; + private static final String[] jsonMainFields = { + "error", + "id", + "response" + }; + private static final String[] requiredFields = { + "api_level", + "url", + "timestamp", + "md5sum", + "channel", + "filename", + "romtype", + "version", + "display_version", + "android_version", + "id" + }; + private static final String[] optionalFields = { + "changes", + "pre_version" + }; + + public static boolean validateJsonFile(File jsonFile) { + try { + if (!jsonFile.exists()) { + Log.i(TAG, "Unable to locate json file"); + return false; + } + + // Read the JSON data from the file + BufferedReader reader = new BufferedReader(new FileReader(jsonFile)); + StringBuilder jsonString = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + jsonString.append(line); + } + reader.close(); + + List missingRequiredFields = new ArrayList<>(); + JSONObject jsonObject = new JSONObject(jsonString.toString()); + for (String field : jsonMainFields) { + if (!isMainFieldValid(jsonObject, field)) { + missingRequiredFields.add(field); + } + } + + if (!missingRequiredFields.isEmpty()) { + Log.i(TAG, "Missing or invalid required field in response object: " + + missingRequiredFields); + return false; + } + } catch (IOException | JSONException e) { + Log.i(TAG, "Unable to parse the json file:" + e); + return false; + } + + return true; + } + + public static boolean validateResponseObject(JSONObject responseObject) { + List missingRequiredFields = new ArrayList<>(); + List missingOptionalFields = new ArrayList<>(); + + for (String field : requiredFields) { + if (!isObjectFieldValid(responseObject, field)) { + missingRequiredFields.add(field); + } + } + + for (String field : optionalFields) { + if (!isObjectFieldValid(responseObject, field)) { + missingOptionalFields.add(field); + } + } + + String filename = responseObject.optString("filename", ""); + if (!missingRequiredFields.isEmpty()) { + if (!filename.isEmpty()) { + Log.i(TAG, "Missing or invalid required field in response object for " + + filename + ": " + missingRequiredFields); + } + return false; + } else if (!missingOptionalFields.isEmpty() && !filename.isEmpty()) { + Log.i(TAG, "Missing or invalid optional field in response object for " + + filename + ": " + missingOptionalFields); + } + + return true; // All required fields are present and valid + } + + private static boolean isMainFieldValid(JSONObject jsonObject, String field) { + if (!jsonObject.has(field)) { + return false; + } + + String value = jsonObject.optString(field, ""); // Return empty string if field doesn't exist + + switch (field) { + case "id": + case "error": + // Allow null, as it's a valid value + return true; + case "response": + // Check if it's an array with a length >= 0 + return jsonObject.optJSONArray(field) != null + && jsonObject.optJSONArray(field).length() >= 0; + default: + return !value.isEmpty(); + } + } + + private static boolean isObjectFieldValid(JSONObject jsonObject, String field) { + if (!jsonObject.has(field) || jsonObject.isNull(field)) { + return false; + } + + String value = jsonObject.optString(field, ""); // Return empty string if field doesn't exist + if (value.isEmpty()) { + return false; + } + + switch (field) { + case "datetime": + case "timestamp": + case "size": + case "api_level": + return Utils.isInteger(value); + case "is_upgrade_supported": + return "true".equals(value) || "false".equals(value); + case "android_version": + // Check if it's a valid Android version number + return value.matches("^(\\d+)$|^(\\d+\\.\\d+)$|^(\\d+\\.\\d+\\.\\d+)$"); + case "md5sum": + // Check if it's a valid MD5 checksum + return value.matches("^[a-fA-F0-9]{32}$"); + case "filename": + return value.endsWith(".zip"); + case "id": + // Check if it's a valid SHA-256 checksum + return value.matches("^[a-fA-F0-9]{64}$"); + case "url": + // Check if it's a valid URL format + return value.startsWith("http://") || value.startsWith("https://"); + default: + return true; + } + } +} diff --git a/app/src/main/java/org/lineageos/updater/misc/Utils.java b/app/src/main/java/org/lineageos/updater/misc/Utils.java index 518c8e72858c65ee4bc73aa756ad7396071aa00d..021dc1a8b549794ed6afe609dc73a83a5ced6dcc 100644 --- a/app/src/main/java/org/lineageos/updater/misc/Utils.java +++ b/app/src/main/java/org/lineageos/updater/misc/Utils.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2017-2025 The LineageOS Project + * Copyright (C) 2025 eFoundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +16,34 @@ */ package org.lineageos.updater.misc; +import static android.os.SystemUpdateManager.KEY_STATUS; +import static android.os.SystemUpdateManager.KEY_TITLE; +import static android.os.SystemUpdateManager.STATUS_IDLE; +import static android.os.SystemUpdateManager.STATUS_WAITING_DOWNLOAD; + import android.app.AlarmManager; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.database.Cursor; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; +import android.net.Uri; +import android.os.BatteryManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.PersistableBundle; +import android.os.StatFs; import android.os.SystemProperties; +import android.os.SystemUpdateManager; import android.os.storage.StorageManager; +import android.provider.Settings; import android.util.Log; import android.widget.Toast; @@ -36,7 +53,9 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.lineageos.updater.R; +import org.lineageos.updater.UpdateImporter; import org.lineageos.updater.UpdatesDbHelper; +import org.lineageos.updater.controller.UpdaterController; import org.lineageos.updater.controller.UpdaterService; import org.lineageos.updater.model.Update; import org.lineageos.updater.model.UpdateBaseInfo; @@ -46,18 +65,26 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Enumeration; -import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.Set; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.UUID; +import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public class Utils { + private static final int BATTERY_PLUGGED_ANY = BatteryManager.BATTERY_PLUGGED_AC + | BatteryManager.BATTERY_PLUGGED_USB + | BatteryManager.BATTERY_PLUGGED_WIRELESS; + private static final String TAG = "Utils"; + private static final String CONTENT_URI_PATH = "content://custom.setting.Provider.OTA_SERVER/cte"; private Utils() { } @@ -70,6 +97,11 @@ public class Utils { return new File(context.getCacheDir(), "updates.json"); } + public static String generateRandomID() { + String uuid = UUID.randomUUID().toString().replace("-", ""); + return "anon" + uuid; + } + // This should really return an UpdateBaseInfo object, but currently this only // used to initialize UpdateInfo objects private static UpdateInfo parseJsonUpdate(JSONObject object) throws JSONException { @@ -81,14 +113,46 @@ public class Utils { update.setFileSize(object.getLong("size")); update.setDownloadUrl(object.getString("url")); update.setVersion(object.getString("version")); + update.setAndroidVersion(object.getString("android_version")); + if (object.has("pre_version") && !object.getString("pre_version").isEmpty()) { + update.setDisplayVersion(object.getString("version") + "-" + object.getString("pre_version")); + } else { + update.setDisplayVersion(object.getString("version")); + } return update; } + public static int parseAndroidVersion(String versionString) { + // Parse android versions such as 8.1.0. + // Older updates still shows in ota requests. + Pattern pattern = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)"); + Matcher matcher = pattern.matcher(versionString); + return matcher.matches() ? Integer.parseInt(Objects.requireNonNull(matcher.group(1))) + : Float.valueOf(versionString).intValue(); + } + public static boolean isCompatible(UpdateBaseInfo update) { - if (update.getVersion().compareTo(SystemProperties.get(Constants.PROP_BUILD_VERSION)) < 0) { - Log.d(TAG, update.getName() + " is older than current Android version"); - return false; + String updateAndroidVersion = update.getAndroidVersion(); + if (!updateAndroidVersion.isEmpty()) { + final int updateOSVersion = parseAndroidVersion(updateAndroidVersion); + final int deviceOSVersion = parseAndroidVersion(Build.VERSION.RELEASE); + if (deviceOSVersion > updateOSVersion) { + Log.d(TAG, "Update : Skipping " + update.getName() + " since the installed version " + + deviceOSVersion + " is newer than update " + updateOSVersion); + return false; + } } + + int[] updateVersionParts = parseSemVer(update.getVersion()); + int updateMajorVersion = updateVersionParts[0]; + int updateMinorVersion = updateVersionParts[1]; + Log.d(TAG, "Update : Major "+updateMajorVersion +" Minor "+ updateMinorVersion ); + + int[] deviceVersionParts = parseSemVer(SystemProperties.get(Constants.PROP_BUILD_VERSION)); + int deviceMajorVersion = deviceVersionParts[0]; + int deviceMinorVersion = deviceVersionParts[1]; + Log.d(TAG, "Device : Major "+ deviceMajorVersion +" Minor "+ deviceMinorVersion ); + if (!SystemProperties.getBoolean(Constants.PROP_UPDATER_ALLOW_DOWNGRADING, false) && update.getTimestamp() <= SystemProperties.getLong(Constants.PROP_BUILD_DATE, 0)) { Log.d(TAG, update.getName() + " is older than/equal to the current build"); @@ -98,35 +162,46 @@ public class Utils { Log.d(TAG, update.getName() + " has type " + update.getType()); return false; } + if(updateMajorVersion > deviceMajorVersion){ + Log.d(TAG, update.getName() + " is Newer to current Major version"); + return true; + } + if(updateMajorVersion < deviceMajorVersion){ + Log.d(TAG, update.getName() + " is Older to current Major version"); + return false; + } + if(updateMinorVersion < deviceMinorVersion){ + Log.d(TAG, update.getName() + " is Older to current Minor version"); + return false; + } + return true; } - private static boolean compareVersions(String a, String b, boolean allowMajorUpgrades) { - try { - int majorA = Integer.parseInt(a.split("\\.")[0]); - int minorA = Integer.parseInt(a.split("\\.")[1]); + public static int[] parseSemVer(String versionCode) { + String[] versionParts = versionCode.split(Pattern.quote(".")); + int major = Integer.parseInt(versionParts[0]); + int minor = Integer.parseInt(versionParts[1]); + return new int[]{ major, minor }; + } - int majorB = Integer.parseInt(b.split("\\.")[0]); - int minorB = Integer.parseInt(b.split("\\.")[1]); + public static long availableFreeSpace() { + StatFs stats = new StatFs(Environment.getDataDirectory().getAbsolutePath()); + return stats.getAvailableBlocksLong() * stats.getBlockSizeLong(); + } - // Return early and allow if we allow major version upgrades - return (allowMajorUpgrades && majorA > majorB) - || (majorA == majorB && minorA >= minorB); - } catch (ArrayIndexOutOfBoundsException | NumberFormatException e) { - return false; - } + // https://stackoverflow.com/a/28527441 + public static String getFileSize(long size) { + if (size <= 0) + return "0"; + final String[] units = new String[] { "B", "KB", "MB", "GB", "TB" }; + int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); + return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; } public static boolean canInstall(UpdateBaseInfo update) { - boolean allowMajorUpgrades = SystemProperties.getBoolean( - Constants.PROP_ALLOW_MAJOR_UPGRADES, false); - return (SystemProperties.getBoolean(Constants.PROP_UPDATER_ALLOW_DOWNGRADING, false) || - update.getTimestamp() > SystemProperties.getLong(Constants.PROP_BUILD_DATE, 0)) && - compareVersions( - update.getVersion(), - SystemProperties.get(Constants.PROP_BUILD_VERSION), - allowMajorUpgrades); + update.getTimestamp() > SystemProperties.getLong(Constants.PROP_BUILD_DATE, 0)); } public static List parseJson(File file, boolean compatibleOnly) @@ -147,6 +222,12 @@ public class Utils { continue; } try { + boolean isValidated = JsonValidator.validateResponseObject( + updatesList.getJSONObject(i)); + if (!isValidated) { + Log.d(TAG, "Ignoring incompatible update"); + continue; + } UpdateInfo update = parseJsonUpdate(updatesList.getJSONObject(i)); if (!compatibleOnly || isCompatible(update)) { updates.add(update); @@ -166,10 +247,27 @@ public class Utils { String device = SystemProperties.get(Constants.PROP_NEXT_DEVICE, SystemProperties.get(Constants.PROP_DEVICE)); String type = SystemProperties.get(Constants.PROP_RELEASE_TYPE).toLowerCase(Locale.ROOT); + String anonHash = Settings.Secure.getString(context.getContentResolver(), + Constants.OTA_ANON_HASH); String serverUrl = SystemProperties.get(Constants.PROP_UPDATER_URI); + if (retrieveStatus(context) != null && retrieveStatus(context).equals("true") + && isDevModeOn(context)) { + serverUrl = context.getString(R.string.e_ota_staging_server_url); + } + if (serverUrl.trim().isEmpty()) { - serverUrl = context.getString(R.string.updater_server_url); + serverUrl = context.getString(R.string.e_updater_server_url); + } + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean showAllUpdates = preferences.getBoolean(Constants.PREF_ALL_UPDATES, false) + || isDevModeOn(context); + + if (anonHash != null && !anonHash.isEmpty()) { + serverUrl += "?ota_anon_hash=" + anonHash + "&strict=" + !showAllUpdates; + } else { + serverUrl += "?strict=" + !showAllUpdates; } return serverUrl.replace("{device}", device) @@ -177,6 +275,18 @@ public class Utils { .replace("{incr}", incrementalVersion); } + /*get the status from database that ota option is on or off*/ + public static String retrieveStatus(Context context) { + String status = null; + Cursor cursor = context.getContentResolver().query(Uri.parse(CONTENT_URI_PATH), null, "id=?", new String[]{"1"}, "Status"); + if (cursor.moveToFirst()) { + do { + status = cursor.getString(cursor.getColumnIndex("Status")); + } while (cursor.moveToNext()); + } + return status; + } + public static String getUpgradeBlockedURL(Context context) { String device = SystemProperties.get(Constants.PROP_NEXT_DEVICE, SystemProperties.get(Constants.PROP_DEVICE)); @@ -184,9 +294,8 @@ public class Utils { } public static String getChangelogURL(Context context) { - String device = SystemProperties.get(Constants.PROP_NEXT_DEVICE, - SystemProperties.get(Constants.PROP_DEVICE)); - return context.getString(R.string.menu_changelog_url, device); + String buildVersion = SystemProperties.get(Constants.PROP_BUILD_VERSION); + return context.getString(R.string.e_menu_changelog_url, buildVersion); } public static void triggerUpdate(Context context, String downloadId) { @@ -220,26 +329,47 @@ public class Utils { /** * Compares two json formatted updates list files * - * @param oldJson old update list * @param newJson new update list - * @return true if newJson has at least a compatible update not available in oldJson + * @return true if newJson has an update with higher version than the installed system */ - public static boolean checkForNewUpdates(File oldJson, File newJson) + public static boolean checkForNewUpdates(File newJson, Context context) throws IOException, JSONException { - List oldList = parseJson(oldJson, true); List newList = parseJson(newJson, true); - Set oldIds = new HashSet<>(); - for (UpdateInfo update : oldList) { - oldIds.add(update.getDownloadId()); - } - // In case of no new updates, the old list should - // have all (if not more) the updates + int[] deviceVersionParts = parseSemVer(SystemProperties.get(Constants.PROP_BUILD_VERSION)); + int deviceMajorVersion = deviceVersionParts[0]; + int deviceMinorVersion = deviceVersionParts[1]; + int deviceMaintenanceVersion = deviceVersionParts.length > 2 ? deviceVersionParts[2] : 0; + int highestMajorVersion = deviceMajorVersion; + int highestMinorVersion = deviceMinorVersion; + int highestMaintenanceVersion = deviceMaintenanceVersion; + boolean hasUpdate = false; for (UpdateInfo update : newList) { - if (!oldIds.contains(update.getDownloadId())) { - return true; + if (isCompatible(update)) { + Log.d(TAG, "New compatible update available"); + int[] updateVersionParts = parseSemVer(update.getVersion()); + int updateMajorVersion = updateVersionParts[0]; + int updateMinorVersion = updateVersionParts[1]; + int updateMaintenanceVersion = updateVersionParts.length > 2 + ? updateVersionParts[2] : 0; + if (updateMajorVersion * 10000 + updateMinorVersion * 100 + updateMaintenanceVersion + >= highestMajorVersion * 10000 + highestMinorVersion * 100 + + highestMaintenanceVersion) { + highestMajorVersion = updateMajorVersion; + highestMinorVersion = updateMinorVersion; + highestMaintenanceVersion = updateMaintenanceVersion; + } + hasUpdate = true; } } - return false; + String updateVersion = highestMajorVersion + "." + highestMinorVersion + + (highestMaintenanceVersion > 0 ? "." + highestMaintenanceVersion : ""); + if (hasUpdate) { + updateSystemUpdaterService(context, STATUS_WAITING_DOWNLOAD, updateVersion); + return true; + } else { + updateSystemUpdaterService(context, STATUS_IDLE, updateVersion); + return false; + } } /** @@ -284,6 +414,14 @@ public class Utils { } } + public static void removeLocalUpdate(Context context) { + File downloadPath = getDownloadPath(context); + File localUpdate = new File(downloadPath, UpdateImporter.FILE_NAME); + if (localUpdate.exists()) { + Log.d(TAG, "Deleting local update: " + localUpdate.delete()); + } + } + /** * Cleanup the download directory, which is assumed to be a privileged location * the user can't access and that might have stale files. This can happen if @@ -300,7 +438,7 @@ public class Utils { long prevTimestamp = preferences.getLong(Constants.PREF_INSTALL_OLD_TIMESTAMP, 0); String lastUpdatePath = preferences.getString(Constants.PREF_INSTALL_PACKAGE_PATH, null); boolean reinstalling = preferences.getBoolean(Constants.PREF_INSTALL_AGAIN, false); - boolean deleteUpdates = preferences.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, false); + boolean deleteUpdates = preferences.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, true); if ((buildTimestamp != prevTimestamp || reinstalling) && deleteUpdates && lastUpdatePath != null) { File lastUpdate = new File(lastUpdatePath); @@ -344,6 +482,18 @@ public class Utils { preferences.edit().putBoolean(DOWNLOADS_CLEANUP_DONE, true).apply(); } + public static void removeInstalledUpdate(Context context) { + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); + String downloadId = pref.getString(Constants.PREF_NEEDS_DELETE_ID, null); + if (downloadId != null) { + UpdaterController controller = UpdaterController.getInstance(context); + if (controller != null) { + controller.deleteUpdate(downloadId); + } + pref.edit().remove(Constants.PREF_NEEDS_DELETE_ID).apply(); + } + } + public static File appendSequentialNumber(final File file) { String name; String extension; @@ -381,6 +531,18 @@ public class Utils { return isAB; } + public static boolean defaultABPerfMode(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean oldPrefValue = prefs.getBoolean(Constants.PREF_AB_PERF_MODE_OLD, false); + return (context.getResources().getBoolean(R.bool.config_prioritizeUpdateProcess) + || oldPrefValue); + } + + public static boolean isDevModeOn(Context context) { + return Settings.Global.getInt(context.getContentResolver(), + Settings.Global.DEVELOPMENT_SETTINGS_ENABLED , 0) == 1; + } + public static boolean hasTouchscreen(Context context) { return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); } @@ -402,7 +564,45 @@ public class Utils { public static int getUpdateCheckSetting(Context context) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); return preferences.getInt(Constants.PREF_AUTO_UPDATES_CHECK_INTERVAL, - Constants.AUTO_UPDATES_CHECK_INTERVAL_WEEKLY); + Constants.AUTO_UPDATES_CHECK_INTERVAL_DAILY); + } + + public static void updateSystemUpdaterService(Context context, int status, String version) { + final SystemUpdateManager updateManager = context.getSystemService(SystemUpdateManager.class); + + final Bundle oldInfo = updateManager.retrieveSystemUpdateInfo(); + final int oldStatus = oldInfo.getInt(SystemUpdateManager.KEY_STATUS); + + if (status != oldStatus) { + PersistableBundle infoBundle = new PersistableBundle(); + infoBundle.putInt(KEY_STATUS, status); + infoBundle.putString(KEY_TITLE, version); + updateManager.updateSystemUpdateInfo(infoBundle); + } + } + + public static boolean isBatteryLevelOk(Context context) { + Intent intent = context.registerReceiver(null, + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + if (intent == null || !intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false)) { + return true; + } + int percent = Math.round(100.f * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 100) / + intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)); + int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); + int required = (plugged & BATTERY_PLUGGED_ANY) != 0 ? + context.getResources().getInteger(R.integer.battery_ok_percentage_charging) : + context.getResources().getInteger(R.integer.battery_ok_percentage_discharging); + return percent >= required; + } + + public static boolean isInteger(String value) { + try { + Integer.parseInt(value); + return true; + } catch (NumberFormatException e) { + return false; + } } public static boolean isUpdateCheckEnabled(Context context) { @@ -411,6 +611,12 @@ public class Utils { public static long getUpdateCheckInterval(Context context) { switch (Utils.getUpdateCheckSetting(context)) { + case Constants.AUTO_UPDATES_CHECK_INTERVAL_5_MINUTES: + return AlarmManager.INTERVAL_FIFTEEN_MINUTES / 3; + case Constants.AUTO_UPDATES_CHECK_INTERVAL_10_MINUTES: + return AlarmManager.INTERVAL_HALF_HOUR / 3; + case Constants.AUTO_UPDATES_CHECK_INTERVAL_30_MINUTES: + return AlarmManager.INTERVAL_HALF_HOUR; case Constants.AUTO_UPDATES_CHECK_INTERVAL_DAILY: return AlarmManager.INTERVAL_DAY; case Constants.AUTO_UPDATES_CHECK_INTERVAL_WEEKLY: diff --git a/app/src/main/java/org/lineageos/updater/model/UpdateBase.java b/app/src/main/java/org/lineageos/updater/model/UpdateBase.java index 8fcf09c8f962ec56a7787e42ae0f9754325fb521..167e36295c6a0435e77746ade0d0ddd63dfebdf0 100644 --- a/app/src/main/java/org/lineageos/updater/model/UpdateBase.java +++ b/app/src/main/java/org/lineageos/updater/model/UpdateBase.java @@ -23,6 +23,8 @@ public class UpdateBase implements UpdateBaseInfo { private long mTimestamp; private String mType; private String mVersion; + private String mDisplayVersion; + private String mAndroidVersion; private long mFileSize; public UpdateBase() { @@ -35,6 +37,8 @@ public class UpdateBase implements UpdateBaseInfo { mTimestamp = update.getTimestamp(); mType = update.getType(); mVersion = update.getVersion(); + mDisplayVersion = update.getDisplayVersion(); + mAndroidVersion = update.getAndroidVersion(); mFileSize = update.getFileSize(); } @@ -83,6 +87,15 @@ public class UpdateBase implements UpdateBaseInfo { mVersion = version; } + @Override + public String getDisplayVersion() { + return mDisplayVersion; + } + + public void setDisplayVersion(String displayVersion) { + mDisplayVersion = displayVersion; + } + @Override public String getDownloadUrl() { return mDownloadUrl; @@ -100,4 +113,13 @@ public class UpdateBase implements UpdateBaseInfo { public void setFileSize(long fileSize) { mFileSize = fileSize; } + + @Override + public String getAndroidVersion() { + return mAndroidVersion; + } + + public void setAndroidVersion(String androidVersion) { + mAndroidVersion = androidVersion; + } } diff --git a/app/src/main/java/org/lineageos/updater/model/UpdateBaseInfo.java b/app/src/main/java/org/lineageos/updater/model/UpdateBaseInfo.java index 2041582ad0c613431559f54bb4a04fc6d8aae5ba..aea17bd80661f3ce8cd99919c17cad79490d4a80 100644 --- a/app/src/main/java/org/lineageos/updater/model/UpdateBaseInfo.java +++ b/app/src/main/java/org/lineageos/updater/model/UpdateBaseInfo.java @@ -26,7 +26,11 @@ public interface UpdateBaseInfo { String getVersion(); + String getDisplayVersion(); + String getDownloadUrl(); long getFileSize(); + + String getAndroidVersion(); } diff --git a/app/src/main/res/layout-television/activity_updates.xml b/app/src/main/res/layout-television/activity_updates.xml index efabfea41ce8a68136e2d69d0c1c982004b3a79a..df63d6c806cb2da120ddd8a5f9d8e2cbd16a6880 100644 --- a/app/src/main/res/layout-television/activity_updates.xml +++ b/app/src/main/res/layout-television/activity_updates.xml @@ -105,7 +105,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" - android:text="@string/list_no_updates" + android:text="@string/e_list_no_updates" android:textColor="?android:textColorSecondary" /> diff --git a/app/src/main/res/layout/activity_updates.xml b/app/src/main/res/layout/activity_updates.xml index adfe1a27147de46995d227924a449f07be561cfa..1dc5d5e8b8ab94d563baa58a29097e8cd3d047c9 100644 --- a/app/src/main/res/layout/activity_updates.xml +++ b/app/src/main/res/layout/activity_updates.xml @@ -15,7 +15,7 @@ android:fitsSystemWindows="true" android:outlineAmbientShadowColor="@android:color/transparent" android:outlineSpotShadowColor="@android:color/transparent" - android:background="?android:attr/colorPrimary" + android:background="@color/toolbar_collapsed" android:theme="@style/Theme.CollapsingToolbar.Settings"> + + @@ -108,17 +117,37 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" - android:text="@string/list_no_updates" + android:text="@string/e_list_no_updates" android:textColor="?android:textColorSecondary" /> - + android:orientation="vertical" + android:visibility="gone" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + + + + diff --git a/app/src/main/res/layout/preferences_dialog.xml b/app/src/main/res/layout/preferences_dialog.xml index c1b435605c5ca5c3cabe089f1b3524cac3d6f1e6..f0cb89a0fad5b06eb3058177284cdaddf66c19d3 100644 --- a/app/src/main/res/layout/preferences_dialog.xml +++ b/app/src/main/res/layout/preferences_dialog.xml @@ -25,10 +25,17 @@ android:id="@+id/preferences_auto_updates_check_interval" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_weight="1" - android:entries="@array/menu_auto_updates_check_interval_entries" /> + android:layout_weight="1" /> + + + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical"> - - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal" + android:weightSum="1" + android:baselineAligned="false"> - + android:layout_weight="1" + android:orientation="vertical"> - - - - - - - - + android:alpha="0.87" + android:drawablePadding="8dp" + android:maxLines="1" + android:paddingBottom="8sp" + android:textColor="?android:attr/textColorPrimary" + android:textSize="16sp" + tools:text="LineageOS 20" /> - - - - + android:maxLines="1" + android:textSize="14sp" + tools:text="18 June 2023" /> + -