Updater: add support for local updates
Allow importing and installation of OTA files already downloaded instead of requiring to reboot to recovery to install them. Squash of: - Add support for importing local updates Signed-off-by: Joey <jbevilacqua@shiftphones.com> Change-Id: I64ca3a6af29bdf8b2c6023a502f23080a27fd79e - OTA: read timestamp from imported zip metadata Signed-off-by: Joey <jbevilacqua@shiftphones.com> Change-Id: I93a5c0be81adab9ba8e50afde0e09839f059c9e0 - OTA: fix UI issues with local update Signed-off-by: Joey <jbevilacqua@shiftphones.com> Change-Id: I07c8f5507bc52c254c3dc1468fea495a073ae96c - OTA: fix local updates not being shown in UI (pt.2) Signed-off-by: Joey <jbevilacqua@shiftphones.com> Change-Id: Ife40eea05099eca9e1ee84c6f87d2715e5981cab - OTA: ignore download status changes for local updates Signed-off-by: Joey <jbevilacqua@shiftphones.com> Change-Id: I198f9b5462718f8a6e5687c891f3bfc6b1c645bd - UpdaterService: fix crash with local install Change-Id: I27b187cf4adec986d516e3017d1b3877691029b2 Signed-off-by: Alexander Martinz <amartinz@shiftphones.com> - Local updates: do not remove local update from ui after installation Change-Id: I869e090f26273006f933ad99c42b7c6a2e963797 Signed-off-by: Alexander Martinz <amartinz@shiftphones.com> - Local updates: modify display version Change-Id: I8a39e0936040bb9546499754ab4a9ef60c56aca0 Signed-off-by: Alexander Martinz <amartinz@shiftphones.com> - Local updates: show build date in import dialog Change-Id: I9014358ea1cf941e76fdd80a5147e9d924fc1a8f Signed-off-by: Alexander Martinz <amartinz@shiftphones.com> Change-Id: I64ca3a6af29bdf8b2c6023a502f23080a27fd79e Signed-off-by: Joey <joey@lineageos.org> Signed-off-by: Alexander Martinz <amartinz@shiftphones.com>
This commit is contained in:
249
app/src/main/java/org/lineageos/updater/UpdateImporter.java
Normal file
249
app/src/main/java/org/lineageos/updater/UpdateImporter.java
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017-2022 The LineageOS Project
|
||||||
|
* Copyright (C) 2020-2022 SHIFT GmbH
|
||||||
|
*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.lineageos.updater.controller.UpdaterController;
|
||||||
|
import org.lineageos.updater.controller.UpdaterService;
|
||||||
|
import org.lineageos.updater.misc.StringGenerator;
|
||||||
|
import org.lineageos.updater.misc.Utils;
|
||||||
|
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.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
public class UpdateImporter {
|
||||||
|
private static final int REQUEST_PICK = 9061;
|
||||||
|
private static final String TAG = "UpdateImporter";
|
||||||
|
private static final String MIME_ZIP = "application/zip";
|
||||||
|
private static final String FILE_NAME = "localUpdate.zip";
|
||||||
|
private static final String METADATA_PATH = "META-INF/com/android/metadata";
|
||||||
|
private static final String METADATA_TIMESTAMP_KEY = "post-timestamp=";
|
||||||
|
|
||||||
|
private final Activity activity;
|
||||||
|
private final Callbacks callbacks;
|
||||||
|
|
||||||
|
private Thread workingThread;
|
||||||
|
|
||||||
|
public UpdateImporter(Activity activity, Callbacks callbacks) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.callbacks = callbacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopImport() {
|
||||||
|
if (workingThread != null && workingThread.isAlive()) {
|
||||||
|
workingThread.interrupt();
|
||||||
|
workingThread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openImportPicker() {
|
||||||
|
final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
.setType(MIME_ZIP);
|
||||||
|
activity.startActivityForResult(intent, REQUEST_PICK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean onResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
if (resultCode != Activity.RESULT_OK || requestCode != REQUEST_PICK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return onPicked(data.getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
|
private boolean onPicked(Uri uri) {
|
||||||
|
callbacks.onImportStarted();
|
||||||
|
|
||||||
|
workingThread = new Thread(() -> {
|
||||||
|
File importedFile = null;
|
||||||
|
try {
|
||||||
|
importedFile = importFile(uri);
|
||||||
|
verifyPackage(importedFile);
|
||||||
|
|
||||||
|
final Update update = buildLocalUpdate(importedFile);
|
||||||
|
addUpdate(update);
|
||||||
|
activity.runOnUiThread(() -> callbacks.onImportCompleted(update));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to import update package", e);
|
||||||
|
// Do not store invalid update
|
||||||
|
if (importedFile != null) {
|
||||||
|
importedFile.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.runOnUiThread(() -> callbacks.onImportCompleted(null));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
workingThread.start();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetWorldReadable")
|
||||||
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
|
private File importFile(Uri uri) throws IOException {
|
||||||
|
final ParcelFileDescriptor parcelDescriptor = activity.getContentResolver()
|
||||||
|
.openFileDescriptor(uri, "r");
|
||||||
|
if (parcelDescriptor == null) {
|
||||||
|
throw new IOException("Failed to obtain fileDescriptor");
|
||||||
|
}
|
||||||
|
|
||||||
|
final FileInputStream iStream = new FileInputStream(parcelDescriptor
|
||||||
|
.getFileDescriptor());
|
||||||
|
final File downloadDir = Utils.getDownloadPath(activity);
|
||||||
|
final File outFile = new File(downloadDir, FILE_NAME);
|
||||||
|
if (outFile.exists()) {
|
||||||
|
outFile.delete();
|
||||||
|
}
|
||||||
|
final FileOutputStream oStream = new FileOutputStream(outFile);
|
||||||
|
|
||||||
|
int read;
|
||||||
|
final byte[] buffer = new byte[4096];
|
||||||
|
while ((read = iStream.read(buffer)) > 0) {
|
||||||
|
oStream.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
oStream.flush();
|
||||||
|
oStream.close();
|
||||||
|
iStream.close();
|
||||||
|
parcelDescriptor.close();
|
||||||
|
|
||||||
|
outFile.setReadable(true, false);
|
||||||
|
|
||||||
|
return outFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Update buildLocalUpdate(File file) {
|
||||||
|
final long timeStamp = getTimeStamp(file);
|
||||||
|
final String buildDate = StringGenerator.getDateLocalizedUTC(
|
||||||
|
activity, DateFormat.MEDIUM, timeStamp);
|
||||||
|
final String name = activity.getString(R.string.local_update_name);
|
||||||
|
final Update update = new Update();
|
||||||
|
update.setAvailableOnline(false);
|
||||||
|
update.setName(name);
|
||||||
|
update.setFile(file);
|
||||||
|
update.setFileSize(file.length());
|
||||||
|
update.setDownloadId(Update.LOCAL_ID);
|
||||||
|
update.setTimestamp(timeStamp);
|
||||||
|
update.setStatus(UpdateStatus.VERIFIED);
|
||||||
|
update.setPersistentStatus(UpdateStatus.Persistent.VERIFIED);
|
||||||
|
update.setVersion(String.format("%s (%s)", name, buildDate));
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
|
private void verifyPackage(File file) throws Exception {
|
||||||
|
try {
|
||||||
|
android.os.RecoverySystem.verifyPackage(file, null, null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete();
|
||||||
|
throw new Exception("Verification failed, file has been deleted");
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addUpdate(Update update) {
|
||||||
|
UpdaterController controller = UpdaterController.getInstance(activity);
|
||||||
|
controller.addUpdate(update, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getTimeStamp(File file) {
|
||||||
|
try {
|
||||||
|
final String metadataContent = readZippedFile(file, METADATA_PATH);
|
||||||
|
final String[] lines = metadataContent.split("\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
if (!line.startsWith(METADATA_TIMESTAMP_KEY)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String timeStampStr = line.replace(METADATA_TIMESTAMP_KEY, "");
|
||||||
|
return Long.parseLong(timeStampStr);
|
||||||
|
}
|
||||||
|
} 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, "Couldn't find timestamp in zip file, falling back to $now");
|
||||||
|
return System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readZippedFile(File file, String path) throws IOException {
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
InputStream iStream = null;
|
||||||
|
|
||||||
|
try (final ZipFile zip = new ZipFile(file)) {
|
||||||
|
final Enumeration<? extends ZipEntry> iterator = zip.entries();
|
||||||
|
while (iterator.hasMoreElements()) {
|
||||||
|
final ZipEntry entry = iterator.nextElement();
|
||||||
|
if (!METADATA_PATH.equals(entry.getName())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
iStream = zip.getInputStream(entry);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iStream == null) {
|
||||||
|
throw new FileNotFoundException("Couldn't find " + path + " in " + file.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] buffer = new byte[1024];
|
||||||
|
int read;
|
||||||
|
while ((read = iStream.read(buffer)) > 0) {
|
||||||
|
sb.append(new String(buffer, 0, read, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Failed to read file from zip package", e);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
if (iStream != null) {
|
||||||
|
iStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Callbacks {
|
||||||
|
void onImportStarted();
|
||||||
|
|
||||||
|
void onImportCompleted(Update update);
|
||||||
|
}
|
||||||
|
}
|
@@ -17,6 +17,7 @@ package org.lineageos.updater;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
import android.app.UiModeManager;
|
import android.app.UiModeManager;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
@@ -49,6 +50,7 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.widget.SwitchCompat;
|
import androidx.appcompat.widget.SwitchCompat;
|
||||||
@@ -72,6 +74,7 @@ import org.lineageos.updater.misc.BuildInfoUtils;
|
|||||||
import org.lineageos.updater.misc.Constants;
|
import org.lineageos.updater.misc.Constants;
|
||||||
import org.lineageos.updater.misc.StringGenerator;
|
import org.lineageos.updater.misc.StringGenerator;
|
||||||
import org.lineageos.updater.misc.Utils;
|
import org.lineageos.updater.misc.Utils;
|
||||||
|
import org.lineageos.updater.model.Update;
|
||||||
import org.lineageos.updater.model.UpdateInfo;
|
import org.lineageos.updater.model.UpdateInfo;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -80,7 +83,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class UpdatesActivity extends UpdatesListActivity {
|
public class UpdatesActivity extends UpdatesListActivity implements UpdateImporter.Callbacks {
|
||||||
|
|
||||||
private static final String TAG = "UpdatesActivity";
|
private static final String TAG = "UpdatesActivity";
|
||||||
private UpdaterService mUpdaterService;
|
private UpdaterService mUpdaterService;
|
||||||
@@ -106,11 +109,17 @@ public class UpdatesActivity extends UpdatesListActivity {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private UpdateImporter mUpdateImporter;
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
private ProgressDialog importDialog;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_updates);
|
setContentView(R.layout.activity_updates);
|
||||||
|
|
||||||
|
mUpdateImporter = new UpdateImporter(this, this);
|
||||||
|
|
||||||
UiModeManager uiModeManager = getSystemService(UiModeManager.class);
|
UiModeManager uiModeManager = getSystemService(UiModeManager.class);
|
||||||
mIsTV = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
|
mIsTV = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
|
||||||
|
|
||||||
@@ -234,6 +243,17 @@ public class UpdatesActivity extends UpdatesListActivity {
|
|||||||
LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter);
|
LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
if (importDialog != null) {
|
||||||
|
importDialog.dismiss();
|
||||||
|
importDialog = null;
|
||||||
|
mUpdateImporter.stopImport();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver);
|
LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver);
|
||||||
@@ -263,6 +283,9 @@ public class UpdatesActivity extends UpdatesListActivity {
|
|||||||
Uri.parse(Utils.getChangelogURL(this)));
|
Uri.parse(Utils.getChangelogURL(this)));
|
||||||
startActivity(openUrl);
|
startActivity(openUrl);
|
||||||
return true;
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_local_update) {
|
||||||
|
mUpdateImporter.openImportPicker();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
@@ -273,8 +296,60 @@ public class UpdatesActivity extends UpdatesListActivity {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final ServiceConnection mConnection = new ServiceConnection() {
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||||
|
if (!mUpdateImporter.onResult(requestCode, resultCode, data)) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public void onImportStarted() {
|
||||||
|
if (importDialog != null && importDialog.isShowing()) {
|
||||||
|
importDialog.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
importDialog = ProgressDialog.show(this, getString(R.string.local_update_import),
|
||||||
|
getString(R.string.local_update_import_progress), true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onImportCompleted(Update update) {
|
||||||
|
if (importDialog != null) {
|
||||||
|
importDialog.dismiss();
|
||||||
|
importDialog = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update == null) {
|
||||||
|
new AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.local_update_import)
|
||||||
|
.setMessage(R.string.local_update_import_failure)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mAdapter.notifyDataSetChanged();
|
||||||
|
|
||||||
|
final Runnable deleteUpdate = () -> UpdaterController.getInstance(this)
|
||||||
|
.deleteUpdate(update.getDownloadId());
|
||||||
|
|
||||||
|
new AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.local_update_import)
|
||||||
|
.setMessage(getString(R.string.local_update_import_success, update.getVersion()))
|
||||||
|
.setPositiveButton(R.string.local_update_import_install, (dialog, which) -> {
|
||||||
|
mAdapter.addItem(update.getDownloadId());
|
||||||
|
// Update UI
|
||||||
|
getUpdatesList();
|
||||||
|
Utils.triggerUpdate(this, update.getDownloadId());
|
||||||
|
})
|
||||||
|
.setNegativeButton(android.R.string.cancel, (dialog, which) -> deleteUpdate.run())
|
||||||
|
.setOnCancelListener((dialog) -> deleteUpdate.run())
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ServiceConnection mConnection = new ServiceConnection() {
|
||||||
@Override
|
@Override
|
||||||
public void onServiceConnected(ComponentName className,
|
public void onServiceConnected(ComponentName className,
|
||||||
IBinder service) {
|
IBinder service) {
|
||||||
@@ -425,6 +500,10 @@ public class UpdatesActivity extends UpdatesListActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleDownloadStatusChange(String downloadId) {
|
private void handleDownloadStatusChange(String downloadId) {
|
||||||
|
if (Update.LOCAL_ID.equals(downloadId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
UpdateInfo update = mUpdaterService.getUpdaterController().getUpdate(downloadId);
|
UpdateInfo update = mUpdaterService.getUpdaterController().getUpdate(downloadId);
|
||||||
switch (update.getStatus()) {
|
switch (update.getStatus()) {
|
||||||
case PAUSED_ERROR:
|
case PAUSED_ERROR:
|
||||||
|
@@ -65,6 +65,7 @@ import org.lineageos.updater.model.UpdateStatus;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class UpdatesListAdapter extends RecyclerView.Adapter<UpdatesListAdapter.ViewHolder> {
|
public class UpdatesListAdapter extends RecyclerView.Adapter<UpdatesListAdapter.ViewHolder> {
|
||||||
@@ -297,6 +298,14 @@ public class UpdatesListAdapter extends RecyclerView.Adapter<UpdatesListAdapter.
|
|||||||
mDownloadIds = downloadIds;
|
mDownloadIds = downloadIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addItem(String downloadId) {
|
||||||
|
if (mDownloadIds == null) {
|
||||||
|
mDownloadIds = new ArrayList<>();
|
||||||
|
}
|
||||||
|
mDownloadIds.add(0, downloadId);
|
||||||
|
notifyItemInserted(0);
|
||||||
|
}
|
||||||
|
|
||||||
public void notifyItemChanged(String downloadId) {
|
public void notifyItemChanged(String downloadId) {
|
||||||
if (mDownloadIds == null) {
|
if (mDownloadIds == null) {
|
||||||
return;
|
return;
|
||||||
|
@@ -159,6 +159,10 @@ class ABUpdateInstaller {
|
|||||||
mDownloadId = downloadId;
|
mDownloadId = downloadId;
|
||||||
|
|
||||||
File file = mUpdaterController.getActualUpdate(mDownloadId).getFile();
|
File file = mUpdaterController.getActualUpdate(mDownloadId).getFile();
|
||||||
|
install(file, downloadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void install(File file, String downloadId) {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
Log.e(TAG, "The given update doesn't exist");
|
Log.e(TAG, "The given update doesn't exist");
|
||||||
mUpdaterController.getActualUpdate(downloadId)
|
mUpdaterController.getActualUpdate(downloadId)
|
||||||
|
@@ -66,7 +66,7 @@ public class UpdaterController {
|
|||||||
private int mActiveDownloads = 0;
|
private int mActiveDownloads = 0;
|
||||||
private final Set<String> mVerifyingUpdates = new HashSet<>();
|
private final Set<String> mVerifyingUpdates = new HashSet<>();
|
||||||
|
|
||||||
protected static synchronized UpdaterController getInstance(Context context) {
|
public static synchronized UpdaterController getInstance(Context context) {
|
||||||
if (sUpdaterController == null) {
|
if (sUpdaterController == null) {
|
||||||
sUpdaterController = new UpdaterController(context);
|
sUpdaterController = new UpdaterController(context);
|
||||||
}
|
}
|
||||||
@@ -330,7 +330,7 @@ public class UpdaterController {
|
|||||||
return addUpdate(update, true);
|
return addUpdate(update, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean addUpdate(final UpdateInfo updateInfo, boolean availableOnline) {
|
public boolean addUpdate(final UpdateInfo updateInfo, boolean availableOnline) {
|
||||||
Log.d(TAG, "Adding download: " + updateInfo.getDownloadId());
|
Log.d(TAG, "Adding download: " + updateInfo.getDownloadId());
|
||||||
if (mDownloads.containsKey(updateInfo.getDownloadId())) {
|
if (mDownloads.containsKey(updateInfo.getDownloadId())) {
|
||||||
Log.d(TAG, "Download (" + updateInfo.getDownloadId() + ") already added");
|
Log.d(TAG, "Download (" + updateInfo.getDownloadId() + ") already added");
|
||||||
@@ -470,7 +470,7 @@ public class UpdaterController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void deleteUpdate(String downloadId) {
|
public void deleteUpdate(String downloadId) {
|
||||||
Log.d(TAG, "Cancelling " + downloadId);
|
Log.d(TAG, "Deleting update: " + downloadId);
|
||||||
if (!mDownloads.containsKey(downloadId) || isDownloading(downloadId)) {
|
if (!mDownloads.containsKey(downloadId) || isDownloading(downloadId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -482,7 +482,8 @@ public class UpdaterController {
|
|||||||
update.setPersistentStatus(UpdateStatus.Persistent.UNKNOWN);
|
update.setPersistentStatus(UpdateStatus.Persistent.UNKNOWN);
|
||||||
deleteUpdateAsync(update);
|
deleteUpdateAsync(update);
|
||||||
|
|
||||||
if (!update.getAvailableOnline()) {
|
final boolean isLocalUpdate = Update.LOCAL_ID.equals(downloadId);
|
||||||
|
if (!isLocalUpdate && !update.getAvailableOnline()) {
|
||||||
Log.d(TAG, "Download no longer available online, removing");
|
Log.d(TAG, "Download no longer available online, removing");
|
||||||
mDownloads.remove(downloadId);
|
mDownloads.remove(downloadId);
|
||||||
notifyUpdateDelete(downloadId);
|
notifyUpdateDelete(downloadId);
|
||||||
|
@@ -41,9 +41,11 @@ import org.lineageos.updater.misc.BuildInfoUtils;
|
|||||||
import org.lineageos.updater.misc.Constants;
|
import org.lineageos.updater.misc.Constants;
|
||||||
import org.lineageos.updater.misc.StringGenerator;
|
import org.lineageos.updater.misc.StringGenerator;
|
||||||
import org.lineageos.updater.misc.Utils;
|
import org.lineageos.updater.misc.Utils;
|
||||||
|
import org.lineageos.updater.model.Update;
|
||||||
import org.lineageos.updater.model.UpdateInfo;
|
import org.lineageos.updater.model.UpdateInfo;
|
||||||
import org.lineageos.updater.model.UpdateStatus;
|
import org.lineageos.updater.model.UpdateStatus;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
@@ -122,8 +124,10 @@ public class UpdaterService extends Service {
|
|||||||
setNotificationTitle(update);
|
setNotificationTitle(update);
|
||||||
handleInstallProgress(update);
|
handleInstallProgress(update);
|
||||||
} else if (UpdaterController.ACTION_UPDATE_REMOVED.equals(intent.getAction())) {
|
} else if (UpdaterController.ACTION_UPDATE_REMOVED.equals(intent.getAction())) {
|
||||||
|
final boolean isLocalUpdate = Update.LOCAL_ID.equals(downloadId);
|
||||||
Bundle extras = mNotificationBuilder.getExtras();
|
Bundle extras = mNotificationBuilder.getExtras();
|
||||||
if (downloadId.equals(extras.getString(UpdaterController.EXTRA_DOWNLOAD_ID))) {
|
if (extras != null && !isLocalUpdate && downloadId.equals(
|
||||||
|
extras.getString(UpdaterController.EXTRA_DOWNLOAD_ID))) {
|
||||||
mNotificationBuilder.setExtras(null);
|
mNotificationBuilder.setExtras(null);
|
||||||
UpdateInfo update = mUpdaterController.getUpdate(downloadId);
|
UpdateInfo update = mUpdaterController.getUpdate(downloadId);
|
||||||
if (update.getStatus() != UpdateStatus.INSTALLED) {
|
if (update.getStatus() != UpdateStatus.INSTALLED) {
|
||||||
@@ -408,7 +412,9 @@ public class UpdaterService extends Service {
|
|||||||
|
|
||||||
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
|
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
boolean deleteUpdate = pref.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, false);
|
boolean deleteUpdate = pref.getBoolean(Constants.PREF_AUTO_DELETE_UPDATES, false);
|
||||||
if (deleteUpdate) {
|
boolean isLocal = Update.LOCAL_ID.equals(update.getDownloadId());
|
||||||
|
// Always delete local updates
|
||||||
|
if (deleteUpdate || isLocal) {
|
||||||
mUpdaterController.deleteUpdate(update.getDownloadId());
|
mUpdaterController.deleteUpdate(update.getDownloadId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -18,6 +18,7 @@ package org.lineageos.updater.model;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
public class Update extends UpdateBase implements UpdateInfo {
|
public class Update extends UpdateBase implements UpdateInfo {
|
||||||
|
public static final String LOCAL_ID = "local";
|
||||||
|
|
||||||
private UpdateStatus mStatus = UpdateStatus.UNKNOWN;
|
private UpdateStatus mStatus = UpdateStatus.UNKNOWN;
|
||||||
private int mPersistentStatus = UpdateStatus.Persistent.UNKNOWN;
|
private int mPersistentStatus = UpdateStatus.Persistent.UNKNOWN;
|
||||||
|
@@ -6,6 +6,10 @@
|
|||||||
android:icon="@drawable/ic_menu_refresh"
|
android:icon="@drawable/ic_menu_refresh"
|
||||||
android:title="@string/menu_refresh"
|
android:title="@string/menu_refresh"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_local_update"
|
||||||
|
android:title="@string/local_update_import"
|
||||||
|
app:showAsAction="never" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_preferences"
|
android:id="@+id/menu_preferences"
|
||||||
android:title="@string/menu_preferences"
|
android:title="@string/menu_preferences"
|
||||||
|
@@ -157,4 +157,11 @@
|
|||||||
<string name="info_dialog_title">Did you know?</string>
|
<string name="info_dialog_title">Did you know?</string>
|
||||||
<string name="info_dialog_message">LineageOS updates are full installation packages. That means you can always install only the latest update, even if you skipped some in between!</string>
|
<string name="info_dialog_message">LineageOS updates are full installation packages. That means you can always install only the latest update, even if you skipped some in between!</string>
|
||||||
<string name="info_dialog_ok">Thanks for the info!</string>
|
<string name="info_dialog_ok">Thanks for the info!</string>
|
||||||
|
|
||||||
|
<string name="local_update_import">Local update</string>
|
||||||
|
<string name="local_update_import_progress">Importing local update\u2026</string>
|
||||||
|
<string name="local_update_import_success">%1$s has been imported. Do you want to install it?</string>
|
||||||
|
<string name="local_update_import_failure">Failed to import local update</string>
|
||||||
|
<string name="local_update_import_install">Install</string>
|
||||||
|
<string name="local_update_name">Local update</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Reference in New Issue
Block a user