/* * 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 parseJson(File file, boolean compatibleOnly) throws IOException, JSONException { List 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 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 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 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 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); } }