diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index cf480441b..470d68f28 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -15,7 +15,7 @@ jobs: - uses: subosito/flutter-action@v1 with: channel: stable - flutter-version: '1.22.1' + flutter-version: '1.22.2' - name: Clone the repository. uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 280202c25..7836d6835 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - uses: subosito/flutter-action@v1 with: channel: stable - flutter-version: '1.22.1' + flutter-version: '1.22.2' # Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1): # https://issuetracker.google.com/issues/144111441 @@ -50,8 +50,8 @@ jobs: echo "${{ secrets.KEY_JKS }}" > release.keystore.asc gpg -d --passphrase "${{ secrets.KEY_JKS_PASSPHRASE }}" --batch release.keystore.asc > $AVES_STORE_FILE rm release.keystore.asc - flutter build apk --bundle-sksl-path shaders_1.22.1.sksl.json - flutter build appbundle --bundle-sksl-path shaders_1.22.1.sksl.json + flutter build apk --bundle-sksl-path shaders_1.22.2.sksl.json + flutter build appbundle --bundle-sksl-path shaders_1.22.2.sksl.json rm $AVES_STORE_FILE env: AVES_STORE_FILE: ${{ github.workspace }}/key.jks diff --git a/android/app/build.gradle b/android/app/build.gradle index 3d0d01350..e7ab475f7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,13 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' + id 'com.google.gms.google-services' + id 'com.google.firebase.crashlytics' +} + +// Flutter properties + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -5,31 +15,20 @@ if (localPropertiesFile.exists()) { localProperties.load(reader) } } - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' +def flutterRoot = localProperties.getProperty('flutter.sdk') apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +// Keys + def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { // for release using credentials stored in a local file - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + keystorePropertiesFile.withReader('UTF-8') { reader -> + keystoreProperties.load(reader) + } } else { // for release using credentials in environment variables set up by Github Actions // warning: in property file, single quotes should be escaped with a backslash @@ -42,7 +41,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 30 // latest (or latest-1 if the sources of latest SDK are unavailable) + compileSdkVersion 30 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -54,24 +53,14 @@ android { defaultConfig { applicationId "deckers.thibault.aves" - // some Java 8 APIs (java.util.stream, etc.) require minSdkVersion 24 - // Gradle plugin 4.0 desugaring features allow targeting older SDKs - // but Flutter (as of v1.17.3) fails to run in release mode when using Gradle plugin 4.0: - // https://github.com/flutter/flutter/issues/58247 + // TODO TLAD try minSdkVersion 23 when kotlin migration is done minSdkVersion 24 targetSdkVersion 30 // same as compileSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName - manifestPlaceholders = [googleApiKey:keystoreProperties['googleApiKey']] + manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey']] } -// compileOptions { -// // enable support for Java 8 language APIs (stream, optional, etc.) -// coreLibraryDesugaringEnabled true -// sourceCompatibility JavaVersion.VERSION_1_8 -// targetCompatibility JavaVersion.VERSION_1_8 -// } - signingConfigs { release { keyAlias keystoreProperties['keyAlias'] @@ -105,22 +94,16 @@ repositories { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - // enable support for Java 8 language APIs (stream, optional, etc.) -// coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9' - - implementation 'androidx.core:core:1.5.0-alpha04' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts - implementation "androidx.exifinterface:exifinterface:1.3.0" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' + implementation 'androidx.core:core-ktx:1.5.0-alpha04' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts + implementation 'androidx.exifinterface:exifinterface:1.3.1' implementation 'com.commonsware.cwac:document:0.4.1' implementation 'com.drewnoakes:metadata-extractor:2.15.0' implementation 'com.github.bumptech.glide:glide:4.11.0' - implementation 'com.google.guava:guava:29.0-android' + implementation 'com.google.guava:guava:30.0-android' - annotationProcessor 'androidx.annotation:annotation:1.1.0' - annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' + kapt 'androidx.annotation:annotation:1.1.0' + kapt 'com.github.bumptech.glide:compiler:4.11.0' compileOnly rootProject.findProject(':streams_channel') } - -apply plugin: 'com.google.gms.google-services' -apply plugin: 'com.google.firebase.crashlytics' diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index ebcc4799f..000000000 --- a/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ac8bdd2e6..0fdcda2c5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -24,7 +24,8 @@ + android:maxSdkVersion="29" + tools:ignore="ScopedStorage" /> diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java deleted file mode 100644 index 0c586276a..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java +++ /dev/null @@ -1,283 +0,0 @@ -package deckers.thibault.aves.channel.calls; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.FileProvider; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.DecodeFormat; -import com.bumptech.glide.request.FutureTarget; -import com.bumptech.glide.request.RequestOptions; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.stream.Collectors; - -import deckers.thibault.aves.utils.LogUtils; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; - -public class AppAdapterHandler implements MethodChannel.MethodCallHandler { - private static final String LOG_TAG = LogUtils.createTag(AppAdapterHandler.class); - - public static final String CHANNEL = "deckers.thibault/aves/app"; - - private Context context; - - public AppAdapterHandler(Context context) { - this.context = context; - } - - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - switch (call.method) { - case "getAppIcon": { - new Thread(() -> getAppIcon(call, new MethodResultWrapper(result))).start(); - break; - } - case "getAppNames": { - result.success(getAppNames()); - break; - } - case "getEnv": { - result.success(getEnv()); - break; - } - case "edit": { - String title = call.argument("title"); - Uri uri = Uri.parse(call.argument("uri")); - String mimeType = call.argument("mimeType"); - edit(title, uri, mimeType); - result.success(null); - break; - } - case "open": { - String title = call.argument("title"); - Uri uri = Uri.parse(call.argument("uri")); - String mimeType = call.argument("mimeType"); - open(title, uri, mimeType); - result.success(null); - break; - } - case "openMap": { - Uri geoUri = Uri.parse(call.argument("geoUri")); - openMap(geoUri); - result.success(null); - break; - } - case "setAs": { - String title = call.argument("title"); - Uri uri = Uri.parse(call.argument("uri")); - String mimeType = call.argument("mimeType"); - setAs(title, uri, mimeType); - result.success(null); - break; - } - case "share": { - String title = call.argument("title"); - Map> urisByMimeType = call.argument("urisByMimeType"); - shareMultiple(title, urisByMimeType); - result.success(null); - break; - } - default: - result.notImplemented(); - break; - } - } - - private Map getAppNames() { - Map nameMap = new HashMap<>(); - Intent intent = new Intent(Intent.ACTION_MAIN, null); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); - - // apps tend to use their name in English when creating folders - // so we get their names in English as well as the current locale - Configuration config = new Configuration(); - config.setLocale(Locale.ENGLISH); - - PackageManager pm = context.getPackageManager(); - List resolveInfoList = pm.queryIntentActivities(intent, 0); - for (ResolveInfo resolveInfo : resolveInfoList) { - ApplicationInfo ai = resolveInfo.activityInfo.applicationInfo; - boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - if (!isSystemPackage) { - String packageName = ai.packageName; - - String currentLabel = String.valueOf(pm.getApplicationLabel(ai)); - nameMap.put(currentLabel, packageName); - - int labelRes = ai.labelRes; - if (labelRes != 0) { - try { - Resources resources = pm.getResourcesForApplication(ai); - // `updateConfiguration` is deprecated but it seems to be the only way - // to query resources from another app with a specific locale. - // The following methods do not work: - // - `resources.getConfiguration().setLocale(...)` - // - getting a package manager from a custom context with `context.createConfigurationContext(config)` - resources.updateConfiguration(config, resources.getDisplayMetrics()); - String englishLabel = resources.getString(labelRes); - if (!TextUtils.equals(englishLabel, currentLabel)) { - nameMap.put(englishLabel, packageName); - } - } catch (PackageManager.NameNotFoundException e) { - Log.w(LOG_TAG, "failed to get app englishLabel for packageName=" + packageName, e); - } - } - } - } - return nameMap; - } - - private void getAppIcon(MethodCall call, MethodChannel.Result result) { - String packageName = call.argument("packageName"); - Double sizeDip = call.argument("sizeDip"); - if (packageName == null || sizeDip == null) { - result.error("getAppIcon-args", "failed because of missing arguments", null); - return; - } - - // convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter - float density = context.getResources().getDisplayMetrics().density; - int size = (int) Math.round(sizeDip * density); - - byte[] data = null; - try { - int iconResourceId = context.getPackageManager().getApplicationInfo(packageName, 0).icon; - Uri uri = new Uri.Builder() - .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) - .authority(packageName) - .path(String.valueOf(iconResourceId)) - .build(); - - RequestOptions options = new RequestOptions() - .format(DecodeFormat.PREFER_RGB_565) - .centerCrop() - .override(size, size); - FutureTarget target = Glide.with(context) - .asBitmap() - .apply(options) - .load(uri) - .submit(size, size); - - try { - Bitmap bitmap = target.get(); - if (bitmap != null) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream); - data = stream.toByteArray(); - } - } catch (Exception e) { - Log.w(LOG_TAG, "failed to decode app icon for packageName=" + packageName, e); - } - Glide.with(context).clear(target); - } catch (PackageManager.NameNotFoundException e) { - Log.w(LOG_TAG, "failed to get app info for packageName=" + packageName, e); - return; - } - if (data != null) { - result.success(data); - } else { - result.error("getAppIcon-null", "failed to get icon for packageName=" + packageName, null); - } - } - - private Map getEnv() { - return System.getenv(); - } - - private void edit(String title, Uri uri, String mimeType) { - Intent intent = new Intent(Intent.ACTION_EDIT); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - intent.setDataAndType(uri, mimeType); - context.startActivity(Intent.createChooser(intent, title)); - } - - private void open(String title, Uri uri, String mimeType) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(uri, mimeType); - context.startActivity(Intent.createChooser(intent, title)); - } - - private void openMap(Uri geoUri) { - Intent intent = new Intent(Intent.ACTION_VIEW, geoUri); - if (intent.resolveActivity(context.getPackageManager()) != null) { - context.startActivity(intent); - } - } - - private void setAs(String title, Uri uri, String mimeType) { - Intent intent = new Intent(Intent.ACTION_ATTACH_DATA); - intent.setDataAndType(uri, mimeType); - context.startActivity(Intent.createChooser(intent, title)); - } - - private void shareSingle(String title, Uri uri, String mimeType) { - Intent intent = new Intent(Intent.ACTION_SEND); - if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme())) { - String path = uri.getPath(); - if (path == null) return; - String applicationId = context.getApplicationContext().getPackageName(); - Uri apkUri = FileProvider.getUriForFile(context, applicationId + ".fileprovider", new File(path)); - intent.putExtra(Intent.EXTRA_STREAM, apkUri); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - } else { - intent.putExtra(Intent.EXTRA_STREAM, uri); - } - intent.setType(mimeType); - context.startActivity(Intent.createChooser(intent, title)); - } - - private void shareMultiple(String title, @Nullable Map> urisByMimeType) { - if (urisByMimeType == null) return; - - ArrayList uriList = urisByMimeType.values().stream().flatMap(Collection::stream).map(Uri::parse).collect(Collectors.toCollection(ArrayList::new)); - String[] mimeTypes = urisByMimeType.keySet().toArray(new String[0]); - - // simplify share intent for a single item, as some apps can handle one item but not more - if (uriList.size() == 1) { - shareSingle(title, uriList.get(0), mimeTypes[0]); - return; - } - - String mimeType = "*/*"; - if (mimeTypes.length == 1) { - // items have the same mime type & subtype - mimeType = mimeTypes[0]; - } else { - // items have different subtypes - String[] mimeTypeTypes = Arrays.stream(mimeTypes).map(mt -> mt.split("/")[0]).distinct().toArray(String[]::new); - if (mimeTypeTypes.length == 1) { - // items have the same mime type - mimeType = mimeTypeTypes[0] + "/*"; - } - } - - Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); - intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList); - intent.setType(mimeType); - context.startActivity(Intent.createChooser(intent, title)); - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppShortcutHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppShortcutHandler.java deleted file mode 100644 index 9d6e3c64c..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppShortcutHandler.java +++ /dev/null @@ -1,82 +0,0 @@ -package deckers.thibault.aves.channel.calls; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.pm.ShortcutInfoCompat; -import androidx.core.content.pm.ShortcutManagerCompat; -import androidx.core.graphics.drawable.IconCompat; - -import java.util.List; - -import deckers.thibault.aves.MainActivity; -import deckers.thibault.aves.R; -import deckers.thibault.aves.utils.BitmapUtils; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; - -public class AppShortcutHandler implements MethodChannel.MethodCallHandler { - public static final String CHANNEL = "deckers.thibault/aves/shortcut"; - - private Context context; - - public AppShortcutHandler(Context context) { - this.context = context; - } - - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - switch (call.method) { - case "canPin": { - result.success(ShortcutManagerCompat.isRequestPinShortcutSupported(context)); - break; - } - case "pin": { - String label = call.argument("label"); - byte[] iconBytes = call.argument("iconBytes"); - List filters = call.argument("filters"); - new Thread(() -> pin(label, iconBytes, filters)).start(); - result.success(null); - break; - } - default: - result.notImplemented(); - break; - } - } - - private void pin(String label, byte[] iconBytes, @Nullable List filters) { - if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context) || filters == null) { - return; - } - - IconCompat icon = null; - if (iconBytes != null && iconBytes.length > 0) { - Bitmap bitmap = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.length); - bitmap = BitmapUtils.centerSquareCrop(context, bitmap, 256); - if (bitmap != null) { - icon = IconCompat.createWithBitmap(bitmap); - } - } - if (icon == null) { - icon = IconCompat.createWithResource(context, R.mipmap.ic_shortcut_collection); - } - - Intent intent = new Intent(Intent.ACTION_MAIN, null, context, MainActivity.class) - .putExtra("page", "/collection") - .putExtra("filters", filters.toArray(new String[0])); - - ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, "collection-" + TextUtils.join("-", filters)) - .setShortLabel(label) - .setIcon(icon) - .setIntent(intent) - .build(); - - ShortcutManagerCompat.requestPinShortcut(context, shortcut, null); - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java deleted file mode 100644 index 17c021b8d..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java +++ /dev/null @@ -1,218 +0,0 @@ -package deckers.thibault.aves.channel.calls; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.provider.MediaStore; -import android.util.Log; -import android.util.Size; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.DecodeFormat; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.request.FutureTarget; -import com.bumptech.glide.request.RequestOptions; -import com.bumptech.glide.signature.ObjectKey; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.concurrent.ExecutionException; - -import deckers.thibault.aves.decoder.VideoThumbnail; -import deckers.thibault.aves.utils.BitmapUtils; -import deckers.thibault.aves.utils.LogUtils; -import deckers.thibault.aves.utils.MimeTypes; -import io.flutter.plugin.common.MethodChannel; - -public class ImageDecodeTask extends AsyncTask { - private static final String LOG_TAG = LogUtils.createTag(ImageDecodeTask.class); - - static class Params { - Uri uri; - String mimeType; - Long dateModifiedSecs; - Integer rotationDegrees, width, height, defaultSize; - Boolean isFlipped; - MethodChannel.Result result; - - Params(@NonNull String uri, @NonNull String mimeType, @NonNull Long dateModifiedSecs, @NonNull Integer rotationDegrees, @NonNull Boolean isFlipped, @Nullable Integer width, @Nullable Integer height, Integer defaultSize, MethodChannel.Result result) { - this.uri = Uri.parse(uri); - this.mimeType = mimeType; - this.dateModifiedSecs = dateModifiedSecs; - this.rotationDegrees = rotationDegrees; - this.isFlipped = isFlipped; - this.width = width; - this.height = height; - this.result = result; - this.defaultSize = defaultSize; - } - } - - static class Result { - Params params; - byte[] data; - String errorDetails; - - Result(Params params, byte[] data, String errorDetails) { - this.params = params; - this.data = data; - this.errorDetails = errorDetails; - } - } - - @SuppressLint("StaticFieldLeak") - private Activity activity; - - ImageDecodeTask(Activity activity) { - this.activity = activity; - } - - @Override - protected Result doInBackground(Params... params) { - Params p = params[0]; - Bitmap bitmap = null; - Exception exception = null; - if (!this.isCancelled()) { - Integer w = p.width; - Integer h = p.height; - // fetch low quality thumbnails when size is not specified - if (w == null || h == null || w == 0 || h == 0) { - p.width = p.defaultSize; - p.height = p.defaultSize; - // EXIF orientations with flipping are not well supported by the Media Store: - // the content resolver may return a thumbnail that is automatically rotated - // according to EXIF orientation, but not flip it when necessary - if (!p.isFlipped) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - bitmap = getThumbnailBytesByResolver(p); - } else { - bitmap = getThumbnailBytesByMediaStore(p); - } - } catch (Exception e) { - exception = e; - } - } - } - - // fallback if the native methods failed or for higher quality thumbnails - try { - if (bitmap == null) { - bitmap = getThumbnailByGlide(p); - } - } catch (Exception e) { - exception = e; - } - } else { - Log.d(LOG_TAG, "getThumbnail with uri=" + p.uri + " cancelled"); - } - - byte[] data = null; - if (bitmap != null) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - // we compress the bitmap because Dart Image.memory cannot decode the raw bytes - // Bitmap.CompressFormat.PNG is slower than JPEG - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream); - data = stream.toByteArray(); - } - - String errorDetails = null; - if (exception != null) { - errorDetails = exception.getMessage(); - if (errorDetails != null && !errorDetails.isEmpty()) { - errorDetails = errorDetails.split("\n", 2)[0]; - } - } - return new Result(p, data, errorDetails); - } - - @RequiresApi(api = Build.VERSION_CODES.Q) - private Bitmap getThumbnailBytesByResolver(Params params) throws IOException { - ContentResolver resolver = activity.getContentResolver(); - Bitmap bitmap = resolver.loadThumbnail(params.uri, new Size(params.width, params.height), null); - String mimeType = params.mimeType; - if (MimeTypes.needRotationAfterContentResolverThumbnail(mimeType)) { - bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, params.rotationDegrees, params.isFlipped); - } - return bitmap; - } - - private Bitmap getThumbnailBytesByMediaStore(Params params) { - long contentId = ContentUris.parseId(params.uri); - - ContentResolver resolver = activity.getContentResolver(); - if (MimeTypes.isVideo(params.mimeType)) { - return MediaStore.Video.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Video.Thumbnails.MINI_KIND, null); - } else { - Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null); - // from Android Q, returned thumbnail is already rotated according to EXIF orientation - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) { - bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, params.rotationDegrees, params.isFlipped); - } - return bitmap; - } - } - - private Bitmap getThumbnailByGlide(Params params) throws ExecutionException, InterruptedException { - Uri uri = params.uri; - String mimeType = params.mimeType; - Long dateModifiedSecs = params.dateModifiedSecs; - Integer rotationDegrees = params.rotationDegrees; - Boolean isFlipped = params.isFlipped; - int width = params.width; - int height = params.height; - - RequestOptions options = new RequestOptions() - .format(DecodeFormat.PREFER_RGB_565) - // add signature to ignore cache for images which got modified but kept the same URI - .signature(new ObjectKey("" + dateModifiedSecs + rotationDegrees + isFlipped + width)) - .override(width, height); - - FutureTarget target; - if (MimeTypes.isVideo(mimeType)) { - options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE); - target = Glide.with(activity) - .asBitmap() - .apply(options) - .load(new VideoThumbnail(activity, uri)) - .submit(width, height); - } else { - target = Glide.with(activity) - .asBitmap() - .apply(options) - .load(uri) - .submit(width, height); - } - - try { - Bitmap bitmap = target.get(); - if (MimeTypes.needRotationAfterGlide(mimeType)) { - bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped); - } - return bitmap; - } finally { - Glide.with(activity).clear(target); - } - } - - @Override - protected void onPostExecute(Result result) { - Params params = result.params; - MethodChannel.Result r = params.result; - String uri = params.uri.toString(); - if (result.data != null) { - r.success(result.data); - } else { - r.error("getThumbnail-null", "failed to get thumbnail for uri=" + uri, result.errorDetails); - } - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java deleted file mode 100644 index 79887396c..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java +++ /dev/null @@ -1,217 +0,0 @@ -package deckers.thibault.aves.channel.calls; - -import android.app.Activity; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; - -import androidx.annotation.NonNull; - -import com.bumptech.glide.Glide; - -import java.util.List; -import java.util.Map; - -import deckers.thibault.aves.model.ExifOrientationOp; -import deckers.thibault.aves.model.provider.ImageProvider; -import deckers.thibault.aves.model.provider.ImageProviderFactory; -import deckers.thibault.aves.model.provider.MediaStoreImageProvider; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; - -public class ImageFileHandler implements MethodChannel.MethodCallHandler { - public static final String CHANNEL = "deckers.thibault/aves/image"; - - private Activity activity; - private float density; - - public ImageFileHandler(Activity activity) { - this.activity = activity; - } - - public float getDensity() { - if (density == 0) { - density = activity.getResources().getDisplayMetrics().density; - } - return density; - } - - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - switch (call.method) { - case "getObsoleteEntries": - new Thread(() -> getObsoleteEntries(call, new MethodResultWrapper(result))).start(); - break; - case "getImageEntry": - new Thread(() -> getImageEntry(call, new MethodResultWrapper(result))).start(); - break; - case "getThumbnail": - new Thread(() -> getThumbnail(call, new MethodResultWrapper(result))).start(); - break; - case "clearSizedThumbnailDiskCache": - new Thread(() -> Glide.get(activity).clearDiskCache()).start(); - result.success(null); - break; - case "rename": - new Thread(() -> rename(call, new MethodResultWrapper(result))).start(); - break; - case "rotate": - new Thread(() -> rotate(call, new MethodResultWrapper(result))).start(); - break; - case "flip": - new Thread(() -> flip(call, new MethodResultWrapper(result))).start(); - break; - default: - result.notImplemented(); - break; - } - } - - private void getThumbnail(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - String uri = call.argument("uri"); - String mimeType = call.argument("mimeType"); - Number dateModifiedSecs = (Number)call.argument("dateModifiedSecs"); - Integer rotationDegrees = call.argument("rotationDegrees"); - Boolean isFlipped = call.argument("isFlipped"); - Double widthDip = call.argument("widthDip"); - Double heightDip = call.argument("heightDip"); - Double defaultSizeDip = call.argument("defaultSizeDip"); - - if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) { - result.error("getThumbnail-args", "failed because of missing arguments", null); - return; - } - // convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter - float density = getDensity(); - int width = (int) Math.round(widthDip * density); - int height = (int) Math.round(heightDip * density); - int defaultSize = (int) Math.round(defaultSizeDip * density); - - new ImageDecodeTask(activity).execute(new ImageDecodeTask.Params(uri, mimeType, dateModifiedSecs.longValue(), rotationDegrees, isFlipped, width, height, defaultSize, result)); - } - - private void getObsoleteEntries(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - List known = call.argument("knownContentIds"); - if (known == null) { - result.error("getObsoleteEntries-args", "failed because of missing arguments", null); - return; - } - List obsolete = new MediaStoreImageProvider().getObsoleteContentIds(activity, known); - result.success(obsolete); - } - - private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - String mimeType = call.argument("mimeType"); - Uri uri = Uri.parse(call.argument("uri")); - - if (uri == null || mimeType == null) { - result.error("getImageEntry-args", "failed because of missing arguments", null); - return; - } - - ImageProvider provider = ImageProviderFactory.getProvider(uri); - if (provider == null) { - result.error("getImageEntry-provider", "failed to find provider for uri=" + uri, null); - return; - } - - provider.fetchSingle(activity, uri, mimeType, new ImageProvider.ImageOpCallback() { - @Override - public void onSuccess(Map entry) { - result.success(entry); - } - - @Override - public void onFailure(Throwable throwable) { - result.error("getImageEntry-failure", "failed to get entry for uri=" + uri, throwable.getMessage()); - } - }); - } - - private void rename(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - Map entryMap = call.argument("entry"); - String newName = call.argument("newName"); - if (entryMap == null || newName == null) { - result.error("rename-args", "failed because of missing arguments", null); - return; - } - Uri uri = Uri.parse((String) entryMap.get("uri")); - String path = (String) entryMap.get("path"); - String mimeType = (String) entryMap.get("mimeType"); - - ImageProvider provider = ImageProviderFactory.getProvider(uri); - if (provider == null) { - result.error("rename-provider", "failed to find provider for uri=" + uri, null); - return; - } - provider.rename(activity, path, uri, mimeType, newName, new ImageProvider.ImageOpCallback() { - @Override - public void onSuccess(Map newFields) { - new Handler(Looper.getMainLooper()).post(() -> result.success(newFields)); - } - - @Override - public void onFailure(Throwable throwable) { - new Handler(Looper.getMainLooper()).post(() -> result.error("rename-failure", "failed to rename", throwable.getMessage())); - } - }); - } - - private void rotate(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - Map entryMap = call.argument("entry"); - Boolean clockwise = call.argument("clockwise"); - if (entryMap == null || clockwise == null) { - result.error("rotate-args", "failed because of missing arguments", null); - return; - } - Uri uri = Uri.parse((String) entryMap.get("uri")); - String path = (String) entryMap.get("path"); - String mimeType = (String) entryMap.get("mimeType"); - - ImageProvider provider = ImageProviderFactory.getProvider(uri); - if (provider == null) { - result.error("rotate-provider", "failed to find provider for uri=" + uri, null); - return; - } - ExifOrientationOp op = clockwise ? ExifOrientationOp.ROTATE_CW : ExifOrientationOp.ROTATE_CCW; - provider.changeOrientation(activity, path, uri, mimeType, op, new ImageProvider.ImageOpCallback() { - @Override - public void onSuccess(Map newFields) { - new Handler(Looper.getMainLooper()).post(() -> result.success(newFields)); - } - - @Override - public void onFailure(Throwable throwable) { - new Handler(Looper.getMainLooper()).post(() -> result.error("rotate-failure", "failed to rotate", throwable.getMessage())); - } - }); - } - - private void flip(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - Map entryMap = call.argument("entry"); - if (entryMap == null) { - result.error("flip-args", "failed because of missing arguments", null); - return; - } - Uri uri = Uri.parse((String) entryMap.get("uri")); - String path = (String) entryMap.get("path"); - String mimeType = (String) entryMap.get("mimeType"); - - ImageProvider provider = ImageProviderFactory.getProvider(uri); - if (provider == null) { - result.error("flip-provider", "failed to find provider for uri=" + uri, null); - return; - } - provider.changeOrientation(activity, path, uri, mimeType, ExifOrientationOp.FLIP, new ImageProvider.ImageOpCallback() { - @Override - public void onSuccess(Map newFields) { - new Handler(Looper.getMainLooper()).post(() -> result.success(newFields)); - } - - @Override - public void onFailure(Throwable throwable) { - new Handler(Looper.getMainLooper()).post(() -> result.error("flip-failure", "failed to flip", throwable.getMessage())); - } - }); - } -} \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/MethodResultWrapper.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/MethodResultWrapper.java deleted file mode 100644 index e6aeaacef..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/MethodResultWrapper.java +++ /dev/null @@ -1,32 +0,0 @@ -package deckers.thibault.aves.channel.calls; - -import android.os.Handler; -import android.os.Looper; - -import io.flutter.plugin.common.MethodChannel; - -// ensure `result` methods are called on the main looper thread -public class MethodResultWrapper implements MethodChannel.Result { - private MethodChannel.Result methodResult; - private Handler handler; - - MethodResultWrapper(MethodChannel.Result result) { - methodResult = result; - handler = new Handler(Looper.getMainLooper()); - } - - @Override - public void success(final Object result) { - handler.post(() -> methodResult.success(result)); - } - - @Override - public void error(final String errorCode, final String errorMessage, final Object errorDetails) { - handler.post(() -> methodResult.error(errorCode, errorMessage, errorDetails)); - } - - @Override - public void notImplemented() { - handler.post(() -> methodResult.notImplemented()); - } -} \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/StorageHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/StorageHandler.java deleted file mode 100644 index c32c82ab4..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/StorageHandler.java +++ /dev/null @@ -1,105 +0,0 @@ -package deckers.thibault.aves.channel.calls; - -import android.content.Context; -import android.media.MediaScannerConnection; -import android.os.Build; -import android.os.storage.StorageManager; -import android.os.storage.StorageVolume; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import deckers.thibault.aves.utils.PermissionManager; -import deckers.thibault.aves.utils.StorageUtils; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; - -public class StorageHandler implements MethodChannel.MethodCallHandler { - public static final String CHANNEL = "deckers.thibault/aves/storage"; - - private Context context; - - public StorageHandler(Context context) { - this.context = context; - } - - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - switch (call.method) { - case "getStorageVolumes": { - List> volumes; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { - volumes = getStorageVolumes(); - } else { - // TODO TLAD find alternative for Android (); - } - result.success(volumes); - break; - } - case "getInaccessibleDirectories": { - List dirPaths = call.argument("dirPaths"); - if (dirPaths == null) { - result.error("getInaccessibleDirectories-args", "failed because of missing arguments", null); - } else { - result.success(PermissionManager.getInaccessibleDirectories(context, dirPaths)); - } - break; - } - case "revokeDirectoryAccess": - String path = call.argument("path"); - PermissionManager.revokeDirectoryAccess(context, path); - result.success(true); - break; - case "getGrantedDirectories": - result.success(new ArrayList<>(PermissionManager.getGrantedDirs(context))); - break; - case "scanFile": - scanFile(call, new MethodResultWrapper(result)); - break; - default: - result.notImplemented(); - break; - } - } - - @RequiresApi(api = Build.VERSION_CODES.N) - private List> getStorageVolumes() { - List> volumes = new ArrayList<>(); - StorageManager sm = context.getSystemService(StorageManager.class); - if (sm != null) { - for (String volumePath : StorageUtils.getVolumePaths(context)) { - try { - StorageVolume volume = sm.getStorageVolume(new File(volumePath)); - if (volume != null) { - Map volumeMap = new HashMap<>(); - volumeMap.put("path", volumePath); - volumeMap.put("description", volume.getDescription(context)); - volumeMap.put("isPrimary", volume.isPrimary()); - volumeMap.put("isRemovable", volume.isRemovable()); - volumeMap.put("isEmulated", volume.isEmulated()); - volumeMap.put("state", volume.getState()); - volumes.add(volumeMap); - } - } catch (IllegalArgumentException e) { - // ignore - } - } - } - return volumes; - } - - private void scanFile(MethodCall call, MethodChannel.Result result) { - String path = call.argument("path"); - String mimeType = call.argument("mimeType"); - MediaScannerConnection.scanFile(context, new String[]{path}, new String[]{mimeType}, (p, uri) -> { - result.success(uri != null ? uri.toString() : null); - }); - } -} \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java deleted file mode 100644 index ce401086f..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.java +++ /dev/null @@ -1,165 +0,0 @@ -package deckers.thibault.aves.channel.streams; - -import android.app.Activity; -import android.content.ContentResolver; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.DecodeFormat; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.request.FutureTarget; -import com.bumptech.glide.request.RequestOptions; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Map; - -import deckers.thibault.aves.decoder.VideoThumbnail; -import deckers.thibault.aves.utils.BitmapUtils; -import deckers.thibault.aves.utils.MimeTypes; -import io.flutter.plugin.common.EventChannel; - -public class ImageByteStreamHandler implements EventChannel.StreamHandler { - public static final String CHANNEL = "deckers.thibault/aves/imagebytestream"; - - private Activity activity; - private Uri uri; - private String mimeType; - private int rotationDegrees; - private boolean isFlipped; - private EventChannel.EventSink eventSink; - private Handler handler; - - @SuppressWarnings("unchecked") - public ImageByteStreamHandler(Activity activity, Object arguments) { - this.activity = activity; - if (arguments instanceof Map) { - Map argMap = (Map) arguments; - this.mimeType = (String) argMap.get("mimeType"); - this.uri = Uri.parse((String) argMap.get("uri")); - this.rotationDegrees = (int) argMap.get("rotationDegrees"); - this.isFlipped = (boolean) argMap.get("isFlipped"); - } - } - - @Override - public void onListen(Object args, EventChannel.EventSink eventSink) { - this.eventSink = eventSink; - this.handler = new Handler(Looper.getMainLooper()); - new Thread(this::getImage).start(); - } - - @Override - public void onCancel(Object o) { - } - - private void success(final byte[] bytes) { - handler.post(() -> eventSink.success(bytes)); - } - - private void error(final String errorCode, final String errorMessage, final Object errorDetails) { - handler.post(() -> eventSink.error(errorCode, errorMessage, errorDetails)); - } - - private void endOfStream() { - handler.post(() -> eventSink.endOfStream()); - } - - // Supported image formats: - // - Flutter (as of v1.20): JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP - // - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats - // - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java - private void getImage() { - // request a fresh image with the highest quality format - RequestOptions options = new RequestOptions() - .format(DecodeFormat.PREFER_ARGB_8888) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true); - - if (MimeTypes.isVideo(mimeType)) { - FutureTarget target = Glide.with(activity) - .asBitmap() - .apply(options) - .load(new VideoThumbnail(activity, uri)) - .submit(); - try { - Bitmap bitmap = target.get(); - if (bitmap != null) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - // we compress the bitmap because Dart Image.memory cannot decode the raw bytes - // Bitmap.CompressFormat.PNG is slower than JPEG - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream); - success(stream.toByteArray()); - } else { - error("getImage-video-null", "failed to get image from uri=" + uri, null); - } - } catch (Exception e) { - error("getImage-video-exception", "failed to get image from uri=" + uri, e.getMessage()); - } finally { - Glide.with(activity).clear(target); - } - } else if (!MimeTypes.isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) { - // we convert the image on platform side first, when Dart Image.memory does not support it - FutureTarget target = Glide.with(activity) - .asBitmap() - .apply(options) - .load(uri) - .submit(); - try { - Bitmap bitmap = target.get(); - if (MimeTypes.needRotationAfterGlide(mimeType)) { - bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped); - } - if (bitmap != null) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - // we compress the bitmap because Dart Image.memory cannot decode the raw bytes - // Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency - if (MimeTypes.canHaveAlpha(mimeType)) { - bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); - } else { - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream); - } - success(stream.toByteArray()); - } else { - error("getImage-image-decode-null", "failed to get image from uri=" + uri, null); - } - } catch (Exception e) { - String errorDetails = e.getMessage(); - if (errorDetails != null && !errorDetails.isEmpty()) { - errorDetails = errorDetails.split("\n", 2)[0]; - } - error("getImage-image-decode-exception", "failed to get image from uri=" + uri, errorDetails); - } finally { - Glide.with(activity).clear(target); - } - } else { - ContentResolver cr = activity.getContentResolver(); - try (InputStream is = cr.openInputStream(uri)) { - if (is != null) { - streamBytes(is); - } else { - error("getImage-image-read-null", "failed to get image from uri=" + uri, null); - } - } catch (IOException e) { - error("getImage-image-read-exception", "failed to get image from uri=" + uri, e.getMessage()); - } - } - endOfStream(); - } - - private void streamBytes(InputStream inputStream) throws IOException { - int bufferSize = 2 << 17; // 256kB - byte[] buffer = new byte[bufferSize]; - int len; - while ((len = inputStream.read(buffer)) != -1) { - // cannot decode image on Flutter side when using `buffer` directly... - byte[] sub = new byte[len]; - System.arraycopy(buffer, 0, sub, 0, len); - success(sub); - } - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.java deleted file mode 100644 index b942f556c..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.java +++ /dev/null @@ -1,152 +0,0 @@ -package deckers.thibault.aves.channel.streams; - -import android.content.Context; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; - -import deckers.thibault.aves.model.AvesImageEntry; -import deckers.thibault.aves.model.provider.ImageProvider; -import deckers.thibault.aves.model.provider.ImageProviderFactory; -import deckers.thibault.aves.utils.LogUtils; -import io.flutter.plugin.common.EventChannel; - -public class ImageOpStreamHandler implements EventChannel.StreamHandler { - private static final String LOG_TAG = LogUtils.createTag(ImageOpStreamHandler.class); - - public static final String CHANNEL = "deckers.thibault/aves/imageopstream"; - - private Context context; - private EventChannel.EventSink eventSink; - private Handler handler; - private Map argMap; - private List> entryMapList; - private String op; - - @SuppressWarnings("unchecked") - public ImageOpStreamHandler(Context context, Object arguments) { - this.context = context; - if (arguments instanceof Map) { - argMap = (Map) arguments; - this.op = (String) argMap.get("op"); - this.entryMapList = new ArrayList<>(); - List> rawEntries = (List>) argMap.get("entries"); - if (rawEntries != null) { - entryMapList.addAll(rawEntries); - } - } - } - - @Override - public void onListen(Object args, EventChannel.EventSink eventSink) { - this.eventSink = eventSink; - this.handler = new Handler(Looper.getMainLooper()); - if ("delete".equals(op)) { - new Thread(this::delete).start(); - } else if ("move".equals(op)) { - new Thread(this::move).start(); - } else { - endOfStream(); - } - } - - @Override - public void onCancel(Object o) { - } - - // {String uri, bool success, [Map newFields]} - private void success(final Map result) { - handler.post(() -> eventSink.success(result)); - } - - private void error(final String errorCode, final String errorMessage, final Object errorDetails) { - handler.post(() -> eventSink.error(errorCode, errorMessage, errorDetails)); - } - - private void endOfStream() { - handler.post(() -> eventSink.endOfStream()); - } - - private void move() { - if (entryMapList.size() == 0) { - endOfStream(); - return; - } - - // assume same provider for all entries - Map firstEntry = entryMapList.get(0); - Uri firstUri = Uri.parse((String) firstEntry.get("uri")); - ImageProvider provider = ImageProviderFactory.getProvider(firstUri); - if (provider == null) { - error("move-provider", "failed to find provider for uri=" + firstUri, null); - return; - } - - Boolean copy = (Boolean) argMap.get("copy"); - String destinationDir = (String) argMap.get("destinationPath"); - if (copy == null || destinationDir == null) return; - - if (!destinationDir.endsWith(File.separator)) { - destinationDir += File.separator; - } - - List entries = entryMapList.stream().map(AvesImageEntry::new).collect(Collectors.toList()); - provider.moveMultiple(context, copy, destinationDir, entries, new ImageProvider.ImageOpCallback() { - @Override - public void onSuccess(Map fields) { - success(fields); - } - - @Override - public void onFailure(Throwable throwable) { - error("move-failure", "failed to move entries", throwable); - } - }); - endOfStream(); - } - - private void delete() { - if (entryMapList.size() == 0) { - endOfStream(); - return; - } - - // assume same provider for all entries - Map firstEntry = entryMapList.get(0); - Uri firstUri = Uri.parse((String) firstEntry.get("uri")); - ImageProvider provider = ImageProviderFactory.getProvider(firstUri); - if (provider == null) { - error("delete-provider", "failed to find provider for uri=" + firstUri, null); - return; - } - - for (Map entryMap : entryMapList) { - String uriString = (String) entryMap.get("uri"); - Uri uri = Uri.parse(uriString); - String path = (String) entryMap.get("path"); - - Map result = new HashMap() {{ - put("uri", uriString); - }}; - try { - provider.delete(context, path, uri).get(); - result.put("success", true); - } catch (ExecutionException | InterruptedException e) { - Log.w(LOG_TAG, "failed to delete entry with path=" + path, e); - result.put("success", false); - } - success(result); - - } - endOfStream(); - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/AvesAppGlideModule.java b/android/app/src/main/java/deckers/thibault/aves/decoder/AvesAppGlideModule.java deleted file mode 100644 index 55dac0d89..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/decoder/AvesAppGlideModule.java +++ /dev/null @@ -1,34 +0,0 @@ -package deckers.thibault.aves.decoder; - -import android.content.Context; -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.GlideBuilder; -import com.bumptech.glide.Registry; -import com.bumptech.glide.annotation.GlideModule; -import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser; -import com.bumptech.glide.module.AppGlideModule; - -@GlideModule -public class AvesAppGlideModule extends AppGlideModule { - @Override - public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { - // hide noisy warning (e.g. for images that can't be decoded) - builder.setLogLevel(Log.ERROR); - } - - @Override - public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { - // prevent ExifInterface error logs - // cf https://github.com/bumptech/glide/issues/3383 - glide.getRegistry().getImageHeaderParsers().removeIf(parser -> parser instanceof ExifInterfaceImageHeaderParser); - } - - @Override - public boolean isManifestParsingEnabled() { - return false; - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnail.java b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnail.java deleted file mode 100644 index ff76284be..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnail.java +++ /dev/null @@ -1,22 +0,0 @@ -package deckers.thibault.aves.decoder; - -import android.content.Context; -import android.net.Uri; - -public class VideoThumbnail { - private Context mContext; - private Uri mUri; - - public VideoThumbnail(Context context, Uri uri) { - mContext = context; - mUri = uri; - } - - public Context getContext() { - return mContext; - } - - Uri getUri() { - return mUri; - } -} \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailFetcher.java b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailFetcher.java deleted file mode 100644 index 9c357eb6f..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailFetcher.java +++ /dev/null @@ -1,73 +0,0 @@ -package deckers.thibault.aves.decoder; - -import android.graphics.Bitmap; -import android.media.MediaMetadataRetriever; - -import androidx.annotation.NonNull; - -import com.bumptech.glide.Priority; -import com.bumptech.glide.load.DataSource; -import com.bumptech.glide.load.data.DataFetcher; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; - -import deckers.thibault.aves.utils.StorageUtils; - -class VideoThumbnailFetcher implements DataFetcher { - private final VideoThumbnail model; - - VideoThumbnailFetcher(VideoThumbnail model) { - this.model = model; - } - - @Override - public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { - MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(model.getContext(), model.getUri()); - if (retriever != null) { - try { - byte[] picture = retriever.getEmbeddedPicture(); - if (picture != null) { - callback.onDataReady(new ByteArrayInputStream(picture)); - } else { - // not ideal: bitmap -> byte[] -> bitmap - // but simple fallback and we cache result - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - Bitmap bitmap = retriever.getFrameAtTime(); - if (bitmap != null) { - bitmap.compress(Bitmap.CompressFormat.PNG, 0, bos); - } - callback.onDataReady(new ByteArrayInputStream(bos.toByteArray())); - } - } catch (Exception e) { - callback.onLoadFailed(e); - } finally { - // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs - retriever.release(); - } - } - } - - @Override - public void cleanup() { - // already cleaned up in loadData and ByteArrayInputStream will be GC'd - } - - @Override - public void cancel() { - // cannot cancel - } - - @NonNull - @Override - public Class getDataClass() { - return InputStream.class; - } - - @NonNull - @Override - public DataSource getDataSource() { - return DataSource.LOCAL; - } -} \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.java b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.java deleted file mode 100644 index 2912c1ebc..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.java +++ /dev/null @@ -1,20 +0,0 @@ -package deckers.thibault.aves.decoder; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.Registry; -import com.bumptech.glide.annotation.GlideModule; -import com.bumptech.glide.module.LibraryGlideModule; - -import java.io.InputStream; - -@GlideModule -public class VideoThumbnailGlideModule extends LibraryGlideModule { - @Override - public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { - registry.append(VideoThumbnail.class, InputStream.class, new VideoThumbnailLoader.Factory()); - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailLoader.java b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailLoader.java deleted file mode 100644 index 6b818eefa..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailLoader.java +++ /dev/null @@ -1,37 +0,0 @@ -package deckers.thibault.aves.decoder; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.bumptech.glide.load.Options; -import com.bumptech.glide.load.model.ModelLoader; -import com.bumptech.glide.load.model.ModelLoaderFactory; -import com.bumptech.glide.load.model.MultiModelLoaderFactory; -import com.bumptech.glide.signature.ObjectKey; - -import java.io.InputStream; - -class VideoThumbnailLoader implements ModelLoader { - @Nullable - @Override - public LoadData buildLoadData(@NonNull VideoThumbnail model, int width, int height, @NonNull Options options) { - return new LoadData<>(new ObjectKey(model.getUri()), new VideoThumbnailFetcher(model)); - } - - @Override - public boolean handles(@NonNull VideoThumbnail videoThumbnail) { - return true; - } - - static class Factory implements ModelLoaderFactory { - @NonNull - @Override - public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { - return new VideoThumbnailLoader(); - } - - @Override - public void teardown() { - } - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/ContentImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/ContentImageProvider.java deleted file mode 100644 index 68e328d9d..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/ContentImageProvider.java +++ /dev/null @@ -1,47 +0,0 @@ -package deckers.thibault.aves.model.provider; - -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.provider.MediaStore; - -import androidx.annotation.NonNull; - -import java.util.HashMap; -import java.util.Map; - -import deckers.thibault.aves.model.SourceImageEntry; - -class ContentImageProvider extends ImageProvider { - @Override - public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) { - Map map = new HashMap<>(); - map.put("uri", uri.toString()); - map.put("sourceMimeType", mimeType); - - String[] projection = { - MediaStore.MediaColumns.SIZE, - MediaStore.MediaColumns.DISPLAY_NAME, - }; - try { - Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null); - if (cursor != null) { - if (cursor.moveToNext()) { - map.put("sizeBytes", cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE))); - map.put("title", cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))); - } - cursor.close(); - } - } catch (Exception e) { - callback.onFailure(e); - return; - } - - SourceImageEntry entry = new SourceImageEntry(map).fillPreCatalogMetadata(context); - if (entry.isSized() || entry.isSvg()) { - callback.onSuccess(entry.toMap()); - } else { - callback.onFailure(new Exception("entry has no size")); - } - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/FileImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/FileImageProvider.java deleted file mode 100644 index 43e00d7c1..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/FileImageProvider.java +++ /dev/null @@ -1,36 +0,0 @@ -package deckers.thibault.aves.model.provider; - -import android.content.Context; -import android.net.Uri; - -import androidx.annotation.NonNull; - -import java.io.File; - -import deckers.thibault.aves.model.SourceImageEntry; - -class FileImageProvider extends ImageProvider { - @Override - public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) { - SourceImageEntry entry = new SourceImageEntry(uri, mimeType); - - String path = uri.getPath(); - if (path != null) { - try { - File file = new File(path); - if (file.exists()) { - entry.initFromFile(path, file.getName(), file.length(), file.lastModified() / 1000); - } - } catch (SecurityException e) { - callback.onFailure(e); - } - } - entry.fillPreCatalogMetadata(context); - - if (entry.isSized() || entry.isSvg()) { - callback.onSuccess(entry.toMap()); - } else { - callback.onFailure(new Exception("entry has no size")); - } - } -} \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java deleted file mode 100644 index d8f60d198..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java +++ /dev/null @@ -1,226 +0,0 @@ -package deckers.thibault.aves.model.provider; - -import android.content.ContentUris; -import android.content.Context; -import android.database.Cursor; -import android.media.MediaScannerConnection; -import android.net.Uri; -import android.provider.MediaStore; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.exifinterface.media.ExifInterface; - -import com.commonsware.cwac.document.DocumentFileCompat; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import deckers.thibault.aves.model.AvesImageEntry; -import deckers.thibault.aves.model.ExifOrientationOp; -import deckers.thibault.aves.utils.LogUtils; -import deckers.thibault.aves.utils.MimeTypes; -import deckers.thibault.aves.utils.StorageUtils; - -// *** about file access to write/rename/delete -// * primary volume -// until 28/Pie, use `File` -// on 29/Q, use `File` after setting `requestLegacyExternalStorage` flag in the manifest -// from 30/R, use `DocumentFile` (not `File`) after requesting permission to the volume root??? -// * non primary volumes -// on 19/KitKat, use `DocumentFile` (not `File`) after getting permission for each file -// from 21/Lollipop, use `DocumentFile` (not `File`) after getting permission to the volume root - -public abstract class ImageProvider { - private static final String LOG_TAG = LogUtils.createTag(ImageProvider.class); - - public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) { - callback.onFailure(new UnsupportedOperationException()); - } - - public ListenableFuture delete(final Context context, final String path, final Uri uri) { - return Futures.immediateFailedFuture(new UnsupportedOperationException()); - } - - public void moveMultiple(final Context context, final Boolean copy, final String destinationDir, final List entries, @NonNull final ImageOpCallback callback) { - callback.onFailure(new UnsupportedOperationException()); - } - - public void rename(final Context context, final String oldPath, final Uri oldMediaUri, final String mimeType, final String newFilename, final ImageOpCallback callback) { - if (oldPath == null) { - callback.onFailure(new IllegalArgumentException("entry does not have a path, uri=" + oldMediaUri)); - return; - } - - File oldFile = new File(oldPath); - File newFile = new File(oldFile.getParent(), newFilename); - if (oldFile.equals(newFile)) { - Log.w(LOG_TAG, "new name and old name are the same, path=" + oldPath); - callback.onSuccess(new HashMap<>()); - return; - } - - DocumentFileCompat df = StorageUtils.getDocumentFile(context, oldPath, oldMediaUri); - try { - boolean renamed = df != null && df.renameTo(newFilename); - if (!renamed) { - callback.onFailure(new Exception("failed to rename entry at path=" + oldPath)); - return; - } - } catch (FileNotFoundException e) { - callback.onFailure(e); - return; - } - - MediaScannerConnection.scanFile(context, new String[]{oldPath}, new String[]{mimeType}, null); - scanNewPath(context, newFile.getPath(), mimeType, callback); - } - - // support for writing EXIF - // as of androidx.exifinterface:exifinterface:1.3.0 - private boolean canEditExif(@NonNull String mimeType) { - switch (mimeType) { - case "image/jpeg": - case "image/png": - case "image/webp": - return true; - default: - return false; - } - } - - public void changeOrientation(final Context context, final String path, final Uri uri, final String mimeType, final ExifOrientationOp op, final ImageOpCallback callback) { - if (!canEditExif(mimeType)) { - callback.onFailure(new UnsupportedOperationException("unsupported mimeType=" + mimeType)); - return; - } - - final DocumentFileCompat originalDocumentFile = StorageUtils.getDocumentFile(context, path, uri); - if (originalDocumentFile == null) { - callback.onFailure(new Exception("failed to get document file for path=" + path + ", uri=" + uri)); - return; - } - - // copy original file to a temporary file for editing - final String editablePath = StorageUtils.copyFileToTemp(originalDocumentFile, path); - if (editablePath == null) { - callback.onFailure(new Exception("failed to create a temporary file for path=" + path)); - return; - } - - Map newFields = new HashMap<>(); - try { - ExifInterface exif = new ExifInterface(editablePath); - // when the orientation is not defined, it returns `undefined (0)` instead of the orientation default value `normal (1)` - // in that case we explicitely set it to `normal` first - // because ExifInterface fails to rotate an image with undefined orientation - // as of androidx.exifinterface:exifinterface:1.3.0 - int currentOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - if (currentOrientation == ExifInterface.ORIENTATION_UNDEFINED) { - exif.setAttribute(ExifInterface.TAG_ORIENTATION, Integer.toString(ExifInterface.ORIENTATION_NORMAL)); - } - switch (op) { - case ROTATE_CW: - exif.rotate(90); - break; - case ROTATE_CCW: - exif.rotate(-90); - break; - case FLIP: - exif.flipHorizontally(); - break; - } - exif.saveAttributes(); - - // copy the edited temporary file back to the original - DocumentFileCompat.fromFile(new File(editablePath)).copyTo(originalDocumentFile); - - newFields.put("rotationDegrees", exif.getRotationDegrees()); - newFields.put("isFlipped", exif.isFlipped()); - } catch (IOException e) { - callback.onFailure(e); - return; - } - - MediaScannerConnection.scanFile(context, new String[]{path}, new String[]{mimeType}, (p, u) -> { - String[] projection = {MediaStore.MediaColumns.DATE_MODIFIED}; - try { - Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null); - if (cursor != null) { - if (cursor.moveToNext()) { - newFields.put("dateModifiedSecs", cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED))); - } - cursor.close(); - } - } catch (Exception e) { - callback.onFailure(e); - return; - } - callback.onSuccess(newFields); - }); - } - - protected void scanNewPath(final Context context, final String path, final String mimeType, final ImageOpCallback callback) { - MediaScannerConnection.scanFile(context, new String[]{path}, new String[]{mimeType}, (newPath, newUri) -> { - long contentId = 0; - Uri contentUri = null; - if (newUri != null) { - // newURI is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872") - // but we need an image/video media URI (e.g. "content://media/external/images/media/62872") - contentId = ContentUris.parseId(newUri); - if (MimeTypes.isImage(mimeType)) { - contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId); - } else if (MimeTypes.isVideo(mimeType)) { - contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId); - } - } - if (contentUri == null) { - callback.onFailure(new Exception("failed to get content URI of item at path=" + path)); - return; - } - - Map newFields = new HashMap<>(); - // we retrieve updated fields as the renamed/moved file became a new entry in the Media Store - String[] projection = { - MediaStore.MediaColumns.DISPLAY_NAME, - MediaStore.MediaColumns.TITLE, - MediaStore.MediaColumns.DATE_MODIFIED, - }; - try { - Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, null); - if (cursor != null) { - if (cursor.moveToNext()) { - newFields.put("uri", contentUri.toString()); - newFields.put("contentId", contentId); - newFields.put("path", path); - newFields.put("displayName", cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))); - newFields.put("title", cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.TITLE))); - newFields.put("dateModifiedSecs", cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED))); - } - cursor.close(); - } - } catch (Exception e) { - callback.onFailure(e); - return; - } - - if (newFields.isEmpty()) { - callback.onFailure(new Exception("failed to get item details from provider at contentUri=" + contentUri)); - } else { - callback.onSuccess(newFields); - } - }); - } - - public interface ImageOpCallback { - void onSuccess(Map fields); - - void onFailure(Throwable throwable); - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProviderFactory.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProviderFactory.java deleted file mode 100644 index 140c871ba..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProviderFactory.java +++ /dev/null @@ -1,28 +0,0 @@ -package deckers.thibault.aves.model.provider; - -import android.content.ContentResolver; -import android.net.Uri; -import android.provider.MediaStore; - -import androidx.annotation.NonNull; - -public class ImageProviderFactory { - public static ImageProvider getProvider(@NonNull Uri uri) { - String scheme = uri.getScheme(); - - if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(scheme)) { - // a URI's authority is [userinfo@]host[:port] - // but we only want the host when comparing to Media Store's "authority" - if (MediaStore.AUTHORITY.equalsIgnoreCase(uri.getHost())) { - return new MediaStoreImageProvider(); - } - return new ContentImageProvider(); - } - - if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(scheme)) { - return new FileImageProvider(); - } - - return null; - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java deleted file mode 100644 index 98ea4c551..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/MediaStoreImageProvider.java +++ /dev/null @@ -1,444 +0,0 @@ -package deckers.thibault.aves.model.provider; - -import android.annotation.SuppressLint; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.storage.StorageManager; -import android.os.storage.StorageVolume; -import android.provider.MediaStore; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import com.commonsware.cwac.document.DocumentFileCompat; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; - -import java.io.File; -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import deckers.thibault.aves.model.AvesImageEntry; -import deckers.thibault.aves.model.SourceImageEntry; -import deckers.thibault.aves.utils.LogUtils; -import deckers.thibault.aves.utils.MimeTypes; -import deckers.thibault.aves.utils.StorageUtils; - -public class MediaStoreImageProvider extends ImageProvider { - private static final String LOG_TAG = LogUtils.createTag(MediaStoreImageProvider.class); - - private static final String[] BASE_PROJECTION = { - MediaStore.MediaColumns._ID, - MediaStore.MediaColumns.DATA, - MediaStore.MediaColumns.MIME_TYPE, - MediaStore.MediaColumns.SIZE, - // TODO TLAD use `DISPLAY_NAME` instead/along `TITLE`? - MediaStore.MediaColumns.TITLE, - MediaStore.MediaColumns.WIDTH, - MediaStore.MediaColumns.HEIGHT, - MediaStore.MediaColumns.DATE_MODIFIED, - }; - - @SuppressLint("InlinedApi") - private static final String[] IMAGE_PROJECTION = Stream.of(BASE_PROJECTION, new String[]{ - // uses MediaStore.Images.Media instead of MediaStore.MediaColumns for APIs < Q - MediaStore.Images.Media.DATE_TAKEN, - MediaStore.Images.Media.ORIENTATION, - }).flatMap(Stream::of).toArray(String[]::new); - - @SuppressLint("InlinedApi") - private static final String[] VIDEO_PROJECTION = Stream.of(BASE_PROJECTION, new String[]{ - // uses MediaStore.Video.Media instead of MediaStore.MediaColumns for APIs < Q - MediaStore.Video.Media.DATE_TAKEN, - MediaStore.Video.Media.DURATION, - }, (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ? - new String[]{ - MediaStore.Video.Media.ORIENTATION, - } : new String[0]).flatMap(Stream::of).toArray(String[]::new); - - public void fetchAll(Context context, Map knownEntries, NewEntryHandler newEntryHandler) { - NewEntryChecker isModified = (contentId, dateModifiedSecs) -> { - final Integer knownDate = knownEntries.get(contentId); - return knownDate == null || knownDate < dateModifiedSecs; - }; - fetchFrom(context, isModified, newEntryHandler, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION); - fetchFrom(context, isModified, newEntryHandler, MediaStore.Video.Media.EXTERNAL_CONTENT_URI, VIDEO_PROJECTION); - } - - @Override - public void fetchSingle(@NonNull final Context context, @NonNull final Uri uri, @NonNull final String mimeType, @NonNull final ImageOpCallback callback) { - long id = ContentUris.parseId(uri); - int entryCount = 0; - NewEntryHandler onSuccess = (entry) -> { - entry.put("uri", uri.toString()); - callback.onSuccess(entry); - }; - NewEntryChecker alwaysValid = (contentId, dateModifiedSecs) -> true; - if (MimeTypes.isImage(mimeType)) { - Uri contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id); - entryCount = fetchFrom(context, alwaysValid, onSuccess, contentUri, IMAGE_PROJECTION); - } else if (MimeTypes.isVideo(mimeType)) { - Uri contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id); - entryCount = fetchFrom(context, alwaysValid, onSuccess, contentUri, VIDEO_PROJECTION); - } - if (entryCount == 0) { - callback.onFailure(new Exception("failed to fetch entry at uri=" + uri)); - } - } - - public List getObsoleteContentIds(Context context, List knownContentIds) { - final ArrayList current = new ArrayList<>(); - current.addAll(getContentIdList(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)); - current.addAll(getContentIdList(context, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)); - return knownContentIds.stream().filter(id -> !current.contains(id)).collect(Collectors.toList()); - } - - private List getContentIdList(Context context, Uri contentUri) { - final ArrayList foundContentIds = new ArrayList<>(); - try { - Cursor cursor = context.getContentResolver().query(contentUri, new String[]{MediaStore.MediaColumns._ID}, null, null, null); - if (cursor != null) { - int idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID); - while (cursor.moveToNext()) { - foundContentIds.add(cursor.getInt(idColumn)); - } - cursor.close(); - } - } catch (Exception e) { - Log.e(LOG_TAG, "failed to get content IDs for contentUri=" + contentUri, e); - } - return foundContentIds; - } - - @SuppressLint("InlinedApi") - private int fetchFrom(final Context context, NewEntryChecker newEntryChecker, NewEntryHandler newEntryHandler, final Uri contentUri, String[] projection) { - int newEntryCount = 0; - final String orderBy = MediaStore.MediaColumns.DATE_MODIFIED + " DESC"; - - final boolean needDuration = projection == VIDEO_PROJECTION; - - try { - Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, orderBy); - if (cursor != null) { - // image & video - int idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID); - int pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA); - int mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE); - int sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE); - int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.TITLE); - int widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH); - int heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT); - int dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED); - int dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN); - - // image & video for API >= Q, only for images for API < Q - int orientationColumn = cursor.getColumnIndex(MediaStore.MediaColumns.ORIENTATION); - - // video only - int durationColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DURATION); - - while (cursor.moveToNext()) { - final int contentId = cursor.getInt(idColumn); - final int dateModifiedSecs = cursor.getInt(dateModifiedColumn); - if (newEntryChecker.where(contentId, dateModifiedSecs)) { - // this is fine if `contentUri` does not already contain the ID - final Uri itemUri = ContentUris.withAppendedId(contentUri, contentId); - final String path = cursor.getString(pathColumn); - final String mimeType = cursor.getString(mimeTypeColumn); - int width = cursor.getInt(widthColumn); - int height = cursor.getInt(heightColumn); - final long durationMillis = durationColumn != -1 ? cursor.getLong(durationColumn) : 0; - - // check whether the field may be `null` to distinguish it from a legitimate `0` - // this can happen for specific formats (e.g. for PNG, WEBP) - // or for JPEG that were not properly registered - - Map entryMap = new HashMap() {{ - put("uri", itemUri.toString()); - put("path", path); - put("sourceMimeType", mimeType); - put("sourceRotationDegrees", orientationColumn != -1 ? cursor.getInt(orientationColumn) : 0); - put("sizeBytes", cursor.getLong(sizeColumn)); - put("title", cursor.getString(titleColumn)); - put("dateModifiedSecs", dateModifiedSecs); - put("sourceDateTakenMillis", cursor.getLong(dateTakenColumn)); - // only for map export - put("contentId", contentId); - }}; - entryMap.put("width", width); - entryMap.put("height", height); - entryMap.put("durationMillis", durationMillis); - - if (((width <= 0 || height <= 0) && needSize(mimeType)) - || (durationMillis == 0 && needDuration)) { - // some images are incorrectly registered in the Media Store, - // they are valid but miss some attributes, such as width, height, orientation - SourceImageEntry entry = new SourceImageEntry(entryMap).fillPreCatalogMetadata(context); - entryMap = entry.toMap(); - } - - newEntryHandler.handleEntry(entryMap); - if (newEntryCount % 30 == 0) { - Thread.sleep(10); - } - newEntryCount++; - } - } - cursor.close(); - } - } catch (Exception e) { - Log.e(LOG_TAG, "failed to get entries", e); - } - return newEntryCount; - } - - private boolean needSize(String mimeType) { - return !MimeTypes.SVG.equals(mimeType); - } - - @Override - public ListenableFuture delete(final Context context, final String path, final Uri mediaUri) { - SettableFuture future = SettableFuture.create(); - - if (StorageUtils.requireAccessPermission(path)) { - // if the file is on SD card, calling the content resolver delete() removes the entry from the Media Store - // but it doesn't delete the file, even if the app has the permission - try { - DocumentFileCompat df = StorageUtils.getDocumentFile(context, path, mediaUri); - if (df != null && df.delete()) { - future.set(null); - } else { - future.setException(new Exception("failed to delete file with df=" + df)); - } - } catch (FileNotFoundException e) { - future.setException(e); - } - return future; - } - - try { - if (context.getContentResolver().delete(mediaUri, null, null) > 0) { - future.set(null); - } else { - future.setException(new Exception("failed to delete row from content provider")); - } - } catch (Exception e) { - Log.e(LOG_TAG, "failed to delete entry", e); - future.setException(e); - } - return future; - } - - private String getVolumeNameForMediaStore(@NonNull Context context, @NonNull String anyPath) { - String volumeName = "external"; - StorageManager sm = context.getSystemService(StorageManager.class); - if (sm != null) { - StorageVolume volume = sm.getStorageVolume(new File(anyPath)); - if (volume != null && !volume.isPrimary()) { - String uuid = volume.getUuid(); - if (uuid != null) { - // the UUID returned may be uppercase - // but it should be lowercase to work with the MediaStore - volumeName = uuid.toLowerCase(); - } - } - } - return volumeName; - } - - @Override - public void moveMultiple(final Context context, final Boolean copy, final String destinationDir, final List entries, @NonNull final ImageOpCallback callback) { - DocumentFileCompat destinationDirDocFile = StorageUtils.createDirectoryIfAbsent(context, destinationDir); - if (destinationDirDocFile == null) { - callback.onFailure(new Exception("failed to create directory at path=" + destinationDir)); - return; - } - - MediaStoreMoveDestination destination = new MediaStoreMoveDestination(context, destinationDir); - if (destination.volumePath == null) { - callback.onFailure(new Exception("failed to set up destination volume path for path=" + destinationDir)); - return; - } - - for (AvesImageEntry entry : entries) { - Uri sourceUri = entry.uri; - String sourcePath = entry.path; - String mimeType = entry.mimeType; - - Map result = new HashMap() {{ - put("uri", sourceUri.toString()); - }}; - - // on API 30 we cannot get access granted directly to a volume root from its document tree, - // but it is still less constraining to use tree document files than to rely on the Media Store - try { - ListenableFuture> newFieldsFuture; -// if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { -// newFieldsFuture = moveSingleByMediaStoreInsert(context, sourcePath, sourceUri, destination, mimeType, copy); -// } else { - newFieldsFuture = moveSingleByTreeDocAndScan(context, sourcePath, sourceUri, destinationDir, destinationDirDocFile, mimeType, copy); -// } - Map newFields = newFieldsFuture.get(); - result.put("success", true); - result.put("newFields", newFields); - } catch (ExecutionException | InterruptedException e) { - Log.w(LOG_TAG, "failed to move to destinationDir=" + destinationDir + " entry with sourcePath=" + sourcePath, e); - result.put("success", false); - } - callback.onSuccess(result); - } - } - - // We can create an item via `ContentResolver.insert()` with a path, and retrieve its content URI, but: - // - the Media Store isolates content by storage volume (e.g. `MediaStore.Images.Media.getContentUri(volumeName)`) - // - the volume name should be lower case, not exactly as the `StorageVolume` UUID - // - inserting on a removable volume works on API 29, but not on API 25 nor 26 (on which API/devices does it work?) - // - there is no documentation regarding support for usage with removable storage - // - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage - @RequiresApi(api = Build.VERSION_CODES.Q) - private ListenableFuture> moveSingleByMediaStoreInsert(final Context context, final String sourcePath, final Uri sourceUri, - final MediaStoreMoveDestination destination, final String mimeType, final boolean copy) { - SettableFuture> future = SettableFuture.create(); - - try { - String displayName = new File(sourcePath).getName(); - String destinationFilePath = destination.fullPath + displayName; - - ContentValues contentValues = new ContentValues(); - contentValues.put(MediaStore.MediaColumns.DATA, destinationFilePath); - contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); - // from API 29, changing MediaColumns.RELATIVE_PATH can move files on disk (same storage device) - contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, destination.relativePath); - contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName); - String volumeName = destination.volumeNameForMediaStore; - Uri tableUrl = MimeTypes.isVideo(mimeType) ? - MediaStore.Video.Media.getContentUri(volumeName) : - MediaStore.Images.Media.getContentUri(volumeName); - Uri destinationUri = context.getContentResolver().insert(tableUrl, contentValues); - if (destinationUri == null) { - future.setException(new Exception("failed to insert row to content resolver")); - } else { - DocumentFileCompat sourceFile = DocumentFileCompat.fromSingleUri(context, sourceUri); - DocumentFileCompat destinationFile = DocumentFileCompat.fromSingleUri(context, destinationUri); - sourceFile.copyTo(destinationFile); - - boolean deletedSource = false; - if (!copy) { - // delete original entry - try { - delete(context, sourcePath, sourceUri).get(); - deletedSource = true; - } catch (ExecutionException | InterruptedException e) { - Log.w(LOG_TAG, "failed to delete entry with path=" + sourcePath, e); - } - } - - Map newFields = new HashMap<>(); - newFields.put("uri", destinationUri.toString()); - newFields.put("contentId", ContentUris.parseId(destinationUri)); - newFields.put("path", destinationFilePath); - newFields.put("deletedSource", deletedSource); - future.set(newFields); - } - } catch (Exception e) { - Log.e(LOG_TAG, "failed to " + (copy ? "copy" : "move") + " entry", e); - future.setException(e); - } - - return future; - } - - // We can create an item via `DocumentFile.createFile()`, but: - // - we need to scan the file to get the Media Store content URI - // - the underlying document provider controls the new file name - private ListenableFuture> moveSingleByTreeDocAndScan(final Context context, final String sourcePath, final Uri sourceUri, final String destinationDir, final DocumentFileCompat destinationDirDocFile, final String mimeType, boolean copy) { - SettableFuture> future = SettableFuture.create(); - - try { - String sourceFileName = new File(sourcePath).getName(); - String desiredNameWithoutExtension = sourceFileName.replaceFirst("[.][^.]+$", ""); - - // the file created from a `TreeDocumentFile` is also a `TreeDocumentFile` - // but in order to open an output stream to it, we need to use a `SingleDocumentFile` - // through a document URI, not a tree URI - // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first - DocumentFileCompat destinationTreeFile = destinationDirDocFile.createFile(mimeType, desiredNameWithoutExtension); - DocumentFileCompat destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.getUri()); - - // `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry - // `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument" - // when used with entry URI as `sourceDocumentUri`, and destinationDirDocFile URI as `targetParentDocumentUri` - DocumentFileCompat source = DocumentFileCompat.fromSingleUri(context, sourceUri); - source.copyTo(destinationDocFile); - - // the source file name and the created document file name can be different when: - // - a file with the same name already exists, so the name gets a suffix like ` (1)` - // - the original extension does not match the extension appended used by the underlying provider - String fileName = destinationDocFile.getName(); - String destinationFullPath = destinationDir + fileName; - - boolean deletedSource = false; - if (!copy) { - // delete original entry - try { - delete(context, sourcePath, sourceUri).get(); - deletedSource = true; - } catch (ExecutionException | InterruptedException e) { - Log.w(LOG_TAG, "failed to delete entry with path=" + sourcePath, e); - } - } - - boolean finalDeletedSource = deletedSource; - scanNewPath(context, destinationFullPath, mimeType, new ImageProvider.ImageOpCallback() { - @Override - public void onSuccess(Map newFields) { - newFields.put("deletedSource", finalDeletedSource); - future.set(newFields); - } - - @Override - public void onFailure(Throwable throwable) { - future.setException(throwable); - } - }); - } catch (Exception e) { - Log.e(LOG_TAG, "failed to " + (copy ? "copy" : "move") + " entry", e); - future.setException(e); - } - - return future; - } - - public interface NewEntryHandler { - void handleEntry(Map entry); - } - - public interface NewEntryChecker { - boolean where(int contentId, int dateModifiedSecs); - } - - class MediaStoreMoveDestination { - final String volumeNameForMediaStore; - final String volumePath; - final String relativePath; - final String fullPath; - - MediaStoreMoveDestination(@NonNull Context context, @NonNull String destinationDir) { - fullPath = destinationDir; - volumeNameForMediaStore = getVolumeNameForMediaStore(context, destinationDir); - volumePath = StorageUtils.getVolumePath(context, destinationDir); - relativePath = volumePath != null ? destinationDir.replaceFirst(volumePath, "") : null; - } - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt index 11fdca71d..7827394d0 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -26,12 +26,13 @@ class MainActivity : FlutterActivity() { } private val intentStreamHandler = IntentStreamHandler() - private var intentDataMap: MutableMap? = null + private lateinit var intentDataMap: MutableMap override fun onCreate(savedInstanceState: Bundle?) { + Log.i(LOG_TAG, "onCreate intent=$intent") super.onCreate(savedInstanceState) - handleIntent(intent) + intentDataMap = extractIntentData(intent) val messenger = flutterEngine!!.dartExecutor.binaryMessenger @@ -50,15 +51,13 @@ class MainActivity : FlutterActivity() { when (call.method) { "getIntentData" -> { result.success(intentDataMap) - intentDataMap = null + intentDataMap.clear() } "pick" -> { - result.success(intentDataMap) - intentDataMap = null - val resultUri = call.argument("uri") - if (resultUri != null) { + val pickedUri = call.argument("uri") + if (pickedUri != null) { val intent = Intent().apply { - data = Uri.parse(resultUri) + data = Uri.parse(pickedUri) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } setResult(RESULT_OK, intent) @@ -82,65 +81,65 @@ class MainActivity : FlutterActivity() { // do not use 'route' as extra key, as the Flutter framework acts on it val search = ShortcutInfoCompat.Builder(this, "search") - .setShortLabel(getString(R.string.search_shortcut_short_label)) - .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_search)) - .setIntent(Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) - .putExtra("page", "/search")) - .build() + .setShortLabel(getString(R.string.search_shortcut_short_label)) + .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_search)) + .setIntent( + Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) + .putExtra("page", "/search") + ) + .build() val videos = ShortcutInfoCompat.Builder(this, "videos") - .setShortLabel(getString(R.string.videos_shortcut_short_label)) - .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_movie)) - .setIntent(Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) - .putExtra("page", "/collection") - .putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}"))) - .build() + .setShortLabel(getString(R.string.videos_shortcut_short_label)) + .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_movie)) + .setIntent( + Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) + .putExtra("page", "/collection") + .putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}")) + ) + .build() ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search)) } override fun onNewIntent(intent: Intent) { + Log.i(LOG_TAG, "onNewIntent intent=$intent") super.onNewIntent(intent) - handleIntent(intent) - intentStreamHandler.notifyNewIntent() + intentStreamHandler.notifyNewIntent(extractIntentData(intent)) } - private fun handleIntent(intent: Intent?) { - Log.i(LOG_TAG, "handleIntent intent=$intent") - if (intent == null) return - when (intent.action) { + private fun extractIntentData(intent: Intent?): MutableMap { + when (intent?.action) { Intent.ACTION_MAIN -> { - val page = intent.getStringExtra("page") - if (page != null) { - intentDataMap = hashMapOf( - "page" to page, - "filters" to intent.getStringArrayExtra("filters")?.toList(), + intent.getStringExtra("page")?.let { page -> + return hashMapOf( + "page" to page, + "filters" to intent.getStringArrayExtra("filters")?.toList(), ) } } Intent.ACTION_VIEW -> { - val uri = intent.data - val mimeType = intent.type - if (uri != null && mimeType != null) { - intentDataMap = hashMapOf( - "action" to "view", - "uri" to uri.toString(), - "mimeType" to mimeType, + intent.data?.let { uri -> + return hashMapOf( + "action" to "view", + "uri" to uri.toString(), + "mimeType" to intent.type, // MIME type is optional ) } } Intent.ACTION_GET_CONTENT, Intent.ACTION_PICK -> { - intentDataMap = hashMapOf( - "action" to "pick", - "mimeType" to intent.type, + return hashMapOf( + "action" to "pick", + "mimeType" to intent.type, ) } } + return HashMap() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == PermissionManager.VOLUME_ACCESS_REQUEST_CODE) { - val treeUri = data.data + val treeUri = data?.data if (resultCode != RESULT_OK || treeUri == null) { PermissionManager.onPermissionResult(requestCode, null) return diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt new file mode 100644 index 000000000..e4c4a5114 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -0,0 +1,257 @@ +package deckers.thibault.aves.channel.calls + +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.graphics.Bitmap +import android.net.Uri +import android.util.Log +import androidx.core.content.FileProvider +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DecodeFormat +import com.bumptech.glide.request.RequestOptions +import deckers.thibault.aves.utils.LogUtils.createTag +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.io.ByteArrayOutputStream +import java.io.File +import java.util.* +import kotlin.collections.ArrayList +import kotlin.math.roundToInt + +class AppAdapterHandler(private val context: Context) : MethodCallHandler { + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "getAppIcon" -> GlobalScope.launch { getAppIcon(call, Coresult(result)) } + "getAppNames" -> GlobalScope.launch { getAppNames(Coresult(result)) } + "getEnv" -> result.success(System.getenv()) + "edit" -> { + val title = call.argument("title") + val uri = call.argument("uri")?.let { Uri.parse(it) } + val mimeType = call.argument("mimeType") + result.success(edit(title, uri, mimeType)) + } + "open" -> { + val title = call.argument("title") + val uri = call.argument("uri")?.let { Uri.parse(it) } + val mimeType = call.argument("mimeType") + result.success(open(title, uri, mimeType)) + } + "openMap" -> { + val geoUri = call.argument("geoUri")?.let { Uri.parse(it) } + result.success(openMap(geoUri)) + } + "setAs" -> { + val title = call.argument("title") + val uri = call.argument("uri")?.let { Uri.parse(it) } + val mimeType = call.argument("mimeType") + result.success(setAs(title, uri, mimeType)) + } + "share" -> { + val title = call.argument("title") + val urisByMimeType = call.argument>>("urisByMimeType")!! + result.success(shareMultiple(title, urisByMimeType)) + } + else -> result.notImplemented() + } + } + + private fun getAppNames(result: MethodChannel.Result) { + val nameMap = HashMap() + val intent = Intent(Intent.ACTION_MAIN, null) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + + // apps tend to use their name in English when creating folders + // so we get their names in English as well as the current locale + val englishConfig = Configuration().apply { setLocale(Locale.ENGLISH) } + + val pm = context.packageManager + for (resolveInfo in pm.queryIntentActivities(intent, 0)) { + val ai = resolveInfo.activityInfo.applicationInfo + val isSystemPackage = ai.flags and ApplicationInfo.FLAG_SYSTEM != 0 + if (!isSystemPackage) { + val packageName = ai.packageName + + val currentLabel = pm.getApplicationLabel(ai).toString() + nameMap[currentLabel] = packageName + + val labelRes = ai.labelRes + if (labelRes != 0) { + try { + val resources = pm.getResourcesForApplication(ai) + // `updateConfiguration` is deprecated but it seems to be the only way + // to query resources from another app with a specific locale. + // The following methods do not work: + // - `resources.getConfiguration().setLocale(...)` + // - getting a package manager from a custom context with `context.createConfigurationContext(config)` + @Suppress("DEPRECATION") + resources.updateConfiguration(englishConfig, resources.displayMetrics) + val englishLabel = resources.getString(labelRes) + nameMap[englishLabel] = packageName + } catch (e: PackageManager.NameNotFoundException) { + Log.w(LOG_TAG, "failed to get app label in English for packageName=$packageName", e) + } + } + } + } + result.success(nameMap) + } + + private fun getAppIcon(call: MethodCall, result: MethodChannel.Result) { + val packageName = call.argument("packageName") + val sizeDip = call.argument("sizeDip") + if (packageName == null || sizeDip == null) { + result.error("getAppIcon-args", "failed because of missing arguments", null) + return + } + + // convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter + val density = context.resources.displayMetrics.density + val size = (sizeDip * density).roundToInt() + var data: ByteArray? = null + try { + val iconResourceId = context.packageManager.getApplicationInfo(packageName, 0).icon + val uri = Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(packageName) + .path(iconResourceId.toString()) + .build() + + val options = RequestOptions() + .format(DecodeFormat.PREFER_RGB_565) + .centerCrop() + .override(size, size) + val target = Glide.with(context) + .asBitmap() + .apply(options) + .load(uri) + .submit(size, size) + + try { + val bitmap = target.get() + if (bitmap != null) { + val stream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream) + data = stream.toByteArray() + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e) + } + Glide.with(context).clear(target) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(LOG_TAG, "failed to get app info for packageName=$packageName", e) + return + } + if (data != null) { + result.success(data) + } else { + result.error("getAppIcon-null", "failed to get icon for packageName=$packageName", null) + } + } + + private fun edit(title: String?, uri: Uri?, mimeType: String?): Boolean { + uri ?: return false + + val intent = Intent(Intent.ACTION_EDIT) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + .setDataAndType(uri, mimeType) + return safeStartActivityChooser(title, intent) + } + + private fun open(title: String?, uri: Uri?, mimeType: String?): Boolean { + uri ?: return false + + val intent = Intent(Intent.ACTION_VIEW) + .setDataAndType(uri, mimeType) + return safeStartActivityChooser(title, intent) + } + + private fun openMap(geoUri: Uri?): Boolean { + geoUri ?: return false + + val intent = Intent(Intent.ACTION_VIEW, geoUri) + return safeStartActivity(intent) + } + + private fun setAs(title: String?, uri: Uri?, mimeType: String?): Boolean { + uri ?: return false + + val intent = Intent(Intent.ACTION_ATTACH_DATA) + .setDataAndType(uri, mimeType) + return safeStartActivityChooser(title, intent) + } + + private fun shareSingle(title: String?, uri: Uri, mimeType: String): Boolean { + val intent = Intent(Intent.ACTION_SEND) + .setType(mimeType) + when (uri.scheme?.toLowerCase(Locale.ROOT)) { + ContentResolver.SCHEME_FILE -> { + val path = uri.path ?: return false + val applicationId = context.applicationContext.packageName + val apkUri = FileProvider.getUriForFile(context, "$applicationId.fileprovider", File(path)) + intent.putExtra(Intent.EXTRA_STREAM, apkUri) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + else -> intent.putExtra(Intent.EXTRA_STREAM, uri) + } + return safeStartActivityChooser(title, intent) + } + + private fun shareMultiple(title: String?, urisByMimeType: Map>?): Boolean { + urisByMimeType ?: return false + + val uriList = ArrayList(urisByMimeType.values.flatten().mapNotNull { Uri.parse(it) }) + val mimeTypes = urisByMimeType.keys.toTypedArray() + + // simplify share intent for a single item, as some apps can handle one item but not more + if (uriList.size == 1) { + return shareSingle(title, uriList.first(), mimeTypes.first()) + } + + var mimeType = "*/*" + if (mimeTypes.size == 1) { + // items have the same mime type & subtype + mimeType = mimeTypes.first() + } else { + // items have different subtypes + val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct() + if (mimeTypeTypes.size == 1) { + // items have the same mime type + mimeType = "${mimeTypeTypes.first()}/*" + } + } + + val intent = Intent(Intent.ACTION_SEND_MULTIPLE) + .putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList) + .setType(mimeType) + return safeStartActivityChooser(title, intent) + } + + private fun safeStartActivity(intent: Intent): Boolean { + val canResolve = intent.resolveActivity(context.packageManager) != null + if (canResolve) { + context.startActivity(intent) + } + return canResolve + } + + private fun safeStartActivityChooser(title: String?, intent: Intent): Boolean { + val canResolve = intent.resolveActivity(context.packageManager) != null + if (canResolve) { + context.startActivity(Intent.createChooser(intent, title)) + } + return canResolve + } + + companion object { + private val LOG_TAG = createTag(AppAdapterHandler::class.java) + const val CHANNEL = "deckers.thibault/aves/app" + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppShortcutHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppShortcutHandler.kt new file mode 100644 index 000000000..591459ba9 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppShortcutHandler.kt @@ -0,0 +1,66 @@ +package deckers.thibault.aves.channel.calls + +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import deckers.thibault.aves.MainActivity +import deckers.thibault.aves.R +import deckers.thibault.aves.utils.BitmapUtils.centerSquareCrop +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class AppShortcutHandler(private val context: Context) : MethodCallHandler { + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "canPin" -> result.success(canPin()) + "pin" -> { + GlobalScope.launch { pin(call) } + result.success(null) + } + else -> result.notImplemented() + } + } + + private fun canPin() = ShortcutManagerCompat.isRequestPinShortcutSupported(context) + + private fun pin(call: MethodCall) { + if (!canPin()) return + + val label = call.argument("label") ?: return + val iconBytes = call.argument("iconBytes") + val filters = call.argument>("filters") ?: return + + var icon: IconCompat? = null + if (iconBytes?.isNotEmpty() == true) { + var bitmap = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size) + bitmap = centerSquareCrop(context, bitmap, 256) + if (bitmap != null) { + icon = IconCompat.createWithBitmap(bitmap) + } + } + if (icon == null) { + icon = IconCompat.createWithResource(context, R.mipmap.ic_shortcut_collection) + } + + val intent = Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java) + .putExtra("page", "/collection") + .putExtra("filters", filters.toTypedArray()) + + val shortcut = ShortcutInfoCompat.Builder(context, "collection-${filters.joinToString("-")}") + .setShortLabel(label) + .setIcon(icon) + .setIntent(intent) + .build() + ShortcutManagerCompat.requestPinShortcut(context, shortcut, null) + } + + companion object { + const val CHANNEL = "deckers.thibault/aves/shortcut" + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt new file mode 100644 index 000000000..d2db62561 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt @@ -0,0 +1,23 @@ +package deckers.thibault.aves.channel.calls + +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +// ensure `result` methods are called on the main looper thread +class Coresult internal constructor(private val methodResult: MethodChannel.Result) : MethodChannel.Result { + private val mainScope = CoroutineScope(Dispatchers.Main) + + override fun success(result: Any?) { + mainScope.launch { methodResult.success(result) } + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + mainScope.launch { methodResult.error(errorCode, errorMessage, errorDetails) } + } + + override fun notImplemented() { + mainScope.launch { methodResult.notImplemented() } + } +} diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt new file mode 100644 index 000000000..893aca1f5 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt @@ -0,0 +1,170 @@ +package deckers.thibault.aves.channel.calls + +import android.app.Activity +import android.net.Uri +import com.bumptech.glide.Glide +import deckers.thibault.aves.model.ExifOrientationOp +import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback +import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider +import deckers.thibault.aves.model.provider.MediaStoreImageProvider +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +class ImageFileHandler(private val activity: Activity) : MethodCallHandler { + private val density = activity.resources.displayMetrics.density + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "getObsoleteEntries" -> GlobalScope.launch { getObsoleteEntries(call, Coresult(result)) } + "getImageEntry" -> GlobalScope.launch { getImageEntry(call, Coresult(result)) } + "getThumbnail" -> GlobalScope.launch { getThumbnail(call, Coresult(result)) } + "clearSizedThumbnailDiskCache" -> { + GlobalScope.launch { Glide.get(activity).clearDiskCache() } + result.success(null) + } + "rename" -> GlobalScope.launch { rename(call, Coresult(result)) } + "rotate" -> GlobalScope.launch { rotate(call, Coresult(result)) } + "flip" -> GlobalScope.launch { flip(call, Coresult(result)) } + else -> result.notImplemented() + } + } + + private fun getObsoleteEntries(call: MethodCall, result: MethodChannel.Result) { + val known = call.argument>("knownContentIds") + if (known == null) { + result.error("getObsoleteEntries-args", "failed because of missing arguments", null) + return + } + result.success(MediaStoreImageProvider().getObsoleteContentIds(activity, known)) + } + + private fun getThumbnail(call: MethodCall, result: MethodChannel.Result) { + val uri = call.argument("uri") + val mimeType = call.argument("mimeType") + val dateModifiedSecs = call.argument("dateModifiedSecs")?.toLong() + val rotationDegrees = call.argument("rotationDegrees") + val isFlipped = call.argument("isFlipped") + val widthDip = call.argument("widthDip") + val heightDip = call.argument("heightDip") + val defaultSizeDip = call.argument("defaultSizeDip") + if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) { + result.error("getThumbnail-args", "failed because of missing arguments", null) + return + } + + // convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter + GlobalScope.launch { + ThumbnailFetcher( + activity, + uri, + mimeType, + dateModifiedSecs, + rotationDegrees, + isFlipped, + width = (widthDip * density).roundToInt(), + height = (heightDip * density).roundToInt(), + defaultSize = (defaultSizeDip * density).roundToInt(), + Coresult(result), + ).fetch() + } + } + + private fun getImageEntry(call: MethodCall, result: MethodChannel.Result) { + val mimeType = call.argument("mimeType") // MIME type is optional + val uri = call.argument("uri")?.let { Uri.parse(it) } + if (uri == null) { + result.error("getImageEntry-args", "failed because of missing arguments", null) + return + } + + val provider = getProvider(uri) + if (provider == null) { + result.error("getImageEntry-provider", "failed to find provider for uri=$uri", null) + return + } + + provider.fetchSingle(activity, uri, mimeType, object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = result.success(fields) + override fun onFailure(throwable: Throwable) = result.error("getImageEntry-failure", "failed to get entry for uri=$uri", throwable.message) + }) + } + + private fun rename(call: MethodCall, result: MethodChannel.Result) { + val entryMap = call.argument("entry") + val newName = call.argument("newName") + if (entryMap == null || newName == null) { + result.error("rename-args", "failed because of missing arguments", null) + return + } + + val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) } + val path = entryMap["path"] as String? + val mimeType = entryMap["mimeType"] as String? + if (uri == null || path == null || mimeType == null) { + result.error("rename-args", "failed because entry fields are missing", null) + return + } + + val provider = getProvider(uri) + if (provider == null) { + result.error("rename-provider", "failed to find provider for uri=$uri", null) + return + } + + provider.rename(activity, path, uri, mimeType, newName, object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = result.success(fields) + override fun onFailure(throwable: Throwable) = result.error("rename-failure", "failed to rename", throwable.message) + }) + } + + private fun rotate(call: MethodCall, result: MethodChannel.Result) { + val clockwise = call.argument("clockwise") + if (clockwise == null) { + result.error("rotate-args", "failed because of missing arguments", null) + return + } + + val op = if (clockwise) ExifOrientationOp.ROTATE_CW else ExifOrientationOp.ROTATE_CCW + changeOrientation(call, result, op) + } + + private fun flip(call: MethodCall, result: MethodChannel.Result) { + changeOrientation(call, result, ExifOrientationOp.FLIP) + } + + private fun changeOrientation(call: MethodCall, result: MethodChannel.Result, op: ExifOrientationOp) { + val entryMap = call.argument("entry") + if (entryMap == null) { + result.error("changeOrientation-args", "failed because of missing arguments", null) + return + } + + val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) } + val path = entryMap["path"] as String? + val mimeType = entryMap["mimeType"] as String? + if (uri == null || path == null || mimeType == null) { + result.error("changeOrientation-args", "failed because entry fields are missing", null) + return + } + + val provider = getProvider(uri) + if (provider == null) { + result.error("changeOrientation-provider", "failed to find provider for uri=$uri", null) + return + } + + provider.changeOrientation(activity, path, uri, mimeType, op, object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = result.success(fields) + override fun onFailure(throwable: Throwable) = result.error("changeOrientation-failure", "failed to change orientation", throwable.message) + }) + } + + companion object { + const val CHANNEL = "deckers.thibault/aves/image" + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index 8f158f27e..37535b3cc 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -55,6 +55,8 @@ import deckers.thibault.aves.utils.StorageUtils import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import java.io.ByteArrayOutputStream import java.io.IOException import java.util.* @@ -63,15 +65,15 @@ import kotlin.math.roundToLong class MetadataHandler(private val context: Context) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getAllMetadata" -> Thread { getAllMetadata(call, MethodResultWrapper(result)) }.start() - "getCatalogMetadata" -> Thread { getCatalogMetadata(call, MethodResultWrapper(result)) }.start() - "getOverlayMetadata" -> Thread { getOverlayMetadata(call, MethodResultWrapper(result)) }.start() - "getContentResolverMetadata" -> Thread { getContentResolverMetadata(call, MethodResultWrapper(result)) }.start() - "getExifInterfaceMetadata" -> Thread { getExifInterfaceMetadata(call, MethodResultWrapper(result)) }.start() - "getMediaMetadataRetrieverMetadata" -> Thread { getMediaMetadataRetrieverMetadata(call, MethodResultWrapper(result)) }.start() - "getEmbeddedPictures" -> Thread { getEmbeddedPictures(call, MethodResultWrapper(result)) }.start() - "getExifThumbnails" -> Thread { getExifThumbnails(call, MethodResultWrapper(result)) }.start() - "getXmpThumbnails" -> Thread { getXmpThumbnails(call, MethodResultWrapper(result)) }.start() + "getAllMetadata" -> GlobalScope.launch { getAllMetadata(call, Coresult(result)) } + "getCatalogMetadata" -> GlobalScope.launch { getCatalogMetadata(call, Coresult(result)) } + "getOverlayMetadata" -> GlobalScope.launch { getOverlayMetadata(call, Coresult(result)) } + "getContentResolverMetadata" -> GlobalScope.launch { getContentResolverMetadata(call, Coresult(result)) } + "getExifInterfaceMetadata" -> GlobalScope.launch { getExifInterfaceMetadata(call, Coresult(result)) } + "getMediaMetadataRetrieverMetadata" -> GlobalScope.launch { getMediaMetadataRetrieverMetadata(call, Coresult(result)) } + "getEmbeddedPictures" -> GlobalScope.launch { getEmbeddedPictures(call, Coresult(result)) } + "getExifThumbnails" -> GlobalScope.launch { getExifThumbnails(call, Coresult(result)) } + "getXmpThumbnails" -> GlobalScope.launch { getXmpThumbnails(call, Coresult(result)) } else -> result.notImplemented() } } @@ -374,7 +376,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { val num = it.numerator val denom = it.denominator metadataMap[KEY_EXPOSURE_TIME] = when { - num >= denom -> it.toSimpleString(true) + "″" + num >= denom -> "${it.toSimpleString(true)}″" num != 1L && num != 0L -> Rational(1, (denom / num.toDouble()).roundToLong()).toString() else -> it.toString() } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt new file mode 100644 index 000000000..5115f7c70 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt @@ -0,0 +1,96 @@ +package deckers.thibault.aves.channel.calls + +import android.content.Context +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Build +import android.os.storage.StorageManager +import androidx.annotation.RequiresApi +import deckers.thibault.aves.utils.PermissionManager +import deckers.thibault.aves.utils.StorageUtils.getVolumePaths +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.io.File +import java.util.* + +class StorageHandler(private val context: Context) : MethodCallHandler { + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "getStorageVolumes" -> { + val volumes: List> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + storageVolumes + } else { + // TODO TLAD find alternative for Android result.success(ArrayList(PermissionManager.getGrantedDirs(context))) + "getInaccessibleDirectories" -> getInaccessibleDirectories(call, result) + "revokeDirectoryAccess" -> revokeDirectoryAccess(call, result) + "scanFile" -> GlobalScope.launch { scanFile(call, Coresult(result)) } + else -> result.notImplemented() + } + } + + private val storageVolumes: List> + @RequiresApi(api = Build.VERSION_CODES.N) + get() { + val volumes = ArrayList>() + val sm = context.getSystemService(StorageManager::class.java) + if (sm != null) { + for (volumePath in getVolumePaths(context)) { + try { + sm.getStorageVolume(File(volumePath))?.let { + val volumeMap = HashMap() + volumeMap["path"] = volumePath + volumeMap["description"] = it.getDescription(context) + volumeMap["isPrimary"] = it.isPrimary + volumeMap["isRemovable"] = it.isRemovable + volumeMap["isEmulated"] = it.isEmulated + volumeMap["state"] = it.state + volumes.add(volumeMap) + } + } catch (e: IllegalArgumentException) { + // ignore + } + } + } + return volumes + } + + private fun getInaccessibleDirectories(call: MethodCall, result: MethodChannel.Result) { + val dirPaths = call.argument>("dirPaths") + if (dirPaths == null) { + result.error("getInaccessibleDirectories-args", "failed because of missing arguments", null) + return + } + + val dirs = PermissionManager.getInaccessibleDirectories(context, dirPaths) + result.success(dirs) + } + + private fun revokeDirectoryAccess(call: MethodCall, result: MethodChannel.Result) { + val path = call.argument("path") + if (path == null) { + result.error("revokeDirectoryAccess-args", "failed because of missing arguments", null) + return + } + + val success = PermissionManager.revokeDirectoryAccess(context, path) + result.success(success) + } + + private fun scanFile(call: MethodCall, result: MethodChannel.Result) { + val path = call.argument("path") + val mimeType = call.argument("mimeType") + MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, uri: Uri? -> result.success(uri?.toString()) } + } + + companion object { + const val CHANNEL = "deckers.thibault/aves/storage" + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt new file mode 100644 index 000000000..80da74a90 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt @@ -0,0 +1,141 @@ +package deckers.thibault.aves.channel.calls + +import android.app.Activity +import android.content.ContentUris +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.util.Size +import androidx.annotation.RequiresApi +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DecodeFormat +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.signature.ObjectKey +import deckers.thibault.aves.decoder.VideoThumbnail +import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation +import deckers.thibault.aves.utils.MimeTypes.isVideo +import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail +import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide +import io.flutter.plugin.common.MethodChannel +import java.io.ByteArrayOutputStream + +class ThumbnailFetcher internal constructor( + private val activity: Activity, + uri: String, + private val mimeType: String, + private val dateModifiedSecs: Long, + private val rotationDegrees: Int, + private val isFlipped: Boolean, + width: Int?, + height: Int?, + private val defaultSize: Int, + private val result: MethodChannel.Result, +) { + val uri: Uri = Uri.parse(uri) + val width: Int = if (width?.takeIf { it > 0 } != null) width else defaultSize + val height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize + + fun fetch() { + var bitmap: Bitmap? = null + var exception: Exception? = null + + // fetch low quality thumbnails when size is not specified + if ((width == defaultSize || height == defaultSize) && !isFlipped) { + // as of Android R, the Media Store content resolver may return a thumbnail + // that is automatically rotated according to EXIF orientation, + // but not flipped when necessary + // so we skip this step for flipped entries + try { + bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) getByResolver() else getByMediaStore() + } catch (e: Exception) { + exception = e + } + } + + // fallback if the native methods failed or for higher quality thumbnails + if (bitmap == null) { + try { + bitmap = getByGlide() + } catch (e: Exception) { + exception = e + } + } + + if (bitmap != null) { + val stream = ByteArrayOutputStream() + // we compress the bitmap because Dart Image.memory cannot decode the raw bytes + // Bitmap.CompressFormat.PNG is slower than JPEG + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream) + result.success(stream.toByteArray()) + return + } + + var errorDetails: String? = exception?.message + if (errorDetails?.isNotEmpty() == true) { + errorDetails = errorDetails.split("\n".toRegex(), 2).first() + } + result.error("getThumbnail-null", "failed to get thumbnail for uri=$uri", errorDetails) + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + private fun getByResolver(): Bitmap? { + val resolver = activity.contentResolver + var bitmap: Bitmap? = resolver.loadThumbnail(uri, Size(width, height), null) + if (needRotationAfterContentResolverThumbnail(mimeType)) { + bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped) + } + return bitmap + } + + private fun getByMediaStore(): Bitmap? { + val contentId = ContentUris.parseId(uri) + val resolver = activity.contentResolver + return if (isVideo(mimeType)) { + @Suppress("DEPRECATION") + MediaStore.Video.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Video.Thumbnails.MINI_KIND, null) + } else { + @Suppress("DEPRECATION") + var bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null) + // from Android Q, returned thumbnail is already rotated according to EXIF orientation + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) { + bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped) + } + bitmap + } + } + + private fun getByGlide(): Bitmap? { + // add signature to ignore cache for images which got modified but kept the same URI + var options = RequestOptions() + .format(DecodeFormat.PREFER_RGB_565) + .signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width")) + .override(width, height) + + val target = if (isVideo(mimeType)) { + options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE) + Glide.with(activity) + .asBitmap() + .apply(options) + .load(VideoThumbnail(activity, uri)) + .submit(width, height) + } else { + Glide.with(activity) + .asBitmap() + .apply(options) + .load(uri) + .submit(width, height) + } + + return try { + var bitmap = target.get() + if (needRotationAfterGlide(mimeType)) { + bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped) + } + bitmap + } finally { + Glide.with(activity).clear(target) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt new file mode 100644 index 000000000..1a4da7f1c --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt @@ -0,0 +1,172 @@ +package deckers.thibault.aves.channel.streams + +import android.app.Activity +import android.graphics.Bitmap +import android.net.Uri +import android.os.Handler +import android.os.Looper +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DecodeFormat +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.RequestOptions +import deckers.thibault.aves.decoder.VideoThumbnail +import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation +import deckers.thibault.aves.utils.MimeTypes.canHaveAlpha +import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter +import deckers.thibault.aves.utils.MimeTypes.isVideo +import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide +import deckers.thibault.aves.utils.StorageUtils.openInputStream +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream + +class ImageByteStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler { + private lateinit var eventSink: EventSink + private lateinit var handler: Handler + + override fun onListen(args: Any, eventSink: EventSink) { + this.eventSink = eventSink + handler = Handler(Looper.getMainLooper()) + + GlobalScope.launch { streamImage() } + } + + override fun onCancel(o: Any) {} + + private fun success(bytes: ByteArray) { + handler.post { eventSink.success(bytes) } + } + + private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) { + handler.post { eventSink.error(errorCode, errorMessage, errorDetails) } + } + + private fun endOfStream() { + handler.post { eventSink.endOfStream() } + } + + // Supported image formats: + // - Flutter (as of v1.20): JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP + // - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats + // - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java + private fun streamImage() { + if (arguments !is Map<*, *>) { + endOfStream() + return + } + + val mimeType = arguments["mimeType"] as String? + val uri = (arguments["uri"] as String?)?.let { Uri.parse(it) } + val rotationDegrees = arguments["rotationDegrees"] as Int + val isFlipped = arguments["isFlipped"] as Boolean + + if (mimeType == null || uri == null) { + error("streamImage-args", "failed because of missing arguments", null) + endOfStream() + return + } + + if (isVideo(mimeType)) { + streamVideoByGlide(uri) + } else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) { + // decode exotic format on platform side, then encode it in portable format for Flutter + streamImageByGlide(uri, mimeType, rotationDegrees, isFlipped) + } else { + // to be decoded by Flutter + streamImageAsIs(uri) + } + endOfStream() + } + + private fun streamImageAsIs(uri: Uri) { + try { + openInputStream(activity, uri).use { input -> input?.let { streamBytes(it) } } + } catch (e: IOException) { + error("streamImage-image-read-exception", "failed to get image from uri=$uri", e.message) + } + } + + private fun streamImageByGlide(uri: Uri, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) { + val target = Glide.with(activity) + .asBitmap() + .apply(options) + .load(uri) + .submit() + try { + var bitmap = target.get() + if (needRotationAfterGlide(mimeType)) { + bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped) + } + if (bitmap != null) { + val stream = ByteArrayOutputStream() + // we compress the bitmap because Dart Image.memory cannot decode the raw bytes + // Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency + if (canHaveAlpha(mimeType)) { + bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream) + } else { + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) + } + success(stream.toByteArray()) + } else { + error("streamImage-image-decode-null", "failed to get image from uri=$uri", null) + } + } catch (e: Exception) { + var errorDetails = e.message + if (errorDetails?.isNotEmpty() == true) { + errorDetails = errorDetails.split("\n".toRegex(), 2).first() + } + error("streamImage-image-decode-exception", "failed to get image from uri=$uri", errorDetails) + } finally { + Glide.with(activity).clear(target) + } + } + + private fun streamVideoByGlide(uri: Uri) { + val target = Glide.with(activity) + .asBitmap() + .apply(options) + .load(VideoThumbnail(activity, uri)) + .submit() + try { + val bitmap = target.get() + if (bitmap != null) { + val stream = ByteArrayOutputStream() + // we compress the bitmap because Dart Image.memory cannot decode the raw bytes + // Bitmap.CompressFormat.PNG is slower than JPEG + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) + success(stream.toByteArray()) + } else { + error("streamImage-video-null", "failed to get image from uri=$uri", null) + } + } catch (e: Exception) { + error("streamImage-video-exception", "failed to get image from uri=$uri", e.message) + } finally { + Glide.with(activity).clear(target) + } + } + + private fun streamBytes(inputStream: InputStream) { + val buffer = ByteArray(bufferSize) + var len: Int + while (inputStream.read(buffer).also { len = it } != -1) { + // cannot decode image on Flutter side when using `buffer` directly + success(buffer.copyOf(len)) + } + } + + companion object { + const val CHANNEL = "deckers.thibault/aves/imagebytestream" + + const val bufferSize = 2 shl 17 // 256kB + + // request a fresh image with the highest quality format + val options = RequestOptions() + .format(DecodeFormat.PREFER_ARGB_8888) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt new file mode 100644 index 000000000..d8d2c15b2 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt @@ -0,0 +1,136 @@ +package deckers.thibault.aves.channel.streams + +import android.content.Context +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.util.Log +import deckers.thibault.aves.model.AvesImageEntry +import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback +import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider +import deckers.thibault.aves.utils.LogUtils.createTag +import deckers.thibault.aves.utils.StorageUtils +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.util.* +import java.util.concurrent.ExecutionException + +class ImageOpStreamHandler(private val context: Context, private val arguments: Any?) : EventChannel.StreamHandler { + private lateinit var eventSink: EventSink + private lateinit var handler: Handler + + private var op: String? = null + private val entryMapList = ArrayList() + + init { + if (arguments is Map<*, *>) { + op = arguments["op"] as String? + @Suppress("UNCHECKED_CAST") + val rawEntries = arguments["entries"] as List? + if (rawEntries != null) { + entryMapList.addAll(rawEntries) + } + } + } + + override fun onListen(args: Any, eventSink: EventSink) { + this.eventSink = eventSink + handler = Handler(Looper.getMainLooper()) + + when (op) { + "delete" -> GlobalScope.launch { delete() } + "move" -> GlobalScope.launch { move() } + else -> endOfStream() + } + } + + override fun onCancel(o: Any) {} + + // {String uri, bool success, [Map newFields]} + private fun success(result: Map) { + handler.post { eventSink.success(result) } + } + + private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) { + handler.post { eventSink.error(errorCode, errorMessage, errorDetails) } + } + + private fun endOfStream() { + handler.post { eventSink.endOfStream() } + } + + private fun move() { + if (arguments !is Map<*, *> || entryMapList.isEmpty()) { + endOfStream() + return + } + + // assume same provider for all entries + val firstEntry = entryMapList.first() + val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) } + if (provider == null) { + error("move-provider", "failed to find provider for entry=$firstEntry", null) + return + } + + val copy = arguments["copy"] as Boolean? + var destinationDir = arguments["destinationPath"] as String? + if (copy == null || destinationDir == null) { + error("move-args", "failed because of missing arguments", null) + return + } + + destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir) + val entries = entryMapList.map(::AvesImageEntry) + provider.moveMultiple(context, copy, destinationDir, entries, object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = success(fields) + override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable) + }) + endOfStream() + } + + private fun delete() { + if (entryMapList.isEmpty()) { + endOfStream() + return + } + + // assume same provider for all entries + val firstEntry = entryMapList.first() + val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) } + if (provider == null) { + error("delete-provider", "failed to find provider for entry=$firstEntry", null) + return + } + + for (entryMap in entryMapList) { + val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) } + val path = entryMap["path"] as String? + if (uri != null) { + val result = hashMapOf( + "uri" to uri.toString(), + ) + try { + provider.delete(context, uri, path).get() + result["success"] = true + } catch (e: ExecutionException) { + Log.w(LOG_TAG, "failed to delete entry with path=$path", e) + result["success"] = false + } catch (e: InterruptedException) { + Log.w(LOG_TAG, "failed to delete entry with path=$path", e) + result["success"] = false + } + success(result) + } + } + endOfStream() + } + + companion object { + private val LOG_TAG = createTag(ImageOpStreamHandler::class.java) + const val CHANNEL = "deckers.thibault/aves/imageopstream" + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt index caa69e352..abd594c58 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt @@ -4,7 +4,10 @@ import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel.EventSink class IntentStreamHandler : EventChannel.StreamHandler { - private lateinit var eventSink: EventSink + // cannot use `lateinit` because we cannot guarantee + // its initialization in `onListen` at the right time + // e.g. when resuming the app after the activity got destroyed + private var eventSink: EventSink? = null override fun onListen(arguments: Any?, eventSink: EventSink) { this.eventSink = eventSink @@ -12,7 +15,7 @@ class IntentStreamHandler : EventChannel.StreamHandler { override fun onCancel(arguments: Any?) {} - fun notifyNewIntent() { - eventSink.success(true) + fun notifyNewIntent(intentData: MutableMap?) { + eventSink?.success(intentData) } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt index 0f71b94d6..62e542d9b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt @@ -3,31 +3,36 @@ package deckers.thibault.aves.channel.streams import android.content.Context import android.os.Handler import android.os.Looper +import deckers.thibault.aves.model.provider.FieldMap import deckers.thibault.aves.model.provider.MediaStoreImageProvider import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel.EventSink +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : EventChannel.StreamHandler { private lateinit var eventSink: EventSink private lateinit var handler: Handler - private var knownEntries: Map? = null + + private var knownEntries: Map? = null init { if (arguments is Map<*, *>) { @Suppress("UNCHECKED_CAST") - knownEntries = arguments["knownEntries"] as Map? + knownEntries = arguments["knownEntries"] as Map? } } override fun onListen(arguments: Any?, eventSink: EventSink) { this.eventSink = eventSink handler = Handler(Looper.getMainLooper()) - Thread { fetchAll() }.start() + + GlobalScope.launch { fetchAll() } } override fun onCancel(arguments: Any?) {} - private fun success(result: Map) { + private fun success(result: FieldMap) { handler.post { eventSink.success(result) } } @@ -36,7 +41,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E } private fun fetchAll() { - MediaStoreImageProvider().fetchAll(context, knownEntries) { success(it) } + MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap()) { success(it) } endOfStream() } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt new file mode 100644 index 000000000..59d9725cb --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt @@ -0,0 +1,27 @@ +package deckers.thibault.aves.decoder + +import android.content.Context +import android.util.Log +import com.bumptech.glide.Glide +import com.bumptech.glide.GlideBuilder +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.ImageHeaderParser +import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser +import com.bumptech.glide.module.AppGlideModule + +@GlideModule +class AvesAppGlideModule : AppGlideModule() { + override fun applyOptions(context: Context, builder: GlideBuilder) { + // hide noisy warning (e.g. for images that can't be decoded) + builder.setLogLevel(Log.ERROR) + } + + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + // prevent ExifInterface error logs + // cf https://github.com/bumptech/glide/issues/3383 + glide.registry.imageHeaderParsers.removeIf { parser: ImageHeaderParser? -> parser is ExifInterfaceImageHeaderParser } + } + + override fun isManifestParsingEnabled(): Boolean = false +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt new file mode 100644 index 000000000..8b88d2f85 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt @@ -0,0 +1,80 @@ +package deckers.thibault.aves.decoder + +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import com.bumptech.glide.Glide +import com.bumptech.glide.Priority +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.data.DataFetcher.DataCallback +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.module.LibraryGlideModule +import com.bumptech.glide.signature.ObjectKey +import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream + +@GlideModule +class VideoThumbnailGlideModule : LibraryGlideModule() { + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + registry.append(VideoThumbnail::class.java, InputStream::class.java, VideoThumbnailLoader.Factory()) + } +} + +class VideoThumbnail(val context: Context, val uri: Uri) + +internal class VideoThumbnailLoader : ModelLoader { + override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData? { + return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model)) + } + + override fun handles(videoThumbnail: VideoThumbnail): Boolean = true + + internal class Factory : ModelLoaderFactory { + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = VideoThumbnailLoader() + + override fun teardown() {} + } +} + +internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFetcher { + override fun loadData(priority: Priority, callback: DataCallback) { + val retriever = openMetadataRetriever(model.context, model.uri) + if (retriever != null) { + try { + var picture = retriever.embeddedPicture + if (picture == null) { + // not ideal: bitmap -> byte[] -> bitmap + // but simple fallback and we cache result + val stream = ByteArrayOutputStream() + val bitmap = retriever.frameAtTime + bitmap?.compress(Bitmap.CompressFormat.PNG, 0, stream) + picture = stream.toByteArray() + } + callback.onDataReady(ByteArrayInputStream(picture)) + } catch (e: Exception) { + callback.onLoadFailed(e) + } finally { + // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs + retriever.release() + } + } + } + + // already cleaned up in loadData and ByteArrayInputStream will be GC'd + override fun cleanup() {} + + // cannot cancel + override fun cancel() {} + + override fun getDataClass(): Class = InputStream::class.java + + override fun getDataSource(): DataSource = DataSource.LOCAL +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt index ab8c97df1..2e324310a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt @@ -19,185 +19,186 @@ object ExifInterfaceHelper { // ExifInterface always states it has the following attributes // and returns "0" instead of "null" when they are actually missing private val neverNullTags = listOf( - ExifInterface.TAG_IMAGE_LENGTH, - ExifInterface.TAG_IMAGE_WIDTH, - ExifInterface.TAG_LIGHT_SOURCE, - ExifInterface.TAG_ORIENTATION, + ExifInterface.TAG_IMAGE_LENGTH, + ExifInterface.TAG_IMAGE_WIDTH, + ExifInterface.TAG_LIGHT_SOURCE, + ExifInterface.TAG_ORIENTATION, ) - private val baseTags: Map = hashMapOf( - ExifInterface.TAG_APERTURE_VALUE to TagMapper(ExifDirectoryBase.TAG_APERTURE, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_ARTIST to TagMapper(ExifDirectoryBase.TAG_ARTIST, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_BITS_PER_SAMPLE to TagMapper(ExifDirectoryBase.TAG_BITS_PER_SAMPLE, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_BODY_SERIAL_NUMBER to TagMapper(ExifDirectoryBase.TAG_BODY_SERIAL_NUMBER, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_BRIGHTNESS_VALUE to TagMapper(ExifDirectoryBase.TAG_BRIGHTNESS_VALUE, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_CAMERA_OWNER_NAME to TagMapper(ExifDirectoryBase.TAG_CAMERA_OWNER_NAME, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_CFA_PATTERN to TagMapper(ExifDirectoryBase.TAG_CFA_PATTERN, DirType.EXIF_IFD0, TagFormat.UNDEFINED), - ExifInterface.TAG_COLOR_SPACE to TagMapper(ExifDirectoryBase.TAG_COLOR_SPACE, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_COMPONENTS_CONFIGURATION to TagMapper(ExifDirectoryBase.TAG_COMPONENTS_CONFIGURATION, DirType.EXIF_IFD0, TagFormat.UNDEFINED), - ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL to TagMapper(ExifDirectoryBase.TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_COMPRESSION to TagMapper(ExifDirectoryBase.TAG_COMPRESSION, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_CONTRAST to TagMapper(ExifDirectoryBase.TAG_CONTRAST, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_COPYRIGHT to TagMapper(ExifDirectoryBase.TAG_COPYRIGHT, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_CUSTOM_RENDERED to TagMapper(ExifDirectoryBase.TAG_CUSTOM_RENDERED, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_DATETIME to TagMapper(ExifDirectoryBase.TAG_DATETIME, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_DATETIME_DIGITIZED to TagMapper(ExifDirectoryBase.TAG_DATETIME_DIGITIZED, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_DATETIME_ORIGINAL to TagMapper(ExifDirectoryBase.TAG_DATETIME_ORIGINAL, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION to TagMapper(ExifDirectoryBase.TAG_DEVICE_SETTING_DESCRIPTION, DirType.EXIF_IFD0, TagFormat.UNDEFINED), - ExifInterface.TAG_DIGITAL_ZOOM_RATIO to TagMapper(ExifDirectoryBase.TAG_DIGITAL_ZOOM_RATIO, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_EXIF_VERSION to TagMapper(ExifDirectoryBase.TAG_EXIF_VERSION, DirType.EXIF_IFD0, TagFormat.UNDEFINED), - ExifInterface.TAG_EXPOSURE_BIAS_VALUE to TagMapper(ExifDirectoryBase.TAG_EXPOSURE_BIAS, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_EXPOSURE_INDEX to TagMapper(ExifDirectoryBase.TAG_EXPOSURE_INDEX, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_EXPOSURE_MODE to TagMapper(ExifDirectoryBase.TAG_EXPOSURE_MODE, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_EXPOSURE_PROGRAM to TagMapper(ExifDirectoryBase.TAG_EXPOSURE_PROGRAM, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_EXPOSURE_TIME to TagMapper(ExifDirectoryBase.TAG_EXPOSURE_TIME, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_FILE_SOURCE to TagMapper(ExifDirectoryBase.TAG_FILE_SOURCE, DirType.EXIF_IFD0, TagFormat.UNDEFINED), - ExifInterface.TAG_FLASH to TagMapper(ExifDirectoryBase.TAG_FLASH, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_FLASHPIX_VERSION to TagMapper(ExifDirectoryBase.TAG_FLASHPIX_VERSION, DirType.EXIF_IFD0, TagFormat.UNDEFINED), - ExifInterface.TAG_FLASH_ENERGY to TagMapper(ExifDirectoryBase.TAG_FLASH_ENERGY, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_FOCAL_LENGTH to TagMapper(ExifDirectoryBase.TAG_FOCAL_LENGTH, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM to TagMapper(ExifDirectoryBase.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT to TagMapper(ExifDirectoryBase.TAG_FOCAL_PLANE_RESOLUTION_UNIT, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION to TagMapper(ExifDirectoryBase.TAG_FOCAL_PLANE_X_RESOLUTION, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION to TagMapper(ExifDirectoryBase.TAG_FOCAL_PLANE_Y_RESOLUTION, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_F_NUMBER to TagMapper(ExifDirectoryBase.TAG_FNUMBER, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_GAIN_CONTROL to TagMapper(ExifDirectoryBase.TAG_GAIN_CONTROL, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_GAMMA to TagMapper(ExifDirectoryBase.TAG_GAMMA, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_IMAGE_DESCRIPTION to TagMapper(ExifDirectoryBase.TAG_IMAGE_DESCRIPTION, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_IMAGE_LENGTH to TagMapper(ExifDirectoryBase.TAG_IMAGE_HEIGHT, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_IMAGE_UNIQUE_ID to TagMapper(ExifDirectoryBase.TAG_IMAGE_UNIQUE_ID, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_IMAGE_WIDTH to TagMapper(ExifDirectoryBase.TAG_IMAGE_WIDTH, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_INTEROPERABILITY_INDEX to TagMapper(ExifDirectoryBase.TAG_INTEROP_INDEX, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_ISO_SPEED to TagMapper(ExifDirectoryBase.TAG_ISO_SPEED, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY to TagMapper(ExifDirectoryBase.TAG_ISO_SPEED_LATITUDE_YYY, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ to TagMapper(ExifDirectoryBase.TAG_ISO_SPEED_LATITUDE_ZZZ, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_LENS_MAKE to TagMapper(ExifDirectoryBase.TAG_LENS_MAKE, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_LENS_MODEL to TagMapper(ExifDirectoryBase.TAG_LENS_MODEL, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_LENS_SERIAL_NUMBER to TagMapper(ExifDirectoryBase.TAG_LENS_SERIAL_NUMBER, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_LENS_SPECIFICATION to TagMapper(ExifDirectoryBase.TAG_LENS_SPECIFICATION, DirType.EXIF_IFD0, TagFormat.RATIONAL_ARRAY), - ExifInterface.TAG_LIGHT_SOURCE to TagMapper(ExifDirectoryBase.TAG_WHITE_BALANCE, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_MAKE to TagMapper(ExifDirectoryBase.TAG_MAKE, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_MAKER_NOTE to TagMapper(ExifDirectoryBase.TAG_MAKERNOTE, DirType.EXIF_IFD0, TagFormat.UNDEFINED), - ExifInterface.TAG_MAX_APERTURE_VALUE to TagMapper(ExifDirectoryBase.TAG_MAX_APERTURE, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_METERING_MODE to TagMapper(ExifDirectoryBase.TAG_METERING_MODE, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_MODEL to TagMapper(ExifDirectoryBase.TAG_MODEL, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_NEW_SUBFILE_TYPE to TagMapper(ExifDirectoryBase.TAG_NEW_SUBFILE_TYPE, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_OECF to TagMapper(ExifDirectoryBase.TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION, DirType.EXIF_IFD0, TagFormat.UNDEFINED), - ExifInterface.TAG_OFFSET_TIME to TagMapper(ExifDirectoryBase.TAG_TIME_ZONE, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_OFFSET_TIME_DIGITIZED to TagMapper(ExifDirectoryBase.TAG_TIME_ZONE_DIGITIZED, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_OFFSET_TIME_ORIGINAL to TagMapper(ExifDirectoryBase.TAG_TIME_ZONE_ORIGINAL, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_ORIENTATION to TagMapper(ExifDirectoryBase.TAG_ORIENTATION, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY to TagMapper(ExifDirectoryBase.TAG_ISO_EQUIVALENT, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION to TagMapper(ExifDirectoryBase.TAG_PHOTOMETRIC_INTERPRETATION, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_PIXEL_X_DIMENSION to TagMapper(ExifDirectoryBase.TAG_EXIF_IMAGE_WIDTH, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_PIXEL_Y_DIMENSION to TagMapper(ExifDirectoryBase.TAG_EXIF_IMAGE_HEIGHT, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_PLANAR_CONFIGURATION to TagMapper(ExifDirectoryBase.TAG_PLANAR_CONFIGURATION, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_PRIMARY_CHROMATICITIES to TagMapper(ExifDirectoryBase.TAG_PRIMARY_CHROMATICITIES, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX to TagMapper(ExifDirectoryBase.TAG_RECOMMENDED_EXPOSURE_INDEX, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_REFERENCE_BLACK_WHITE to TagMapper(ExifDirectoryBase.TAG_REFERENCE_BLACK_WHITE, DirType.EXIF_IFD0, TagFormat.RATIONAL_ARRAY), - ExifInterface.TAG_RELATED_SOUND_FILE to TagMapper(ExifDirectoryBase.TAG_RELATED_SOUND_FILE, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_RESOLUTION_UNIT to TagMapper(ExifDirectoryBase.TAG_RESOLUTION_UNIT, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_ROWS_PER_STRIP to TagMapper(ExifDirectoryBase.TAG_ROWS_PER_STRIP, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_SAMPLES_PER_PIXEL to TagMapper(ExifDirectoryBase.TAG_SAMPLES_PER_PIXEL, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_SATURATION to TagMapper(ExifDirectoryBase.TAG_SATURATION, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_SCENE_CAPTURE_TYPE to TagMapper(ExifDirectoryBase.TAG_SCENE_CAPTURE_TYPE, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_SCENE_TYPE to TagMapper(ExifDirectoryBase.TAG_SCENE_TYPE, DirType.EXIF_IFD0, TagFormat.UNDEFINED), - ExifInterface.TAG_SENSING_METHOD to TagMapper(ExifDirectoryBase.TAG_SENSING_METHOD, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_SENSITIVITY_TYPE to TagMapper(ExifDirectoryBase.TAG_SENSITIVITY_TYPE, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_SHARPNESS to TagMapper(ExifDirectoryBase.TAG_SHARPNESS, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_SHUTTER_SPEED_VALUE to TagMapper(ExifDirectoryBase.TAG_SHUTTER_SPEED, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_SOFTWARE to TagMapper(ExifDirectoryBase.TAG_SOFTWARE, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE to TagMapper(ExifDirectoryBase.TAG_SPATIAL_FREQ_RESPONSE, DirType.EXIF_IFD0, TagFormat.UNDEFINED), - ExifInterface.TAG_SPECTRAL_SENSITIVITY to TagMapper(ExifDirectoryBase.TAG_SPECTRAL_SENSITIVITY, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY to TagMapper(ExifDirectoryBase.TAG_STANDARD_OUTPUT_SENSITIVITY, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_STRIP_BYTE_COUNTS to TagMapper(ExifDirectoryBase.TAG_STRIP_BYTE_COUNTS, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_STRIP_OFFSETS to TagMapper(ExifDirectoryBase.TAG_STRIP_OFFSETS, DirType.EXIF_IFD0, TagFormat.LONG), - ExifInterface.TAG_SUBFILE_TYPE to TagMapper(ExifDirectoryBase.TAG_SUBFILE_TYPE, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_SUBJECT_AREA to TagMapper(ExifDirectoryBase.TAG_SUBJECT_LOCATION_TIFF_EP, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_SUBJECT_DISTANCE to TagMapper(ExifDirectoryBase.TAG_SUBJECT_DISTANCE, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_SUBJECT_DISTANCE_RANGE to TagMapper(ExifDirectoryBase.TAG_SUBJECT_DISTANCE_RANGE, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_SUBJECT_LOCATION to TagMapper(ExifDirectoryBase.TAG_SUBJECT_LOCATION, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_SUBSEC_TIME to TagMapper(ExifDirectoryBase.TAG_SUBSECOND_TIME, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_SUBSEC_TIME_DIGITIZED to TagMapper(ExifDirectoryBase.TAG_SUBSECOND_TIME_DIGITIZED, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_SUBSEC_TIME_ORIGINAL to TagMapper(ExifDirectoryBase.TAG_SUBSECOND_TIME_ORIGINAL, DirType.EXIF_IFD0, TagFormat.ASCII), - ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH to TagMapper(ExifDirectoryBase.TAG_IMAGE_HEIGHT, DirType.EXIF_IFD0, TagFormat.LONG), // IFD_THUMBNAIL_TAGS 0x0101 - ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH to TagMapper(ExifDirectoryBase.TAG_IMAGE_WIDTH, DirType.EXIF_IFD0, TagFormat.LONG), // IFD_THUMBNAIL_TAGS 0x0100 - ExifInterface.TAG_TRANSFER_FUNCTION to TagMapper(ExifDirectoryBase.TAG_TRANSFER_FUNCTION, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_USER_COMMENT to TagMapper(ExifDirectoryBase.TAG_USER_COMMENT, DirType.EXIF_IFD0, TagFormat.COMMENT), - ExifInterface.TAG_WHITE_BALANCE to TagMapper(ExifDirectoryBase.TAG_WHITE_BALANCE, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_WHITE_POINT to TagMapper(ExifDirectoryBase.TAG_WHITE_POINT, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_X_RESOLUTION to TagMapper(ExifDirectoryBase.TAG_X_RESOLUTION, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_Y_CB_CR_COEFFICIENTS to TagMapper(ExifDirectoryBase.TAG_YCBCR_COEFFICIENTS, DirType.EXIF_IFD0, TagFormat.RATIONAL), - ExifInterface.TAG_Y_CB_CR_POSITIONING to TagMapper(ExifDirectoryBase.TAG_YCBCR_POSITIONING, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING to TagMapper(ExifDirectoryBase.TAG_YCBCR_SUBSAMPLING, DirType.EXIF_IFD0, TagFormat.SHORT), - ExifInterface.TAG_Y_RESOLUTION to TagMapper(ExifDirectoryBase.TAG_Y_RESOLUTION, DirType.EXIF_IFD0, TagFormat.RATIONAL), + private fun isNeverNull(tag: String): Boolean = neverNullTags.contains(tag) + + private val baseTags: Map = mapOf( + ExifInterface.TAG_APERTURE_VALUE to TagMapper(ExifDirectoryBase.TAG_APERTURE, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_ARTIST to TagMapper(ExifDirectoryBase.TAG_ARTIST, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_BITS_PER_SAMPLE to TagMapper(ExifDirectoryBase.TAG_BITS_PER_SAMPLE, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_BODY_SERIAL_NUMBER to TagMapper(ExifDirectoryBase.TAG_BODY_SERIAL_NUMBER, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_BRIGHTNESS_VALUE to TagMapper(ExifDirectoryBase.TAG_BRIGHTNESS_VALUE, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_CAMERA_OWNER_NAME to TagMapper(ExifDirectoryBase.TAG_CAMERA_OWNER_NAME, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_CFA_PATTERN to TagMapper(ExifDirectoryBase.TAG_CFA_PATTERN, DirType.EXIF_IFD0, TagFormat.BYTE), // spec format: UNDEFINED, e.g. [Red,Green][Green,Blue] + ExifInterface.TAG_COLOR_SPACE to TagMapper(ExifDirectoryBase.TAG_COLOR_SPACE, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_COMPONENTS_CONFIGURATION to TagMapper(ExifDirectoryBase.TAG_COMPONENTS_CONFIGURATION, DirType.EXIF_IFD0, TagFormat.BYTE), // spec format: UNDEFINED, e.g. [Y,Cb,Cr] + ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL to TagMapper(ExifDirectoryBase.TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_COMPRESSION to TagMapper(ExifDirectoryBase.TAG_COMPRESSION, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_CONTRAST to TagMapper(ExifDirectoryBase.TAG_CONTRAST, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_COPYRIGHT to TagMapper(ExifDirectoryBase.TAG_COPYRIGHT, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_CUSTOM_RENDERED to TagMapper(ExifDirectoryBase.TAG_CUSTOM_RENDERED, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_DATETIME to TagMapper(ExifDirectoryBase.TAG_DATETIME, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_DATETIME_DIGITIZED to TagMapper(ExifDirectoryBase.TAG_DATETIME_DIGITIZED, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_DATETIME_ORIGINAL to TagMapper(ExifDirectoryBase.TAG_DATETIME_ORIGINAL, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION to TagMapper(ExifDirectoryBase.TAG_DEVICE_SETTING_DESCRIPTION, DirType.EXIF_IFD0, TagFormat.UNDEFINED), + ExifInterface.TAG_DIGITAL_ZOOM_RATIO to TagMapper(ExifDirectoryBase.TAG_DIGITAL_ZOOM_RATIO, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_EXIF_VERSION to TagMapper(ExifDirectoryBase.TAG_EXIF_VERSION, DirType.EXIF_IFD0, TagFormat.UNDEFINED), + ExifInterface.TAG_EXPOSURE_BIAS_VALUE to TagMapper(ExifDirectoryBase.TAG_EXPOSURE_BIAS, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_EXPOSURE_INDEX to TagMapper(ExifDirectoryBase.TAG_EXPOSURE_INDEX, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_EXPOSURE_MODE to TagMapper(ExifDirectoryBase.TAG_EXPOSURE_MODE, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_EXPOSURE_PROGRAM to TagMapper(ExifDirectoryBase.TAG_EXPOSURE_PROGRAM, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_EXPOSURE_TIME to TagMapper(ExifDirectoryBase.TAG_EXPOSURE_TIME, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_FILE_SOURCE to TagMapper(ExifDirectoryBase.TAG_FILE_SOURCE, DirType.EXIF_IFD0, TagFormat.SHORT), // spec format: UNDEFINED + ExifInterface.TAG_FLASH to TagMapper(ExifDirectoryBase.TAG_FLASH, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_FLASHPIX_VERSION to TagMapper(ExifDirectoryBase.TAG_FLASHPIX_VERSION, DirType.EXIF_IFD0, TagFormat.UNDEFINED), + ExifInterface.TAG_FLASH_ENERGY to TagMapper(ExifDirectoryBase.TAG_FLASH_ENERGY, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_FOCAL_LENGTH to TagMapper(ExifDirectoryBase.TAG_FOCAL_LENGTH, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM to TagMapper(ExifDirectoryBase.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT to TagMapper(ExifDirectoryBase.TAG_FOCAL_PLANE_RESOLUTION_UNIT, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION to TagMapper(ExifDirectoryBase.TAG_FOCAL_PLANE_X_RESOLUTION, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION to TagMapper(ExifDirectoryBase.TAG_FOCAL_PLANE_Y_RESOLUTION, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_F_NUMBER to TagMapper(ExifDirectoryBase.TAG_FNUMBER, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_GAIN_CONTROL to TagMapper(ExifDirectoryBase.TAG_GAIN_CONTROL, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_GAMMA to TagMapper(ExifDirectoryBase.TAG_GAMMA, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_IMAGE_DESCRIPTION to TagMapper(ExifDirectoryBase.TAG_IMAGE_DESCRIPTION, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_IMAGE_LENGTH to TagMapper(ExifDirectoryBase.TAG_IMAGE_HEIGHT, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_IMAGE_UNIQUE_ID to TagMapper(ExifDirectoryBase.TAG_IMAGE_UNIQUE_ID, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_IMAGE_WIDTH to TagMapper(ExifDirectoryBase.TAG_IMAGE_WIDTH, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_INTEROPERABILITY_INDEX to TagMapper(ExifDirectoryBase.TAG_INTEROP_INDEX, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_ISO_SPEED to TagMapper(ExifDirectoryBase.TAG_ISO_SPEED, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY to TagMapper(ExifDirectoryBase.TAG_ISO_SPEED_LATITUDE_YYY, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ to TagMapper(ExifDirectoryBase.TAG_ISO_SPEED_LATITUDE_ZZZ, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_LENS_MAKE to TagMapper(ExifDirectoryBase.TAG_LENS_MAKE, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_LENS_MODEL to TagMapper(ExifDirectoryBase.TAG_LENS_MODEL, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_LENS_SERIAL_NUMBER to TagMapper(ExifDirectoryBase.TAG_LENS_SERIAL_NUMBER, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_LENS_SPECIFICATION to TagMapper(ExifDirectoryBase.TAG_LENS_SPECIFICATION, DirType.EXIF_IFD0, TagFormat.RATIONAL_ARRAY), + ExifInterface.TAG_LIGHT_SOURCE to TagMapper(ExifDirectoryBase.TAG_WHITE_BALANCE, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_MAKE to TagMapper(ExifDirectoryBase.TAG_MAKE, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_MAKER_NOTE to TagMapper(ExifDirectoryBase.TAG_MAKERNOTE, DirType.EXIF_IFD0, TagFormat.UNDEFINED), + ExifInterface.TAG_MAX_APERTURE_VALUE to TagMapper(ExifDirectoryBase.TAG_MAX_APERTURE, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_METERING_MODE to TagMapper(ExifDirectoryBase.TAG_METERING_MODE, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_MODEL to TagMapper(ExifDirectoryBase.TAG_MODEL, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_NEW_SUBFILE_TYPE to TagMapper(ExifDirectoryBase.TAG_NEW_SUBFILE_TYPE, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_OECF to TagMapper(ExifDirectoryBase.TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION, DirType.EXIF_IFD0, TagFormat.UNDEFINED), + ExifInterface.TAG_OFFSET_TIME to TagMapper(ExifDirectoryBase.TAG_TIME_ZONE, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_OFFSET_TIME_DIGITIZED to TagMapper(ExifDirectoryBase.TAG_TIME_ZONE_DIGITIZED, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_OFFSET_TIME_ORIGINAL to TagMapper(ExifDirectoryBase.TAG_TIME_ZONE_ORIGINAL, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_ORIENTATION to TagMapper(ExifDirectoryBase.TAG_ORIENTATION, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY to TagMapper(ExifDirectoryBase.TAG_ISO_EQUIVALENT, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION to TagMapper(ExifDirectoryBase.TAG_PHOTOMETRIC_INTERPRETATION, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_PIXEL_X_DIMENSION to TagMapper(ExifDirectoryBase.TAG_EXIF_IMAGE_WIDTH, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_PIXEL_Y_DIMENSION to TagMapper(ExifDirectoryBase.TAG_EXIF_IMAGE_HEIGHT, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_PLANAR_CONFIGURATION to TagMapper(ExifDirectoryBase.TAG_PLANAR_CONFIGURATION, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_PRIMARY_CHROMATICITIES to TagMapper(ExifDirectoryBase.TAG_PRIMARY_CHROMATICITIES, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX to TagMapper(ExifDirectoryBase.TAG_RECOMMENDED_EXPOSURE_INDEX, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_REFERENCE_BLACK_WHITE to TagMapper(ExifDirectoryBase.TAG_REFERENCE_BLACK_WHITE, DirType.EXIF_IFD0, TagFormat.RATIONAL_ARRAY), + ExifInterface.TAG_RELATED_SOUND_FILE to TagMapper(ExifDirectoryBase.TAG_RELATED_SOUND_FILE, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_RESOLUTION_UNIT to TagMapper(ExifDirectoryBase.TAG_RESOLUTION_UNIT, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_ROWS_PER_STRIP to TagMapper(ExifDirectoryBase.TAG_ROWS_PER_STRIP, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_SAMPLES_PER_PIXEL to TagMapper(ExifDirectoryBase.TAG_SAMPLES_PER_PIXEL, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_SATURATION to TagMapper(ExifDirectoryBase.TAG_SATURATION, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_SCENE_CAPTURE_TYPE to TagMapper(ExifDirectoryBase.TAG_SCENE_CAPTURE_TYPE, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_SCENE_TYPE to TagMapper(ExifDirectoryBase.TAG_SCENE_TYPE, DirType.EXIF_IFD0, TagFormat.SHORT), // spec format: UNDEFINED + ExifInterface.TAG_SENSING_METHOD to TagMapper(ExifDirectoryBase.TAG_SENSING_METHOD, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_SENSITIVITY_TYPE to TagMapper(ExifDirectoryBase.TAG_SENSITIVITY_TYPE, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_SHARPNESS to TagMapper(ExifDirectoryBase.TAG_SHARPNESS, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_SHUTTER_SPEED_VALUE to TagMapper(ExifDirectoryBase.TAG_SHUTTER_SPEED, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_SOFTWARE to TagMapper(ExifDirectoryBase.TAG_SOFTWARE, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE to TagMapper(ExifDirectoryBase.TAG_SPATIAL_FREQ_RESPONSE, DirType.EXIF_IFD0, TagFormat.UNDEFINED), + ExifInterface.TAG_SPECTRAL_SENSITIVITY to TagMapper(ExifDirectoryBase.TAG_SPECTRAL_SENSITIVITY, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY to TagMapper(ExifDirectoryBase.TAG_STANDARD_OUTPUT_SENSITIVITY, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_STRIP_BYTE_COUNTS to TagMapper(ExifDirectoryBase.TAG_STRIP_BYTE_COUNTS, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_STRIP_OFFSETS to TagMapper(ExifDirectoryBase.TAG_STRIP_OFFSETS, DirType.EXIF_IFD0, TagFormat.LONG), + ExifInterface.TAG_SUBFILE_TYPE to TagMapper(ExifDirectoryBase.TAG_SUBFILE_TYPE, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_SUBJECT_AREA to TagMapper(ExifDirectoryBase.TAG_SUBJECT_LOCATION_TIFF_EP, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_SUBJECT_DISTANCE to TagMapper(ExifDirectoryBase.TAG_SUBJECT_DISTANCE, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_SUBJECT_DISTANCE_RANGE to TagMapper(ExifDirectoryBase.TAG_SUBJECT_DISTANCE_RANGE, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_SUBJECT_LOCATION to TagMapper(ExifDirectoryBase.TAG_SUBJECT_LOCATION, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_SUBSEC_TIME to TagMapper(ExifDirectoryBase.TAG_SUBSECOND_TIME, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_SUBSEC_TIME_DIGITIZED to TagMapper(ExifDirectoryBase.TAG_SUBSECOND_TIME_DIGITIZED, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_SUBSEC_TIME_ORIGINAL to TagMapper(ExifDirectoryBase.TAG_SUBSECOND_TIME_ORIGINAL, DirType.EXIF_IFD0, TagFormat.ASCII), + ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH to TagMapper(ExifDirectoryBase.TAG_IMAGE_HEIGHT, DirType.EXIF_IFD0, TagFormat.LONG), // IFD_THUMBNAIL_TAGS 0x0101 + ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH to TagMapper(ExifDirectoryBase.TAG_IMAGE_WIDTH, DirType.EXIF_IFD0, TagFormat.LONG), // IFD_THUMBNAIL_TAGS 0x0100 + ExifInterface.TAG_TRANSFER_FUNCTION to TagMapper(ExifDirectoryBase.TAG_TRANSFER_FUNCTION, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_USER_COMMENT to TagMapper(ExifDirectoryBase.TAG_USER_COMMENT, DirType.EXIF_IFD0, TagFormat.COMMENT), + ExifInterface.TAG_WHITE_BALANCE to TagMapper(ExifDirectoryBase.TAG_WHITE_BALANCE, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_WHITE_POINT to TagMapper(ExifDirectoryBase.TAG_WHITE_POINT, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_X_RESOLUTION to TagMapper(ExifDirectoryBase.TAG_X_RESOLUTION, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_Y_CB_CR_COEFFICIENTS to TagMapper(ExifDirectoryBase.TAG_YCBCR_COEFFICIENTS, DirType.EXIF_IFD0, TagFormat.RATIONAL), + ExifInterface.TAG_Y_CB_CR_POSITIONING to TagMapper(ExifDirectoryBase.TAG_YCBCR_POSITIONING, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING to TagMapper(ExifDirectoryBase.TAG_YCBCR_SUBSAMPLING, DirType.EXIF_IFD0, TagFormat.SHORT), + ExifInterface.TAG_Y_RESOLUTION to TagMapper(ExifDirectoryBase.TAG_Y_RESOLUTION, DirType.EXIF_IFD0, TagFormat.RATIONAL), ) - private val thumbnailTags: Map = hashMapOf( - ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT to TagMapper(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET, DirType.EXIF_THUMBNAIL, TagFormat.LONG), // IFD_TIFF_TAGS or IFD_THUMBNAIL_TAGS 0x0201 - ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH to TagMapper(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH, DirType.EXIF_THUMBNAIL, TagFormat.LONG), // IFD_TIFF_TAGS or IFD_THUMBNAIL_TAGS 0x0202 + private val thumbnailTags: Map = mapOf( + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT to TagMapper(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET, DirType.EXIF_THUMBNAIL, TagFormat.LONG), // IFD_TIFF_TAGS or IFD_THUMBNAIL_TAGS 0x0201 + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH to TagMapper(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH, DirType.EXIF_THUMBNAIL, TagFormat.LONG), // IFD_TIFF_TAGS or IFD_THUMBNAIL_TAGS 0x0202 ) - private val gpsTags: Map = hashMapOf( - ExifInterface.TAG_GPS_ALTITUDE to TagMapper(GpsDirectory.TAG_ALTITUDE, DirType.GPS, TagFormat.RATIONAL), - ExifInterface.TAG_GPS_ALTITUDE_REF to TagMapper(GpsDirectory.TAG_ALTITUDE_REF, DirType.GPS, TagFormat.BYTE), - ExifInterface.TAG_GPS_AREA_INFORMATION to TagMapper(GpsDirectory.TAG_AREA_INFORMATION, DirType.GPS, TagFormat.COMMENT), - ExifInterface.TAG_GPS_DATESTAMP to TagMapper(GpsDirectory.TAG_DATE_STAMP, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_DEST_BEARING to TagMapper(GpsDirectory.TAG_DEST_BEARING, DirType.GPS, TagFormat.RATIONAL), - ExifInterface.TAG_GPS_DEST_BEARING_REF to TagMapper(GpsDirectory.TAG_DEST_BEARING_REF, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_DEST_DISTANCE to TagMapper(GpsDirectory.TAG_DEST_DISTANCE, DirType.GPS, TagFormat.RATIONAL), - ExifInterface.TAG_GPS_DEST_DISTANCE_REF to TagMapper(GpsDirectory.TAG_DEST_DISTANCE_REF, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_DEST_LATITUDE to TagMapper(GpsDirectory.TAG_DEST_LATITUDE, DirType.GPS, TagFormat.RATIONAL_ARRAY), - ExifInterface.TAG_GPS_DEST_LATITUDE_REF to TagMapper(GpsDirectory.TAG_DEST_LATITUDE_REF, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_DEST_LONGITUDE to TagMapper(GpsDirectory.TAG_DEST_LONGITUDE, DirType.GPS, TagFormat.RATIONAL_ARRAY), - ExifInterface.TAG_GPS_DEST_LONGITUDE_REF to TagMapper(GpsDirectory.TAG_DEST_LONGITUDE_REF, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_DIFFERENTIAL to TagMapper(GpsDirectory.TAG_DIFFERENTIAL, DirType.GPS, TagFormat.SHORT), - ExifInterface.TAG_GPS_DOP to TagMapper(GpsDirectory.TAG_DOP, DirType.GPS, TagFormat.RATIONAL), - ExifInterface.TAG_GPS_H_POSITIONING_ERROR to TagMapper(GpsDirectory.TAG_H_POSITIONING_ERROR, DirType.GPS, TagFormat.RATIONAL), - ExifInterface.TAG_GPS_IMG_DIRECTION to TagMapper(GpsDirectory.TAG_IMG_DIRECTION, DirType.GPS, TagFormat.RATIONAL), - ExifInterface.TAG_GPS_IMG_DIRECTION_REF to TagMapper(GpsDirectory.TAG_IMG_DIRECTION_REF, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_LATITUDE to TagMapper(GpsDirectory.TAG_LATITUDE, DirType.GPS, TagFormat.RATIONAL_ARRAY), - ExifInterface.TAG_GPS_LATITUDE_REF to TagMapper(GpsDirectory.TAG_LATITUDE_REF, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_LONGITUDE to TagMapper(GpsDirectory.TAG_LONGITUDE, DirType.GPS, TagFormat.RATIONAL_ARRAY), - ExifInterface.TAG_GPS_LONGITUDE_REF to TagMapper(GpsDirectory.TAG_LONGITUDE_REF, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_MAP_DATUM to TagMapper(GpsDirectory.TAG_MAP_DATUM, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_MEASURE_MODE to TagMapper(GpsDirectory.TAG_MEASURE_MODE, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_PROCESSING_METHOD to TagMapper(GpsDirectory.TAG_PROCESSING_METHOD, DirType.GPS, TagFormat.COMMENT), - ExifInterface.TAG_GPS_SATELLITES to TagMapper(GpsDirectory.TAG_SATELLITES, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_SPEED to TagMapper(GpsDirectory.TAG_SPEED, DirType.GPS, TagFormat.RATIONAL), - ExifInterface.TAG_GPS_SPEED_REF to TagMapper(GpsDirectory.TAG_SPEED_REF, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_STATUS to TagMapper(GpsDirectory.TAG_STATUS, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_TIMESTAMP to TagMapper(GpsDirectory.TAG_TIME_STAMP, DirType.GPS, TagFormat.RATIONAL_ARRAY), - ExifInterface.TAG_GPS_TRACK to TagMapper(GpsDirectory.TAG_TRACK, DirType.GPS, TagFormat.RATIONAL), - ExifInterface.TAG_GPS_TRACK_REF to TagMapper(GpsDirectory.TAG_TRACK_REF, DirType.GPS, TagFormat.ASCII), - ExifInterface.TAG_GPS_VERSION_ID to TagMapper(GpsDirectory.TAG_VERSION_ID, DirType.GPS, TagFormat.BYTE), + private val gpsTags: Map = mapOf( + ExifInterface.TAG_GPS_ALTITUDE to TagMapper(GpsDirectory.TAG_ALTITUDE, DirType.GPS, TagFormat.RATIONAL), + ExifInterface.TAG_GPS_ALTITUDE_REF to TagMapper(GpsDirectory.TAG_ALTITUDE_REF, DirType.GPS, TagFormat.BYTE), + ExifInterface.TAG_GPS_AREA_INFORMATION to TagMapper(GpsDirectory.TAG_AREA_INFORMATION, DirType.GPS, TagFormat.COMMENT), + ExifInterface.TAG_GPS_DATESTAMP to TagMapper(GpsDirectory.TAG_DATE_STAMP, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_DEST_BEARING to TagMapper(GpsDirectory.TAG_DEST_BEARING, DirType.GPS, TagFormat.RATIONAL), + ExifInterface.TAG_GPS_DEST_BEARING_REF to TagMapper(GpsDirectory.TAG_DEST_BEARING_REF, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_DEST_DISTANCE to TagMapper(GpsDirectory.TAG_DEST_DISTANCE, DirType.GPS, TagFormat.RATIONAL), + ExifInterface.TAG_GPS_DEST_DISTANCE_REF to TagMapper(GpsDirectory.TAG_DEST_DISTANCE_REF, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_DEST_LATITUDE to TagMapper(GpsDirectory.TAG_DEST_LATITUDE, DirType.GPS, TagFormat.RATIONAL_ARRAY), + ExifInterface.TAG_GPS_DEST_LATITUDE_REF to TagMapper(GpsDirectory.TAG_DEST_LATITUDE_REF, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_DEST_LONGITUDE to TagMapper(GpsDirectory.TAG_DEST_LONGITUDE, DirType.GPS, TagFormat.RATIONAL_ARRAY), + ExifInterface.TAG_GPS_DEST_LONGITUDE_REF to TagMapper(GpsDirectory.TAG_DEST_LONGITUDE_REF, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_DIFFERENTIAL to TagMapper(GpsDirectory.TAG_DIFFERENTIAL, DirType.GPS, TagFormat.SHORT), + ExifInterface.TAG_GPS_DOP to TagMapper(GpsDirectory.TAG_DOP, DirType.GPS, TagFormat.RATIONAL), + ExifInterface.TAG_GPS_H_POSITIONING_ERROR to TagMapper(GpsDirectory.TAG_H_POSITIONING_ERROR, DirType.GPS, TagFormat.RATIONAL), + ExifInterface.TAG_GPS_IMG_DIRECTION to TagMapper(GpsDirectory.TAG_IMG_DIRECTION, DirType.GPS, TagFormat.RATIONAL), + ExifInterface.TAG_GPS_IMG_DIRECTION_REF to TagMapper(GpsDirectory.TAG_IMG_DIRECTION_REF, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_LATITUDE to TagMapper(GpsDirectory.TAG_LATITUDE, DirType.GPS, TagFormat.RATIONAL_ARRAY), + ExifInterface.TAG_GPS_LATITUDE_REF to TagMapper(GpsDirectory.TAG_LATITUDE_REF, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_LONGITUDE to TagMapper(GpsDirectory.TAG_LONGITUDE, DirType.GPS, TagFormat.RATIONAL_ARRAY), + ExifInterface.TAG_GPS_LONGITUDE_REF to TagMapper(GpsDirectory.TAG_LONGITUDE_REF, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_MAP_DATUM to TagMapper(GpsDirectory.TAG_MAP_DATUM, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_MEASURE_MODE to TagMapper(GpsDirectory.TAG_MEASURE_MODE, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_PROCESSING_METHOD to TagMapper(GpsDirectory.TAG_PROCESSING_METHOD, DirType.GPS, TagFormat.COMMENT), + ExifInterface.TAG_GPS_SATELLITES to TagMapper(GpsDirectory.TAG_SATELLITES, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_SPEED to TagMapper(GpsDirectory.TAG_SPEED, DirType.GPS, TagFormat.RATIONAL), + ExifInterface.TAG_GPS_SPEED_REF to TagMapper(GpsDirectory.TAG_SPEED_REF, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_STATUS to TagMapper(GpsDirectory.TAG_STATUS, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_TIMESTAMP to TagMapper(GpsDirectory.TAG_TIME_STAMP, DirType.GPS, TagFormat.RATIONAL_ARRAY), + ExifInterface.TAG_GPS_TRACK to TagMapper(GpsDirectory.TAG_TRACK, DirType.GPS, TagFormat.RATIONAL), + ExifInterface.TAG_GPS_TRACK_REF to TagMapper(GpsDirectory.TAG_TRACK_REF, DirType.GPS, TagFormat.ASCII), + ExifInterface.TAG_GPS_VERSION_ID to TagMapper(GpsDirectory.TAG_VERSION_ID, DirType.GPS, TagFormat.BYTE), ) - private val xmpTags: Map = hashMapOf( - ExifInterface.TAG_XMP to null, // IFD_TIFF_TAGS 0x02BC + private val xmpTags: Map = mapOf( + ExifInterface.TAG_XMP to null, // IFD_TIFF_TAGS 0x02BC ) - private val rawTags: Map = hashMapOf( - // DNG - ExifInterface.TAG_DEFAULT_CROP_SIZE to null, // IFD_EXIF_TAGS 0xC620 - ExifInterface.TAG_DNG_VERSION to null, // IFD_EXIF_TAGS 0xC612 - // ORF - ExifInterface.TAG_ORF_ASPECT_FRAME to TagMapper(OlympusImageProcessingMakernoteDirectory.TagAspectFrame, DirType.OIPM, TagFormat.LONG), // ORF_IMAGE_PROCESSING_TAGS 0x1113 - ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH to TagMapper(OlympusCameraSettingsMakernoteDirectory.TagPreviewImageLength, DirType.OCSM, TagFormat.LONG), // ORF_CAMERA_SETTINGS_TAGS 0x0102 - ExifInterface.TAG_ORF_PREVIEW_IMAGE_START to TagMapper(OlympusCameraSettingsMakernoteDirectory.TagPreviewImageStart, DirType.OCSM, TagFormat.LONG), // ORF_CAMERA_SETTINGS_TAGS 0x0101 - ExifInterface.TAG_ORF_THUMBNAIL_IMAGE to TagMapper(OlympusMakernoteDirectory.TAG_THUMBNAIL_IMAGE, DirType.OM, TagFormat.UNDEFINED), // ORF_MAKER_NOTE_TAGS 0x0100 - // RW2 - ExifInterface.TAG_RW2_ISO to TagMapper(PanasonicRawIFD0Directory.TagIso, DirType.PRIFD0, TagFormat.LONG), // IFD_TIFF_TAGS 0x0017 - ExifInterface.TAG_RW2_JPG_FROM_RAW to TagMapper(PanasonicRawIFD0Directory.TagJpgFromRaw, DirType.PRIFD0, TagFormat.UNDEFINED), // IFD_TIFF_TAGS 0x002E - ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER to TagMapper(PanasonicRawIFD0Directory.TagSensorBottomBorder, DirType.PRIFD0, TagFormat.LONG), // IFD_TIFF_TAGS 0x0006 - ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER to TagMapper(PanasonicRawIFD0Directory.TagSensorLeftBorder, DirType.PRIFD0, TagFormat.LONG), // IFD_TIFF_TAGS 0x0005 - ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER to TagMapper(PanasonicRawIFD0Directory.TagSensorRightBorder, DirType.PRIFD0, TagFormat.LONG), // IFD_TIFF_TAGS 0x0007 - ExifInterface.TAG_RW2_SENSOR_TOP_BORDER to TagMapper(PanasonicRawIFD0Directory.TagSensorTopBorder, DirType.PRIFD0, TagFormat.LONG), // IFD_TIFF_TAGS 0x0004 + private val rawTags: Map = mapOf( + // DNG + ExifInterface.TAG_DEFAULT_CROP_SIZE to null, // IFD_EXIF_TAGS 0xC620 + ExifInterface.TAG_DNG_VERSION to null, // IFD_EXIF_TAGS 0xC612 + // ORF + ExifInterface.TAG_ORF_ASPECT_FRAME to TagMapper(OlympusImageProcessingMakernoteDirectory.TagAspectFrame, DirType.OIPM, TagFormat.LONG), // ORF_IMAGE_PROCESSING_TAGS 0x1113 + ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH to TagMapper(OlympusCameraSettingsMakernoteDirectory.TagPreviewImageLength, DirType.OCSM, TagFormat.LONG), // ORF_CAMERA_SETTINGS_TAGS 0x0102 + ExifInterface.TAG_ORF_PREVIEW_IMAGE_START to TagMapper(OlympusCameraSettingsMakernoteDirectory.TagPreviewImageStart, DirType.OCSM, TagFormat.LONG), // ORF_CAMERA_SETTINGS_TAGS 0x0101 + ExifInterface.TAG_ORF_THUMBNAIL_IMAGE to TagMapper(OlympusMakernoteDirectory.TAG_THUMBNAIL_IMAGE, DirType.OM, TagFormat.UNDEFINED), // ORF_MAKER_NOTE_TAGS 0x0100 + // RW2 + ExifInterface.TAG_RW2_ISO to TagMapper(PanasonicRawIFD0Directory.TagIso, DirType.PRIFD0, TagFormat.LONG), // IFD_TIFF_TAGS 0x0017 + ExifInterface.TAG_RW2_JPG_FROM_RAW to TagMapper(PanasonicRawIFD0Directory.TagJpgFromRaw, DirType.PRIFD0, TagFormat.UNDEFINED), // IFD_TIFF_TAGS 0x002E + ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER to TagMapper(PanasonicRawIFD0Directory.TagSensorBottomBorder, DirType.PRIFD0, TagFormat.LONG), // IFD_TIFF_TAGS 0x0006 + ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER to TagMapper(PanasonicRawIFD0Directory.TagSensorLeftBorder, DirType.PRIFD0, TagFormat.LONG), // IFD_TIFF_TAGS 0x0005 + ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER to TagMapper(PanasonicRawIFD0Directory.TagSensorRightBorder, DirType.PRIFD0, TagFormat.LONG), // IFD_TIFF_TAGS 0x0007 + ExifInterface.TAG_RW2_SENSOR_TOP_BORDER to TagMapper(PanasonicRawIFD0Directory.TagSensorTopBorder, DirType.PRIFD0, TagFormat.LONG), // IFD_TIFF_TAGS 0x0004 ) // list of known ExifInterface tags (as of androidx.exifinterface:exifinterface:1.3.0) // mapped to metadata-extractor tags (as of v2.14.0) - @JvmField val allTags: Map = hashMapOf( ).apply { putAll(baseTags) @@ -207,7 +208,6 @@ object ExifInterfaceHelper { putAll(rawTags) } - @JvmStatic fun describeAll(exif: ExifInterface): Map> { // initialize metadata-extractor directories that we will fill // by tags converted from the ExifInterface attributes @@ -231,7 +231,7 @@ object ExifInterfaceHelper { for ((exifInterfaceTag, mapper) in tags) { if (exif.hasAttribute(exifInterfaceTag)) { val value: String? = exif.getAttribute(exifInterfaceTag) - if (value != null && (value != "0" || !neverNullTags.contains(exifInterfaceTag))) { + if (value != null && !(value == "0" && isNeverNull(exifInterfaceTag))) { if (mapper != null) { val dir = metadataExtractorDirs[mapper.dirType] ?: error("Directory type ${mapper.dirType} does not have a matching Directory instance") val type = mapper.type @@ -260,7 +260,7 @@ object ExifInterfaceHelper { if (value != null && (value != "0" || !neverNullTags.contains(exifInterfaceTag))) { val obj: Any? = when (mapper.format) { TagFormat.ASCII, TagFormat.COMMENT, TagFormat.UNDEFINED -> value - TagFormat.BYTE -> value.toByteArray() + TagFormat.BYTE -> exif.getAttributeBytes(exifInterfaceTag) TagFormat.SHORT -> value.toShortOrNull() TagFormat.LONG -> value.toLongOrNull() TagFormat.RATIONAL -> toRational(value) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt index 7bcc503b9..ed309f5e7 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt @@ -9,40 +9,38 @@ import java.text.SimpleDateFormat import java.util.* object MediaMetadataRetrieverHelper { - @JvmField val allKeys = hashMapOf( - MediaMetadataRetriever.METADATA_KEY_ALBUM to "Album", - MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST to "Album Artist", - MediaMetadataRetriever.METADATA_KEY_ARTIST to "Artist", - MediaMetadataRetriever.METADATA_KEY_AUTHOR to "Author", - MediaMetadataRetriever.METADATA_KEY_BITRATE to "Bitrate", - MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE to "Capture Framerate", - MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER to "CD Track Number", - MediaMetadataRetriever.METADATA_KEY_COLOR_RANGE to "Color Range", - MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD to "Color Standard", - MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER to "Color Transfer", - MediaMetadataRetriever.METADATA_KEY_COMPILATION to "Compilation", - MediaMetadataRetriever.METADATA_KEY_COMPOSER to "Composer", - MediaMetadataRetriever.METADATA_KEY_DATE to "Date", - MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER to "Disc Number", - MediaMetadataRetriever.METADATA_KEY_DURATION to "Duration", - MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH to "Exif Length", - MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET to "Exif Offset", - MediaMetadataRetriever.METADATA_KEY_GENRE to "Genre", - MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO to "Has Audio", - MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO to "Has Video", - MediaMetadataRetriever.METADATA_KEY_LOCATION to "Location", - MediaMetadataRetriever.METADATA_KEY_MIMETYPE to "MIME Type", - MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS to "Number of Tracks", - MediaMetadataRetriever.METADATA_KEY_TITLE to "Title", - MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT to "Video Height", - MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION to "Video Rotation", - MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH to "Video Width", - MediaMetadataRetriever.METADATA_KEY_WRITER to "Writer", - MediaMetadataRetriever.METADATA_KEY_YEAR to "Year", + MediaMetadataRetriever.METADATA_KEY_ALBUM to "Album", + MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST to "Album Artist", + MediaMetadataRetriever.METADATA_KEY_ARTIST to "Artist", + MediaMetadataRetriever.METADATA_KEY_AUTHOR to "Author", + MediaMetadataRetriever.METADATA_KEY_BITRATE to "Bitrate", + MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE to "Capture Framerate", + MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER to "CD Track Number", + MediaMetadataRetriever.METADATA_KEY_COLOR_RANGE to "Color Range", + MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD to "Color Standard", + MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER to "Color Transfer", + MediaMetadataRetriever.METADATA_KEY_COMPILATION to "Compilation", + MediaMetadataRetriever.METADATA_KEY_COMPOSER to "Composer", + MediaMetadataRetriever.METADATA_KEY_DATE to "Date", + MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER to "Disc Number", + MediaMetadataRetriever.METADATA_KEY_DURATION to "Duration", + MediaMetadataRetriever.METADATA_KEY_GENRE to "Genre", + MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO to "Has Audio", + MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO to "Has Video", + MediaMetadataRetriever.METADATA_KEY_LOCATION to "Location", + MediaMetadataRetriever.METADATA_KEY_MIMETYPE to "MIME Type", + MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS to "Number of Tracks", + MediaMetadataRetriever.METADATA_KEY_TITLE to "Title", + MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT to "Video Height", + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION to "Video Rotation", + MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH to "Video Width", + MediaMetadataRetriever.METADATA_KEY_WRITER to "Writer", + MediaMetadataRetriever.METADATA_KEY_YEAR to "Year", ).apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - putAll(hashMapOf( + putAll( + hashMapOf( MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE to "Has Image", MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT to "Image Count", MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT to "Image Height", @@ -50,7 +48,16 @@ object MediaMetadataRetrieverHelper { MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION to "Image Rotation", MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH to "Image Width", MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT to "Video Frame Count", - )) + ) + ) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + putAll( + hashMapOf( + MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH to "Exif Length", + MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET to "Exif Offset", + ) + ) } } @@ -91,7 +98,7 @@ object MediaMetadataRetrieverHelper { MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT, MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH -> "$value pixels" MediaMetadataRetriever.METADATA_KEY_BITRATE -> { val bitrate = value.toLongOrNull() ?: 0 - if (bitrate > 0) Formatter.formatFileSize(context, bitrate) + "/sec" else null + if (bitrate > 0) "${Formatter.formatFileSize(context, bitrate)}/sec" else null } MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE -> { val framerate = value.toDoubleOrNull() ?: 0.0 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt index 5719dbecc..3ed9f3595 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt @@ -19,7 +19,6 @@ object Metadata { const val DIR_MEDIA = "Media" // interpret EXIF code to angle (0, 90, 180 or 270 degrees) - @JvmStatic fun getRotationDegreesForExifCode(exifOrientation: Int): Int = when (exifOrientation) { ExifInterface.ORIENTATION_ROTATE_90, ExifInterface.ORIENTATION_TRANSVERSE -> 90 ExifInterface.ORIENTATION_ROTATE_180, ExifInterface.ORIENTATION_FLIP_VERTICAL -> 180 @@ -28,13 +27,11 @@ object Metadata { } // interpret EXIF code to whether the image is flipped - @JvmStatic fun isFlippedForExifCode(exifOrientation: Int): Boolean = when (exifOrientation) { ExifInterface.ORIENTATION_FLIP_HORIZONTAL, ExifInterface.ORIENTATION_TRANSVERSE, ExifInterface.ORIENTATION_FLIP_VERTICAL, ExifInterface.ORIENTATION_TRANSPOSE -> true else -> false } - @JvmStatic fun getExifCode(rotationDegrees: Int, isFlipped: Boolean): Int { return when (rotationDegrees) { 90 -> if (isFlipped) ExifInterface.ORIENTATION_TRANSVERSE else ExifInterface.ORIENTATION_ROTATE_90 @@ -45,7 +42,6 @@ object Metadata { } // yyyyMMddTHHmmss(.sss)?(Z|+/-hhmm)? - @JvmStatic fun parseVideoMetadataDate(metadataDate: String?): Long { var dateString = metadataDate ?: return 0 @@ -61,7 +57,7 @@ object Metadata { var timeZone: TimeZone? = null val timeZoneMatcher = Pattern.compile("(Z|[+-]\\d{4})$").matcher(dateString) if (timeZoneMatcher.find()) { - timeZone = TimeZone.getTimeZone("GMT" + timeZoneMatcher.group().replace("Z".toRegex(), "")) + timeZone = TimeZone.getTimeZone("GMT${timeZoneMatcher.group().replace("Z".toRegex(), "")}") dateString = timeZoneMatcher.replaceAll("") } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt index 617fd56d2..06adb5d03 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt @@ -1,23 +1,13 @@ package deckers.thibault.aves.model import android.net.Uri +import deckers.thibault.aves.model.provider.FieldMap -class AvesImageEntry(map: Map) { - @JvmField +class AvesImageEntry(map: FieldMap) { val uri: Uri = Uri.parse(map["uri"] as String) // content or file URI - - @JvmField val path = map["path"] as String? // best effort to get local path - - @JvmField val mimeType = map["mimeType"] as String - - @JvmField val width = map["width"] as Int - - @JvmField val height = map["height"] as Int - - @JvmField val rotationDegrees = map["rotationDegrees"] as Int } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt index bf48ab8e3..0f8ae49f7 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt @@ -24,6 +24,7 @@ import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeLong +import deckers.thibault.aves.model.provider.FieldMap import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils import java.io.IOException @@ -48,7 +49,7 @@ class SourceImageEntry { this.sourceMimeType = sourceMimeType } - constructor(map: Map) { + constructor(map: FieldMap) { uri = Uri.parse(map["uri"] as String) path = map["path"] as String? sourceMimeType = map["sourceMimeType"] as String @@ -69,21 +70,21 @@ class SourceImageEntry { this.dateModifiedSecs = dateModifiedSecs } - fun toMap(): Map { + fun toMap(): FieldMap { return hashMapOf( - "uri" to uri.toString(), - "path" to path, - "sourceMimeType" to sourceMimeType, - "width" to width, - "height" to height, - "sourceRotationDegrees" to (sourceRotationDegrees ?: 0), - "sizeBytes" to sizeBytes, - "title" to title, - "dateModifiedSecs" to dateModifiedSecs, - "sourceDateTakenMillis" to sourceDateTakenMillis, - "durationMillis" to durationMillis, - // only for map export - "contentId" to contentId, + "uri" to uri.toString(), + "path" to path, + "sourceMimeType" to sourceMimeType, + "width" to width, + "height" to height, + "sourceRotationDegrees" to (sourceRotationDegrees ?: 0), + "sizeBytes" to sizeBytes, + "title" to title, + "dateModifiedSecs" to dateModifiedSecs, + "sourceDateTakenMillis" to sourceDateTakenMillis, + "durationMillis" to durationMillis, + // only for map export + "contentId" to contentId, ) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt new file mode 100644 index 000000000..99b0f168e --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt @@ -0,0 +1,45 @@ +package deckers.thibault.aves.model.provider + +import android.content.Context +import android.net.Uri +import android.provider.MediaStore +import deckers.thibault.aves.model.SourceImageEntry + +internal class ContentImageProvider : ImageProvider() { + override fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { + if (mimeType == null) { + callback.onFailure(Exception("MIME type is null for uri=$uri")) + return + } + + val map = hashMapOf( + "uri" to uri.toString(), + "sourceMimeType" to mimeType, + ) + try { + val cursor = context.contentResolver.query(uri, projection, null, null, null) + if (cursor != null && cursor.moveToFirst()) { + cursor.getColumnIndex(MediaStore.MediaColumns.SIZE).let { if (it != -1) map["sizeBytes"] = cursor.getLong(it) } + cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME).let { if (it != -1) map["title"] = cursor.getString(it) } + cursor.close() + } + } catch (e: Exception) { + callback.onFailure(e) + return + } + + val entry = SourceImageEntry(map).fillPreCatalogMetadata(context) + if (entry.isSized || entry.isSvg) { + callback.onSuccess(entry.toMap()) + } else { + callback.onFailure(Exception("entry has no size")) + } + } + + companion object { + private val projection = arrayOf( + MediaStore.MediaColumns.SIZE, + MediaStore.MediaColumns.DISPLAY_NAME + ) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt new file mode 100644 index 000000000..49faa641f --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt @@ -0,0 +1,36 @@ +package deckers.thibault.aves.model.provider + +import android.content.Context +import android.net.Uri +import deckers.thibault.aves.model.SourceImageEntry +import java.io.File + +internal class FileImageProvider : ImageProvider() { + override fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { + if (mimeType == null) { + callback.onFailure(Exception("MIME type is null for uri=$uri")) + return + } + + val entry = SourceImageEntry(uri, mimeType) + + val path = uri.path + if (path != null) { + try { + val file = File(path) + if (file.exists()) { + entry.initFromFile(path, file.name, file.length(), file.lastModified() / 1000) + } + } catch (e: SecurityException) { + callback.onFailure(e) + } + } + entry.fillPreCatalogMetadata(context) + + if (entry.isSized || entry.isSvg) { + callback.onSuccess(entry.toMap()) + } else { + callback.onFailure(Exception("entry has no size")) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt new file mode 100644 index 000000000..b3eb3c03b --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -0,0 +1,194 @@ +package deckers.thibault.aves.model.provider + +import android.content.ContentUris +import android.content.Context +import android.media.MediaScannerConnection +import android.net.Uri +import android.provider.MediaStore +import android.util.Log +import androidx.exifinterface.media.ExifInterface +import com.commonsware.cwac.document.DocumentFileCompat +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture +import deckers.thibault.aves.model.AvesImageEntry +import deckers.thibault.aves.model.ExifOrientationOp +import deckers.thibault.aves.utils.LogUtils.createTag +import deckers.thibault.aves.utils.MimeTypes.isImage +import deckers.thibault.aves.utils.MimeTypes.isVideo +import deckers.thibault.aves.utils.StorageUtils.copyFileToTemp +import deckers.thibault.aves.utils.StorageUtils.getDocumentFile +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.util.* + +abstract class ImageProvider { + open fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { + callback.onFailure(UnsupportedOperationException()) + } + + open fun delete(context: Context, uri: Uri, path: String?): ListenableFuture { + return Futures.immediateFailedFuture(UnsupportedOperationException()) + } + + open fun moveMultiple(context: Context, copy: Boolean, destinationDir: String, entries: List, callback: ImageOpCallback) { + callback.onFailure(UnsupportedOperationException()) + } + + fun rename(context: Context, oldPath: String, oldMediaUri: Uri, mimeType: String, newFilename: String, callback: ImageOpCallback) { + val oldFile = File(oldPath) + val newFile = File(oldFile.parent, newFilename) + if (oldFile == newFile) { + Log.w(LOG_TAG, "new name and old name are the same, path=$oldPath") + callback.onSuccess(HashMap()) + return + } + + val df = getDocumentFile(context, oldPath, oldMediaUri) + try { + val renamed = df != null && df.renameTo(newFilename) + if (!renamed) { + callback.onFailure(Exception("failed to rename entry at path=$oldPath")) + return + } + } catch (e: FileNotFoundException) { + callback.onFailure(e) + return + } + + MediaScannerConnection.scanFile(context, arrayOf(oldPath), arrayOf(mimeType), null) + scanNewPath(context, newFile.path, mimeType, callback) + } + + fun changeOrientation(context: Context, path: String, uri: Uri, mimeType: String, op: ExifOrientationOp, callback: ImageOpCallback) { + if (!canEditExif(mimeType)) { + callback.onFailure(UnsupportedOperationException("unsupported mimeType=$mimeType")) + return + } + + val originalDocumentFile = getDocumentFile(context, path, uri) + if (originalDocumentFile == null) { + callback.onFailure(Exception("failed to get document file for path=$path, uri=$uri")) + return + } + + // copy original file to a temporary file for editing + val editablePath = copyFileToTemp(originalDocumentFile, path) + if (editablePath == null) { + callback.onFailure(Exception("failed to create a temporary file for path=$path")) + return + } + + val newFields = HashMap() + try { + val exif = ExifInterface(editablePath) + // when the orientation is not defined, it returns `undefined (0)` instead of the orientation default value `normal (1)` + // in that case we explicitely set it to `normal` first + // because ExifInterface fails to rotate an image with undefined orientation + // as of androidx.exifinterface:exifinterface:1.3.0 + val currentOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + if (currentOrientation == ExifInterface.ORIENTATION_UNDEFINED) { + exif.setAttribute(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL.toString()) + } + when (op) { + ExifOrientationOp.ROTATE_CW -> exif.rotate(90) + ExifOrientationOp.ROTATE_CCW -> exif.rotate(-90) + ExifOrientationOp.FLIP -> exif.flipHorizontally() + } + exif.saveAttributes() + + // copy the edited temporary file back to the original + DocumentFileCompat.fromFile(File(editablePath)).copyTo(originalDocumentFile) + + newFields["rotationDegrees"] = exif.rotationDegrees + newFields["isFlipped"] = exif.isFlipped + } catch (e: IOException) { + callback.onFailure(e) + return + } + + MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, _ -> + val projection = arrayOf(MediaStore.MediaColumns.DATE_MODIFIED) + try { + val cursor = context.contentResolver.query(uri, projection, null, null, null) + if (cursor != null && cursor.moveToFirst()) { + cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) } + cursor.close() + } + } catch (e: Exception) { + callback.onFailure(e) + return@scanFile + } + callback.onSuccess(newFields) + } + } + + // support for writing EXIF + // as of androidx.exifinterface:exifinterface:1.3.0 + private fun canEditExif(mimeType: String): Boolean { + return when (mimeType) { + "image/jpeg", "image/png", "image/webp" -> true + else -> false + } + } + + protected fun scanNewPath(context: Context, path: String, mimeType: String, callback: ImageOpCallback) { + MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, newUri: Uri? -> + var contentId: Long = 0 + var contentUri: Uri? = null + if (newUri != null) { + // `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872") + // but we need an image/video media URI (e.g. "content://media/external/images/media/62872") + contentId = ContentUris.parseId(newUri) + if (isImage(mimeType)) { + contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId) + } else if (isVideo(mimeType)) { + contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId) + } + } + if (contentUri == null) { + callback.onFailure(Exception("failed to get content URI of item at path=$path")) + return@scanFile + } + + val newFields = HashMap() + // we retrieve updated fields as the renamed/moved file became a new entry in the Media Store + val projection = arrayOf( + MediaStore.MediaColumns.DATE_MODIFIED, + MediaStore.MediaColumns.DISPLAY_NAME, + MediaStore.MediaColumns.TITLE, + ) + try { + val cursor = context.contentResolver.query(contentUri, projection, null, null, null) + if (cursor != null && cursor.moveToFirst()) { + newFields["uri"] = contentUri.toString() + newFields["contentId"] = contentId + newFields["path"] = path + cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) } + cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME).let { if (it != -1) newFields["displayName"] = cursor.getString(it) } + cursor.getColumnIndex(MediaStore.MediaColumns.TITLE).let { if (it != -1) newFields["title"] = cursor.getString(it) } + cursor.close() + } + } catch (e: Exception) { + callback.onFailure(e) + return@scanFile + } + if (newFields.isEmpty()) { + callback.onFailure(Exception("failed to get item details from provider at contentUri=$contentUri")) + } else { + callback.onSuccess(newFields) + } + } + } + + interface ImageOpCallback { + fun onSuccess(fields: FieldMap) + fun onFailure(throwable: Throwable) + } + + companion object { + private val LOG_TAG = createTag(ImageProvider::class.java) + } +} + +typealias FieldMap = MutableMap diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProviderFactory.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProviderFactory.kt new file mode 100644 index 000000000..a9a9045dc --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProviderFactory.kt @@ -0,0 +1,23 @@ +package deckers.thibault.aves.model.provider + +import android.content.ContentResolver +import android.net.Uri +import android.provider.MediaStore +import java.util.* + +object ImageProviderFactory { + fun getProvider(uri: Uri): ImageProvider? { + return when (uri.scheme?.toLowerCase(Locale.ROOT)) { + ContentResolver.SCHEME_CONTENT -> { + // a URI's authority is [userinfo@]host[:port] + // but we only want the host when comparing to Media Store's "authority" + return when (uri.host?.toLowerCase(Locale.ROOT)) { + MediaStore.AUTHORITY -> MediaStoreImageProvider() + else -> ContentImageProvider() + } + } + ContentResolver.SCHEME_FILE -> FileImageProvider() + else -> null + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt new file mode 100644 index 000000000..e58733b11 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -0,0 +1,370 @@ +package deckers.thibault.aves.model.provider + +import android.content.ContentUris +import android.content.Context +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.util.Log +import com.commonsware.cwac.document.DocumentFileCompat +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.SettableFuture +import deckers.thibault.aves.model.AvesImageEntry +import deckers.thibault.aves.model.SourceImageEntry +import deckers.thibault.aves.utils.LogUtils.createTag +import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.MimeTypes.isImage +import deckers.thibault.aves.utils.MimeTypes.isVideo +import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent +import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator +import deckers.thibault.aves.utils.StorageUtils.getDocumentFile +import deckers.thibault.aves.utils.StorageUtils.requireAccessPermission +import java.io.File +import java.io.FileNotFoundException +import java.util.* +import java.util.concurrent.ExecutionException + +class MediaStoreImageProvider : ImageProvider() { + fun fetchAll(context: Context, knownEntries: Map, handleNewEntry: NewEntryHandler) { + val isModified = fun(contentId: Int, dateModifiedSecs: Int): Boolean { + val knownDate = knownEntries[contentId] + return knownDate == null || knownDate < dateModifiedSecs + } + fetchFrom(context, isModified, handleNewEntry, IMAGE_CONTENT_URI, IMAGE_PROJECTION) + fetchFrom(context, isModified, handleNewEntry, VIDEO_CONTENT_URI, VIDEO_PROJECTION) + } + + override fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { + val id = ContentUris.parseId(uri) + val onSuccess = fun(entry: FieldMap) { + entry["uri"] = uri.toString() + callback.onSuccess(entry) + } + val alwaysValid = { _: Int, _: Int -> true } + if (mimeType == null || isImage(mimeType)) { + val contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, id) + if (fetchFrom(context, alwaysValid, onSuccess, contentUri, IMAGE_PROJECTION) > 0) return + } + if (mimeType == null || isVideo(mimeType)) { + val contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, id) + if (fetchFrom(context, alwaysValid, onSuccess, contentUri, VIDEO_PROJECTION) > 0) return + } + callback.onFailure(Exception("failed to fetch entry at uri=$uri")) + } + + fun getObsoleteContentIds(context: Context, knownContentIds: List): List { + val current = arrayListOf().apply { + addAll(getContentIdList(context, IMAGE_CONTENT_URI)) + addAll(getContentIdList(context, VIDEO_CONTENT_URI)) + } + return knownContentIds.filter { id: Int -> !current.contains(id) }.toList() + } + + private fun getContentIdList(context: Context, contentUri: Uri): List { + val foundContentIds = ArrayList() + val projection = arrayOf(MediaStore.MediaColumns._ID) + try { + val cursor = context.contentResolver.query(contentUri, projection, null, null, null) + if (cursor != null) { + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) + while (cursor.moveToNext()) { + foundContentIds.add(cursor.getInt(idColumn)) + } + cursor.close() + } + } catch (e: Exception) { + Log.e(LOG_TAG, "failed to get content IDs for contentUri=$contentUri", e) + } + return foundContentIds + } + + private fun fetchFrom( + context: Context, + isValidEntry: NewEntryChecker, + handleNewEntry: NewEntryHandler, + contentUri: Uri, + projection: Array, + ): Int { + var newEntryCount = 0 + val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC" + try { + val cursor = context.contentResolver.query(contentUri, projection, null, null, orderBy) + if (cursor != null) { + // image & video + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) + val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) + val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE) + val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE) + val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.TITLE) + val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH) + val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT) + val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) + val dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN) + + // image & video for API >= Q, only for images for API < Q + val orientationColumn = cursor.getColumnIndex(MediaStore.MediaColumns.ORIENTATION) + + // video only + val durationColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DURATION) + val needDuration = projection.contentEquals(VIDEO_PROJECTION) + + while (cursor.moveToNext()) { + val contentId = cursor.getInt(idColumn) + val dateModifiedSecs = cursor.getInt(dateModifiedColumn) + if (isValidEntry(contentId, dateModifiedSecs)) { + // building `itemUri` this way is fine if `contentUri` does not already contain the ID + val itemUri = ContentUris.withAppendedId(contentUri, contentId.toLong()) + val mimeType = cursor.getString(mimeTypeColumn) + val width = cursor.getInt(widthColumn) + val height = cursor.getInt(heightColumn) + val durationMillis = if (durationColumn != -1) cursor.getLong(durationColumn) else 0L + + var entryMap: FieldMap = hashMapOf( + "uri" to itemUri.toString(), + "path" to cursor.getString(pathColumn), + "sourceMimeType" to mimeType, + "width" to width, + "height" to height, + "sourceRotationDegrees" to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0, + "sizeBytes" to cursor.getLong(sizeColumn), + "title" to cursor.getString(titleColumn), + "dateModifiedSecs" to dateModifiedSecs, + "sourceDateTakenMillis" to cursor.getLong(dateTakenColumn), + "durationMillis" to durationMillis, + // only for map export + "contentId" to contentId, + ) + + if ((width <= 0 || height <= 0) && needSize(mimeType) + || durationMillis == 0L && needDuration + ) { + // some images are incorrectly registered in the Media Store, + // they are valid but miss some attributes, such as width, height, orientation + val entry = SourceImageEntry(entryMap).fillPreCatalogMetadata(context) + entryMap = entry.toMap() + } + + handleNewEntry(entryMap) + // TODO TLAD is this necessary? + if (newEntryCount % 30 == 0) { + Thread.sleep(10) + } + newEntryCount++ + } + } + cursor.close() + } + } catch (e: Exception) { + Log.e(LOG_TAG, "failed to get entries", e) + } + return newEntryCount + } + + private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType + + // `uri` is a media URI, not a document URI + override fun delete(context: Context, uri: Uri, path: String?): ListenableFuture { + val future = SettableFuture.create() + + if (path == null) { + future.setException(Exception("failed to delete file because path is null")) + return future + } + + if (requireAccessPermission(context, path)) { + // if the file is on SD card, calling the content resolver `delete()` removes the entry from the Media Store + // but it doesn't delete the file, even if the app has the permission + try { + val df = getDocumentFile(context, path, uri) + if (df != null && df.delete()) { + future.set(null) + } else { + future.setException(Exception("failed to delete file with df=$df")) + } + } catch (e: FileNotFoundException) { + future.setException(e) + } + return future + } + + try { + if (context.contentResolver.delete(uri, null, null) > 0) { + future.set(null) + } else { + future.setException(Exception("failed to delete row from content provider")) + } + } catch (e: Exception) { + Log.e(LOG_TAG, "failed to delete entry", e) + future.setException(e) + } + return future + } + + override fun moveMultiple( + context: Context, + copy: Boolean, + destinationDir: String, + entries: List, + callback: ImageOpCallback, + ) { + val destinationDirDocFile = createDirectoryIfAbsent(context, destinationDir) + if (destinationDirDocFile == null) { + callback.onFailure(Exception("failed to create directory at path=$destinationDir")) + return + } + + for (entry in entries) { + val sourceUri = entry.uri + val sourcePath = entry.path + val mimeType = entry.mimeType + + val result = hashMapOf( + "uri" to sourceUri.toString(), + "success" to false, + ) + + if (sourcePath != null) { + // on API 30 we cannot get access granted directly to a volume root from its document tree, + // but it is still less constraining to use tree document files than to rely on the Media Store + // + // Relying on `DocumentFile`, we can create an item via `DocumentFile.createFile()`, but: + // - we need to scan the file to get the Media Store content URI + // - the underlying document provider controls the new file name + // + // Relying on the Media Store, we can create an item via `ContentResolver.insert()` + // with a path, and retrieve its content URI, but: + // - the Media Store isolates content by storage volume (e.g. `MediaStore.Images.Media.getContentUri(volumeName)`) + // - the volume name should be lower case, not exactly as the `StorageVolume` UUID + // - inserting on a removable volume works on API 29, but not on API 25 nor 26 (on which API/devices does it work?) + // - there is no documentation regarding support for usage with removable storage + // - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage + try { + val newFieldsFuture = moveSingleByTreeDocAndScan( + context, sourcePath, sourceUri, destinationDir, destinationDirDocFile, mimeType, copy, + ) + result["newFields"] = newFieldsFuture.get() + result["success"] = true + } catch (e: ExecutionException) { + Log.w(LOG_TAG, "failed to move to destinationDir=$destinationDir entry with sourcePath=$sourcePath", e) + } catch (e: InterruptedException) { + Log.w(LOG_TAG, "failed to move to destinationDir=$destinationDir entry with sourcePath=$sourcePath", e) + } + } + callback.onSuccess(result) + } + } + + private fun moveSingleByTreeDocAndScan( + context: Context, + sourcePath: String, + sourceUri: Uri, + destinationDir: String, + destinationDirDocFile: DocumentFileCompat, + mimeType: String, + copy: Boolean, + ): ListenableFuture { + val future = SettableFuture.create() + + try { + val sourceFile = File(sourcePath) + val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) } + if (sourceDir == destinationDir) { + if (copy) { + future.setException(Exception("file at path=$sourcePath is already in destination directory")) + } else { + future.set(HashMap()) + } + } else { + val sourceFileName = sourceFile.name + val desiredNameWithoutExtension = sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "") + + // the file created from a `TreeDocumentFile` is also a `TreeDocumentFile` + // but in order to open an output stream to it, we need to use a `SingleDocumentFile` + // through a document URI, not a tree URI + // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first + val destinationTreeFile = destinationDirDocFile.createFile(mimeType, desiredNameWithoutExtension) + val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri) + + // `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry + // `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument" + // when used with entry URI as `sourceDocumentUri`, and destinationDirDocFile URI as `targetParentDocumentUri` + val source = DocumentFileCompat.fromSingleUri(context, sourceUri) + source.copyTo(destinationDocFile) + + // the source file name and the created document file name can be different when: + // - a file with the same name already exists, so the name gets a suffix like ` (1)` + // - the original extension does not match the extension added by the underlying provider + val fileName = destinationDocFile.name + val destinationFullPath = destinationDir + fileName + + var deletedSource = false + if (!copy) { + // delete original entry + try { + delete(context, sourceUri, sourcePath).get() + deletedSource = true + } catch (e: ExecutionException) { + Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e) + } catch (e: InterruptedException) { + Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e) + } + } + + scanNewPath(context, destinationFullPath, mimeType, object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) { + fields["deletedSource"] = deletedSource + future.set(fields) + } + + override fun onFailure(throwable: Throwable) { + future.setException(throwable) + } + }) + } + } catch (e: Exception) { + Log.e(LOG_TAG, "failed to ${(if (copy) "copy" else "move")} entry", e) + future.setException(e) + } + + return future + } + + companion object { + private val LOG_TAG = createTag(MediaStoreImageProvider::class.java) + + private val IMAGE_CONTENT_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + private val VIDEO_CONTENT_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + + private val BASE_PROJECTION = arrayOf( + MediaStore.MediaColumns._ID, + MediaStore.MediaColumns.DATA, + MediaStore.MediaColumns.MIME_TYPE, + MediaStore.MediaColumns.SIZE, // TODO TLAD use `DISPLAY_NAME` instead/along `TITLE`? + MediaStore.MediaColumns.TITLE, + MediaStore.MediaColumns.WIDTH, + MediaStore.MediaColumns.HEIGHT, + MediaStore.MediaColumns.DATE_MODIFIED + ) + + private val IMAGE_PROJECTION = arrayOf( + *BASE_PROJECTION, + // uses `MediaStore.Images.Media` instead of `MediaStore.MediaColumns` for APIs < Q + MediaStore.Images.Media.DATE_TAKEN, + MediaStore.Images.Media.ORIENTATION + ) + + private val VIDEO_PROJECTION = arrayOf( + *BASE_PROJECTION, + // uses `MediaStore.Video.Media` instead of `MediaStore.MediaColumns` for APIs < Q + MediaStore.Video.Media.DATE_TAKEN, + MediaStore.Video.Media.DURATION, + *if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf( + MediaStore.Video.Media.ORIENTATION + ) else emptyArray() + ) + } +} + +typealias NewEntryHandler = (entry: FieldMap) -> Unit + +private typealias NewEntryChecker = (contentId: Int, dateModifiedSecs: Int) -> Boolean \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt index 8c34b6ed9..edda55bf4 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt @@ -7,7 +7,6 @@ import com.bumptech.glide.load.resource.bitmap.TransformationUtils import deckers.thibault.aves.metadata.Metadata.getExifCode object BitmapUtils { - @JvmStatic fun applyExifOrientation(context: Context, bitmap: Bitmap?, rotationDegrees: Int?, isFlipped: Boolean?): Bitmap? { if (bitmap == null || rotationDegrees == null || isFlipped == null) return bitmap if (rotationDegrees == 0 && !isFlipped) return bitmap @@ -15,12 +14,10 @@ object BitmapUtils { return TransformationUtils.rotateImageExif(getBitmapPool(context), bitmap, exifOrientation) } - @JvmStatic fun centerSquareCrop(context: Context, bitmap: Bitmap?, size: Int): Bitmap? { bitmap ?: return bitmap return TransformationUtils.centerCrop(getBitmapPool(context), bitmap, size, size) } - @JvmStatic fun getBitmapPool(context: Context) = Glide.get(context).bitmapPool } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/LogUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/LogUtils.kt index 266788e88..c2e60a019 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/LogUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/LogUtils.kt @@ -7,7 +7,6 @@ object LogUtils { private val LOG_TAG_PACKAGE_PATTERN = Pattern.compile("(\\w)(\\w*)\\.") // create an Android logger friendly log tag for the specified class - @JvmStatic fun createTag(clazz: Class<*>): String { // shorten class name to "a.b.CccDdd" var logTag = LOG_TAG_PACKAGE_PATTERN.matcher(clazz.name).replaceAll("$1.") diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index 93532cf2e..876d4e36a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -26,13 +26,10 @@ object MimeTypes { private const val MP2T = "video/mp2t" private const val WEBM = "video/webm" - @JvmStatic fun isImage(mimeType: String?) = mimeType != null && mimeType.startsWith(IMAGE) - @JvmStatic fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith(VIDEO) - @JvmStatic // returns whether the specified MIME type represents // a raster image format that allows an alpha channel fun canHaveAlpha(mimeType: String?) = when (mimeType) { @@ -41,7 +38,6 @@ object MimeTypes { } // as of Flutter v1.22.0 - @JvmStatic fun isSupportedByFlutter(mimeType: String, rotationDegrees: Int?, isFlipped: Boolean?) = when (mimeType) { JPEG, GIF, WEBP, BMP, WBMP, ICO, SVG -> true PNG -> rotationDegrees ?: 0 == 0 && !(isFlipped ?: false) @@ -49,7 +45,6 @@ object MimeTypes { } // as of metadata-extractor v2.14.0 - @JvmStatic fun isSupportedByMetadataExtractor(mimeType: String) = when (mimeType) { WBMP, MP2T, WEBM -> false else -> true @@ -59,7 +54,6 @@ object MimeTypes { // but we need to rotate the decoded bitmap for the other formats // maybe related to ExifInterface version used by Glide: // https://github.com/bumptech/glide/blob/master/gradle.properties#L21 - @JvmStatic fun needRotationAfterGlide(mimeType: String) = when (mimeType) { DNG, HEIC, HEIF, PNG, WEBP -> true else -> false @@ -68,7 +62,6 @@ object MimeTypes { // Thumbnails obtained from the Media Store are automatically rotated // according to EXIF orientation when decoding images of known formats // but we need to rotate the decoded bitmap for the other formats - @JvmStatic fun needRotationAfterContentResolverThumbnail(mimeType: String) = when (mimeType) { DNG, PNG -> true else -> false diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt index 4fd2eff21..2ba59174a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt @@ -22,7 +22,6 @@ object PermissionManager { // permission request code to pending runnable private val pendingPermissionMap = ConcurrentHashMap() - @JvmStatic fun requestVolumeAccess(activity: Activity, path: String, onGranted: () -> Unit, onDenied: () -> Unit) { Log.i(LOG_TAG, "request user to select and grant access permission to volume=$path") pendingPermissionMap[VOLUME_ACCESS_REQUEST_CODE] = PendingPermissionHandler(path, onGranted, onDenied) @@ -47,12 +46,10 @@ object PermissionManager { (if (treeUri != null) handler.onGranted else handler.onDenied)() } - @JvmStatic fun getGrantedDirForPath(context: Context, anyPath: String): String? { return getAccessibleDirs(context).firstOrNull { anyPath.startsWith(it) } } - @JvmStatic fun getInaccessibleDirectories(context: Context, dirPaths: List): List> { val accessibleDirs = getAccessibleDirs(context) @@ -103,16 +100,15 @@ object PermissionManager { return inaccessibleDirs } - @JvmStatic - fun revokeDirectoryAccess(context: Context, path: String) { - StorageUtils.convertDirPathToTreeUri(context, path)?.let { + fun revokeDirectoryAccess(context: Context, path: String): Boolean { + return StorageUtils.convertDirPathToTreeUri(context, path)?.let { val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION context.contentResolver.releasePersistableUriPermission(it, flags) - } + true + } ?: false } // returns paths matching URIs granted by the user - @JvmStatic fun getGrantedDirs(context: Context): Set { val grantedDirs = HashSet() for (uriPermission in context.contentResolver.persistedUriPermissions) { @@ -127,7 +123,7 @@ object PermissionManager { val accessibleDirs = HashSet(getGrantedDirs(context)) // from Android R, we no longer have access permission by default on primary volume if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - accessibleDirs.add(StorageUtils.primaryVolumePath) + accessibleDirs.add(StorageUtils.getPrimaryVolumePath(context)) } Log.d(LOG_TAG, "getAccessibleDirs accessibleDirs=$accessibleDirs") return accessibleDirs diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt index 250dd28c5..ab26c9932 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt @@ -6,7 +6,6 @@ import android.content.Context import android.media.MediaMetadataRetriever import android.net.Uri import android.os.Build -import android.os.Environment import android.os.storage.StorageManager import android.provider.DocumentsContract import android.provider.MediaStore @@ -36,15 +35,13 @@ object StorageUtils { // primary volume path, with trailing "/" private var mPrimaryVolumePath: String? = null - val primaryVolumePath: String - get() { - if (mPrimaryVolumePath == null) { - mPrimaryVolumePath = findPrimaryVolumePath() - } - return mPrimaryVolumePath!! + fun getPrimaryVolumePath(context: Context): String { + if (mPrimaryVolumePath == null) { + mPrimaryVolumePath = findPrimaryVolumePath(context) } + return mPrimaryVolumePath!! + } - @JvmStatic fun getVolumePaths(context: Context): Array { if (mStorageVolumePaths == null) { mStorageVolumePaths = findVolumePaths(context) @@ -52,7 +49,6 @@ object StorageUtils { return mStorageVolumePaths!! } - @JvmStatic fun getVolumePath(context: Context, anyPath: String): String? { return getVolumePaths(context).firstOrNull { anyPath.startsWith(it) } } @@ -76,8 +72,17 @@ object StorageUtils { return pathSteps.iterator() } - private fun findPrimaryVolumePath(): String { - return ensureTrailingSeparator(Environment.getExternalStorageDirectory().absolutePath) + private fun findPrimaryVolumePath(context: Context): String? { + // we want: + // /storage/emulated/0/ + // `Environment.getExternalStorageDirectory()` (deprecated) yields: + // /storage/emulated/0 + // `context.getExternalFilesDir(null)` yields: + // /storage/emulated/0/Android/data/{package_name}/files + return context.getExternalFilesDir(null)?.let { + val appSpecificPath = it.absolutePath + return appSpecificPath.substring(0, appSpecificPath.indexOf("Android/data")) + } } @SuppressLint("ObsoleteSdkInt") @@ -126,10 +131,10 @@ object StorageUtils { } } else { // Device has emulated storage; external storage paths should have userId burned into them. - val path = Environment.getExternalStorageDirectory().absolutePath - val rawUserId = path.split(File.separator).lastOrNull()?.takeIf { TextUtils.isDigitsOnly(it) } ?: "" - // /storage/emulated/0[1,2,...] - if (TextUtils.isEmpty(rawUserId)) { + // /storage/emulated/[0,1,2,...]/ + val path = getPrimaryVolumePath(context) + val rawUserId = path.split(File.separator).lastOrNull(String::isNotEmpty)?.takeIf { TextUtils.isDigitsOnly(it) } ?: "" + if (rawUserId.isEmpty()) { paths.add(rawEmulatedStorageTarget) } else { paths.add(rawEmulatedStorageTarget + File.separator + rawUserId) @@ -145,30 +150,29 @@ object StorageUtils { } // return physicalPaths based on phone model - private val physicalPaths: Array - @SuppressLint("SdCardPath") - get() = arrayOf( - "/storage/sdcard0", - "/storage/sdcard1", //Motorola Xoom - "/storage/extsdcard", //Samsung SGS3 - "/storage/sdcard0/external_sdcard", //User request - "/mnt/extsdcard", - "/mnt/sdcard/external_sd", //Samsung galaxy family - "/mnt/external_sd", - "/mnt/media_rw/sdcard1", //4.4.2 on CyanogenMod S3 - "/removable/microsd", //Asus transformer prime - "/mnt/emmc", - "/storage/external_SD", //LG - "/storage/ext_sd", //HTC One Max - "/storage/removable/sdcard1", //Sony Xperia Z1 - "/data/sdext", - "/data/sdext2", - "/data/sdext3", - "/data/sdext4", - "/sdcard1", //Sony Xperia Z - "/sdcard2", //HTC One M8s - "/storage/microsd" //ASUS ZenFone 2 - ) + @SuppressLint("SdCardPath") + private val physicalPaths = arrayOf( + "/storage/sdcard0", + "/storage/sdcard1", //Motorola Xoom + "/storage/extsdcard", //Samsung SGS3 + "/storage/sdcard0/external_sdcard", //User request + "/mnt/extsdcard", + "/mnt/sdcard/external_sd", //Samsung galaxy family + "/mnt/external_sd", + "/mnt/media_rw/sdcard1", //4.4.2 on CyanogenMod S3 + "/removable/microsd", //Asus transformer prime + "/mnt/emmc", + "/storage/external_SD", //LG + "/storage/ext_sd", //HTC One Max + "/storage/removable/sdcard1", //Sony Xperia Z1 + "/data/sdext", + "/data/sdext2", + "/data/sdext3", + "/data/sdext4", + "/sdcard1", //Sony Xperia Z + "/sdcard2", //HTC One M8s + "/storage/microsd" //ASUS ZenFone 2 + ) /** * Volume tree URIs @@ -194,7 +198,7 @@ object StorageUtils { private fun getVolumePathFromTreeUriUuid(context: Context, uuid: String): String? { if (uuid == "primary") { - return primaryVolumePath + return getPrimaryVolumePath(context) } val sm = context.getSystemService(StorageManager::class.java) if (sm != null) { @@ -255,30 +259,33 @@ object StorageUtils { * Document files */ - @JvmStatic fun getDocumentFile(context: Context, anyPath: String, mediaUri: Uri): DocumentFileCompat? { - if (requireAccessPermission(anyPath)) { - // need a document URI (not a media content URI) to open a `DocumentFile` output stream - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // cleanest API to get it - val docUri = MediaStore.getDocumentUri(context, mediaUri) - if (docUri != null) { - return DocumentFileCompat.fromSingleUri(context, docUri) + try { + if (requireAccessPermission(context, anyPath)) { + // need a document URI (not a media content URI) to open a `DocumentFile` output stream + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isMediaStoreContentUri(mediaUri)) { + // cleanest API to get it + val docUri = MediaStore.getDocumentUri(context, mediaUri) + if (docUri != null) { + return DocumentFileCompat.fromSingleUri(context, docUri) + } } + // fallback for older APIs + return getVolumePath(context, anyPath)?.let { convertDirPathToTreeUri(context, it) }?.let { getDocumentFileFromVolumeTree(context, it, anyPath) } } - // fallback for older APIs - return getVolumePath(context, anyPath)?.let { convertDirPathToTreeUri(context, it) }?.let { getDocumentFileFromVolumeTree(context, it, anyPath) } + // good old `File` + return DocumentFileCompat.fromFile(File(anyPath)) + } catch (e: SecurityException) { + Log.w(LOG_TAG, "failed to get document file from mediaUri=$mediaUri", e) } - // good old `File` - return DocumentFileCompat.fromFile(File(anyPath)) + return null } // returns the directory `DocumentFile` (from tree URI when scoped storage is required, `File` otherwise) // returns null if directory does not exist and could not be created - @JvmStatic fun createDirectoryIfAbsent(context: Context, dirPath: String): DocumentFileCompat? { val cleanDirPath = ensureTrailingSeparator(dirPath) - return if (requireAccessPermission(cleanDirPath)) { + return if (requireAccessPermission(context, cleanDirPath)) { val grantedDir = getGrantedDirForPath(context, cleanDirPath) ?: return null val rootTreeUri = convertDirPathToTreeUri(context, grantedDir) ?: return null var parentFile: DocumentFileCompat? = DocumentFileCompat.fromTreeUri(context, rootTreeUri) ?: return null @@ -311,7 +318,6 @@ object StorageUtils { } } - @JvmStatic fun copyFileToTemp(documentFile: DocumentFileCompat, path: String): String? { val extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(File(path)).toString()) try { @@ -351,13 +357,12 @@ object StorageUtils { * Misc */ - @JvmStatic - fun requireAccessPermission(anyPath: String): Boolean { + fun requireAccessPermission(context: Context, anyPath: String): Boolean { // on Android R, we should always require access permission, even on primary volume if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { return true } - val onPrimaryVolume = anyPath.startsWith(primaryVolumePath) + val onPrimaryVolume = anyPath.startsWith(getPrimaryVolumePath(context)) return !onPrimaryVolume } @@ -380,10 +385,12 @@ object StorageUtils { } catch (e: FileNotFoundException) { Log.w(LOG_TAG, "failed to find file at uri=$effectiveUri") null + } catch (e: SecurityException) { + Log.w(LOG_TAG, "failed to open file at uri=$effectiveUri", e) + null } } - @JvmStatic fun openMetadataRetriever(context: Context, uri: Uri): MediaMetadataRetriever? { var effectiveUri = uri // we get a permission denial if we require original from a provider other than the media store @@ -403,7 +410,7 @@ object StorageUtils { // convenience methods - private fun ensureTrailingSeparator(dirPath: String): String { + fun ensureTrailingSeparator(dirPath: String): String { return if (dirPath.endsWith(File.separator)) dirPath else dirPath + File.separator } @@ -411,7 +418,7 @@ object StorageUtils { class PathSegments(context: Context, fullPath: String) { var volumePath: String? = null // `volumePath` with trailing "/" var relativeDir: String? = null // `relativeDir` with trailing "/" - var filename: String? = null // null for directories + private var filename: String? = null // null for directories init { volumePath = getVolumePath(context, fullPath) diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f88..000000000 --- a/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index cacc0e47e..000000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 780a95fe5..000000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 0f45f4a1c..000000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 4802cf7be..000000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index ffbb6d34c..000000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/build.gradle b/android/build.gradle index ca8d0e7ec..4f36b3be7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,13 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = '1.4.10' repositories { google() jcenter() } - dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' // do not upgrade to 4+ until this is fixed: https://github.com/flutter/flutter/issues/58247 + // TODO TLAD upgrade AGP to 4+ when this is fixed: https://github.com/flutter/flutter/issues/58247 + classpath 'com.android.tools.build:gradle:3.6.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.4' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0' diff --git a/android/gradle.properties b/android/gradle.properties index 38c8d4544..a33b0802a 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,18 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +android.enableR8=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 33268b8ca..c10917bb8 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Apr 21 13:20:37 KST 2020 +#Thu Oct 22 10:54:33 KST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/extra/play/screenshots v1.2.1/S10/1-S10-collection.png b/extra/play/screenshots v1.2.1/S10/1-S10-collection.png index ea99bf07b..bd67a0dab 100644 Binary files a/extra/play/screenshots v1.2.1/S10/1-S10-collection.png and b/extra/play/screenshots v1.2.1/S10/1-S10-collection.png differ diff --git a/extra/play/screenshots v1.2.1/S10/2-S10-image.png b/extra/play/screenshots v1.2.1/S10/2-S10-image.png index 723211466..81159ad19 100644 Binary files a/extra/play/screenshots v1.2.1/S10/2-S10-image.png and b/extra/play/screenshots v1.2.1/S10/2-S10-image.png differ diff --git a/extra/play/screenshots v1.2.1/S10/3-S10-info__basic_.png b/extra/play/screenshots v1.2.1/S10/3-S10-info__basic_.png index b664b270a..e42510bc1 100644 Binary files a/extra/play/screenshots v1.2.1/S10/3-S10-info__basic_.png and b/extra/play/screenshots v1.2.1/S10/3-S10-info__basic_.png differ diff --git a/extra/play/screenshots v1.2.1/S10/4-S10-info__metadata_.png b/extra/play/screenshots v1.2.1/S10/4-S10-info__metadata_.png index 667aa33d6..8a71700f4 100644 Binary files a/extra/play/screenshots v1.2.1/S10/4-S10-info__metadata_.png and b/extra/play/screenshots v1.2.1/S10/4-S10-info__metadata_.png differ diff --git a/extra/play/screenshots v1.2.1/S10/5-S10-stats.png b/extra/play/screenshots v1.2.1/S10/5-S10-stats.png index 8d5f63dbe..5d12670af 100644 Binary files a/extra/play/screenshots v1.2.1/S10/5-S10-stats.png and b/extra/play/screenshots v1.2.1/S10/5-S10-stats.png differ diff --git a/extra/play/screenshots v1.2.1/S10/6-S10-countries.png b/extra/play/screenshots v1.2.1/S10/6-S10-countries.png index 9c810035c..a1f81a12a 100644 Binary files a/extra/play/screenshots v1.2.1/S10/6-S10-countries.png and b/extra/play/screenshots v1.2.1/S10/6-S10-countries.png differ diff --git a/lib/main.dart b/lib/main.dart index 7ccf819e1..6272441c2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -82,13 +82,13 @@ class _AvesAppState extends State { ), ); - Widget get firstPage => settings.hasAcceptedTerms ? HomePage() : WelcomePage(); + Widget getFirstPage({Map intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : WelcomePage(); @override void initState() { super.initState(); _appSetup = _setup(); - _newIntentChannel.receiveBroadcastStream().listen((_) => _onNewIntent()); + _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map)); } Future _setup() async { @@ -109,11 +109,16 @@ class _AvesAppState extends State { await settings.initCrashlytics(); } - void _onNewIntent() { + void _onNewIntent(Map intentData) { + debugPrint('$runtimeType onNewIntent with intentData=$intentData'); + + // do not reset when relaunching the app + if (AvesApp.mode == AppMode.main && (intentData == null || intentData.isEmpty == true)) return; + FirebaseCrashlytics.instance.log('New intent'); _navigatorKey.currentState.pushReplacement(DirectMaterialPageRoute( settings: RouteSettings(name: HomePage.routeName), - builder: (_) => firstPage, + builder: (_) => getFirstPage(intentData: intentData), )); } @@ -125,7 +130,7 @@ class _AvesAppState extends State { future: _appSetup, builder: (context, snapshot) { if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) { - return firstPage; + return getFirstPage(); } return Scaffold( body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(), diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 0315ea393..b9573346d 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -56,6 +56,8 @@ class ImageEntry { this.dateModifiedSecs = dateModifiedSecs; } + bool get canDecode => !MimeTypes.undecodable.contains(mimeType); + ImageEntry copyWith({ @required String uri, @required String path, diff --git a/lib/model/mime_types.dart b/lib/model/mime_types.dart index dbe344c59..9d6483da2 100644 --- a/lib/model/mime_types.dart +++ b/lib/model/mime_types.dart @@ -9,6 +9,9 @@ class MimeTypes { static const String svg = 'image/svg+xml'; static const String webp = 'image/webp'; + static const String tiff = 'image/tiff'; + static const String psd = 'image/vnd.adobe.photoshop'; + static const String arw = 'image/x-sony-arw'; static const String cr2 = 'image/x-canon-cr2'; static const String crw = 'image/x-canon-crw'; @@ -38,4 +41,5 @@ class MimeTypes { // groups static const List rawImages = [arw, cr2, crw, dcr, dng, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f]; + static const List undecodable = [crw, psd, tiff]; // TODO TLAD make it dynamic if it depends on OS/lib versions } diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index d3c80fe7a..7c99427bd 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -136,13 +136,15 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails)); } else { await Future.forEach(movedOps, (movedOp) async { - final sourceUri = movedOp.uri; final newFields = movedOp.newFields; - final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); - if (entry != null) { - fromAlbums.add(entry.directory); - movedEntries.add(entry); - await moveEntry(entry, newFields); + if (newFields.isNotEmpty) { + final sourceUri = movedOp.uri; + final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); + if (entry != null) { + fromAlbums.add(entry.directory); + movedEntries.add(entry); + await moveEntry(entry, newFields); + } } }); } diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index d2375e305..7549d69af 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -41,9 +41,9 @@ class AndroidAppService { return {}; } - static Future edit(String uri, String mimeType) async { + static Future edit(String uri, String mimeType) async { try { - await platform.invokeMethod('edit', { + return await platform.invokeMethod('edit', { 'title': 'Edit with:', 'uri': uri, 'mimeType': mimeType, @@ -51,11 +51,12 @@ class AndroidAppService { } on PlatformException catch (e) { debugPrint('edit failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } + return false; } - static Future open(String uri, String mimeType) async { + static Future open(String uri, String mimeType) async { try { - await platform.invokeMethod('open', { + return await platform.invokeMethod('open', { 'title': 'Open with:', 'uri': uri, 'mimeType': mimeType, @@ -63,22 +64,23 @@ class AndroidAppService { } on PlatformException catch (e) { debugPrint('open failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } + return false; } - static Future openMap(String geoUri) async { - if (geoUri == null) return; + static Future openMap(String geoUri) async { try { - await platform.invokeMethod('openMap', { + return await platform.invokeMethod('openMap', { 'geoUri': geoUri, }); } on PlatformException catch (e) { debugPrint('openMap failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } + return false; } - static Future setAs(String uri, String mimeType) async { + static Future setAs(String uri, String mimeType) async { try { - await platform.invokeMethod('setAs', { + return await platform.invokeMethod('setAs', { 'title': 'Set as:', 'uri': uri, 'mimeType': mimeType, @@ -86,19 +88,21 @@ class AndroidAppService { } on PlatformException catch (e) { debugPrint('setAs failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } + return false; } - static Future share(Iterable entries) async { + static Future share(Iterable entries) async { // loosen mime type to a generic one, so we can share with badly defined apps // e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats final urisByMimeType = groupBy(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); try { - await platform.invokeMethod('share', { + return await platform.invokeMethod('share', { 'title': 'Share via:', 'urisByMimeType': urisByMimeType, }); } on PlatformException catch (e) { debugPrint('share failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } + return false; } } diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 80ae5f9e9..8585dde8a 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -18,6 +18,8 @@ class Constants { offset: Offset(0.5, 1.0), ); + static const String unknown = 'unknown'; + static const pointNemo = Tuple2(-48.876667, -123.393333); static const int infoGroupMaxValueLength = 140; diff --git a/lib/widgets/collection/thumbnail/error.dart b/lib/widgets/collection/thumbnail/error.dart new file mode 100644 index 000000000..ae0d2bdf5 --- /dev/null +++ b/lib/widgets/collection/thumbnail/error.dart @@ -0,0 +1,24 @@ +import 'package:aves/widgets/common/icons.dart'; +import 'package:flutter/material.dart'; + +class ErrorThumbnail extends StatelessWidget { + final double extent; + final String tooltip; + + const ErrorThumbnail({@required this.extent, @required this.tooltip}); + + @override + Widget build(BuildContext context) { + return Center( + child: Tooltip( + message: tooltip, + preferBelow: false, + child: Icon( + AIcons.error, + size: extent / 2, + color: Colors.blueGrey, + ), + ), + ); + } +} diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index e9749dd9b..0cc0b9e35 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:aves/model/image_entry.dart'; import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/collection/thumbnail/error.dart'; import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart'; import 'package:aves/widgets/common/image_providers/uri_image_provider.dart'; import 'package:aves/widgets/common/transition_image.dart'; @@ -71,7 +71,11 @@ class _ThumbnailRasterImageState extends State { _pauseProvider(); } + bool get isSupported => entry.canDecode; + void _initProvider() { + if (!entry.canDecode) return; + _fastThumbnailProvider = ThumbnailProvider( ThumbnailProviderKey.fromEntry(entry), ); @@ -95,6 +99,13 @@ class _ThumbnailRasterImageState extends State { @override Widget build(BuildContext context) { + if (!entry.canDecode) { + return ErrorThumbnail( + extent: extent, + tooltip: '${entry.mimeType} not supported', + ); + } + final fastImage = Image( key: ValueKey('LQ'), image: _fastThumbnailProvider, @@ -127,16 +138,9 @@ class _ThumbnailRasterImageState extends State { child: frame == null ? fastImage : child, ); }, - errorBuilder: (context, error, stackTrace) => Center( - child: Tooltip( - message: error.toString(), - preferBelow: false, - child: Icon( - AIcons.error, - size: extent / 2, - color: Colors.blueGrey, - ), - ), + errorBuilder: (context, error, stackTrace) => ErrorThumbnail( + extent: extent, + tooltip: error.toString(), ), width: extent, height: extent, diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index 26ef150a1..837776778 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -126,7 +126,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin { final selectionCount = selection.length; if (movedCount < selectionCount) { final count = selectionCount - movedCount; - showFeedback(context, 'Failed to move ${Intl.plural(count, one: '$count item', other: '$count items')}'); + showFeedback(context, 'Failed to ${copy ? 'copy' : 'move'} ${Intl.plural(count, one: '$count item', other: '$count items')}'); } else { final count = movedCount; showFeedback(context, '${copy ? 'Copied' : 'Moved'} ${Intl.plural(count, one: '$count item', other: '$count items')}'); diff --git a/lib/widgets/fullscreen/image_view.dart b/lib/widgets/fullscreen/image_view.dart index e92c2b61a..8d62ffc16 100644 --- a/lib/widgets/fullscreen/image_view.dart +++ b/lib/widgets/fullscreen/image_view.dart @@ -93,7 +93,7 @@ class ImageView extends StatelessWidget { initialScale: PhotoViewComputedScale.contained, onTapUp: (tapContext, details, value) => onTap?.call(), ); - } else { + } else if (entry.canDecode) { final uriImage = UriImage( uri: entry.uri, mimeType: entry.mimeType, @@ -111,11 +111,7 @@ class ImageView extends StatelessWidget { context, imageCache.statusForKey(uriImage).keepAlive ? uriImage : fastThumbnailProvider, ), - loadFailedChild: EmptyContent( - icon: AIcons.error, - text: 'Oops!', - alignment: Alignment.center, - ), + loadFailedChild: _buildError(), backgroundDecoration: backgroundDecoration, scaleStateChangedCallback: onScaleChanged, minScale: PhotoViewComputedScale.contained, @@ -123,6 +119,8 @@ class ImageView extends StatelessWidget { onTapUp: (tapContext, details, value) => onTap?.call(), filterQuality: FilterQuality.low, ); + } else { + child = _buildError(); } return heroTag != null @@ -133,4 +131,18 @@ class ImageView extends StatelessWidget { ) : child; } + + Widget _buildError() => GestureDetector( + onTap: () => onTap?.call(), + // use a `Container` with a dummy color to make it expand + // so that we can also detect taps around the title `Text` + child: Container( + color: Colors.transparent, + child: EmptyContent( + icon: AIcons.error, + text: 'Oops!', + alignment: Alignment.center, + ), + ), + ); } diff --git a/lib/widgets/fullscreen/info/basic_section.dart b/lib/widgets/fullscreen/info/basic_section.dart index 1d84d2151..498c41331 100644 --- a/lib/widgets/fullscreen/info/basic_section.dart +++ b/lib/widgets/fullscreen/info/basic_section.dart @@ -6,6 +6,7 @@ import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/mime_types.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; @@ -28,7 +29,7 @@ class BasicSection extends StatelessWidget { @override Widget build(BuildContext context) { final date = entry.bestDate; - final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : '?'; + final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.unknown; final showMegaPixels = entry.isPhoto && entry.megaPixels != null && entry.megaPixels > 0; final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}'; @@ -36,12 +37,12 @@ class BasicSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ InfoRowGroup({ - 'Title': entry.bestTitle ?? '?', + 'Title': entry.bestTitle ?? Constants.unknown, 'Date': dateText, if (entry.isVideo) ..._buildVideoRows(), if (!entry.isSvg) 'Resolution': resolutionText, - 'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : '?', - 'URI': entry.uri ?? '?', + 'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : Constants.unknown, + 'URI': entry.uri ?? Constants.unknown, if (entry.path != null) 'Path': entry.path, }), _buildChips(), diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index cc8450dfb..33556a12a 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -65,6 +65,8 @@ class InfoPageState extends State { return ValueListenableBuilder( valueListenable: widget.entryNotifier, builder: (context, entry, child) { + if (entry == null) return SizedBox.shrink(); + final locationAtTop = split && entry.hasGps; final locationSection = LocationSection( collection: collection, diff --git a/lib/widgets/fullscreen/overlay/bottom.dart b/lib/widgets/fullscreen/overlay/bottom.dart index f9edab547..fd1183db2 100644 --- a/lib/widgets/fullscreen/overlay/bottom.dart +++ b/lib/widgets/fullscreen/overlay/bottom.dart @@ -228,7 +228,7 @@ class _DateRow extends StatelessWidget { @override Widget build(BuildContext context) { final date = entry.bestDate; - final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : '?'; + final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.unknown; final resolution = '${entry.width ?? '?'} × ${entry.height ?? '?'}'; return Row( children: [ diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 8b7eeeba5..1e6ad8ffc 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -24,7 +24,10 @@ import 'package:permission_handler/permission_handler.dart'; class HomePage extends StatefulWidget { static const routeName = '/'; - const HomePage(); + // untyped map as it is coming from the platform + final Map intentData; + + const HomePage({this.intentData}); @override _HomePageState createState() => _HomePageState(); @@ -64,8 +67,8 @@ class _HomePageState extends State { unawaited(androidFileUtils.initAppNames()); AvesApp.mode = AppMode.main; - final intentData = await ViewerService.getIntentData(); - if (intentData != null) { + final intentData = widget.intentData ?? await ViewerService.getIntentData(); + if (intentData?.isNotEmpty == true) { final action = intentData['action']; switch (action) { case 'view': diff --git a/pubspec.lock b/pubspec.lock index 7fdf2b0ce..4a65823c3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -63,7 +63,7 @@ packages: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "2.3.2+1" + version: "2.3.3" characters: dependency: transitive description: @@ -200,7 +200,7 @@ packages: name: firebase url: "https://pub.dartlang.org" source: hosted - version: "7.3.0" + version: "7.3.2" firebase_core: dependency: "direct main" description: @@ -261,7 +261,7 @@ packages: name: flutter_cache_manager url: "https://pub.dartlang.org" source: hosted - version: "1.4.2" + version: "2.0.0" flutter_driver: dependency: "direct dev" description: flutter @@ -360,7 +360,7 @@ packages: name: google_maps_flutter url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" google_maps_flutter_platform_interface: dependency: transitive description: @@ -570,7 +570,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.18" + version: "1.6.21" path_provider_linux: dependency: transitive description: @@ -747,7 +747,7 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "0.5.12" + version: "0.5.12+2" shared_preferences_linux: dependency: transitive description: @@ -962,7 +962,7 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.7.2" + version: "5.7.5" url_launcher_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7efa6e8f4..ab21192cf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.2.2+28 +version: 1.2.3+29 # video_player (as of v0.10.8+2, backed by ExoPlayer): # - does not support content URIs (by default, but trivial by fork) diff --git a/shaders_1.22.1.sksl.json b/shaders_1.22.1.sksl.json deleted file mode 100644 index 38c83ea81..000000000 --- a/shaders_1.22.1.sksl.json +++ /dev/null @@ -1 +0,0 @@ -{"platform":"android","name":"SM G970N","engineRevision":"75bef9f6c8ac2ed4e1e04cdfcd88b177d9f1850d","data":{"CAZACAECBMAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAEAAAIIDAAWAATYABAAAAAABAAAQAABBAMADYAB4AACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1NdAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTE7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMSkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAACcEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1N0YWdlMV9jMC54LCB1Y2xhbXBfU3RhZ2UxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfU3RhZ2UxX2MwLnksIHVjbGFtcF9TdGFnZTFfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJX291dHB1dCA9IHRleHR1cmVDb2xvcjsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE1hdHJpeEVmZmVjdAoJCW91dHB1dF9TdGFnZTEgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAIIDAAWAATYABEAAAAAAAAAAAABBAMADYAB4AACQAAAABQAAAAAAAAAAAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAOxMAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgX2Nvb3Jkcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgR2F1c3NpYW5Db252b2x1dGlvbgoJCWZsb2F0MiBfY29vcmRzID0gdkxvY2FsQ29vcmRfU3RhZ2UwLnh5OwoJCW91dHB1dF9TdGFnZTEgPSBoYWxmNCgwLCAwLCAwLCAwKTsKCQlmbG9hdDIgY29vcmQgPSBfY29vcmRzIC0gMTIuMCAqIHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWZsb2F0MiBjb29yZFNhbXBsZWQgPSBoYWxmMigwLCAwKTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAACAYAABEAJAAABGAABAAOAAEIA777777YZAAAAAFAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCWNvbG9yID0gY29sb3IgKiBpbkNvdmVyYWdlOwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAABVAQAAaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","CAZAAAACAUAABEAAAAABGAABAAOAAAYABQAEIAAAAAAAAAAAAAAAEAAAAAOAAWAA":"AgAAAExTS1MxAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkCgl2SGFpclF1YWRFZGdlX1N0YWdlMCA9IGluSGFpclF1YWRFZGdlOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABjAwAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwp1bmlmb3JtIGhhbGYgdUNvdmVyYWdlX1N0YWdlMDsKaW4gaGFsZjQgdkhhaXJRdWFkRWRnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh1Q292ZXJhZ2VfU3RhZ2UwICogZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADgAAAGluSGFpclF1YWRFZGdlAAABAAAAAAAAAA==","CAZAAAICBIAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAAAQAAAAGQAB6AAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAcQQAAHVuaWZvcm0gZmxvYXQ0IHVjaXJjbGVfU3RhZ2UxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJZmxvYXQyIHRleENvb3JkOwoJCXRleENvb3JkID0gdmxvY2FsQ29vcmRfU3RhZ2UwOwoJCW91dHB1dENvbG9yX1N0YWdlMCA9IChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB0ZXhDb29yZCkgKiBvdXRwdXRDb2xvcl9TdGFnZTApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjbGVFZmZlY3QKCQlmbG9hdDIgcHJldkNlbnRlcjsKCQlmbG9hdCBwcmV2UmFkaXVzID0gLTEuMDAwMDAwOwoJCWhhbGYgZDsKCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TdGFnZTEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TdGFnZTEudykgLSAxLjApICogdWNpcmNsZV9TdGFnZTEueik7CgkJfQoJCWVsc2UgCgkJewoJCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1N0YWdlMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1N0YWdlMS53KSkgKiB1Y2lyY2xlX1N0YWdlMS56KTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlAaWYgKDEgPT0gMSB8fCAxID09IDMpIAoJCXsKCQkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBjbGFtcChkLCAwLjAsIDEuMCk7CgkJfQoJCWVsc2UgCgkJewoJCQlvdXRwdXRfU3RhZ2UxID0gZCA+IDAuNSA/IGlucHV0Q29sb3IgOiBoYWxmNCgwLjApOwoJCX0KCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAICBIAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAoAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAMCBAAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAA2AAAAAAAAAAAAAAABAAAAAKAAC4AAIAAAAAAAAAAAAIAAAABYABMAA":"AgAAAExTS1NGAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAQEAAAAAAAABAQCCBQAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKS5ycnJyOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","CAZAAAACA4AAAAYAAAAAAAAAAAAQAAAACMAACAA4AAIQADYACQAAAAAAAAMAALQAAAAAAAAAAAAAAAQAAAACYACYAA":"AgAAAExTS1PjAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNEaW1lbnNpb25zSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSAodGV4SWR4KTsKCXZJbnRUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAXAwAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBmbG9hdDIgdkludFRleHR1cmVDb29yZHNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGlzdGFuY2VGaWVsZFBhdGgKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfU3RhZ2UwOwoJCWhhbGY0IHRleENvbG9yOwoJCXsKCQkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB1dikucnJycjsKCQl9CgkJaGFsZiBkaXN0YW5jZSA9IDcuOTY4NzUqKHRleENvbG9yLnIgLSAwLjUwMTk2MDc4NDMxKTsKCQloYWxmIGFmd2lkdGg7CgkJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeSh2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTAueSkpKTsKCQloYWxmIHZhbCA9IHNtb290aHN0ZXAoLWFmd2lkdGgsIGFmd2lkdGgsIGRpc3RhbmNlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh2YWwpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZAAAACBAAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAA4AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZACAECBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAIIDAAWAATYABAAAAAAAAAAAAABBAMADYAB4AACAAAAAAAAAAAANAAAAAAAAAAAAAIIDABKAAAQAAQAAAAAAAAAAAAQAAAAGQACYAA":"AgAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAACIAwAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKENvbXBvc2UtT25lIGJlaGF2aW9yKQoJCW91dHB1dF9TdGFnZTEgPSBibGVuZF9tb2R1bGF0ZShNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpKSwgb3V0cHV0Q29sb3JfU3RhZ2UwKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAMCBEAAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAAAAAAAAAQAAAAEAACYAA":"AgAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAEBAAAAAAAAAQEA5QIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAMCBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHWAAAAAYAABQAAQAAAADZAAAAA6YAAAAEAAAGAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBALoEAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7Cglfb3V0cHV0ID0gX2lucHV0ICogYWxwaGE7CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKSAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","CAZAAAMCBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAGAAAAAYAAFYAAQAAAADZAAAAAAYAAAAEAAAGAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAHoGAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBhbHBoYTsKCUBzd2l0Y2ggKDMpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZiB4U3ViLCB5U3ViOwoJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueCksIDAuMCk7CgkJeFN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnogLSBza19GcmFnQ29vcmQueCksIDAuMCk7CgkJeVN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC55IC0gdXJlY3RVbmlmb3JtX1N0YWdlMV9jMC55KSwgMC4wKTsKCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQlhbHBoYSA9ICgxLjAgKyBtYXgoeFN1YiwgLTEuMCkpICogKDEuMCArIG1heCh5U3ViLCAtMS4wKSk7Cgl9CglAaWYgKDMgPT0gMiB8fCAzID09IDMpIAoJewoJCWFscGhhID0gMS4wIC0gYWxwaGE7Cgl9CgloYWxmNCBpbnB1dENvbG9yID0gX2lucHV0OwoJX291dHB1dCA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCkgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAMCBEAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAAAAAAAAAQAAAAEAACYAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAOoCAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECAUAAAEYAAEABYAARAANQAAQAAAAAAAAMABEQAAAAAAAAAAAAAABAAAAAEAAFQAA":"AgAAAExTS1OFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFJSZWN0U2hhZG93Cgl2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAB1AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBoYWxmMyB2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUlJlY3RTaGFkb3cKCQloYWxmMyBzaGFkb3dQYXJhbXM7CgkJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CgkJZmxvYXQyIHV2ID0gZmxvYXQyKHNoYWRvd1BhcmFtcy56ICogKDEuMCAtIGQpLCAwLjUpOwoJCWhhbGYgZmFjdG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdXYpLnJycnIuYTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChmYWN0b3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","CAZACAECBMAAAAIACAAAAAAAAAAAAAAACMAACAA4AAIQB777777RQADSAAAAAAAAEAACOAAEAAAAAAAAAAAAAAAAAAYAAGYAAQAAAAANAAAAAAAAAAAEAAACAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1PNAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHV2aWV3TWF0cml4X1N0YWdlMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVmVydGljZXNHUAoJaGFsZjQgY29sb3IgPSBpbkNvbG9yOwoJY29sb3IgPSBjb2xvci5iZ3JhOwoJY29sb3IgPSBjb2xvcjsKCWNvbG9yID0gaGFsZjQoY29sb3IucmdiICogY29sb3IuYSwgY29sb3IuYSk7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX3Bvc2l0aW9uID0gdXZpZXdNYXRyaXhfU3RhZ2UwLnh6ICogcG9zaXRpb24gKyB1dmlld01hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfcG9zaXRpb24ueCAsIF90bXBfMF9wb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAAhBAAAdW5pZm9ybSBoYWxmNCB1Y29sb3JfU3RhZ2UxX2MwOwppbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDb25zdENvbG9yUHJvY2Vzc29yX1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHVjb2xvcl9TdGFnZTFfYzA7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBCbHVycmVkRWRnZUZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiBpbnB1dEFscGhhID0gX2lucHV0Lnc7CgloYWxmIGZhY3RvciA9IDEuMCAtIGlucHV0QWxwaGE7CglAc3dpdGNoICgwKSAKCXsKCQljYXNlIDA6ICAgICAgICBmYWN0b3IgPSBleHAoKC1mYWN0b3IgKiBmYWN0b3IpICogNC4wKSAtIDAuMDE3OTk5OTk5MjI1MTM5NjE4OwoJCWJyZWFrOwoJCWNhc2UgMTogICAgICAgIGZhY3RvciA9IHNtb290aHN0ZXAoMS4wLCAwLjAsIGZhY3Rvcik7CgkJYnJlYWs7Cgl9Cglfb3V0cHV0ID0gaGFsZjQoZmFjdG9yKTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVmVydGljZXNHUAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKFNrTW9kZSBiZWhhdmlvcikKCQlvdXRwdXRfU3RhZ2UxID0gYmxlbmRfbW9kdWxhdGUoQ29uc3RDb2xvclByb2Nlc3Nvcl9TdGFnZTFfYzAoaGFsZjQoMSkpLCBCbHVycmVkRWRnZUZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMShvdXRwdXRDb2xvcl9TdGFnZTApKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24HAAAAaW5Db2xvcgABAAAAAAAAAA==","CAZAAAECA4AAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAWwEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAECAUAAAAAAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAAAAAAAAABAAAAAEAAFQAA":"AgAAAExTS1NuAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TdGFnZTAgPSBpblF1YWRFZGdlOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAB+AwAAaW4gaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWhhbGYgZWRnZUFscGhhOwoJCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQlpZiAodlF1YWRFZGdlX1N0YWdlMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TdGFnZTAudyA+IDAuMCkgCgkJewoJCQllZGdlQWxwaGEgPSBtaW4obWluKHZRdWFkRWRnZV9TdGFnZTAueiwgdlF1YWRFZGdlX1N0YWdlMC53KSArIDAuNSwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWhhbGYyIGdGID0gaGFsZjIoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KTsKCQkJZWRnZUFscGhhID0gKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","CAZAAAACAYAABAAIAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1M3AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJdmluQ292ZXJhZ2VfU3RhZ2UwID0gaW5Db3ZlcmFnZTsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAArAEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCQloYWxmIGFscGhhID0gMS4wOwoJCWFscGhhID0gdmluQ292ZXJhZ2VfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZAAAACBAAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAArQIAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZACAACB4AAAAAAAAAGOAIAAAJQAAIACIAAAAA4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAACAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAQAAAALQAEAAAEAAAAAAAAAAAAKAAAABWAAWAA":"AgAAAExTS1McAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0IHZjb3ZlcmFnZV9TdGFnZTA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBwb3NpdGlvbiA9IHBvc2l0aW9uLnh5OwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJdmNvdmVyYWdlX1N0YWdlMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBsb2NhbENvb3JkLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAvQcAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gKGhhbGY0KDEuMCkgLSBvdXRwdXRfU3RhZ2UxKSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAECAYAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAA2AAAAAAAAAAAAAAACAAAAAKAALAAA":"AgAAAExTS1NGAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAZAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKS5ycnJyOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZACAACB4AAAAAAAAAGOAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAACAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAQAAAALQAEAAAEAAAAAAAAAAAAEAAAABWAAWAA":"AgAAAExTS1OvAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAYQcAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yCgkJaGFsZjQgY29uc3RDb2xvcjsKCQlAaWYgKGZhbHNlKSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgwKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCk7CgkJfQoJCW91dHB1dF9TdGFnZTEgPSBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGNvbnN0Q29sb3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAMCBMAAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAABAAAAA6IAAAAEAAAXAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQBvBwAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMTsKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCV9vdXRwdXQgPSBfaW5wdXQgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7CgkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAEAAAIIDAAWAATYABEAAAAABAAAQAABBAMADYAB4AACQAAAABQAAAAABAAAQAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAshQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gY2xhbXAoc3Vic2V0Q29vcmQueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC55LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgY2xhbXBlZENvb3JkKTsKCV9vdXRwdXQgPSB0ZXh0dXJlQ29sb3I7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgR2F1c3NpYW5Db252b2x1dGlvbgoJCWZsb2F0MiBfY29vcmRzID0gdkxvY2FsQ29vcmRfU3RhZ2UwLnh5OwoJCW91dHB1dF9TdGFnZTEgPSBoYWxmNCgwLCAwLCAwLCAwKTsKCQlmbG9hdDIgY29vcmQgPSBfY29vcmRzIC0gMTIuMCAqIHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWZsb2F0MiBjb29yZFNhbXBsZWQgPSBoYWxmMigwLCAwKTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAICBIAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEANwQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjdWxhclJSZWN0CgkJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCQlmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxLlJCOwoJCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTEueCAtIGxlbmd0aChkeHkpKSk7CgkJb3V0cHV0X1N0YWdlMSA9IG91dHB1dENvdmVyYWdlX1N0YWdlMCAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECA4AAAAIAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAADgAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJCWVkZ2VBbHBoYSAqPSBpbm5lckFscGhhOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","CAZAAAECA4AAAAAAAAABKAADAAKQAAYACUAAGAATAAAQAFIAAMABKAADAAOAAEIAEAADEAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MXBAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgcmFkaWk7CglyYWRpaS54ID0gZG90KHJhZGlpX3NlbGVjdG9yLCByYWRpaV94KTsKCXJhZGlpLnkgPSBkb3QocmFkaWlfc2VsZWN0b3IsIHJhZGlpX3kpOwoJYm9vbCBpc19hcmNfc2VjdGlvbiA9IChyYWRpaS54ID4gMCk7CglyYWRpaSA9IGFicyhyYWRpaSk7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpOwoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZTsKCWlmIChpc19hcmNfc2VjdGlvbikgCgl7CgkJdmFyY2Nvb3JkX1N0YWdlMC54eSA9IDEgLSBhYnMocmFkaXVzX291dHNldCk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqICh2YXJjY29vcmRfU3RhZ2UwLnh5L3JhZGlpICogY29ybmVyICogMik7Cgl9CgllbHNlIAoJewoJCXZhcmNjb29yZF9TdGFnZTAgPSBmbG9hdDQoMCk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAACgCAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0NCB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgkJaWYgKGZsb2F0MigwKSAhPSB2YXJjY29vcmRfU3RhZ2UwLnh5KSAKCQl7CgkJCWZsb2F0IGZuID0gZG90KHZhcmNjb29yZF9TdGFnZTAueHksIHZhcmNjb29yZF9TdGFnZTAueHkpIC0gMTsKCQkJaWYgKGZuID4gMCkgCgkJCXsKCQkJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDApOwoJCQl9CgkJfQoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAHAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAACAYAAAEAYAAABGAABAAOAAEIA7777777777776FAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1NnAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBjb2xvciA9IGluQ29sb3I7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAVQEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgABAAAAAAAAAA==","CAZAAAACAYAAAAAIAAABGAABAD7777777777777777776FAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1PkAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAWgEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAEAAAAKAAAAaW5Qb3NpdGlvbgAAAQAAAAAAAAA=","CAZAAAECA4AAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABMAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAICBQAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAIAAAAHSAAAABCAAFYAAQAAAAAAAAAAAAQAAAAFIACYAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEA0AcAAHVuaWZvcm0gZmxvYXQ0IHVyZWN0VW5pZm9ybV9TdGFnZTE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTFfYzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCV9vdXRwdXQgPSBfaW5wdXQgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBBQVJlY3RFZmZlY3QKCQlmbG9hdDQgcHJldlJlY3QgPSBmbG9hdDQoLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwKTsKCQloYWxmIGFscGhhOwoJCUBzd2l0Y2ggKDEpIAoJCXsKCQkJY2FzZSAwOiAgICBjYXNlIDI6ICAgICAgICBhbHBoYSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TdGFnZTEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1N0YWdlMS54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJCQlicmVhazsKCQkJZGVmYXVsdDogICAgICAgIGhhbGYgeFN1YiwgeVN1YjsKCQkJeFN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC54IC0gdXJlY3RVbmlmb3JtX1N0YWdlMS54KSwgMC4wKTsKCQkJeFN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UxLnogLSBza19GcmFnQ29vcmQueCksIDAuMCk7CgkJCXlTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueSAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueSksIDAuMCk7CgkJCXlTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS53IC0gc2tfRnJhZ0Nvb3JkLnkpLCAwLjApOwoJCQlhbHBoYSA9ICgxLjAgKyBtYXgoeFN1YiwgLTEuMCkpICogKDEuMCArIG1heCh5U3ViLCAtMS4wKSk7CgkJfQoJCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgkJewoJCQlhbHBoYSA9IDEuMCAtIGFscGhhOwoJCX0KCQloYWxmNCBpbnB1dENvbG9yID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAEAAAIIDAAWAATYABEAAAAAAAAAQAABBAMADYAB4AACQAAAABQAAAAAAAAAQAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAfRQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gc3Vic2V0Q29vcmQueTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAECA4AAAAIAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAOACAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQloYWxmIGRpc3RhbmNlVG9Jbm5lckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqIChkIC0gY2lyY2xlRWRnZS53KSk7CgkJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgkJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAECA4AAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAGABAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAICBIAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAQEAAAAAAAABAQDCAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAACBAAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAFgIAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAECAYAAABAAAAAACAAAAAJQAAIA777777YPAAKAAABBAMABIAA2AAAAAAAAAAAAAAACAAAAAKAALAAA":"AgAAAExTS1P9AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoaW5Qb3NpdGlvbi54ICwgaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABLAgAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gaW50IHZUZXhJbmRleF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZjQgdGV4Q29sb3I7CgkJewoJCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHZUZXh0dXJlQ29vcmRzX1N0YWdlMCk7CgkJfQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IG91dHB1dENvbG9yX1N0YWdlMCAqIHRleENvbG9yOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZAAAICBIAAAAIAAAABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1M6CQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqIChhcmNjb29yZC9yYWRpaSAqIDIpOwoJfQoJc2tfUG9zaXRpb24gPSBmbG9hdDQoZGV2Y29vcmQueCAsIGRldmNvb3JkLnksIDAsIDEpOwp9CgAAAAEBAAAAAAAAAQEAdQQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZ3g9dmFyY2Nvb3JkX1N0YWdlMC56LCBneT12YXJjY29vcmRfU3RhZ2UwLnc7CgkJCWZsb2F0IGZud2lkdGggPSBhYnMoZ3gpICsgYWJzKGd5KTsKCQkJaGFsZiBkID0gaGFsZihmbi9mbndpZHRoKTsKCQkJY292ZXJhZ2UgPSBjbGFtcCguNSAtIGQsIDAsIDEpOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAAAAAIIDAAWAATYABEAAAAABAAAAAABBAMADYAB4AACQAAAABQAAAAABAAAAAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAfRQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TdGFnZTFfYzBfYzAueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC53KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAECA4AAAAAAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAEwCAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA=="}} \ No newline at end of file diff --git a/shaders_1.22.2.sksl.json b/shaders_1.22.2.sksl.json new file mode 100644 index 000000000..8116385d1 --- /dev/null +++ b/shaders_1.22.2.sksl.json @@ -0,0 +1 @@ +{"platform":"android","name":"SM G970N","engineRevision":"b8752bbfff0419c8bf616b602bc59fd28f6a3d1b","data":{"CAZAAAECAYAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAA2AAAAAAAAAAAAAAACAAAAAKAALAAA":"AgAAAExTS1NGAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAZAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKS5ycnJyOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZAAAECA4AAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAGABAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAICBQAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAIAAAAHSAAAABCAAFYAAQAAAAAAAAAAAAQAAAAFIACYAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEA0AcAAHVuaWZvcm0gZmxvYXQ0IHVyZWN0VW5pZm9ybV9TdGFnZTE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTFfYzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCV9vdXRwdXQgPSBfaW5wdXQgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBBQVJlY3RFZmZlY3QKCQlmbG9hdDQgcHJldlJlY3QgPSBmbG9hdDQoLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwKTsKCQloYWxmIGFscGhhOwoJCUBzd2l0Y2ggKDEpIAoJCXsKCQkJY2FzZSAwOiAgICBjYXNlIDI6ICAgICAgICBhbHBoYSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TdGFnZTEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1N0YWdlMS54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJCQlicmVhazsKCQkJZGVmYXVsdDogICAgICAgIGhhbGYgeFN1YiwgeVN1YjsKCQkJeFN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC54IC0gdXJlY3RVbmlmb3JtX1N0YWdlMS54KSwgMC4wKTsKCQkJeFN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UxLnogLSBza19GcmFnQ29vcmQueCksIDAuMCk7CgkJCXlTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueSAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueSksIDAuMCk7CgkJCXlTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS53IC0gc2tfRnJhZ0Nvb3JkLnkpLCAwLjApOwoJCQlhbHBoYSA9ICgxLjAgKyBtYXgoeFN1YiwgLTEuMCkpICogKDEuMCArIG1heCh5U3ViLCAtMS4wKSk7CgkJfQoJCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgkJewoJCQlhbHBoYSA9IDEuMCAtIGFscGhhOwoJCX0KCQloYWxmNCBpbnB1dENvbG9yID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","CAZAAAECAYAAABAAAAAACAAAAAJQAAIA777777YPAAKAAABBAMABIAA2AAAAAAAAAAAAAAACAAAAAKAALAAA":"AgAAAExTS1P9AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoaW5Qb3NpdGlvbi54ICwgaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABLAgAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gaW50IHZUZXhJbmRleF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZjQgdGV4Q29sb3I7CgkJewoJCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHZUZXh0dXJlQ29vcmRzX1N0YWdlMCk7CgkJfQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IG91dHB1dENvbG9yX1N0YWdlMCAqIHRleENvbG9yOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZAAAACBAAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAArQIAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAECA4AAAAAAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAEwCAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZAAAACA4AAAAYAAAAAAAAAAAAQAAAACMAACAA4AAIQADYACQAAAAAAAAMAALQAAAAAAAAAAAAAAAQAAAACYACYAA":"AgAAAExTS1PjAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNEaW1lbnNpb25zSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSAodGV4SWR4KTsKCXZJbnRUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAXAwAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBmbG9hdDIgdkludFRleHR1cmVDb29yZHNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGlzdGFuY2VGaWVsZFBhdGgKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfU3RhZ2UwOwoJCWhhbGY0IHRleENvbG9yOwoJCXsKCQkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB1dikucnJycjsKCQl9CgkJaGFsZiBkaXN0YW5jZSA9IDcuOTY4NzUqKHRleENvbG9yLnIgLSAwLjUwMTk2MDc4NDMxKTsKCQloYWxmIGFmd2lkdGg7CgkJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeSh2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTAueSkpKTsKCQloYWxmIHZhbCA9IHNtb290aHN0ZXAoLWFmd2lkdGgsIGFmd2lkdGgsIGRpc3RhbmNlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh2YWwpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZAAAECA4AAAAIAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAADgAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJCWVkZ2VBbHBoYSAqPSBpbm5lckFscGhhOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","CAZAAAMCBEAAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAAAAAAAAAQAAAAEAACYAA":"AgAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAEBAAAAAAAAAQEA5QIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAECAUAAAAAAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAAAAAAAAABAAAAAEAAFQAA":"AgAAAExTS1NuAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TdGFnZTAgPSBpblF1YWRFZGdlOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAB+AwAAaW4gaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWhhbGYgZWRnZUFscGhhOwoJCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQlpZiAodlF1YWRFZGdlX1N0YWdlMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TdGFnZTAudyA+IDAuMCkgCgkJewoJCQllZGdlQWxwaGEgPSBtaW4obWluKHZRdWFkRWRnZV9TdGFnZTAueiwgdlF1YWRFZGdlX1N0YWdlMC53KSArIDAuNSwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWhhbGYyIGdGID0gaGFsZjIoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KTsKCQkJZWRnZUFscGhhID0gKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","CAZAAAMCBAAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAA2AAAAAAAAAAAAAAABAAAAAKAAC4AAIAAAAAAAAAAAAIAAAABYABMAA":"AgAAAExTS1NGAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAQEAAAAAAAABAQCCBQAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKS5ycnJyOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAAAAAIIDAAWAATYABEAAAAABAAAAAABBAMADYAB4AACQAAAABQAAAAABAAAAAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAfRQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TdGFnZTFfYzBfYzAueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC53KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAACBAAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAA4AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZACAACB4AAAAAAAAAGOAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAACAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAQAAAALQAEAAAEAAAAAAAAAAAAEAAAABWAAWAA":"AgAAAExTS1OvAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAYQcAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yCgkJaGFsZjQgY29uc3RDb2xvcjsKCQlAaWYgKGZhbHNlKSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgwKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCk7CgkJfQoJCW91dHB1dF9TdGFnZTEgPSBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGNvbnN0Q29sb3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAMCBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAGAAAAAYAAFYAAQAAAADZAAAAAAYAAAAEAAAGAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAHoGAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBhbHBoYTsKCUBzd2l0Y2ggKDMpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZiB4U3ViLCB5U3ViOwoJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueCksIDAuMCk7CgkJeFN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnogLSBza19GcmFnQ29vcmQueCksIDAuMCk7CgkJeVN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC55IC0gdXJlY3RVbmlmb3JtX1N0YWdlMV9jMC55KSwgMC4wKTsKCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQlhbHBoYSA9ICgxLjAgKyBtYXgoeFN1YiwgLTEuMCkpICogKDEuMCArIG1heCh5U3ViLCAtMS4wKSk7Cgl9CglAaWYgKDMgPT0gMiB8fCAzID09IDMpIAoJewoJCWFscGhhID0gMS4wIC0gYWxwaGE7Cgl9CgloYWxmNCBpbnB1dENvbG9yID0gX2lucHV0OwoJX291dHB1dCA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCkgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECA4AAAAIAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAOACAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQloYWxmIGRpc3RhbmNlVG9Jbm5lckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqIChkIC0gY2lyY2xlRWRnZS53KSk7CgkJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgkJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAACAYAABAAIAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1M3AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJdmluQ292ZXJhZ2VfU3RhZ2UwID0gaW5Db3ZlcmFnZTsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAArAEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCQloYWxmIGFscGhhID0gMS4wOwoJCWFscGhhID0gdmluQ292ZXJhZ2VfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZAAAACAUAABEAAAAABGAABAAOAAAYABQAEIAAAAAAAAAAAAAAAEAAAAAOAAWAA":"AgAAAExTS1MxAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkCgl2SGFpclF1YWRFZGdlX1N0YWdlMCA9IGluSGFpclF1YWRFZGdlOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABjAwAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwp1bmlmb3JtIGhhbGYgdUNvdmVyYWdlX1N0YWdlMDsKaW4gaGFsZjQgdkhhaXJRdWFkRWRnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh1Q292ZXJhZ2VfU3RhZ2UwICogZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADgAAAGluSGFpclF1YWRFZGdlAAABAAAAAAAAAA==","CAZAAAECA4AAAAAAAAABKAADAAKQAAYACUAAGAATAAAQAFIAAMABKAADAAOAAEIAEAADEAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MXBAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgcmFkaWk7CglyYWRpaS54ID0gZG90KHJhZGlpX3NlbGVjdG9yLCByYWRpaV94KTsKCXJhZGlpLnkgPSBkb3QocmFkaWlfc2VsZWN0b3IsIHJhZGlpX3kpOwoJYm9vbCBpc19hcmNfc2VjdGlvbiA9IChyYWRpaS54ID4gMCk7CglyYWRpaSA9IGFicyhyYWRpaSk7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpOwoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZTsKCWlmIChpc19hcmNfc2VjdGlvbikgCgl7CgkJdmFyY2Nvb3JkX1N0YWdlMC54eSA9IDEgLSBhYnMocmFkaXVzX291dHNldCk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqICh2YXJjY29vcmRfU3RhZ2UwLnh5L3JhZGlpICogY29ybmVyICogMik7Cgl9CgllbHNlIAoJewoJCXZhcmNjb29yZF9TdGFnZTAgPSBmbG9hdDQoMCk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAACgCAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0NCB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgkJaWYgKGZsb2F0MigwKSAhPSB2YXJjY29vcmRfU3RhZ2UwLnh5KSAKCQl7CgkJCWZsb2F0IGZuID0gZG90KHZhcmNjb29yZF9TdGFnZTAueHksIHZhcmNjb29yZF9TdGFnZTAueHkpIC0gMTsKCQkJaWYgKGZuID4gMCkgCgkJCXsKCQkJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDApOwoJCQl9CgkJfQoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAHAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAICBIAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEANwQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjdWxhclJSZWN0CgkJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCQlmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxLlJCOwoJCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTEueCAtIGxlbmd0aChkeHkpKSk7CgkJb3V0cHV0X1N0YWdlMSA9IG91dHB1dENvdmVyYWdlX1N0YWdlMCAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAMCBEAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAAAAAAAAAQAAAAEAACYAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAOoCAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZACAECBMAAAAIACAAAAAAAAAAAAAAACMAACAA4AAIQB777777RQADSAAAAAAAAEAACOAAEAAAAAAAAAAAAAAAAAAYAAGYAAQAAAAANAAAAAAAAAAAEAAACAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1PNAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHV2aWV3TWF0cml4X1N0YWdlMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVmVydGljZXNHUAoJaGFsZjQgY29sb3IgPSBpbkNvbG9yOwoJY29sb3IgPSBjb2xvci5iZ3JhOwoJY29sb3IgPSBjb2xvcjsKCWNvbG9yID0gaGFsZjQoY29sb3IucmdiICogY29sb3IuYSwgY29sb3IuYSk7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX3Bvc2l0aW9uID0gdXZpZXdNYXRyaXhfU3RhZ2UwLnh6ICogcG9zaXRpb24gKyB1dmlld01hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfcG9zaXRpb24ueCAsIF90bXBfMF9wb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAAhBAAAdW5pZm9ybSBoYWxmNCB1Y29sb3JfU3RhZ2UxX2MwOwppbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDb25zdENvbG9yUHJvY2Vzc29yX1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHVjb2xvcl9TdGFnZTFfYzA7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBCbHVycmVkRWRnZUZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiBpbnB1dEFscGhhID0gX2lucHV0Lnc7CgloYWxmIGZhY3RvciA9IDEuMCAtIGlucHV0QWxwaGE7CglAc3dpdGNoICgwKSAKCXsKCQljYXNlIDA6ICAgICAgICBmYWN0b3IgPSBleHAoKC1mYWN0b3IgKiBmYWN0b3IpICogNC4wKSAtIDAuMDE3OTk5OTk5MjI1MTM5NjE4OwoJCWJyZWFrOwoJCWNhc2UgMTogICAgICAgIGZhY3RvciA9IHNtb290aHN0ZXAoMS4wLCAwLjAsIGZhY3Rvcik7CgkJYnJlYWs7Cgl9Cglfb3V0cHV0ID0gaGFsZjQoZmFjdG9yKTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVmVydGljZXNHUAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKFNrTW9kZSBiZWhhdmlvcikKCQlvdXRwdXRfU3RhZ2UxID0gYmxlbmRfbW9kdWxhdGUoQ29uc3RDb2xvclByb2Nlc3Nvcl9TdGFnZTFfYzAoaGFsZjQoMSkpLCBCbHVycmVkRWRnZUZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMShvdXRwdXRDb2xvcl9TdGFnZTApKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24HAAAAaW5Db2xvcgABAAAAAAAAAA==","CAZAAAECAUAAAEYAAEABYAARAANQAAQAAAAAAAAMABEQAAAAAAAAAAAAAABAAAAAEAAFQAA":"AgAAAExTS1OFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFJSZWN0U2hhZG93Cgl2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAB1AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBoYWxmMyB2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUlJlY3RTaGFkb3cKCQloYWxmMyBzaGFkb3dQYXJhbXM7CgkJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CgkJZmxvYXQyIHV2ID0gZmxvYXQyKHNoYWRvd1BhcmFtcy56ICogKDEuMCAtIGQpLCAwLjUpOwoJCWhhbGYgZmFjdG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdXYpLnJycnIuYTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChmYWN0b3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAEAAAIIDAAWAATYABEAAAAAAAAAQAABBAMADYAB4AACQAAAABQAAAAAAAAAQAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAfRQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gc3Vic2V0Q29vcmQueTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAIIDAAWAATYABEAAAAAAAAAAAABBAMADYAB4AACQAAAABQAAAAAAAAAAAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAOxMAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgX2Nvb3Jkcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgR2F1c3NpYW5Db252b2x1dGlvbgoJCWZsb2F0MiBfY29vcmRzID0gdkxvY2FsQ29vcmRfU3RhZ2UwLnh5OwoJCW91dHB1dF9TdGFnZTEgPSBoYWxmNCgwLCAwLCAwLCAwKTsKCQlmbG9hdDIgY29vcmQgPSBfY29vcmRzIC0gMTIuMCAqIHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWZsb2F0MiBjb29yZFNhbXBsZWQgPSBoYWxmMigwLCAwKTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAICBIAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAQEAAAAAAAABAQDCAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAMCBMAAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAABAAAAA6IAAAAEAAAXAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQBvBwAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMTsKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCV9vdXRwdXQgPSBfaW5wdXQgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7CgkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZAAAACAYAAAEAYAAABGAABAAOAAEIA7777777777776FAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1NnAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBjb2xvciA9IGluQ29sb3I7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAVQEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgABAAAAAAAAAA==","CAZAAAICBIAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAoAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAECA4AAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABMAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAMCBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHWAAAAAYAABQAAQAAAADZAAAAA6YAAAAEAAAGAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBALoEAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7Cglfb3V0cHV0ID0gX2lucHV0ICogYWxwaGE7CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKSAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","CAZACAECBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAIIDAAWAATYABAAAAAAAAAAAAABBAMADYAB4AACAAAAAAAAAAAANAAAAAAAAAAAAAIIDABKAAAQAAQAAAAAAAAAAAAQAAAAGQACYAA":"AgAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAACIAwAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKENvbXBvc2UtT25lIGJlaGF2aW9yKQoJCW91dHB1dF9TdGFnZTEgPSBibGVuZF9tb2R1bGF0ZShNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpKSwgb3V0cHV0Q29sb3JfU3RhZ2UwKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAACAYAABEAJAAABGAABAAOAAEIA777777YZAAAAAFAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCWNvbG9yID0gY29sb3IgKiBpbkNvdmVyYWdlOwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAABVAQAAaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","CAZACAACB4AAAAAAAAAGOAIAAAJQAAIACIAAAAA4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAACAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAQAAAALQAEAAAEAAAAAAAAAAAAKAAAABWAAWAA":"AgAAAExTS1McAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0IHZjb3ZlcmFnZV9TdGFnZTA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBwb3NpdGlvbiA9IHBvc2l0aW9uLnh5OwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJdmNvdmVyYWdlX1N0YWdlMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBsb2NhbENvb3JkLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAvQcAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gKGhhbGY0KDEuMCkgLSBvdXRwdXRfU3RhZ2UxKSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAACBAAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAFgIAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAEAAAIIDAAWAATYABEAAAAABAAAQAABBAMADYAB4AACQAAAABQAAAAABAAAQAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAshQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gY2xhbXAoc3Vic2V0Q29vcmQueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC55LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgY2xhbXBlZENvb3JkKTsKCV9vdXRwdXQgPSB0ZXh0dXJlQ29sb3I7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgR2F1c3NpYW5Db252b2x1dGlvbgoJCWZsb2F0MiBfY29vcmRzID0gdkxvY2FsQ29vcmRfU3RhZ2UwLnh5OwoJCW91dHB1dF9TdGFnZTEgPSBoYWxmNCgwLCAwLCAwLCAwKTsKCQlmbG9hdDIgY29vcmQgPSBfY29vcmRzIC0gMTIuMCAqIHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWZsb2F0MiBjb29yZFNhbXBsZWQgPSBoYWxmMigwLCAwKTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZACAECBMAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAEAAAIIDAAWAATYABAAAAAABAAAQAABBAMADYAB4AACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1NdAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTE7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMSkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAACcEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1N0YWdlMV9jMC54LCB1Y2xhbXBfU3RhZ2UxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfU3RhZ2UxX2MwLnksIHVjbGFtcF9TdGFnZTFfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJX291dHB1dCA9IHRleHR1cmVDb2xvcjsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE1hdHJpeEVmZmVjdAoJCW91dHB1dF9TdGFnZTEgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAICBIAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAAAQAAAAGQAB6AAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAcQQAAHVuaWZvcm0gZmxvYXQ0IHVjaXJjbGVfU3RhZ2UxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJZmxvYXQyIHRleENvb3JkOwoJCXRleENvb3JkID0gdmxvY2FsQ29vcmRfU3RhZ2UwOwoJCW91dHB1dENvbG9yX1N0YWdlMCA9IChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB0ZXhDb29yZCkgKiBvdXRwdXRDb2xvcl9TdGFnZTApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjbGVFZmZlY3QKCQlmbG9hdDIgcHJldkNlbnRlcjsKCQlmbG9hdCBwcmV2UmFkaXVzID0gLTEuMDAwMDAwOwoJCWhhbGYgZDsKCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TdGFnZTEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TdGFnZTEudykgLSAxLjApICogdWNpcmNsZV9TdGFnZTEueik7CgkJfQoJCWVsc2UgCgkJewoJCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1N0YWdlMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1N0YWdlMS53KSkgKiB1Y2lyY2xlX1N0YWdlMS56KTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlAaWYgKDEgPT0gMSB8fCAxID09IDMpIAoJCXsKCQkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBjbGFtcChkLCAwLjAsIDEuMCk7CgkJfQoJCWVsc2UgCgkJewoJCQlvdXRwdXRfU3RhZ2UxID0gZCA+IDAuNSA/IGlucHV0Q29sb3IgOiBoYWxmNCgwLjApOwoJCX0KCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAACAYAAAAAIAAABGAABAD7777777777777777776FAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1PkAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAWgEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAEAAAAKAAAAaW5Qb3NpdGlvbgAAAQAAAAAAAAA=","CAZAAAECA4AAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAWwEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAICBIAAAAIAAAABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1M6CQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqIChhcmNjb29yZC9yYWRpaSAqIDIpOwoJfQoJc2tfUG9zaXRpb24gPSBmbG9hdDQoZGV2Y29vcmQueCAsIGRldmNvb3JkLnksIDAsIDEpOwp9CgAAAAEBAAAAAAAAAQEAdQQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZ3g9dmFyY2Nvb3JkX1N0YWdlMC56LCBneT12YXJjY29vcmRfU3RhZ2UwLnc7CgkJCWZsb2F0IGZud2lkdGggPSBhYnMoZ3gpICsgYWJzKGd5KTsKCQkJaGFsZiBkID0gaGFsZihmbi9mbndpZHRoKTsKCQkJY292ZXJhZ2UgPSBjbGFtcCguNSAtIGQsIDAsIDEpOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA=="}} \ No newline at end of file