diff --git a/app/Android.bp b/app/Android.bp index 4f79f13217074fa00374ab24df4386901ec0148c..a370fb09daefabcd05103193bba77700557eccc7 100644 --- a/app/Android.bp +++ b/app/Android.bp @@ -10,7 +10,10 @@ android_app { // Include SettingsLib and its dependencies defaults: ["SettingsLibDefaults"], - srcs: ["src/main/java/**/*.java"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], resource_dirs: ["src/main/res"], manifest: "src/main/AndroidManifest.xml", 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 80fd2bcb5f154f8e4d6d72ed0b1e44cec03454b8..6f60113d2e03ac17f29461c32290bb5b97c8360e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,8 +11,11 @@ + + + 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); } 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); sortedUpdates.sort((u1, u2) -> Long.compare(u2.getTimestamp(), u1.getTimestamp())); for (UpdateInfo update : sortedUpdates) { updateIds.add(update.getDownloadId()); @@ -426,7 +428,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 @@ -554,7 +556,7 @@ 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.no_new_updates_view).setVisibility(View.GONE); findViewById(R.id.refresh_progress).setVisibility(View.VISIBLE); } @@ -568,11 +570,12 @@ 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); + } else { + findViewById(R.id.no_new_updates_view).setVisibility(View.VISIBLE); } } @@ -581,6 +584,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); @@ -591,11 +595,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, + getResources().getBoolean(R.bool.config_prioritizeUpdateProcess))); if (getResources().getBoolean(R.bool.config_hideRecoveryUpdate)) { // Hide the update feature if explicitly requested. @@ -633,6 +657,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()) diff --git a/app/src/main/java/org/lineageos/updater/UpdatesCheckReceiver.java b/app/src/main/java/org/lineageos/updater/UpdatesCheckReceiver.java index 9f454239338717f4a93dedfd3e7354745ce13f53..8ad0f4de669f8e5d4618494bae21bf7445fb002d 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,53 @@ 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(), + "e_license_id"); + String anonHash = Settings.Secure.getString(context.getContentResolver(), + Settings.Secure.OTA_ANON_HASH); + if (anonHash == null || anonHash.isEmpty()) { + Settings.Secure.putString(context.getContentResolver(), + Settings.Secure.OTA_ANON_HASH, (eLicenseID != null && + !eLicenseID.isEmpty()) ? eLicenseID : Utils.generateRandomID()); + } + 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 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 +117,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 +190,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 +209,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 30144701df96ebea1a9f818200cea581aa763fff..c65fdf2e0a37d1c0cb4bb79450e62ebc80a06120 100644 --- a/app/src/main/java/org/lineageos/updater/UpdatesListAdapter.java +++ b/app/src/main/java/org/lineageos/updater/UpdatesListAdapter.java @@ -15,13 +15,10 @@ */ package org.lineageos.updater; -import android.app.Activity; import android.content.Intent; -import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Resources; -import android.net.Uri; -import android.os.BatteryManager; +import android.os.Build; import android.os.PowerManager; import android.text.SpannableString; import android.text.format.Formatter; @@ -40,8 +37,6 @@ import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.view.ContextThemeWrapper; @@ -55,17 +50,18 @@ import com.google.android.material.snackbar.Snackbar; import org.lineageos.updater.controller.UpdaterController; import org.lineageos.updater.controller.UpdaterService; -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 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; @@ -75,10 +71,6 @@ public class UpdatesListAdapter extends RecyclerView.Adapter mDownloadIds; @@ -103,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); @@ -342,7 +373,8 @@ public class UpdatesListAdapter extends RecyclerView.Adapter { if (checkbox.isChecked()) { preferences.edit() @@ -361,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); @@ -379,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); @@ -395,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 { @@ -468,8 +517,8 @@ public class UpdatesListAdapter extends RecyclerView.Adapter { - Utils.triggerUpdate(mActivity, downloadId); - maybeShowInfoDialog(); - }) + (dialog, which) -> Utils.triggerUpdate(mActivity, downloadId)) .setNegativeButton(android.R.string.cancel, null); } @@ -525,21 +603,6 @@ public class UpdatesListAdapter extends RecyclerView.Adapter preferences.edit() - .putBoolean(Constants.HAS_SEEN_INFO_DIALOG, true) - .apply()) - .show(); - } - private void startActionMode(final UpdateInfo update, final boolean canDelete, View anchor) { mSelectedDownload = update.getDownloadId(); notifyItemChanged(update.getDownloadId()); @@ -604,21 +667,6 @@ 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/UpdateInstaller.java b/app/src/main/java/org/lineageos/updater/controller/UpdateInstaller.java index b111f9f33275d3810f6bd6a5f890f9d131591823..720d13ec5a5d45c17fb38b927ddbec20ff6f9326 100644 --- a/app/src/main/java/org/lineageos/updater/controller/UpdateInstaller.java +++ b/app/src/main/java/org/lineageos/updater/controller/UpdateInstaller.java @@ -31,8 +31,8 @@ import org.lineageos.updater.model.UpdateStatus; import java.io.File; import java.io.IOException; -import java.nio.file.attribute.PosixFilePermission; import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermission; import java.util.HashSet; import java.util.Set; 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 e1e0c998a1a9eedaeafdc2697f218c240ab93f33..24236f6fef8d0b4685fb8eb03f734935b75be412 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,7 +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.BuildInfoUtils; +import org.lineageos.updater.misc.ConnectionStateMonitor; import org.lineageos.updater.misc.Constants; import org.lineageos.updater.misc.StringGenerator; import org.lineageos.updater.misc.Utils; @@ -46,7 +54,6 @@ import org.lineageos.updater.model.Update; import org.lineageos.updater.model.UpdateInfo; import org.lineageos.updater.model.UpdateStatus; -import java.io.File; import java.io.IOException; import java.text.DateFormat; import java.text.NumberFormat; @@ -73,6 +80,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; @@ -82,11 +90,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( @@ -250,15 +268,34 @@ public class UpdaterService extends Service { private void tryStopSelf() { if (!mHasClients && !mUpdaterController.hasActiveDownloads() && - !mUpdaterController.isInstallingUpdate()) { + !mUpdaterController.isInstallingUpdate() && !isNetworkCallBackActive() + && !areNotificationsActive()) { Log.d(TAG, "Service no longer needed, stopping"); stopSelf(); } } + private boolean areNotificationsActive() { + NotificationManager notificationManager = + (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + StatusBarNotification[] notifications = notificationManager.getActiveNotifications(); + if (notifications != null && notifications.length > 0) { + for (StatusBarNotification notification : notifications) { + if (notification.getId() == NOTIFICATION_ID && + notification.getPackageName().equals(getPackageName())) { + return true; + } + } + } + return false; + } + private void handleUpdateStatusChange(UpdateInfo update) { + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = pref.edit(); switch (update.getStatus()) { case DELETED: { + notifySystemUpdaterService(STATUS_WAITING_DOWNLOAD, update); stopForeground(STOP_FOREGROUND_DETACH); mNotificationBuilder.setOngoing(false); mNotificationManager.cancel(NOTIFICATION_ID); @@ -266,6 +303,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); @@ -282,6 +320,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); @@ -289,6 +328,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); @@ -296,6 +339,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); @@ -307,6 +351,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); @@ -315,6 +363,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 @@ -331,10 +380,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); @@ -347,20 +401,55 @@ 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); @@ -375,6 +464,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); @@ -398,6 +488,7 @@ public class UpdaterService extends Service { break; } case INSTALLED: { + notifySystemUpdaterService(STATUS_WAITING_REBOOT, update); stopForeground(STOP_FOREGROUND_DETACH); mNotificationBuilder.mActions.clear(); mNotificationBuilder.setStyle(null); @@ -413,8 +504,7 @@ 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, 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) { @@ -425,6 +515,7 @@ public class UpdaterService extends Service { break; } case INSTALLATION_FAILED: { + notifySystemUpdaterService(STATUS_WAITING_DOWNLOAD, update); stopForeground(STOP_FOREGROUND_DETACH); mNotificationBuilder.setStyle(null); mNotificationBuilder.setSmallIcon(android.R.drawable.stat_sys_warning); @@ -439,11 +530,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); @@ -465,6 +558,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); @@ -499,12 +594,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); @@ -537,10 +651,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 b9c4b5dc694562cc46c7ef0bd0bee114fc0e43ad..606bef16ccf08c3667eaea8bb5882966977953b3 100644 --- a/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java +++ b/app/src/main/java/org/lineageos/updater/download/HttpURLConnectionClient.java @@ -16,8 +16,11 @@ package org.lineageos.updater.download; import android.os.SystemClock; +import android.os.SystemProperties; import android.util.Log; +import org.lineageos.updater.misc.Constants; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -58,6 +61,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 beb9423f1e7e50d7693314508ab462afb455c33d..b75b4c1bcf4b5eaa75ab037d8aef8267d16af4a8 100644 --- a/app/src/main/java/org/lineageos/updater/misc/Constants.java +++ b/app/src/main/java/org/lineageos/updater/misc/Constants.java @@ -27,26 +27,33 @@ public final class Constants { 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_ALL_UPDATES = "all_updates"; public static final String PREF_AB_PERF_MODE = "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_NETWORK_CALLBACK_ACTIVE = "pref_network_callback_active"; public static final String UNCRYPT_FILE_EXT = ".uncrypt"; public static final String PROP_AB_DEVICE = "ro.build.ab_update"; 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"; @@ -57,5 +64,6 @@ public final class Constants { public static final String UPDATE_RECOVERY_EXEC = "/vendor/bin/install-recovery.sh"; 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"; } 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 b7ba720f5037735a890a07e90cb62990d42e5242..43b276d1a4afc1dc7244f817e3bad0df4e57673a 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-2023 The LineageOS Project + * Copyright (C) 2022 ECORP SAS * * 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; @@ -46,18 +63,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.UUID; +import java.util.regex.Matcher; +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() { } @@ -81,6 +106,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 { @@ -92,16 +122,48 @@ 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)) { + update.getTimestamp() <= SystemProperties.getLong(Constants.PROP_BUILD_DATE, 0)) { Log.d(TAG, update.getName() + " is older than/equal to the current build"); return false; } @@ -109,14 +171,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; } + 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 }; + } + + public static long availableFreeSpace() { + StatFs stats = new StatFs(Environment.getDataDirectory().getAbsolutePath()); + return stats.getAvailableBlocksLong() * stats.getBlockSizeLong(); + } + + // 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) { return (SystemProperties.getBoolean(Constants.PROP_UPDATER_ALLOW_DOWNGRADING, false) || - update.getTimestamp() > SystemProperties.getLong(Constants.PROP_BUILD_DATE, 0)) && - update.getVersion().equalsIgnoreCase( - SystemProperties.get(Constants.PROP_BUILD_VERSION)); + update.getTimestamp() > SystemProperties.getLong(Constants.PROP_BUILD_DATE, 0)); } public static List parseJson(File file, boolean compatibleOnly) @@ -137,6 +231,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); @@ -156,17 +256,48 @@ 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(), + Settings.Secure.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.ota_staging_server_url); + } + if (serverUrl.trim().isEmpty()) { serverUrl = context.getString(R.string.updater_server_url); } + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean allUpdates = preferences.getBoolean(Constants.PREF_ALL_UPDATES, false); + if (Utils.isDevModeOn(context)) { + allUpdates = true; + } + + if (anonHash != null && !anonHash.isEmpty()) { + serverUrl += "?ota_anon_hash=" + anonHash + "&strict=" + !allUpdates; + } else { + serverUrl += "?strict=" + !allUpdates; + } + return serverUrl.replace("{device}", device) .replace("{type}", type) .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)); @@ -174,9 +305,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.menu_changelog_url, buildVersion); } public static void triggerUpdate(Context context, String downloadId) { @@ -210,26 +340,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; + } } /** @@ -290,7 +441,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); @@ -370,6 +521,11 @@ public class Utils { return isAB; } + public static boolean isDevModeOn(Context context) { + return Settings.Secure.getInt(context.getContentResolver(), + Settings.Global.DEVELOPMENT_SETTINGS_ENABLED , 0) == 1; + } + public static boolean hasTouchscreen(Context context) { return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); } @@ -391,7 +547,7 @@ 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 boolean isUpdateCheckEnabled(Context context) { @@ -400,6 +556,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: @@ -414,14 +576,41 @@ public class Utils { return new File(Constants.UPDATE_RECOVERY_EXEC).exists(); } - public static String getDisplayVersion(String version) { - float floatVersion = 0; + public static boolean isBatteryLevelOk(Context context) { + Intent intent = context.registerReceiver(null, + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + if (!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 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 isInteger(String value) { try { - floatVersion = Float.parseFloat(version); - } catch (NumberFormatException ignored) { - // ignore + Integer.parseInt(value); + return true; + } catch (NumberFormatException e) { + return false; } - // Lineage 20 and up should only be integer values (we don't have minor versions anymore) - return (floatVersion >= 20) ? String.valueOf((int)floatVersion) : version; } } 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..677282f4b8c22ef694d4f4083e2e7f8bdee578d6 100644 --- a/app/src/main/res/layout/activity_updates.xml +++ b/app/src/main/res/layout/activity_updates.xml @@ -48,14 +48,24 @@ android:paddingStart="16dp" app:layout_collapseMode="parallax"> + + @@ -108,17 +117,36 @@ 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..bad8f44a5412f030088f80c35b087709917353d8 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" /> + -