From 2614c7eb6be7502786685c6083961aa3f4928e4f Mon Sep 17 00:00:00 2001 From: Mishiranu Date: Sat, 21 Nov 2020 21:36:11 +0300 Subject: [PATCH] Implement new update data format --- src/chan/content/ChanLocator.java | 4 +- src/chan/content/ChanManager.java | 94 +-- .../dashchan/content/UpdaterActivity.java | 7 +- .../dashchan/content/async/ReadFileTask.java | 48 +- .../content/async/ReadUpdateTask.java | 643 +++++++++++++----- .../dashchan/content/async/ReadVideoTask.java | 2 +- .../content/async/SendLocalArchiveTask.java | 6 +- .../content/async/TimedProgressHandler.java | 7 +- .../content/service/DownloadService.java | 67 +- .../mishiranu/dashchan/media/VideoPlayer.java | 2 +- .../dashchan/ui/ExtensionsTrustLoop.java | 6 +- .../ui/preference/ThemesFragment.java | 4 +- .../ui/preference/UpdateFragment.java | 217 +++--- src/com/mishiranu/dashchan/util/IOUtils.java | 15 - 14 files changed, 752 insertions(+), 370 deletions(-) diff --git a/src/chan/content/ChanLocator.java b/src/chan/content/ChanLocator.java index d66aeb74..0c53498e 100644 --- a/src/chan/content/ChanLocator.java +++ b/src/chan/content/ChanLocator.java @@ -513,9 +513,9 @@ public final Uri buildQueryWithSchemeHost(boolean useHttps, String host, String return builder.build(); } - public final Uri setScheme(Uri uri) { + public final Uri setSchemeIfEmpty(Uri uri, String fallback) { if (uri != null && StringUtils.isEmpty(uri.getScheme())) { - return uri.buildUpon().scheme(getPreferredScheme()).build(); + return uri.buildUpon().scheme(fallback != null ? fallback : getPreferredScheme()).build(); } return uri; } diff --git a/src/chan/content/ChanManager.java b/src/chan/content/ChanManager.java index 43ff6a9a..23f628e3 100644 --- a/src/chan/content/ChanManager.java +++ b/src/chan/content/ChanManager.java @@ -66,6 +66,7 @@ public class ChanManager { private static final String FEATURE_CHAN_EXTENSION = "chan.extension"; private static final String META_CHAN_EXTENSION_NAME = "chan.extension.name"; + private static final String META_CHAN_EXTENSION_TITLE = "chan.extension.title"; private static final String META_CHAN_EXTENSION_VERSION = "chan.extension.version"; private static final String META_CHAN_EXTENSION_ICON = "chan.extension.icon"; private static final String META_CHAN_EXTENSION_SOURCE = "chan.extension.source"; @@ -76,6 +77,7 @@ public class ChanManager { private static final String FEATURE_LIB_EXTENSION = "lib.extension"; private static final String META_LIB_EXTENSION_NAME = "lib.extension.name"; + private static final String META_LIB_EXTENSION_TITLE = "lib.extension.title"; private static final String META_LIB_EXTENSION_SOURCE = "lib.extension.source"; @SuppressWarnings("deprecation") @@ -137,14 +139,15 @@ public enum Type {CHAN, LIBRARY} public enum TrustState {UNTRUSTED, TRUSTED, DISCARDED} public final Type type; - public final String extensionName; + public final String name; + public final String title; public final TrustState trustState; public final String packageName; public final String versionName; public final long versionCode; private final ApplicationInfo applicationInfo; public final Fingerprints fingerprints; - public final int version; + public final int apiVersion; public final boolean supported; public final int iconResId; public final Uri updateUri; @@ -158,19 +161,20 @@ public String getNativeLibraryDir() { return applicationInfo.nativeLibraryDir; } - private ExtensionItem(Type type, String extensionName, TrustState trustState, + private ExtensionItem(Type type, String name, String title, TrustState trustState, String packageName, String versionName, long versionCode, ApplicationInfo applicationInfo, - Fingerprints fingerprints, int version, boolean supported, int iconResId, Uri updateUri, + Fingerprints fingerprints, int apiVersion, boolean supported, int iconResId, Uri updateUri, String classConfiguration, String classPerformer, String classLocator, String classMarkup) { this.type = type; - this.extensionName = extensionName; + this.name = name; + this.title = title; this.trustState = trustState; this.packageName = packageName; this.versionName = versionName; this.versionCode = versionCode; this.applicationInfo = applicationInfo; this.fingerprints = fingerprints; - this.version = version; + this.apiVersion = apiVersion; this.supported = supported; this.iconResId = iconResId; this.updateUri = updateUri; @@ -181,19 +185,21 @@ private ExtensionItem(Type type, String extensionName, TrustState trustState, this.classMarkup = classMarkup; } - public ExtensionItem(String chanName, String packageName, + public ExtensionItem(String name, String title, String packageName, String versionName, long versionCode, ApplicationInfo applicationInfo, - Fingerprints fingerprints, int version, boolean supported, int iconResId, Uri updateUri, + Fingerprints fingerprints, int apiVersion, boolean supported, int iconResId, Uri updateUri, String classConfiguration, String classPerformer, String classLocator, String classMarkup) { - this(Type.CHAN, chanName, TrustState.UNTRUSTED, packageName, versionName, versionCode, applicationInfo, - fingerprints, version, supported, iconResId, updateUri, + this(Type.CHAN, name, title, TrustState.UNTRUSTED, + packageName, versionName, versionCode, applicationInfo, + fingerprints, apiVersion, supported, iconResId, updateUri, classConfiguration, classPerformer, classLocator, classMarkup); } - public ExtensionItem(String libName, String packageName, + public ExtensionItem(String name, String title, String packageName, String versionName, long versionCode, ApplicationInfo applicationInfo, Fingerprints fingerprints, Uri updateUri) { - this(Type.LIBRARY, libName, TrustState.UNTRUSTED, packageName, versionName, versionCode, applicationInfo, + this(Type.LIBRARY, name, title, TrustState.UNTRUSTED, + packageName, versionName, versionCode, applicationInfo, fingerprints, 0, true, 0, updateUri, null, null, null, null); } @@ -202,9 +208,9 @@ public ExtensionItem changeTrustState(boolean trusted) { throw new IllegalStateException(); } TrustState trustState = trusted ? TrustState.TRUSTED : TrustState.DISCARDED; - return new ExtensionItem(type, extensionName, trustState, + return new ExtensionItem(type, name, title, trustState, packageName, versionName, versionCode, applicationInfo, - fingerprints, version, supported, iconResId, updateUri, + fingerprints, apiVersion, supported, iconResId, updateUri, classConfiguration, classPerformer, classLocator, classMarkup); } } @@ -236,7 +242,7 @@ private static String extendClassName(String className, String packageName) { private static Map extensionsMap(List extensions) { LinkedHashMap map = new LinkedHashMap<>(); for (Extension extension : extensions) { - map.put(extension.item.extensionName, extension); + map.put(extension.item.name, extension); } return Collections.unmodifiableMap(map); } @@ -275,7 +281,7 @@ private ChanManager() { applicationFingerprints, usedExtensionNames, Collections.emptyMap()); if (extension != null) { extensions.add(extension); - usedExtensionNames.add(extension.item.extensionName); + usedExtensionNames.add(extension.item.name); } } } @@ -320,7 +326,7 @@ private void registerReceiver() { applicationFingerprints, Collections.emptySet(), extensions); if (newExtension != null) { boolean newTrusted = newExtension.item.trustState == ExtensionItem.TrustState.TRUSTED; - Extension oldExtension = extensions.get(newExtension.item.extensionName); + Extension oldExtension = extensions.get(newExtension.item.name); if (oldExtension == null) { updateExtensions(newExtension, null, true); for (Callback callback : observable) { @@ -356,7 +362,7 @@ private void registerReceiver() { if (packageName.equals(extension.item.packageName)) { if (extension.item.type == ExtensionItem.Type.CHAN) { if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { - updateExtensions(null, extension.item.extensionName, + updateExtensions(null, extension.item.name, extension.item.trustState == ExtensionItem.TrustState.TRUSTED); if (extension.chan != null) { for (Callback callback : observable) { @@ -384,7 +390,7 @@ private void updateExtensions(Extension newExtension, String deleteExtensionName } LinkedHashMap extensions = new LinkedHashMap<>(this.extensions); if (newExtension != null) { - extensions.put(newExtension.item.extensionName, newExtension); + extensions.put(newExtension.item.name, newExtension); } if (deleteExtensionName != null) { extensions.remove(deleteExtensionName); @@ -393,7 +399,7 @@ private void updateExtensions(Extension newExtension, String deleteExtensionName for (String chanName : orderedChanNames) { Extension extension = extensions.get(chanName); if (extension != null) { - ordered.put(extension.item.extensionName, extension); + ordered.put(extension.item.name, extension); } } extensions.keySet().removeAll(orderedChanNames); @@ -415,7 +421,7 @@ private void updateExtensions(Extension newExtension, String deleteExtensionName archiveChanNames = new ArrayList<>(); archiveMap.put(chanName, archiveChanNames); } - archiveChanNames.add(extension.item.extensionName); + archiveChanNames.add(extension.item.name); } } } @@ -472,30 +478,35 @@ private static Extension loadExtension(PackageInfo packageInfo, } long versionCode = PackageInfoCompat.getLongVersionCode(packageInfo); Bundle data = applicationInfo.metaData; - String extensionName = data.getString(chanExtension ? META_CHAN_EXTENSION_NAME + String name = data.getString(chanExtension ? META_CHAN_EXTENSION_NAME : libExtension ? META_LIB_EXTENSION_NAME : null); - if (extensionName == null || !VALID_EXTENSION_NAME.matcher(extensionName).matches() || - (chanExtension ? RESERVED_CHAN_NAMES : RESERVED_EXTENSION_NAMES).contains(extensionName)) { - Log.persistent().write("Invalid extension name: " + extensionName); + if (name == null || !VALID_EXTENSION_NAME.matcher(name).matches() || + (chanExtension ? RESERVED_CHAN_NAMES : RESERVED_EXTENSION_NAMES).contains(name)) { + Log.persistent().write("Invalid extension name: " + name); return null; } boolean nameConflict; - if (usedExtensionNames.contains(extensionName)) { + if (usedExtensionNames.contains(name)) { nameConflict = true; } else { - Extension extension = extensions.get(extensionName); + Extension extension = extensions.get(name); nameConflict = extension != null && !extension.item.packageName.equals(packageInfo.packageName); } if (nameConflict) { - Log.persistent().write("Extension name conflict: " + extensionName + " already exists"); + Log.persistent().write("Extension name conflict: " + name + " already exists"); return null; } + String title = data.getString(chanExtension ? META_CHAN_EXTENSION_TITLE + : libExtension ? META_LIB_EXTENSION_TITLE : null); + if (title == null) { + title = name; + } Fingerprints fingerprints = extractFingerprints(packageInfo); ExtensionItem extensionItem; if (chanExtension) { int invalidVersion = Integer.MIN_VALUE; - int version = data.getInt(META_CHAN_EXTENSION_VERSION, invalidVersion); - if (version == invalidVersion) { + int apiVersion = data.getInt(META_CHAN_EXTENSION_VERSION, invalidVersion); + if (apiVersion == invalidVersion) { Log.persistent().write("Invalid extension version"); return null; } @@ -515,17 +526,16 @@ private static Extension loadExtension(PackageInfo packageInfo, classPerformer = extendClassName(classPerformer, packageInfo.packageName); classLocator = extendClassName(classLocator, packageInfo.packageName); classMarkup = extendClassName(classMarkup, packageInfo.packageName); - boolean supported = version >= MIN_VERSION && version <= MAX_VERSION; - extensionItem = new ExtensionItem(extensionName, packageInfo.packageName, + boolean supported = apiVersion >= MIN_VERSION && apiVersion <= MAX_VERSION; + extensionItem = new ExtensionItem(name, title, packageInfo.packageName, packageInfo.versionName, versionCode, applicationInfo, - fingerprints, version, supported, iconResId, updateUri, + fingerprints, apiVersion, supported, iconResId, updateUri, classConfiguration, classPerformer, classLocator, classMarkup); } else if (libExtension) { String source = data.getString(META_LIB_EXTENSION_SOURCE); Uri updateUri = source != null ? Uri.parse(source) : null; - extensionItem = new ExtensionItem(extensionName, packageInfo.packageName, - packageInfo.versionName, versionCode, applicationInfo, - fingerprints, updateUri); + extensionItem = new ExtensionItem(name, title, packageInfo.packageName, + packageInfo.versionName, versionCode, applicationInfo, fingerprints, updateUri); } else { throw new RuntimeException(); } @@ -544,7 +554,7 @@ private static Extension loadExtension(PackageInfo packageInfo, private static Chan loadChan(ExtensionItem chanItem, PackageManager packageManager) { if (chanItem.supported) { - String chanName = chanItem.extensionName; + String chanName = chanItem.name; try { String nativeLibraryDir = chanItem.applicationInfo.nativeLibraryDir; if (nativeLibraryDir != null && !new File(nativeLibraryDir).exists()) { @@ -574,8 +584,8 @@ private static Chan loadChan(ExtensionItem chanItem, PackageManager packageManag return null; } - private void loadLibrary(ExtensionItem libItem) { - switch (libItem.extensionName) { + private void loadLibrary(ExtensionItem libraryItem) { + switch (libraryItem.name) { case EXTENSION_NAME_LIB_WEBM: { if (Preferences.isUseVideoPlayer()) { VideoPlayer.loadLibraries(MainApplication.getInstance()); @@ -730,8 +740,8 @@ public ExtensionItem getFirstUntrustedExtension() { return null; } - public ExtensionItem getLibExtension(String libName) { - Extension extension = extensions.get(libName); + public ExtensionItem getLibraryExtension(String libraryName) { + Extension extension = extensions.get(libraryName); return extension != null && extension.item.type == ExtensionItem.Type.LIBRARY ? extension.item : null; } @@ -784,7 +794,7 @@ String getChanNameByHost(String host) { if (host != null) { for (Extension extension : extensions.values()) { if (extension.chan != null && extension.chan.locator.isChanHost(host)) { - return extension.item.extensionName; + return extension.item.name; } } } diff --git a/src/com/mishiranu/dashchan/content/UpdaterActivity.java b/src/com/mishiranu/dashchan/content/UpdaterActivity.java index 88211a54..393122c7 100644 --- a/src/com/mishiranu/dashchan/content/UpdaterActivity.java +++ b/src/com/mishiranu/dashchan/content/UpdaterActivity.java @@ -180,11 +180,13 @@ public static class Request { public final String extensionName; public final String versionName; public final Uri uri; + public final byte[] sha256sum; - public Request(String extensionName, String versionName, Uri uri) { + public Request(String extensionName, String versionName, Uri uri, byte[] sha256sum) { this.extensionName = extensionName; this.versionName = versionName; this.uri = uri; + this.sha256sum = sha256sum; } } @@ -193,7 +195,8 @@ public static void startUpdater(List requests) { ArrayList downloadItems = new ArrayList<>(); for (Request request : requests) { String name = request.extensionName + "-" + request.versionName + ".apk"; - DownloadService.DownloadItem downloadItem = new DownloadService.DownloadItem(null, request.uri, name); + DownloadService.DownloadItem downloadItem = new DownloadService.DownloadItem(null, + request.uri, name, request.sha256sum); if (ChanManager.EXTENSION_NAME_CLIENT.equals(request.extensionName)) { clientDownloadItem = downloadItem; } else { diff --git a/src/com/mishiranu/dashchan/content/async/ReadFileTask.java b/src/com/mishiranu/dashchan/content/async/ReadFileTask.java index 517a289c..50c7d09b 100644 --- a/src/com/mishiranu/dashchan/content/async/ReadFileTask.java +++ b/src/com/mishiranu/dashchan/content/async/ReadFileTask.java @@ -13,7 +13,6 @@ import chan.util.DataFile; import com.mishiranu.dashchan.content.CacheManager; import com.mishiranu.dashchan.content.model.ErrorItem; -import com.mishiranu.dashchan.util.IOUtils; import com.mishiranu.dashchan.util.Log; import java.io.File; import java.io.FileInputStream; @@ -21,6 +20,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; public class ReadFileTask extends HttpHolderTask { private static final int CONNECT_TIMEOUT = 15000; @@ -52,6 +54,7 @@ default void onFinishDownloading(boolean success, Uri uri, DataFile file, ErrorI private final DataFile toFile; private final File cachedMediaFile; private final boolean overwrite; + private final byte[] checkSha256; private ErrorItem errorItem; @@ -67,21 +70,21 @@ public void onProgressChange(long progress, long progressMax) { public static ReadFileTask createCachedMediaFile(Context context, FileCallback callback, Chan chan, Uri fromUri, File cachedMediaFile) { DataFile toFile = DataFile.obtain(context, DataFile.Target.CACHE, cachedMediaFile.getName()); - return new ReadFileTask(callback, chan, fromUri, toFile, null, true); + return new ReadFileTask(callback, chan, fromUri, toFile, null, true, null); } public static ReadFileTask createShared(Callback callback, Chan chan, - Uri fromUri, DataFile toFile, boolean overwrite) { + Uri fromUri, DataFile toFile, boolean overwrite, byte[] checkSha256) { File cachedMediaFile = CacheManager.getInstance().getMediaFile(fromUri, true); if (cachedMediaFile == null || !cachedMediaFile.exists() || CacheManager.getInstance().cancelCachedMediaBusy(cachedMediaFile)) { cachedMediaFile = null; } - return new ReadFileTask(callback, chan, fromUri, toFile, cachedMediaFile, overwrite); + return new ReadFileTask(callback, chan, fromUri, toFile, cachedMediaFile, overwrite, checkSha256); } private ReadFileTask(Callback callback, Chan chan, Uri fromUri, DataFile toFile, - File cachedMediaFile, boolean overwrite) { + File cachedMediaFile, boolean overwrite, byte[] checkSha256) { super(chan); this.callback = callback; this.chan = chan; @@ -89,6 +92,7 @@ private ReadFileTask(Callback callback, Chan chan, Uri fromUri, DataFile toFile, this.toFile = toFile; this.cachedMediaFile = cachedMediaFile; this.overwrite = overwrite; + this.checkSha256 = checkSha256; } @Override @@ -96,11 +100,34 @@ protected void onPrepare() { callback.onStartDownloading(); } + private static void copyStream(InputStream input, OutputStream output, + TimedProgressHandler progressHandler, MessageDigest digest) throws IOException { + byte[] data = new byte[8192]; + int count; + long read = 0; + while ((count = input.read(data)) != -1) { + output.write(data, 0, count); + read += count; + progressHandler.updateProgress(read); + if (digest != null) { + digest.update(data, 0, count); + } + } + } + @Override protected Boolean run(HttpHolder holder) { boolean success = false; try { loadingStarted = true; + MessageDigest digest = null; + if (checkSha256 != null) { + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } // noinspection StatementWithEmptyBody if (!overwrite && toFile.exists()) { // Do nothing @@ -108,7 +135,7 @@ protected Boolean run(HttpHolder holder) { progressHandler.setInputProgressMax(cachedMediaFile.length()); try (FileInputStream input = new FileInputStream(cachedMediaFile); OutputStream output = toFile.openOutputStream()) { - IOUtils.copyStream(input, output, progressHandler); + copyStream(input, output, progressHandler, digest); } catch (IOException e) { ErrorItem.Type type = getErrorTypeFromExceptionAndHandle(e); errorItem = new ErrorItem(type != null ? type : ErrorItem.Type.UNKNOWN); @@ -136,7 +163,7 @@ protected Boolean run(HttpHolder holder) { progressHandler.setInputProgressMax(response.getLength()); try (InputStream input = response.open(); OutputStream output = toFile.openOutputStream()) { - IOUtils.copyStream(input, output, progressHandler); + copyStream(input, output, progressHandler, digest); } catch (IOException e) { ErrorItem.Type errorType = getErrorTypeFromExceptionAndHandle(e); if (errorType != null) { @@ -149,6 +176,13 @@ protected Boolean run(HttpHolder holder) { response.cleanupAndDisconnect(); } } + if (digest != null) { + byte[] sha256 = digest.digest(); + if (!Arrays.equals(sha256, checkSha256)) { + errorItem = new ErrorItem(ErrorItem.Type.INVALID_RESPONSE); + return false; + } + } success = true; return true; } catch (ExtensionException | HttpException | InvalidResponseException e) { diff --git a/src/com/mishiranu/dashchan/content/async/ReadUpdateTask.java b/src/com/mishiranu/dashchan/content/async/ReadUpdateTask.java index c9a801a7..6097aeab 100644 --- a/src/com/mishiranu/dashchan/content/async/ReadUpdateTask.java +++ b/src/com/mishiranu/dashchan/content/async/ReadUpdateTask.java @@ -20,6 +20,7 @@ import com.mishiranu.dashchan.content.model.ErrorItem; import com.mishiranu.dashchan.util.Log; import java.io.File; +import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -39,16 +40,15 @@ public class ReadUpdateTask extends HttpHolderTask> update; - private final Map> install; + private final Map update; + private final Map install; - private UpdateDataMap(Map> update, - Map> install) { + private UpdateDataMap(Map update, Map install) { this.update = update; this.install = install; } - public List get(String extensionName, boolean installed) { + public ApplicationItem get(String extensionName, boolean installed) { return (installed ? update : install).get(extensionName); } @@ -56,22 +56,21 @@ public Collection extensionNames(boolean installed) { return (installed ? update: install).keySet(); } - private static void writeGroupMap(Parcel dest, Map> map) { + private static void writeMap(Parcel dest, int flags, Map map) { dest.writeInt(map.size()); - for (Map.Entry> entry : map.entrySet()) { + for (Map.Entry entry : map.entrySet()) { dest.writeString(entry.getKey()); - dest.writeTypedList(entry.getValue()); + entry.getValue().writeToParcel(dest, flags); } } - private static Map> readGroupMap(Parcel in, - Parcelable.Creator creator) { - int count = in.readInt(); - Map> map = new HashMap<>(); + private static Map readMap(Parcel source, Parcelable.Creator creator) { + int count = source.readInt(); + Map map = new HashMap<>(); for (int i = 0; i < count; i++) { - String key = in.readString(); - List values = in.createTypedArrayList(creator); - map.put(key, values); + String key = source.readString(); + T value = creator.createFromParcel(source); + map.put(key, value); } return map; } @@ -83,15 +82,15 @@ public int describeContents() { @Override public void writeToParcel(Parcel dest, int flags) { - writeGroupMap(dest, update); - writeGroupMap(dest, install); + writeMap(dest, flags, update); + writeMap(dest, flags, install); } public static final Creator CREATOR = new Creator() { @Override - public UpdateDataMap createFromParcel(Parcel in) { - Map> update = readGroupMap(in, UpdateItem.CREATOR); - Map> install = readGroupMap(in, UpdateItem.CREATOR); + public UpdateDataMap createFromParcel(Parcel source) { + Map update = readMap(source, ApplicationItem.CREATOR); + Map install = readMap(source, ApplicationItem.CREATOR); return new UpdateDataMap(update, install); } @@ -102,38 +101,100 @@ public UpdateDataMap[] newArray(int size) { }; } - public static class UpdateItem implements Parcelable { + public static class ApplicationItem implements Parcelable { + public enum Type {CLIENT, LIBRARY, CHAN} + + public final Type type; + public final String name; + public final String title; + public final List packageItems; + + public ApplicationItem(Type type, String name, String title, List packageItems) { + this.name = name; + this.type = type; + this.title = title; + this.packageItems = packageItems; + } + + private static ApplicationItem fromJsonV1(JSONObject jsonObject) throws JSONException { + String name = jsonObject.getString("name"); + String typeString = jsonObject.getString("type"); + String title = jsonObject.getString("title"); + Type type; + switch (typeString) { + case "client": { + type = Type.CLIENT; + break; + } + case "chan": { + type = Type.CHAN; + break; + } + case "library": { + type = Type.LIBRARY; + break; + } + default: { + throw new JSONException("Invalid type"); + } + } + return new ApplicationItem(type, name, title, Collections.emptyList()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(type.name()); + dest.writeString(name); + dest.writeString(title); + dest.writeTypedList(packageItems); + } + + public static final Creator CREATOR = new Creator() { + @Override + public ApplicationItem createFromParcel(Parcel source) { + Type type = Type.valueOf(source.readString()); + String name = source.readString(); + String title = source.readString(); + List packageItems = source.createTypedArrayList(PackageItem.CREATOR); + return new ApplicationItem(type, name, title, packageItems); + } + + @Override + public ApplicationItem[] newArray(int size) { + return new ApplicationItem[size]; + } + }; + } + + public static class PackageItem implements Parcelable { public final String repository; public final String title; - public final String name; - public final long code; - public final int minVersion; - public final int version; + public final String versionName; + public final long versionCode; + public final int minApiVersion; + public final int maxApiVersion; + public final int apiVersion; public final long length; - public final String source; - public final boolean ignoreVersion; + public final Uri source; + public final byte[] sha256sum; - private UpdateItem(String repository, String title, String name, long code, - int minVersion, int version, long length, String source, boolean ignoreVersion) { + public PackageItem(String repository, String title, String versionName, long versionCode, + int minApiVersion, int maxApiVersion, int apiVersion, long length, Uri source, byte[] sha256sum) { this.repository = repository; this.title = title; - this.name = name; - this.code = code; - this.minVersion = minVersion; - this.version = version; + this.versionName = versionName; + this.versionCode = versionCode; + this.minApiVersion = minApiVersion; + this.maxApiVersion = maxApiVersion; + this.apiVersion = apiVersion; this.length = length; this.source = source; - this.ignoreVersion = ignoreVersion; - } - - public static UpdateItem createExtension(String repository, String title, String name, long code, - int version, long length, String source, boolean ignoreVersion) { - return new UpdateItem(repository, title, name, code, -1, version, length, source, ignoreVersion); - } - - public static UpdateItem createClient(String repository, String title, String name, long code, - int minVersion, int maxVersion, long length, String source) { - return new UpdateItem(repository, title, name, code, minVersion, maxVersion, length, source, false); + this.sha256sum = sha256sum; } @Override @@ -145,34 +206,37 @@ public int describeContents() { public void writeToParcel(Parcel dest, int flags) { dest.writeString(repository); dest.writeString(title); - dest.writeString(name); - dest.writeLong(code); - dest.writeInt(minVersion); - dest.writeInt(version); + dest.writeString(versionName); + dest.writeLong(versionCode); + dest.writeInt(minApiVersion); + dest.writeInt(maxApiVersion); + dest.writeInt(apiVersion); dest.writeLong(length); - dest.writeString(source); - dest.writeByte((byte) (ignoreVersion ? 1 : 0)); + dest.writeString(source != null ? source.toString() : null); + dest.writeByteArray(sha256sum); } - public static final Creator CREATOR = new Creator() { + public static final Creator CREATOR = new Creator() { @Override - public UpdateItem createFromParcel(Parcel in) { - String repository = in.readString(); - String title = in.readString(); - String name = in.readString(); - long code = in.readLong(); - int minVersion = in.readInt(); - int version = in.readInt(); - long length = in.readLong(); - String source = in.readString(); - boolean ignoreVersion = in.readByte() != 0; - return new UpdateItem(repository, title, name, code, - minVersion, version, length, source, ignoreVersion); + public PackageItem createFromParcel(Parcel source) { + String repository = source.readString(); + String title = source.readString(); + String versionName = source.readString(); + long versionCode = source.readLong(); + int minVersion = source.readInt(); + int maxVersion = source.readInt(); + int version = source.readInt(); + long length = source.readLong(); + String sourceString = source.readString(); + Uri sourceUri = sourceString != null ? Uri.parse(sourceString) : null; + byte[] sha256sum = source.createByteArray(); + return new PackageItem(repository, title, versionName, versionCode, + minVersion, maxVersion, version, length, sourceUri, sha256sum); } @Override - public UpdateItem[] newArray(int size) { - return new UpdateItem[size]; + public PackageItem[] newArray(int size) { + return new PackageItem[size]; } }; } @@ -187,38 +251,103 @@ public ReadUpdateTask(Context context, Callback callback) { this.callback = callback; } - public static Uri normalizeUri(Uri uri, Uri base) { - boolean noScheme = uri.getScheme() == null; - boolean noHost = uri.getHost() == null; - if (noScheme || noHost) { - Uri.Builder redirectUriBuilder = uri.buildUpon(); + public static Uri normalizeRelativeUri(Uri base, String uriOrPath) { + Uri uri = Uri.parse(uriOrPath); + boolean noScheme = StringUtils.isEmpty(uri.getScheme()); + boolean noHost = StringUtils.isEmpty(uri.getHost()); + boolean noPath = StringUtils.isEmpty(uri.getPath()); + if (noScheme || noHost || noPath) { + Uri.Builder builder = uri.buildUpon(); if (noScheme) { - redirectUriBuilder.scheme(base.getScheme()); + builder.scheme(base.getScheme()); } if (noHost) { - redirectUriBuilder.authority(base.getHost()); + builder.authority(base.getHost()); } - uri = redirectUriBuilder.build(); + if (noPath) { + builder.path(base.getPath()); + } else if (noScheme && noHost && !uriOrPath.startsWith("/")) { + String path = uri.getPath(); + String basePath = base.getPath(); + int index = basePath.lastIndexOf('/'); + basePath = index >= 0 ? basePath.substring(0, index + 1) : "/"; + builder.path(basePath + path); + } + uri = builder.build(); } return uri; } - private static UpdateItem extractUpdateItem(String extensionName, String repository, Uri uri, Long installedCode, - ChanManager.Fingerprints fingerprints, boolean ignoreVersion, JSONObject chanObject) throws JSONException { - int minSdk = chanObject.optInt("minSdk"); - int maxSdk = chanObject.optInt("maxSdk"); + private static PackageItem extractPackageItem(DataVersion dataVersion, JSONObject chanObject, + String extensionName, String repository, Uri uri, Long installedCode, + ChanManager.Fingerprints fingerprints) throws JSONException { + String title; + String versionName; + long versionCode; + int minSdk; + int maxSdk; + int minApiVersion; + int maxApiVersion; + int apiVersion; + long length; + String source; + JSONArray fingerprintsArray; + String fingerprint; + String sha256sumString; + boolean requireFingerprintChecksum; + switch (dataVersion) { + case LEGACY: { + title = CommonUtils.getJsonString(chanObject, "title"); + versionName = CommonUtils.getJsonString(chanObject, "name"); + versionCode = chanObject.getInt("code"); + minApiVersion = chanObject.optInt("minVersion"); + maxApiVersion = chanObject.optInt("maxVersion"); + apiVersion = chanObject.optInt("version"); + minSdk = chanObject.optInt("minSdk"); + maxSdk = chanObject.optInt("maxSdk"); + length = chanObject.getLong("length"); + source = CommonUtils.getJsonString(chanObject, "source"); + fingerprintsArray = chanObject.optJSONArray("fingerprints"); + fingerprint = CommonUtils.optJsonString(chanObject, "fingerprint"); + sha256sumString = null; + requireFingerprintChecksum = false; + break; + } + case V1: { + title = CommonUtils.getJsonString(chanObject, "title"); + versionName = CommonUtils.getJsonString(chanObject, "version_name"); + versionCode = chanObject.getInt("version_code"); + minApiVersion = chanObject.optInt("min_api_version"); + maxApiVersion = chanObject.optInt("max_api_version"); + apiVersion = chanObject.optInt("api_version"); + minSdk = chanObject.optInt("min_sdk"); + maxSdk = chanObject.optInt("max_sdk"); + length = chanObject.getLong("length"); + source = CommonUtils.getJsonString(chanObject, "source"); + fingerprintsArray = chanObject.optJSONArray("fingerprints"); + fingerprint = CommonUtils.optJsonString(chanObject, "fingerprint"); + sha256sumString = CommonUtils.getJsonString(chanObject, "sha256sum"); + requireFingerprintChecksum = true; + break; + } + default: { + throw new IllegalStateException(); + } + } if (minSdk > 0 && minSdk > Build.VERSION.SDK_INT || maxSdk > 0 && maxSdk < Build.VERSION.SDK_INT) { return null; } ArrayList rawFingerprints = new ArrayList<>(); - JSONArray fingerprintsArray = chanObject.optJSONArray("fingerprints"); if (fingerprintsArray != null) { for (int j = 0; j < fingerprintsArray.length(); j++) { rawFingerprints.add(fingerprintsArray.optString(j)); } } else { - rawFingerprints.add(CommonUtils.optJsonString(chanObject, "fingerprint")); + rawFingerprints.add(fingerprint); + } + if (requireFingerprintChecksum && rawFingerprints.isEmpty()) { + return null; } if (fingerprints != null) { HashSet fingerprintsSet = new HashSet<>(); @@ -226,7 +355,7 @@ private static UpdateItem extractUpdateItem(String extensionName, String reposit if (!StringUtils.isEmpty(rawFingerprint)) { rawFingerprint = rawFingerprint.replaceAll("[^a-fA-F0-9]", "") .toLowerCase(Locale.US); - if (!StringUtils.isEmpty(rawFingerprint)) { + if (rawFingerprint.length() == 64) { fingerprintsSet.add(rawFingerprint); } } @@ -237,45 +366,167 @@ private static UpdateItem extractUpdateItem(String extensionName, String reposit return null; } } - String title = CommonUtils.getJsonString(chanObject, "title"); - String name = CommonUtils.getJsonString(chanObject, "name"); - int code = chanObject.getInt("code"); - if (installedCode != null && code < installedCode) { + if (sha256sumString != null) { + sha256sumString = sha256sumString.replaceAll("[^a-fA-F0-9]", "").toLowerCase(Locale.US); + } + if (installedCode != null && versionCode < installedCode || source == null) { return null; } - long length = chanObject.getLong("length"); - String source = CommonUtils.getJsonString(chanObject, "source"); - if (source == null) { + byte[] sha256sum = null; + if (sha256sumString != null && sha256sumString.length() == 64) { + sha256sum = new byte[sha256sumString.length() / 2]; + for (int i = 0; i < sha256sum.length; i++) { + int h = sha256sumString.charAt(2 * i); + int l = sha256sumString.charAt(2 * i + 1); + h = h >= 'a' ? h - 'a' + 10 : h - '0'; + l = l >= 'a' ? l - 'a' + 10 : l - '0'; + sha256sum[i] = (byte) ((h << 4) | l); + } + } else if (requireFingerprintChecksum) { return null; } - Uri sourceUri = normalizeUri(Uri.parse(source), uri); - source = sourceUri.toString(); + Uri sourceUri = normalizeRelativeUri(uri, source); if (ChanManager.EXTENSION_NAME_CLIENT.equals(extensionName)) { - int minVersion = chanObject.getInt("minVersion"); - int maxVersion = chanObject.getInt("maxVersion"); - return UpdateItem.createClient(repository, title, name, code, minVersion, maxVersion, length, source); + if (minApiVersion <= 0 || maxApiVersion <= 0) { + return null; + } + return new PackageItem(repository, title, versionName, versionCode, + minApiVersion, maxApiVersion, 0, length, sourceUri, sha256sum); } else { - int version = chanObject.optInt("version"); - return UpdateItem.createExtension(repository, title, name, code, version, length, source, ignoreVersion); + return new PackageItem(repository, title, versionName, versionCode, + 0, 0, apiVersion, length, sourceUri, sha256sum); + } + } + + private static void handleUpdateItems(Response response, String extensionName, + HashMap updateDataMap, HashMap fingerprintsMap, + JSONArray packagesArray, DataVersion dataVersion, + ApplicationItem updateApplicationItem) throws JSONException { + ApplicationItem applicationItem = updateDataMap.get(extensionName); + if (updateApplicationItem == null || applicationItem.type == updateApplicationItem.type) { + long installedCode = applicationItem.packageItems.get(0).versionCode; + ChanManager.Fingerprints fingerprints = fingerprintsMap.get(extensionName); + for (int i = 0; i < packagesArray.length(); i++) { + PackageItem packageItem = extractPackageItem(dataVersion, packagesArray.getJSONObject(i), + extensionName, response.getRepositoryName(), response.uri, installedCode, fingerprints); + if (packageItem != null) { + applicationItem.packageItems.add(packageItem); + } + } + } + } + + private static void handleInstallItems(Response response, String extensionName, + HashMap installDataMap, JSONArray packagesArray, DataVersion dataVersion, + ApplicationItem installApplicationItem) throws JSONException { + for (int i = 0; i < packagesArray.length(); i++) { + PackageItem packageItem = extractPackageItem(dataVersion, packagesArray.getJSONObject(i), + extensionName, response.getRepositoryName(), response.uri, null, null); + if (packageItem != null) { + ApplicationItem applicationItem = installDataMap.get(extensionName); + if (applicationItem == null) { + if (installApplicationItem != null) { + applicationItem = new ApplicationItem(installApplicationItem.type, installApplicationItem.name, + installApplicationItem.title, new ArrayList<>()); + } else { + applicationItem = new ApplicationItem(ApplicationItem.Type.CHAN, extensionName, + extensionName, new ArrayList<>()); + } + installDataMap.put(extensionName, applicationItem); + } else if (installApplicationItem != null && applicationItem.type != installApplicationItem.type) { + continue; + } + applicationItem.packageItems.add(packageItem); + } + } + } + + private enum DataVersion { + V1("data-v1.json"), + LEGACY("data.json"); + + public final String fileName; + + DataVersion(String fileName) { + this.fileName = fileName; + } + } + + private static class TargetUri { + public final Uri uri; + public final boolean directory; + + public TargetUri(Uri uri) { + Uri.Builder builder = uri.buildUpon(); + builder.scheme(""); + String name = uri.getLastPathSegment(); + boolean directory = false; + for (DataVersion dataVersion : DataVersion.values()) { + if (name.equals(dataVersion.fileName)) { + directory = true; + break; + } + } + if (directory) { + String path = uri.getPath(); + builder.path(path.substring(0, path.lastIndexOf('/'))); + } + this.uri = builder.build(); + this.directory = directory; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof TargetUri) { + TargetUri targetUri = (TargetUri) o; + return uri.equals(targetUri.uri) && directory == targetUri.directory; + } + return false; + } + + @Override + public int hashCode() { + int prime = 31; + int result = 1; + result = prime * result + uri.hashCode(); + result = prime * result + (directory ? 1231 : 1237); + return result; } } private static class Response { public Uri uri; + public final DataVersion dataVersion; public final JSONObject jsonObject; public final HashSet extensionNames; - public Response(Uri uri, JSONObject jsonObject, HashSet extensionNames) { + public Response(Uri uri, DataVersion dataVersion, JSONObject jsonObject, HashSet extensionNames) { this.uri = uri; + this.dataVersion = dataVersion; this.jsonObject = jsonObject; this.extensionNames = extensionNames; } public String getRepositoryName() { - JSONObject metaObject = jsonObject.optJSONObject(ChanManager.EXTENSION_NAME_META); String repository = null; - if (metaObject != null) { - repository = metaObject.optString("repository"); + switch (dataVersion) { + case LEGACY: { + JSONObject metaObject = jsonObject.optJSONObject(ChanManager.EXTENSION_NAME_META); + if (metaObject != null) { + repository = metaObject.optString("repository"); + } + break; + } + case V1: { + repository = jsonObject.optString("title"); + break; + } + default: { + throw new IllegalStateException(); + } } if (StringUtils.isEmpty(repository)) { repository = "Unknown repository"; @@ -284,49 +535,92 @@ public String getRepositoryName() { } } - private static Iterable readData(HttpHolder holder, - Iterable extensionItems) { + private static Iterable readData(HttpHolder holder, Iterable extensionItems) { Chan chan = Chan.getFallback(); - LinkedHashMap> targets = new LinkedHashMap<>(); + LinkedHashMap> targets = new LinkedHashMap<>(); + HashMap requestedScheme = new HashMap<>(); { - Uri applicationUri = chan.locator.setScheme(Uri.parse(BuildConfig.URI_UPDATES)); + Uri uri = Uri.parse(BuildConfig.URI_UPDATES); + TargetUri targetUri = new TargetUri(uri); HashSet extensionNames = new HashSet<>(); extensionNames.add(ChanManager.EXTENSION_NAME_CLIENT); - targets.put(applicationUri, extensionNames); + targets.put(targetUri, extensionNames); + String scheme = uri.getScheme(); + if (!StringUtils.isEmpty(scheme)) { + requestedScheme.put(targetUri, scheme); + } } for (ChanManager.ExtensionItem extensionItem : extensionItems) { if (extensionItem.updateUri != null) { - Uri uri = chan.locator.setScheme(extensionItem.updateUri); - HashSet extensionNames = targets.get(uri); + TargetUri targetUri = new TargetUri(extensionItem.updateUri); + HashSet extensionNames = targets.get(targetUri); if (extensionNames == null) { extensionNames = new HashSet<>(); - targets.put(uri, extensionNames); + targets.put(targetUri, extensionNames); + } + extensionNames.add(extensionItem.name); + String scheme = extensionItem.updateUri.getScheme(); + if (!StringUtils.isEmpty(scheme)) { + requestedScheme.put(targetUri, scheme); } - extensionNames.add(extensionItem.extensionName); } } - LinkedHashMap responses = new LinkedHashMap<>(); - for (Map.Entry> entry : targets.entrySet()) { + LinkedHashMap responses = new LinkedHashMap<>(); + for (Map.Entry> entry : targets.entrySet()) { try { - Uri uri = entry.getKey(); + TargetUri targetUri = entry.getKey(); + String targetScheme = requestedScheme.get(targetUri); int redirects = 0; while (redirects++ < 5) { - String uriKey = uri.buildUpon().scheme("").build().toString(); - Response response = responses.get(uriKey); + Response response = responses.get(targetUri); if (response != null) { - if ("http".equals(response.uri.getScheme()) && "https".equals(uri.getScheme())) { - response.uri = uri; + if ("http".equals(response.uri.getScheme()) && "https".equals(targetScheme)) { + response.uri = response.uri.buildUpon().scheme("https").build(); } response.extensionNames.addAll(entry.getValue()); break; } - JSONObject jsonObject = new JSONObject(new HttpRequest(uri, holder).perform().readString()); + Uri responseUri = null; + String responseText = null; + DataVersion responseDataVersion = null; + if (targetUri.directory) { + HttpException lastHttpException = null; + Uri directoryUri = chan.locator.setSchemeIfEmpty(targetUri.uri, targetScheme); + for (DataVersion dataVersion : DataVersion.values()) { + Uri uri = directoryUri.buildUpon().appendPath(dataVersion.fileName).build(); + try { + responseUri = uri; + responseText = new HttpRequest(uri, holder).perform().readString(); + responseDataVersion = dataVersion; + lastHttpException = null; + break; + } catch (HttpException e) { + if (!e.isHttpException() || e.getResponseCode() != HttpURLConnection.HTTP_NOT_FOUND) { + throw e; + } else { + lastHttpException = e; + } + } + } + if (lastHttpException != null) { + throw lastHttpException; + } + } else { + Uri uri = chan.locator.setSchemeIfEmpty(targetUri.uri, targetScheme); + responseUri = uri; + responseText = new HttpRequest(uri, holder).perform().readString(); + responseDataVersion = DataVersion.LEGACY; + } + JSONObject jsonObject = new JSONObject(responseText); String redirect = CommonUtils.optJsonString(jsonObject, "redirect"); if (redirect != null) { - uri = normalizeUri(Uri.parse(redirect), uri); + Uri uri = normalizeRelativeUri(responseUri, redirect); + targetUri = new TargetUri(uri); + targetScheme = uri.getScheme(); } else { - responses.put(uriKey, new Response(uri, jsonObject, new HashSet<>(entry.getValue()))); + responses.put(targetUri, new Response(responseUri, responseDataVersion, + jsonObject, new HashSet<>(entry.getValue()))); break; } } @@ -362,29 +656,34 @@ protected Pair run(HttpHolder holder) { fingerprintsMap.put(ChanManager.EXTENSION_NAME_CLIENT, ChanManager.getInstance().getApplicationFingerprints()); for (ChanManager.ExtensionItem extensionItem : extensionItems) { - fingerprintsMap.put(extensionItem.extensionName, extensionItem.fingerprints); + fingerprintsMap.put(extensionItem.name, extensionItem.fingerprints); } + String applicationTitle; String applicationVersionName; long applicationVersionCode; try { PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + applicationTitle = packageInfo.applicationInfo.loadLabel(context.getPackageManager()).toString(); applicationVersionName = packageInfo.versionName; applicationVersionCode = PackageInfoCompat.getLongVersionCode(packageInfo); } catch (Exception e) { throw new RuntimeException(e); } - HashMap> updateDataMap = new HashMap<>(); + HashMap updateDataMap = new HashMap<>(); { - List updateItems = new ArrayList<>(); - updateItems.add(UpdateItem.createClient(null, null, applicationVersionName, applicationVersionCode, - ChanManager.MIN_VERSION, ChanManager.MAX_VERSION, -1, null)); - updateDataMap.put(ChanManager.EXTENSION_NAME_CLIENT, updateItems); + ApplicationItem applicationItem = new ApplicationItem(ApplicationItem.Type.CLIENT, + ChanManager.EXTENSION_NAME_CLIENT, applicationTitle, new ArrayList<>()); + applicationItem.packageItems.add(new PackageItem(null, null, + applicationVersionName, applicationVersionCode, + ChanManager.MIN_VERSION, ChanManager.MAX_VERSION, 0, -1, null, null)); + updateDataMap.put(ChanManager.EXTENSION_NAME_CLIENT, applicationItem); for (ChanManager.ExtensionItem extensionItem : extensionItems) { - updateItems = new ArrayList<>(); - updateItems.add(UpdateItem.createExtension(null, null, extensionItem.versionName, - extensionItem.versionCode, extensionItem.version, -1, null, - extensionItem.type == ChanManager.ExtensionItem.Type.LIBRARY)); - updateDataMap.put(extensionItem.extensionName, updateItems); + applicationItem = new ApplicationItem(extensionItem.type == ChanManager.ExtensionItem.Type.LIBRARY + ? ApplicationItem.Type.LIBRARY : ApplicationItem.Type.CHAN, + extensionItem.name, extensionItem.title, new ArrayList<>()); + applicationItem.packageItems.add(new PackageItem(null, null, extensionItem.versionName, + extensionItem.versionCode, 0, 0, extensionItem.apiVersion, -1, null, null)); + updateDataMap.put(extensionItem.name, applicationItem); } } if (isCancelled()) { @@ -401,26 +700,36 @@ protected Pair run(HttpHolder holder) { for (Response response : responses) { try { - Iterator keys = response.jsonObject.keys(); - while (keys.hasNext()) { - String extensionName = keys.next(); - if (ChanManager.EXTENSION_NAME_META.equals(extensionName)) { - continue; + switch (response.dataVersion) { + case LEGACY: { + Iterator keys = response.jsonObject.keys(); + while (keys.hasNext()) { + String extensionName = keys.next(); + if (ChanManager.EXTENSION_NAME_META.equals(extensionName)) { + continue; + } + if (response.extensionNames.contains(extensionName)) { + JSONArray packagesArray = response.jsonObject.getJSONArray(extensionName); + handleUpdateItems(response, extensionName, updateDataMap, fingerprintsMap, + packagesArray, DataVersion.LEGACY, null); + } + } + break; } - if (response.extensionNames.contains(extensionName)) { - List updateItems = updateDataMap.get(extensionName); - long installedCode = updateItems.get(0).code; - ChanManager.Fingerprints fingerprints = fingerprintsMap.get(extensionName); - boolean ignoreVersion = updateItems.get(0).ignoreVersion; - JSONArray jsonArray = response.jsonObject.getJSONArray(extensionName); - for (int i = 0; i < jsonArray.length(); i++) { - UpdateItem updateItem = extractUpdateItem(extensionName, response.getRepositoryName(), - response.uri, installedCode, fingerprints, ignoreVersion, - jsonArray.getJSONObject(i)); - if (updateItem != null) { - updateItems.add(updateItem); + case V1: { + JSONArray jsonArray = response.jsonObject.optJSONArray("applications"); + if (jsonArray != null && jsonArray.length() > 0) { + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + ApplicationItem extractedItem = ApplicationItem.fromJsonV1(jsonObject); + if (response.extensionNames.contains(extractedItem.name)) { + JSONArray packagesArray = jsonObject.getJSONArray("packages"); + handleUpdateItems(response, extractedItem.name, updateDataMap, fingerprintsMap, + packagesArray, DataVersion.V1, extractedItem); + } } } + break; } } } catch (JSONException e) { @@ -431,29 +740,39 @@ protected Pair run(HttpHolder holder) { } } - HashMap> installDataMap = new HashMap<>(); + HashMap installDataMap = new HashMap<>(); for (Response response : responses) { try { - Iterator keys = response.jsonObject.keys(); - while (keys.hasNext()) { - String extensionName = keys.next(); - if (ChanManager.EXTENSION_NAME_META.equals(extensionName)) { - continue; + switch (response.dataVersion) { + case LEGACY: { + Iterator keys = response.jsonObject.keys(); + while (keys.hasNext()) { + String extensionName = keys.next(); + if (ChanManager.EXTENSION_NAME_META.equals(extensionName)) { + continue; + } + if (!updateDataMap.containsKey(extensionName)) { + JSONArray packagesArray = response.jsonObject.getJSONArray(extensionName); + handleInstallItems(response, extensionName, installDataMap, + packagesArray, DataVersion.LEGACY, null); + } + } + break; } - if (!updateDataMap.containsKey(extensionName)) { - JSONArray jsonArray = response.jsonObject.getJSONArray(extensionName); - for (int i = 0; i < jsonArray.length(); i++) { - UpdateItem updateItem = extractUpdateItem(extensionName, response.getRepositoryName(), - response.uri, null, null, false, jsonArray.getJSONObject(i)); - if (updateItem != null) { - List updateItems = installDataMap.get(extensionName); - if (updateItems == null) { - updateItems = new ArrayList<>(); - installDataMap.put(extensionName, updateItems); + case V1: { + JSONArray jsonArray = response.jsonObject.optJSONArray("applications"); + if (jsonArray != null && jsonArray.length() > 0) { + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + ApplicationItem extractedItem = ApplicationItem.fromJsonV1(jsonObject); + if (!updateDataMap.containsKey(extractedItem.name)) { + JSONArray packagesArray = jsonObject.getJSONArray("packages"); + handleInstallItems(response, extractedItem.name, installDataMap, + packagesArray, DataVersion.V1, extractedItem); } - updateItems.add(updateItem); } } + break; } } } catch (JSONException e) { diff --git a/src/com/mishiranu/dashchan/content/async/ReadVideoTask.java b/src/com/mishiranu/dashchan/content/async/ReadVideoTask.java index 3e59e6a1..f6016b6f 100644 --- a/src/com/mishiranu/dashchan/content/async/ReadVideoTask.java +++ b/src/com/mishiranu/dashchan/content/async/ReadVideoTask.java @@ -128,7 +128,7 @@ protected Boolean run(HttpHolder holder) { file.write(buffer, 0, count); read += count; if (start <= 0) { - progressHandler.onProgressUpdate(read); + progressHandler.updateProgress(read); } else { notifyProgress(new long[] {read}); } diff --git a/src/com/mishiranu/dashchan/content/async/SendLocalArchiveTask.java b/src/com/mishiranu/dashchan/content/async/SendLocalArchiveTask.java index eb616fbf..81a1de4f 100644 --- a/src/com/mishiranu/dashchan/content/async/SendLocalArchiveTask.java +++ b/src/com/mishiranu/dashchan/content/async/SendLocalArchiveTask.java @@ -237,7 +237,7 @@ protected Result run() { String iconPath = archiveName + "/" + DIRECTORY_THUMBNAILS + "/" + iconName; htmlBuilder.addIcon(iconPath, icon.title); if (downloadIcon && saveThumbnails) { - thumbnailsToDownload.add(new DownloadService.DownloadItem(chan.name, iconUri, iconName)); + thumbnailsToDownload.add(new DownloadService.DownloadItem(chan.name, iconUri, iconName, null)); } } } @@ -265,11 +265,11 @@ protected Result run() { file.width, file.height); if (saveFiles) { filesToDownload.add(new DownloadService.DownloadItem(chan.name, - fileUri, fileName)); + fileUri, fileName, null)); } if (saveThumbnails && thumbnailUri != null) { thumbnailsToDownload.add(new DownloadService.DownloadItem(chan.name, - thumbnailUri, thumbnailName)); + thumbnailUri, thumbnailName, null)); } } } diff --git a/src/com/mishiranu/dashchan/content/async/TimedProgressHandler.java b/src/com/mishiranu/dashchan/content/async/TimedProgressHandler.java index 5f3ed07e..c2fc4365 100644 --- a/src/com/mishiranu/dashchan/content/async/TimedProgressHandler.java +++ b/src/com/mishiranu/dashchan/content/async/TimedProgressHandler.java @@ -3,10 +3,8 @@ import android.os.SystemClock; import chan.http.HttpRequest; import chan.http.MultipartEntity; -import com.mishiranu.dashchan.util.IOUtils; -public class TimedProgressHandler implements IOUtils.CopyProgressListener, HttpRequest.OutputListener, - MultipartEntity.OpenableOutputListener { +public class TimedProgressHandler implements HttpRequest.OutputListener, MultipartEntity.OpenableOutputListener { private final long[] lastProgressUpdate = new long[3]; private long progressMax = -1; @@ -19,8 +17,7 @@ private boolean checkNeedToUpdate(int index, long progress, long progressMax) { return false; } - @Override - public void onProgressUpdate(long count) { + public void updateProgress(long count) { if (checkNeedToUpdate(0, count, progressMax)) { onProgressChange(count, progressMax); } diff --git a/src/com/mishiranu/dashchan/content/service/DownloadService.java b/src/com/mishiranu/dashchan/content/service/DownloadService.java index 9ad5249b..54c3f41a 100644 --- a/src/com/mishiranu/dashchan/content/service/DownloadService.java +++ b/src/com/mishiranu/dashchan/content/service/DownloadService.java @@ -266,11 +266,11 @@ private void startNextTask() { IOUtils.close(output); } onFinishDownloadingInternal(success, new TaskData(taskData.chanName, taskData.overwrite, - (InputStream) null, taskData.target, taskData.path, taskData.name, taskData.allowWrite)); + null, taskData.target, taskData.path, taskData.name, taskData.allowWrite)); } else { Chan chan = Chan.getPreferred(taskData.chanName, taskData.uri); ReadFileTask readFileTask = ReadFileTask.createShared(this, chan, - taskData.uri, getDataFile(taskData), taskData.overwrite); + taskData.uri, getDataFile(taskData), taskData.overwrite, taskData.checkSha256); activeTask = new Pair<>(taskData, readFileTask); readFileTask.execute(SINGLE_THREAD_EXECUTOR); } @@ -320,9 +320,9 @@ private void handleRequests() { directRequest.target, directRequest.path, downloadItem.name, directRequest.allowWrite)); } else { for (DownloadItem downloadItem : directRequest.downloadItems) { - enqueue(new TaskData(downloadItem.chanName, directRequest.overwrite, downloadItem.uri, - directRequest.target, directRequest.path, downloadItem.name, - directRequest.allowWrite)); + enqueue(new TaskData(downloadItem.chanName, directRequest.overwrite, + downloadItem.uri, downloadItem.checkSha256, directRequest.target, + directRequest.path, downloadItem.name, directRequest.allowWrite)); } } } @@ -470,7 +470,8 @@ private DirectRequest createDirectRequestKeepAll(ReplaceRequest replaceRequest, } while (children.contains(name.toLowerCase(Locale.getDefault())) || keys.contains(key) || activeKeys.contains(key)); keys.add(key); - finalItems.add(new DownloadService.DownloadItem(downloadItem.chanName, downloadItem.uri, name)); + finalItems.add(new DownloadItem(downloadItem.chanName, + downloadItem.uri, name, downloadItem.checkSha256)); } if (Thread.interrupted()) { throw new InterruptedException(); @@ -639,7 +640,7 @@ private void open(DataFile file, boolean allowWrite) { String extension = StringUtils.getFileExtension(file.getName()); String type = MimeTypes.forExtension(extension, "image/jpeg"); if (file.exists()) { - DownloadService.ScanCallback callback = uri -> { + ScanCallback callback = uri -> { try { startActivity(new Intent(Intent.ACTION_VIEW) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION | @@ -666,7 +667,7 @@ public void downloadDirect(DataFile.Target target, String path, boolean overwrit public void downloadDirect(DataFile.Target target, String path, String name, InputStream input) { directRequests.add(new DirectRequest(target, path, true, - Collections.singletonList(new DownloadItem(null, null, name)), input, false)); + Collections.singletonList(new DownloadItem(null, null, name, null)), input, false)); handleRequests(); } @@ -816,18 +817,21 @@ private static class TaskData implements Parcelable { public final boolean overwrite; public final InputStream input; public final Uri uri; + public final byte[] checkSha256; public final DataFile.Target target; public final String path; public final String name; public final boolean allowWrite; private TaskData(String chanName, boolean finishedFromCache, boolean overwrite, - InputStream input, Uri uri, DataFile.Target target, String path, String name, boolean allowWrite) { + InputStream input, Uri uri, byte[] checkSha256, + DataFile.Target target, String path, String name, boolean allowWrite) { this.chanName = chanName; this.finishedFromCache = finishedFromCache; this.overwrite = overwrite; this.input = input; this.uri = uri; + this.checkSha256 = checkSha256; this.target = target; this.path = path; this.name = name; @@ -836,17 +840,17 @@ private TaskData(String chanName, boolean finishedFromCache, boolean overwrite, public TaskData(String chanName, boolean overwrite, InputStream input, DataFile.Target target, String path, String name, boolean allowWrite) { - this(chanName, true, overwrite, input, null, target, path, name, allowWrite); + this(chanName, true, overwrite, input, null, null, target, path, name, allowWrite); } public TaskData(String chanName, boolean overwrite, - Uri from, DataFile.Target target, String path, String name, boolean allowWrite) { - this(chanName, false, overwrite, null, from, target, path, name, allowWrite); + Uri from, byte[] checkSha256, DataFile.Target target, String path, String name, boolean allowWrite) { + this(chanName, false, overwrite, null, from, checkSha256, target, path, name, allowWrite); } public TaskData newFinishedFromCache(boolean finishedFromCache) { - return this.finishedFromCache == finishedFromCache ? this - : new TaskData(chanName, finishedFromCache, overwrite, input, uri, target, path, name, allowWrite); + return this.finishedFromCache == finishedFromCache ? this : new TaskData(chanName, finishedFromCache, + overwrite, input, uri, checkSha256, target, path, name, allowWrite); } public String getKey() { @@ -864,6 +868,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeByte((byte) (finishedFromCache ? 1 : 0)); dest.writeByte((byte) (overwrite ? 1 : 0)); dest.writeParcelable(uri, flags); + dest.writeByteArray(checkSha256); dest.writeString(target.name()); dest.writeString(path); dest.writeString(name); @@ -872,16 +877,18 @@ public void writeToParcel(Parcel dest, int flags) { public static final Creator CREATOR = new Creator() { @Override - public TaskData createFromParcel(Parcel in) { - String chanName = in.readString(); - boolean finishedFromCache = in.readByte() != 0; - boolean overwrite = in.readByte() != 0; - Uri uri = in.readParcelable(getClass().getClassLoader()); - DataFile.Target target = DataFile.Target.valueOf(in.readString()); - String path = in.readString(); - String name = in.readString(); - boolean allowWrite = in.readByte() != 0; - return new TaskData(chanName, finishedFromCache, overwrite, null, uri, target, path, name, allowWrite); + public TaskData createFromParcel(Parcel source) { + String chanName = source.readString(); + boolean finishedFromCache = source.readByte() != 0; + boolean overwrite = source.readByte() != 0; + Uri uri = source.readParcelable(getClass().getClassLoader()); + byte[] checkSha256 = source.createByteArray(); + DataFile.Target target = DataFile.Target.valueOf(source.readString()); + String path = source.readString(); + String name = source.readString(); + boolean allowWrite = source.readByte() != 0; + return new TaskData(chanName, finishedFromCache, overwrite, null, uri, checkSha256, + target, path, name, allowWrite); } @Override @@ -1358,7 +1365,7 @@ public DirectRequest complete(String path, boolean detailName, boolean originalN for (RequestItem requestItem : items) { downloadItems.add(new DownloadItem(chanName, requestItem.uri, getDesiredFileName(requestItem.uri, requestItem.fileName, originalName ? requestItem.originalName : null, detailName, - chanName, boardName, threadNumber))); + chanName, boardName, threadNumber), null)); } return new DirectRequest(DataFile.Target.DOWNLOADS, path, true, downloadItems, null, allowWrite); } @@ -1389,7 +1396,7 @@ public boolean allowOriginalName() { public DirectRequest complete(String path, boolean detailName, boolean originalName) { String fileName = detailName ? getFileNameWithChanBoardThreadData(this.fileName, chanName, boardName, threadNumber) : this.fileName; - DownloadItem downloadItem = new DownloadService.DownloadItem(chanName, null, fileName); + DownloadItem downloadItem = new DownloadItem(chanName, null, fileName, null); return new DirectRequest(DataFile.Target.DOWNLOADS, path, true, Collections.singletonList(downloadItem), input, allowWrite); } @@ -1418,11 +1425,13 @@ public static class DownloadItem implements Parcelable { public final String chanName; public final Uri uri; public final String name; + public final byte[] checkSha256; - public DownloadItem(String chanName, Uri uri, String name) { + public DownloadItem(String chanName, Uri uri, String name, byte[] checkSha256) { this.chanName = chanName; this.uri = uri; this.name = name; + this.checkSha256 = checkSha256; } @Override @@ -1435,6 +1444,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(chanName); dest.writeString(uri != null ? uri.toString() : null); dest.writeString(name); + dest.writeByteArray(checkSha256); } public static final Creator CREATOR = new Creator() { @@ -1448,7 +1458,8 @@ public DownloadItem createFromParcel(Parcel source) { String chanName = source.readString(); String uriString = source.readString(); String name = source.readString(); - return new DownloadItem(chanName, uriString != null ? Uri.parse(uriString) : null, name); + byte[] checkSha256 = source.createByteArray(); + return new DownloadItem(chanName, uriString != null ? Uri.parse(uriString) : null, name, checkSha256); } }; diff --git a/src/com/mishiranu/dashchan/media/VideoPlayer.java b/src/com/mishiranu/dashchan/media/VideoPlayer.java index 23a2c99a..185cd5ce 100644 --- a/src/com/mishiranu/dashchan/media/VideoPlayer.java +++ b/src/com/mishiranu/dashchan/media/VideoPlayer.java @@ -40,7 +40,7 @@ public static Pair loadLibraries(Context context) { return new Pair<>(true, null); } ChanManager.ExtensionItem extensionItem = ChanManager.getInstance() - .getLibExtension(ChanManager.EXTENSION_NAME_LIB_WEBM); + .getLibraryExtension(ChanManager.EXTENSION_NAME_LIB_WEBM); if (extensionItem != null) { String dir = extensionItem.getNativeLibraryDir(); if (dir != null) { diff --git a/src/com/mishiranu/dashchan/ui/ExtensionsTrustLoop.java b/src/com/mishiranu/dashchan/ui/ExtensionsTrustLoop.java index b1a07670..5ab71a9b 100644 --- a/src/com/mishiranu/dashchan/ui/ExtensionsTrustLoop.java +++ b/src/com/mishiranu/dashchan/ui/ExtensionsTrustLoop.java @@ -75,14 +75,14 @@ public static void handleUntrustedExtensions(Context context, State state) { SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); message.append("SHA-256 fingerprint:\n").append(fingerprints); AlertDialog dialog = new AlertDialog.Builder(context) - .setTitle(extensionItem.extensionName).setMessage(message) + .setTitle(extensionItem.title).setMessage(message) .setCancelable(false) .setPositiveButton(android.R.string.ok, (d, w) -> { - ChanManager.getInstance().changeUntrustedExtensionState(extensionItem.extensionName, true); + ChanManager.getInstance().changeUntrustedExtensionState(extensionItem.name, true); handleUntrustedExtensions(context, state); }) .setNegativeButton(android.R.string.cancel, (d, w) -> { - ChanManager.getInstance().changeUntrustedExtensionState(extensionItem.extensionName, false); + ChanManager.getInstance().changeUntrustedExtensionState(extensionItem.name, false); handleUntrustedExtensions(context, state); }) .setNeutralButton(R.string.details, (d, w) -> { diff --git a/src/com/mishiranu/dashchan/ui/preference/ThemesFragment.java b/src/com/mishiranu/dashchan/ui/preference/ThemesFragment.java index 40ddfa9c..7c12a29b 100644 --- a/src/com/mishiranu/dashchan/ui/preference/ThemesFragment.java +++ b/src/com/mishiranu/dashchan/ui/preference/ThemesFragment.java @@ -413,7 +413,7 @@ public ReadThemesTask(ThemesViewModel viewModel) { @Override protected Pair> run(HttpHolder holder) { try { - Uri uri = Chan.getFallback().locator.setScheme(Uri.parse(BuildConfig.URI_THEMES)); + Uri uri = Chan.getFallback().locator.setSchemeIfEmpty(Uri.parse(BuildConfig.URI_THEMES), null); int redirects = 0; while (redirects++ < 5) { JSONObject jsonObject = new JSONObject(new HttpRequest(uri, holder).perform().readString()); @@ -422,7 +422,7 @@ protected Pair> run(HttpHolder holder) { } String redirect = CommonUtils.optJsonString(jsonObject, "redirect"); if (redirect != null) { - uri = ReadUpdateTask.normalizeUri(Uri.parse(redirect), uri); + uri = ReadUpdateTask.normalizeRelativeUri(uri, redirect); continue; } JSONArray jsonArray = jsonObject.getJSONArray("themes"); diff --git a/src/com/mishiranu/dashchan/ui/preference/UpdateFragment.java b/src/com/mishiranu/dashchan/ui/preference/UpdateFragment.java index 2fcb2022..6eb774f3 100644 --- a/src/com/mishiranu/dashchan/ui/preference/UpdateFragment.java +++ b/src/com/mishiranu/dashchan/ui/preference/UpdateFragment.java @@ -3,7 +3,6 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.net.Uri; import android.os.Bundle; import android.text.SpannableString; import android.text.SpannableStringBuilder; @@ -49,6 +48,7 @@ import com.mishiranu.dashchan.widget.ViewFactory; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.List; @@ -74,10 +74,10 @@ private static final class ListItem { public int targetIndex; public String warning; - public static ListItem create(String extensionName, boolean enabled, boolean installed) { + public static ListItem create(String extensionName, String extensionTitle, boolean enabled, boolean installed) { String title = Chan.get(extensionName).configuration.getTitle(); if (title == null) { - title = extensionName; + title = extensionTitle; } return new ListItem(extensionName, title, enabled, installed); } @@ -97,16 +97,16 @@ public boolean willBeInstalled() { return !isHeader() && (installed && targetIndex > 0 || !installed && targetIndex >= 0); } - public void setTarget(Context context, List updateItems, int targetIndex) { + public void setTarget(Context context, ReadUpdateTask.ApplicationItem applicationItem, int targetIndex) { this.targetIndex = targetIndex; if (installed && targetIndex > 0 || !installed && targetIndex >= 0) { - ReadUpdateTask.UpdateItem updateItem = updateItems.get(targetIndex); - String target = updateItem.title; + ReadUpdateTask.PackageItem packageItem = applicationItem.packageItems.get(targetIndex); + String target = packageItem.title; if (context != null) { - target = context.getString(R.string.__enumeration_format, target, updateItem.name); - if (updateItem.length > 0) { + target = context.getString(R.string.__enumeration_format, target, packageItem.versionName); + if (packageItem.length > 0) { target = context.getString(R.string.__enumeration_format, target, - StringUtils.formatFileSize(updateItem.length, false)); + StringUtils.formatFileSize(packageItem.length, false)); } } this.target = target; @@ -232,34 +232,36 @@ public void onSaveInstanceState(@NonNull Bundle outState) { return ((Adapter) getRecyclerView().getAdapter()).configureDivider(configuration, position); } - private static boolean checkVersionValid(ReadUpdateTask.UpdateItem updateItem, int minVersion, int maxVersion) { - return updateItem.ignoreVersion || updateItem.version >= minVersion && updateItem.version <= maxVersion; + private static boolean checkVersionValid(ReadUpdateTask.ApplicationItem applicationItem, + ReadUpdateTask.PackageItem packageItem, int minApiVersion, int maxApiVersion) { + return applicationItem.type != ReadUpdateTask.ApplicationItem.Type.CHAN || + packageItem.apiVersion >= minApiVersion && packageItem.apiVersion <= maxApiVersion; } - private static boolean compareForUpdates(ReadUpdateTask.UpdateItem installedUpdateItem, - ReadUpdateTask.UpdateItem newUpdateItem) { - return newUpdateItem.code > installedUpdateItem.code || - !CommonUtils.equals(newUpdateItem.name, installedUpdateItem.name); + private static boolean compareForUpdates(ReadUpdateTask.PackageItem installed, ReadUpdateTask.PackageItem update) { + return update.versionCode > installed.versionCode || + !CommonUtils.equals(installed.versionName, update.versionName); } - private static ListItem handleAddListItem(Context context, List updateItems, - String extensionName, Bundle savedInstanceState, int minVersion, int maxVersion, + private static ListItem handleAddListItem(Context context, ReadUpdateTask.ApplicationItem applicationItem, + Bundle savedInstanceState, int minApiVersion, int maxApiVersion, boolean installed, String warningUnsupported) { - ListItem listItem = ListItem.create(extensionName, updateItems.size() >= (installed ? 2 : 1), installed); + ListItem listItem = ListItem.create(applicationItem.name, applicationItem.title, + applicationItem.packageItems.size() >= (installed ? 2 : 1), installed); int targetIndex = savedInstanceState != null ? savedInstanceState - .getInt(EXTRA_TARGET_PREFIX + extensionName, -1) : -1; + .getInt(EXTRA_TARGET_PREFIX + applicationItem.name, -1) : -1; if (targetIndex < 0) { if (installed) { - ReadUpdateTask.UpdateItem installedExtensionData = updateItems.get(0); - if (checkVersionValid(installedExtensionData, minVersion, maxVersion)) { + ReadUpdateTask.PackageItem installedExtensionData = applicationItem.packageItems.get(0); + if (checkVersionValid(applicationItem, installedExtensionData, minApiVersion, maxApiVersion)) { targetIndex = 0; } - for (int i = 1; i < updateItems.size(); i++) { - ReadUpdateTask.UpdateItem newUpdateItem = updateItems.get(i); - if (checkVersionValid(newUpdateItem, minVersion, maxVersion)) { + for (int i = 1; i < applicationItem.packageItems.size(); i++) { + ReadUpdateTask.PackageItem updatePackageItem = applicationItem.packageItems.get(i); + if (checkVersionValid(applicationItem, updatePackageItem, minApiVersion, maxApiVersion)) { // targetIndex < 0 - means installed version is not supported - if (targetIndex < 0 || VERSION_TITLE_RELEASE.equals(newUpdateItem.title) - && compareForUpdates(installedExtensionData, newUpdateItem)) { + if (targetIndex < 0 || VERSION_TITLE_RELEASE.equals(updatePackageItem.title) + && compareForUpdates(installedExtensionData, updatePackageItem)) { targetIndex = i; break; } @@ -272,94 +274,112 @@ && compareForUpdates(installedExtensionData, newUpdateItem)) { } } else { // Restore state - if (!checkVersionValid(updateItems.get(targetIndex), minVersion, maxVersion)) { + ReadUpdateTask.PackageItem packageItem = applicationItem.packageItems.get(targetIndex); + if (!checkVersionValid(applicationItem, packageItem, minApiVersion, maxApiVersion)) { listItem.warning = warningUnsupported; } } - listItem.setTarget(context, updateItems, targetIndex); + listItem.setTarget(context, applicationItem, targetIndex); return listItem; } + @SuppressWarnings("ComparatorCombinators") + private static final Comparator UPDATE_DATA_COMPARATOR = (lhs, rhs) -> { + int result = lhs.type.compareTo(rhs.type); + if (result != 0) { + return result; + } + result = lhs.title.compareTo(rhs.title); + if (result != 0) { + return result; + } + return lhs.name.compareTo(rhs.name); + }; + + private static ArrayList collectSorted + (ReadUpdateTask.UpdateDataMap updateDataMap, boolean installed) { + ArrayList applicationItems = new ArrayList<>(); + for (String extensionName : updateDataMap.extensionNames(installed)) { + applicationItems.add(updateDataMap.get(extensionName, installed)); + } + Collections.sort(applicationItems, UPDATE_DATA_COMPARATOR); + return applicationItems; + } + private static List buildData(Context context, ReadUpdateTask.UpdateDataMap updateDataMap, Bundle savedInstanceState) { ArrayList listItems = new ArrayList<>(); String warningUnsupported = context != null ? context.getString(R.string.unsupported_version) : null; HashSet handledExtensionNames = new HashSet<>(); - int minVersion; - int maxVersion; + int minApiVersion; + int maxApiVersion; { - List updateItems = updateDataMap.get(ChanManager.EXTENSION_NAME_CLIENT, true); + ReadUpdateTask.ApplicationItem applicationItem = updateDataMap.get(ChanManager.EXTENSION_NAME_CLIENT, true); ListItem listItem = new ListItem(ChanManager.EXTENSION_NAME_CLIENT, context != null ? AndroidUtils.getApplicationLabel(context) : null, - updateItems.size() >= 2, true); + applicationItem.packageItems.size() >= 2, true); int targetIndex = savedInstanceState != null ? savedInstanceState.getInt(EXTRA_TARGET_PREFIX + listItem.extensionName, -1) : -1; if (targetIndex < 0) { targetIndex = 0; - for (int i = 1; i < updateItems.size(); i++) { - ReadUpdateTask.UpdateItem newUpdateItem = updateItems.get(i); - if (VERSION_TITLE_RELEASE.equals(newUpdateItem.title) && - compareForUpdates(updateItems.get(0), newUpdateItem)) { + for (int i = 1; i < applicationItem.packageItems.size(); i++) { + ReadUpdateTask.PackageItem updatePackageItem = applicationItem.packageItems.get(i); + if (VERSION_TITLE_RELEASE.equals(updatePackageItem.title) && + compareForUpdates(applicationItem.packageItems.get(0), updatePackageItem)) { targetIndex = 1; break; } } } - listItem.setTarget(context, updateItems, targetIndex); - ReadUpdateTask.UpdateItem currentAppUpdateItem = updateItems.get(targetIndex); - minVersion = currentAppUpdateItem.minVersion; - maxVersion = currentAppUpdateItem.version; + listItem.setTarget(context, applicationItem, targetIndex); + ReadUpdateTask.PackageItem currentApplicationPackageItem = applicationItem.packageItems.get(targetIndex); + minApiVersion = currentApplicationPackageItem.minApiVersion; + maxApiVersion = currentApplicationPackageItem.maxApiVersion; listItems.add(listItem); } handledExtensionNames.add(ChanManager.EXTENSION_NAME_CLIENT); ChanManager manager = ChanManager.getInstance(); - for (Chan chan : manager.getAvailableChans()) { - List updateItems = updateDataMap.get(chan.name, true); - if (updateItems != null) { - ListItem listItem = handleAddListItem(context, updateItems, chan.name, - savedInstanceState, minVersion, maxVersion, true, warningUnsupported); - listItems.add(listItem); - handledExtensionNames.add(chan.name); - } - } for (ChanManager.ExtensionItem extensionItem : manager.getExtensionItems()) { if (extensionItem.type == ChanManager.ExtensionItem.Type.LIBRARY) { - List updateItems = updateDataMap.get(extensionItem.extensionName, true); - if (updateItems != null) { - ListItem listItem = handleAddListItem(context, updateItems, extensionItem.extensionName, - savedInstanceState, minVersion, maxVersion, true, warningUnsupported); + ReadUpdateTask.ApplicationItem applicationItem = updateDataMap.get(extensionItem.name, true); + if (applicationItem != null) { + ListItem listItem = handleAddListItem(context, applicationItem, + savedInstanceState, minApiVersion, maxApiVersion, true, warningUnsupported); listItems.add(listItem); - handledExtensionNames.add(extensionItem.extensionName); + handledExtensionNames.add(extensionItem.name); } } } - List updateExtensionNames = new ArrayList<>(updateDataMap.extensionNames(true)); - Collections.sort(updateExtensionNames); - for (String extensionName : updateExtensionNames) { - if (!handledExtensionNames.contains(extensionName)) { - List updateItems = updateDataMap.get(extensionName, true); - ListItem listItem = handleAddListItem(context, updateItems, extensionName, savedInstanceState, - minVersion, maxVersion, true, warningUnsupported); + for (Chan chan : manager.getAvailableChans()) { + ReadUpdateTask.ApplicationItem applicationItem = updateDataMap.get(chan.name, true); + if (applicationItem != null) { + ListItem listItem = handleAddListItem(context, applicationItem, + savedInstanceState, minApiVersion, maxApiVersion, true, warningUnsupported); + listItems.add(listItem); + handledExtensionNames.add(chan.name); + } + } + for (ReadUpdateTask.ApplicationItem applicationItem : collectSorted(updateDataMap, true)) { + if (!handledExtensionNames.contains(applicationItem.name)) { + ListItem listItem = handleAddListItem(context, applicationItem, savedInstanceState, + minApiVersion, maxApiVersion, true, warningUnsupported); listItems.add(listItem); - handledExtensionNames.add(extensionName); + handledExtensionNames.add(applicationItem.name); } } - List installExtensionNames = new ArrayList<>(updateDataMap.extensionNames(false)); - Collections.sort(installExtensionNames); - boolean headerAdded = false; - for (String extensionName : installExtensionNames) { - if (!handledExtensionNames.contains(extensionName)) { - if (!headerAdded) { + boolean availableHeaderAdded = false; + for (ReadUpdateTask.ApplicationItem applicationItem : collectSorted(updateDataMap, false)) { + if (!handledExtensionNames.contains(applicationItem.name)) { + if (!availableHeaderAdded) { if (context != null) { listItems.add(new ListItem("", context.getString(R.string.available__plural), false, false)); } - headerAdded = true; + availableHeaderAdded = true; } - List updateItems = updateDataMap.get(extensionName, false); - ListItem listItem = handleAddListItem(context, updateItems, extensionName, savedInstanceState, - minVersion, maxVersion, false, warningUnsupported); + ListItem listItem = handleAddListItem(context, applicationItem, savedInstanceState, + minApiVersion, maxApiVersion, false, warningUnsupported); listItems.add(listItem); - handledExtensionNames.add(extensionName); + handledExtensionNames.add(applicationItem.name); } } return listItems; @@ -369,21 +389,22 @@ private void onItemClick(ListItem listItem) { ArrayList targets = new ArrayList<>(); ArrayList repositories = new ArrayList<>(); int targetIndex; - List updateItems = updateDataMap.get(listItem.extensionName, listItem.installed); + ReadUpdateTask.ApplicationItem applicationItem = updateDataMap.get(listItem.extensionName, listItem.installed); if (listItem.installed) { targets.add(getString(R.string.keep_current_version)); repositories.add(null); - for (ReadUpdateTask.UpdateItem updateItem : updateItems.subList(1, updateItems.size())) { - targets.add(updateItem.title); - repositories.add(updateItem.repository); + for (ReadUpdateTask.PackageItem packageItem : applicationItem.packageItems + .subList(1, applicationItem.packageItems.size())) { + targets.add(packageItem.title); + repositories.add(packageItem.repository); } targetIndex = listItem.targetIndex; } else { targets.add(getString(R.string.dont_install)); repositories.add(null); - for (ReadUpdateTask.UpdateItem updateItem : updateItems) { - targets.add(updateItem.title); - repositories.add(updateItem.repository); + for (ReadUpdateTask.PackageItem packageItem : applicationItem.packageItems) { + targets.add(packageItem.title); + repositories.add(packageItem.repository); } targetIndex = listItem.targetIndex + 1; } @@ -409,7 +430,7 @@ public void onPrepareOptionsMenu(@NonNull Menu menu) { for (ListItem listItem : adapter.listItems) { if (listItem.willBeInstalled()) { length += updateDataMap.get(listItem.extensionName, listItem.installed) - .get(listItem.targetIndex).length; + .packageItems.get(listItem.targetIndex).length; } } } @@ -431,11 +452,12 @@ public boolean onOptionsItemSelected(MenuItem item) { Adapter adapter = (Adapter) getRecyclerView().getAdapter(); for (ListItem listItem : adapter.listItems) { if (listItem.willBeInstalled()) { - ReadUpdateTask.UpdateItem updateItem = updateDataMap - .get(listItem.extensionName, listItem.installed).get(listItem.targetIndex); - if (updateItem.source != null) { + ReadUpdateTask.PackageItem packageItem = updateDataMap + .get(listItem.extensionName, listItem.installed) + .packageItems.get(listItem.targetIndex); + if (packageItem.source != null) { requests.add(new UpdaterActivity.Request(listItem.extensionName, - updateItem.name, Uri.parse(updateItem.source))); + packageItem.versionName, packageItem.source, packageItem.sha256sum)); } } } @@ -467,12 +489,13 @@ private static void displayUpdateReminderDialog(FragmentManager fragmentManager) } private static void handleListItemValidity(ReadUpdateTask.UpdateDataMap updateDataMap, - ListItem listItem, int minVersion, int maxVersion, String warningUnsupported) { + ListItem listItem, int minApiVersion, int maxApiVersion, String warningUnsupported) { boolean valid = true; if (listItem.targetIndex >= 0) { - ReadUpdateTask.UpdateItem updateItem = updateDataMap - .get(listItem.extensionName, listItem.installed).get(listItem.targetIndex); - valid = checkVersionValid(updateItem, minVersion, maxVersion); + ReadUpdateTask.ApplicationItem applicationItem = updateDataMap + .get(listItem.extensionName, listItem.installed); + ReadUpdateTask.PackageItem packageItem = applicationItem.packageItems.get(listItem.targetIndex); + valid = checkVersionValid(applicationItem, packageItem, minApiVersion, maxApiVersion); } listItem.warning = valid ? null : warningUnsupported; } @@ -484,19 +507,19 @@ private static void onTargetChanged(Context context, Adapter adapter, if (!ChanManager.EXTENSION_NAME_CLIENT.equals(applicationListItem.extensionName)) { throw new IllegalStateException(); } - ReadUpdateTask.UpdateItem applicationUpdateItem = updateDataMap - .get(ChanManager.EXTENSION_NAME_CLIENT, true).get(applicationListItem.targetIndex); - int minVersion = applicationUpdateItem.minVersion; - int maxVersion = applicationUpdateItem.version; + ReadUpdateTask.PackageItem applicationPackageItem = updateDataMap + .get(ChanManager.EXTENSION_NAME_CLIENT, true).packageItems.get(applicationListItem.targetIndex); + int minApiVersion = applicationPackageItem.minApiVersion; + int maxApiVersion = applicationPackageItem.maxApiVersion; if (ChanManager.EXTENSION_NAME_CLIENT.equals(listItem.extensionName)) { for (ListItem invalidateListItem : adapter.listItems) { if (!invalidateListItem.isHeader()) { handleListItemValidity(updateDataMap, invalidateListItem, - minVersion, maxVersion, warningUnsupported); + minApiVersion, maxApiVersion, warningUnsupported); } } } else { - handleListItemValidity(updateDataMap, listItem, minVersion, maxVersion, warningUnsupported); + handleListItemValidity(updateDataMap, listItem, minApiVersion, maxApiVersion, warningUnsupported); } } @@ -505,12 +528,12 @@ private void onTargetSelected(String extensionName, int targetIndex) { for (int i = 0; i < adapter.listItems.size(); i++) { ListItem listItem = adapter.listItems.get(i); if (extensionName.equals(listItem.extensionName)) { - List updateItems = updateDataMap.get(extensionName, listItem.installed); + ReadUpdateTask.ApplicationItem applicationItem = updateDataMap.get(extensionName, listItem.installed); if (!listItem.installed) { targetIndex--; } if (listItem.targetIndex != targetIndex) { - listItem.setTarget(requireContext(), updateItems, targetIndex); + listItem.setTarget(requireContext(), applicationItem, targetIndex); onTargetChanged(requireContext(), adapter, updateDataMap, listItem); adapter.notifyDataSetChanged(); requireActivity().invalidateOptionsMenu(); diff --git a/src/com/mishiranu/dashchan/util/IOUtils.java b/src/com/mishiranu/dashchan/util/IOUtils.java index 536fed7c..1ecc089b 100644 --- a/src/com/mishiranu/dashchan/util/IOUtils.java +++ b/src/com/mishiranu/dashchan/util/IOUtils.java @@ -67,10 +67,6 @@ public static boolean readExactlyCheck(InputStream input, byte[] buffer, int off return readExactly(input, buffer, offset, count) == count; } - public interface CopyProgressListener { - void onProgressUpdate(long count); - } - public static void copyStream(InputStream from, OutputStream to) throws IOException { byte[] data = new byte[8192]; int count; @@ -79,17 +75,6 @@ public static void copyStream(InputStream from, OutputStream to) throws IOExcept } } - public static void copyStream(InputStream from, OutputStream to, CopyProgressListener listener) throws IOException { - byte[] data = new byte[8192]; - int count; - long read = 0; - while ((count = from.read(data)) != -1) { - to.write(data, 0, count); - read += count; - listener.onProgressUpdate(read); - } - } - public static boolean close(Closeable closeable) { try { if (closeable != null) {