* To allow transitions from e.g. variant->unified build * Example process, using klteusc->klte as the example: - Set ro.updater.next_device to klte in klteusc's tree - Wait for build to roll out with ro.updater.next_device - Pull klteusc from build roster Change-Id: I3c70d54de3f9e036cd8700edc4ee0b11093740d2
345 lines
13 KiB
Java
345 lines
13 KiB
Java
/*
|
|
* Copyright (C) 2017 The LineageOS Project
|
|
*
|
|
* 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.ClipData;
|
|
import android.content.ClipboardManager;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.content.pm.PackageManager;
|
|
import android.net.ConnectivityManager;
|
|
import android.net.NetworkInfo;
|
|
import android.os.Environment;
|
|
import android.os.SystemProperties;
|
|
import android.os.storage.StorageManager;
|
|
import android.preference.PreferenceManager;
|
|
import android.util.Log;
|
|
import android.widget.Toast;
|
|
|
|
import org.json.JSONArray;
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
import org.lineageos.updater.R;
|
|
import org.lineageos.updater.UpdatesDbHelper;
|
|
import org.lineageos.updater.controller.UpdaterService;
|
|
import org.lineageos.updater.model.UpdateBaseInfo;
|
|
import org.lineageos.updater.model.Update;
|
|
import org.lineageos.updater.model.UpdateInfo;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.File;
|
|
import java.io.FileReader;
|
|
import java.io.FilenameFilter;
|
|
import java.io.IOException;
|
|
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.zip.ZipEntry;
|
|
import java.util.zip.ZipFile;
|
|
|
|
public class Utils {
|
|
|
|
private static final String TAG = "Utils";
|
|
|
|
private Utils() {
|
|
}
|
|
|
|
public static File getDownloadPath(Context context) {
|
|
return new File(context.getString(R.string.download_path));
|
|
}
|
|
|
|
public static File getExportPath(Context context) {
|
|
File dir = new File(Environment.getExternalStorageDirectory(),
|
|
context.getString(R.string.export_path));
|
|
if (!dir.isDirectory()) {
|
|
if (dir.exists() || !dir.mkdirs()) {
|
|
throw new RuntimeException("Could not create directory");
|
|
}
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
public static File getCachedUpdateList(Context context) {
|
|
return new File(context.getCacheDir(), "updates.json");
|
|
}
|
|
|
|
// This should really return an UpdateBaseInfo object, but currently this only
|
|
// used to initialize UpdateInfo objects
|
|
private static UpdateInfo parseJsonUpdate(JSONObject object) throws JSONException {
|
|
Update update = new Update();
|
|
update.setTimestamp(object.getLong("datetime"));
|
|
update.setName(object.getString("filename"));
|
|
update.setDownloadId(object.getString("id"));
|
|
update.setType(object.getString("romtype"));
|
|
update.setDownloadUrl(object.getString("url"));
|
|
update.setVersion(object.getString("version"));
|
|
return update;
|
|
}
|
|
|
|
public static boolean isCompatible(UpdateBaseInfo update) {
|
|
if (update.getTimestamp() < SystemProperties.getLong(Constants.PROP_BUILD_DATE, 0)) {
|
|
Log.d(TAG, update.getName() + " is older than current build");
|
|
return false;
|
|
}
|
|
if (!update.getType().equalsIgnoreCase(SystemProperties.get(Constants.PROP_RELEASE_TYPE))) {
|
|
Log.d(TAG, update.getName() + " has type " + update.getType());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static boolean canInstall(UpdateBaseInfo update) {
|
|
return update.getTimestamp() >= SystemProperties.getLong(Constants.PROP_BUILD_DATE, 0) &&
|
|
update.getVersion().equalsIgnoreCase(
|
|
SystemProperties.get(Constants.PROP_BUILD_VERSION));
|
|
}
|
|
|
|
public static List<UpdateInfo> parseJson(File file, boolean compatibleOnly)
|
|
throws IOException, JSONException {
|
|
List<UpdateInfo> updates = new ArrayList<>();
|
|
|
|
String json = "";
|
|
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
|
|
for (String line; (line = br.readLine()) != null;) {
|
|
json += line;
|
|
}
|
|
}
|
|
|
|
JSONObject obj = new JSONObject(json);
|
|
JSONArray updatesList = obj.getJSONArray("response");
|
|
for (int i = 0; i < updatesList.length(); i++) {
|
|
if (updatesList.isNull(i)) {
|
|
continue;
|
|
}
|
|
try {
|
|
UpdateInfo update = parseJsonUpdate(updatesList.getJSONObject(i));
|
|
if (!compatibleOnly || isCompatible(update)) {
|
|
updates.add(update);
|
|
} else {
|
|
Log.d(TAG, "Ignoring incompatible update " + update.getName());
|
|
}
|
|
} catch (JSONException e) {
|
|
Log.e(TAG, "Could not parse update object, index=" + i);
|
|
}
|
|
}
|
|
|
|
return updates;
|
|
}
|
|
|
|
public static String getServerURL(Context context) {
|
|
String serverUrl = SystemProperties.get(Constants.PROP_UPDATER_URI);
|
|
if (serverUrl.trim().isEmpty()) {
|
|
serverUrl = context.getString(R.string.conf_update_server_url_def);
|
|
}
|
|
String incrementalVersion = SystemProperties.get(Constants.PROP_BUILD_VERSION_INCREMENTAL);
|
|
String device = SystemProperties.get(Constants.PROP_NEXT_DEVICE,
|
|
SystemProperties.get(Constants.PROP_DEVICE));
|
|
String type = SystemProperties.get(Constants.PROP_RELEASE_TYPE).toLowerCase(Locale.ROOT);
|
|
return serverUrl + "/v1/" + device + "/" + type + "/" + incrementalVersion;
|
|
}
|
|
|
|
public static void triggerUpdate(Context context, String downloadId) {
|
|
final Intent intent = new Intent(context, UpdaterService.class);
|
|
intent.setAction(UpdaterService.ACTION_INSTALL_UPDATE);
|
|
intent.putExtra(UpdaterService.EXTRA_DOWNLOAD_ID, downloadId);
|
|
context.startService(intent);
|
|
}
|
|
|
|
public static boolean isNetworkAvailable(Context context) {
|
|
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(
|
|
Context.CONNECTIVITY_SERVICE);
|
|
NetworkInfo info = cm.getActiveNetworkInfo();
|
|
return !(info == null || !info.isConnected() || !info.isAvailable());
|
|
}
|
|
|
|
public static boolean isOnWifiOrEthernet(Context context) {
|
|
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(
|
|
Context.CONNECTIVITY_SERVICE);
|
|
NetworkInfo info = cm.getActiveNetworkInfo();
|
|
return (info != null && (info.getType() == ConnectivityManager.TYPE_ETHERNET
|
|
|| info.getType() == ConnectivityManager.TYPE_WIFI));
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @throws IOException
|
|
* @throws JSONException
|
|
*/
|
|
public static boolean checkForNewUpdates(File oldJson, File newJson)
|
|
throws IOException, JSONException {
|
|
List<UpdateInfo> oldList = parseJson(oldJson, true);
|
|
List<UpdateInfo> newList = parseJson(newJson, true);
|
|
Set<String> 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
|
|
for (UpdateInfo update : newList) {
|
|
if (!oldIds.contains(update.getDownloadId())) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the offset to the compressed data of a file inside the given zip
|
|
*
|
|
* @param zipFile input zip file
|
|
* @param entryPath full path of the entry
|
|
* @return the offset of the compressed, or -1 if not found
|
|
* @throws IllegalArgumentException if the given entry is not found
|
|
*/
|
|
public static long getZipEntryOffset(ZipFile zipFile, String entryPath) {
|
|
// Each entry has an header of (30 + n + m) bytes
|
|
// 'n' is the length of the file name
|
|
// 'm' is the length of the extra field
|
|
final int FIXED_HEADER_SIZE = 30;
|
|
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
|
|
long offset = 0;
|
|
while (zipEntries.hasMoreElements()) {
|
|
ZipEntry entry = zipEntries.nextElement();
|
|
int n = entry.getName().length();
|
|
int m = entry.getExtra() == null ? 0 : entry.getExtra().length;
|
|
int headerSize = FIXED_HEADER_SIZE + n + m;
|
|
offset += headerSize;
|
|
if (entry.getName().equals(entryPath)) {
|
|
return offset;
|
|
}
|
|
offset += entry.getCompressedSize();
|
|
}
|
|
Log.e(TAG, "Entry " + entryPath + " not found");
|
|
throw new IllegalArgumentException("The given entry was not found");
|
|
}
|
|
|
|
public static void removeUncryptFiles(File downloadPath) {
|
|
File[] uncryptFiles = downloadPath.listFiles(new FilenameFilter() {
|
|
@Override
|
|
public boolean accept(File dir, String name) {
|
|
return name.endsWith(Constants.UNCRYPT_FILE_EXT);
|
|
}
|
|
});
|
|
if (uncryptFiles == null) {
|
|
return;
|
|
}
|
|
for (File file : uncryptFiles) {
|
|
file.delete();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleanup the download directory, which is assumed to be a privileged location
|
|
* the user can't access and that might have stale files. This can happen if
|
|
* the data of the application are wiped.
|
|
*
|
|
* @param context
|
|
*/
|
|
public static void cleanupDownloadsDir(Context context) {
|
|
File downloadPath = getDownloadPath(context);
|
|
|
|
removeUncryptFiles(downloadPath);
|
|
|
|
final String DOWNLOADS_CLEANUP_DONE = "cleanup_done";
|
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
|
if (preferences.getBoolean(DOWNLOADS_CLEANUP_DONE, false)) {
|
|
return;
|
|
}
|
|
|
|
Log.d(TAG, "Cleaning " + downloadPath);
|
|
if (!downloadPath.isDirectory()) {
|
|
return;
|
|
}
|
|
File[] files = downloadPath.listFiles();
|
|
if (files == null) {
|
|
return;
|
|
}
|
|
|
|
// Ideally the database is empty when we get here
|
|
UpdatesDbHelper dbHelper = new UpdatesDbHelper(context);
|
|
List<String> knownPaths = new ArrayList<>();
|
|
for (UpdateInfo update : dbHelper.getUpdates()) {
|
|
knownPaths.add(update.getFile().getAbsolutePath());
|
|
}
|
|
for (File file : files) {
|
|
if (!knownPaths.contains(file.getAbsolutePath())) {
|
|
Log.d(TAG, "Deleting " + file.getAbsolutePath());
|
|
file.delete();
|
|
}
|
|
}
|
|
|
|
preferences.edit().putBoolean(DOWNLOADS_CLEANUP_DONE, true).apply();
|
|
}
|
|
|
|
public static File appendSequentialNumber(final File file) {
|
|
String name;
|
|
String extension;
|
|
int extensionPosition = file.getName().lastIndexOf(".");
|
|
if (extensionPosition > 0) {
|
|
name = file.getName().substring(0, extensionPosition);
|
|
extension = file.getName().substring(extensionPosition);
|
|
} else {
|
|
name = file.getName();
|
|
extension = "";
|
|
}
|
|
final File parent = file.getParentFile();
|
|
for (int i = 1; i < Integer.MAX_VALUE; i++) {
|
|
File newFile = new File(parent, name + "-" + i + extension);
|
|
if (!newFile.exists()) {
|
|
return newFile;
|
|
}
|
|
}
|
|
throw new IllegalStateException();
|
|
}
|
|
|
|
public static boolean isABUpdate(ZipFile zipFile) {
|
|
return zipFile.getEntry(Constants.AB_PAYLOAD_BIN_PATH) != null &&
|
|
zipFile.getEntry(Constants.AB_PAYLOAD_PROPERTIES_PATH) != null;
|
|
}
|
|
|
|
public static boolean isABUpdate(File file) throws IOException {
|
|
ZipFile zipFile = new ZipFile(file);
|
|
boolean isAB = isABUpdate(zipFile);
|
|
zipFile.close();
|
|
return isAB;
|
|
}
|
|
|
|
public static boolean hasTouchscreen(Context context) {
|
|
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN);
|
|
}
|
|
|
|
public static void addToClipboard(Context context, String label, String text, String toastMessage) {
|
|
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(
|
|
Context.CLIPBOARD_SERVICE);
|
|
ClipData clip = ClipData.newPlainText(label, text);
|
|
clipboard.setPrimaryClip(clip);
|
|
Toast.makeText(context, toastMessage, Toast.LENGTH_SHORT).show();
|
|
}
|
|
|
|
public static boolean isEncrypted(Context context, File file) {
|
|
StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
|
|
return sm.isEncrypted(file);
|
|
}
|
|
}
|