diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 3b4fc72b1..555bbde2e 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.5' + flutter-version: '1.22.6' - name: Clone the repository. uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4bd20b5c..d35450f7f 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.5' + flutter-version: '1.22.6' # 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.5.sksl.json - flutter build appbundle --bundle-sksl-path shaders_1.22.5.sksl.json + flutter build apk --bundle-sksl-path shaders_1.22.6.sksl.json + flutter build appbundle --bundle-sksl-path shaders_1.22.6.sksl.json rm $AVES_STORE_FILE env: AVES_STORE_FILE: ${{ github.workspace }}/key.jks diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a089b539..44e021228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.3.3] - 2021-01-31 +### Added +- Viewer: support for multi-track HEIF +- Viewer: export image (including multipage TIFF/HEIF and images embedded in XMP) +- Info: show owner app (Android Q and up) +- listen to Media Store changes + +### Changed +- upgraded Flutter to stable v1.22.6 +- check connectivity before using features that need it + +### Fixed +- checkerboard background performance +- deleting files that no longer exist but are still registered in the Media Store +- insets handling on Android 11 + ## [v1.3.2] - 2021-01-17 ### Added Collection: identify multipage TIFF & multitrack HEIC/HEIF diff --git a/README.md b/README.md index 52ed6d473..34fbae115 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt ## Features -- support raster images: JPEG, GIF, PNG, HEIC (from Android Pie), WEBP, TIFF, BMP, WBMP, ICO +- support raster images: JPEG, GIF, PNG, HEIC/HEIF (including multi-track, from Android Pie), WEBP, TIFF (including multi-page), BMP, WBMP, ICO - support animated images: GIF, WEBP - support raw images: ARW, CR2, DNG, NEF, NRW, ORF, PEF, RAF, RW2, SRW - support vector images: SVG @@ -36,10 +36,10 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt | Model | Name | Android Version | API | | ----------- | -------------------------- | --------------- | ---:| -| SM-G970N | Samsung Galaxy S10e | 10 (Android10) | 29 | +| SM-G981N | Samsung Galaxy S20 5G | 11 | 30 | +| SM-G970N | Samsung Galaxy S10e | 10 (Q) | 29 | | SM-P580 | Samsung Galaxy Tab A 10.1 | 8.1.0 (Oreo) | 27 | | SM-G930S | Samsung Galaxy S7 | 8.0.0 (Oreo) | 26 | -| E5823 | Sony Xperia Z5 Compact | 7.1.1 (Nougat) | 25 | ## Project Setup diff --git a/android/app/build.gradle b/android/app/build.gradle index 7fd1f982e..e8cea44fa 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,15 +98,15 @@ repositories { dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' - implementation 'androidx.core:core-ktx:1.5.0-alpha05' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts + implementation 'androidx.core:core-ktx:1.5.0-beta01' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts implementation 'androidx.exifinterface:exifinterface:1.3.2' implementation 'com.commonsware.cwac:document:0.4.1' implementation 'com.drewnoakes:metadata-extractor:2.15.0' implementation 'com.github.deckerst:Android-TiffBitmapFactory:f87db4305d' // forked, built by JitPack - implementation 'com.github.bumptech.glide:glide:4.11.0' + implementation 'com.github.bumptech.glide:glide:4.12.0' kapt 'androidx.annotation:annotation:1.1.0' - kapt 'com.github.bumptech.glide:compiler:4.11.0' + kapt 'com.github.bumptech.glide:compiler:4.12.0' compileOnly rootProject.findProject(':streams_channel') } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1faac6cc8..66f30eaf6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -40,7 +40,6 @@ - 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 6ee05d8d1..a6742b22c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -16,24 +16,18 @@ import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.PermissionManager import io.flutter.embedding.android.FlutterActivity import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { - companion object { - private val LOG_TAG = LogUtils.createTag(MainActivity::class.java) - const val INTENT_CHANNEL = "deckers.thibault/aves/intent" - const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer" - } - - private val intentStreamHandler = IntentStreamHandler() + private lateinit var contentStreamHandler: ContentChangeStreamHandler + private lateinit var intentStreamHandler: IntentStreamHandler private lateinit var intentDataMap: MutableMap override fun onCreate(savedInstanceState: Bundle?) { Log.i(LOG_TAG, "onCreate intent=$intent") super.onCreate(savedInstanceState) - intentDataMap = extractIntentData(intent) - val messenger = flutterEngine!!.dartExecutor.binaryMessenger MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this)) @@ -48,59 +42,34 @@ class MainActivity : FlutterActivity() { StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) } StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory { args -> StorageAccessStreamHandler(this, args) } + // Media Store change monitoring + contentStreamHandler = ContentChangeStreamHandler(this).apply { + EventChannel(messenger, ContentChangeStreamHandler.CHANNEL).setStreamHandler(this) + } + + // intent handling + intentStreamHandler = IntentStreamHandler().apply { + EventChannel(messenger, IntentStreamHandler.CHANNEL).setStreamHandler(this) + } + intentDataMap = extractIntentData(intent) MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "getIntentData" -> { result.success(intentDataMap) intentDataMap.clear() } - "pick" -> { - val pickedUri = call.argument("uri") - if (pickedUri != null) { - val intent = Intent().apply { - data = Uri.parse(pickedUri) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - setResult(RESULT_OK, intent) - } else { - setResult(RESULT_CANCELED) - } - finish() - } - + "pick" -> pick(call) } } - EventChannel(messenger, INTENT_CHANNEL).setStreamHandler(intentStreamHandler) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { setupShortcuts() } } - @RequiresApi(Build.VERSION_CODES.N_MR1) - private fun setupShortcuts() { - // 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() - - 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() - - ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search)) + override fun onDestroy() { + contentStreamHandler.dispose() + super.onDestroy() } override fun onNewIntent(intent: Intent) { @@ -109,6 +78,25 @@ class MainActivity : FlutterActivity() { intentStreamHandler.notifyNewIntent(extractIntentData(intent)) } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == PermissionManager.VOLUME_ACCESS_REQUEST_CODE) { + val treeUri = data?.data + if (resultCode != RESULT_OK || treeUri == null) { + PermissionManager.onPermissionResult(requestCode, null) + return + } + + // save access permissions across reboots + val takeFlags = (data.flags + and (Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) + contentResolver.takePersistableUriPermission(treeUri, takeFlags) + + // resume pending action + PermissionManager.onPermissionResult(requestCode, treeUri) + } + } + private fun extractIntentData(intent: Intent?): MutableMap { when (intent?.action) { Intent.ACTION_MAIN -> { @@ -138,22 +126,48 @@ class MainActivity : FlutterActivity() { return HashMap() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == PermissionManager.VOLUME_ACCESS_REQUEST_CODE) { - val treeUri = data?.data - if (resultCode != RESULT_OK || treeUri == null) { - PermissionManager.onPermissionResult(requestCode, null) - return + private fun pick(call: MethodCall) { + val pickedUri = call.argument("uri") + if (pickedUri != null) { + val intent = Intent().apply { + data = Uri.parse(pickedUri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - - // save access permissions across reboots - val takeFlags = (data.flags - and (Intent.FLAG_GRANT_READ_URI_PERMISSION - or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) - contentResolver.takePersistableUriPermission(treeUri, takeFlags) - - // resume pending action - PermissionManager.onPermissionResult(requestCode, treeUri) + setResult(RESULT_OK, intent) + } else { + setResult(RESULT_CANCELED) } + finish() + } + + @RequiresApi(Build.VERSION_CODES.N_MR1) + private fun setupShortcuts() { + // 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() + + 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() + + ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search)) + } + + companion object { + private val LOG_TAG = LogUtils.createTag(MainActivity::class.java) + const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer" } } \ No newline at end of file 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 index 6930273bc..c4b8e3377 100644 --- 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 @@ -12,6 +12,8 @@ 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.channel.calls.Coresult.Companion.safe +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.LogUtils import io.flutter.plugin.common.MethodCall @@ -28,8 +30,8 @@ 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(Dispatchers.IO) { getAppIcon(call, Coresult(result)) } - "getAppNames" -> GlobalScope.launch(Dispatchers.IO) { getAppNames(Coresult(result)) } + "getPackages" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPackages) } + "getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAppIcon) } "edit" -> { val title = call.argument("title") val uri = call.argument("uri")?.let { Uri.parse(it) } @@ -61,46 +63,51 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { } } - 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) + private fun getPackages(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { + val packages = HashMap() - // 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) } + fun addPackageDetails(intent: Intent) { + // 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) + val pm = context.packageManager + for (resolveInfo in pm.queryIntentActivities(intent, 0)) { + val appInfo = resolveInfo.activityInfo.applicationInfo + val packageName = appInfo.packageName + if (!packages.containsKey(packageName)) { + val currentLabel = pm.getApplicationLabel(appInfo).toString() + val englishLabel: String? = appInfo.labelRes.takeIf { it != 0 }?.let { labelRes -> + var englishLabel: String? = null + try { + val resources = pm.getResourcesForApplication(appInfo) + // `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) + englishLabel = resources.getString(labelRes) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(LOG_TAG, "failed to get app label in English for packageName=$packageName", e) + } + englishLabel } + packages[packageName] = hashMapOf( + "packageName" to packageName, + "categoryLauncher" to intent.hasCategory(Intent.CATEGORY_LAUNCHER), + "isSystem" to (appInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0), + "currentLabel" to currentLabel, + "englishLabel" to englishLabel, + ) } } } - result.success(nameMap) + + addPackageDetails(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)) + addPackageDetails(Intent(Intent.ACTION_MAIN)) + result.success(ArrayList(packages.values)) } private fun getAppIcon(call: MethodCall, result: MethodChannel.Result) { 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 index d2db62561..5207bde7d 100644 --- 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 @@ -1,9 +1,11 @@ package deckers.thibault.aves.channel.calls +import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlin.reflect.KSuspendFunction2 // ensure `result` methods are called on the main looper thread class Coresult internal constructor(private val methodResult: MethodChannel.Result) : MethodChannel.Result { @@ -20,4 +22,24 @@ class Coresult internal constructor(private val methodResult: MethodChannel.Resu override fun notImplemented() { mainScope.launch { methodResult.notImplemented() } } + + companion object { + fun safe(call: MethodCall, result: MethodChannel.Result, function: (call: MethodCall, result: MethodChannel.Result) -> Unit) { + val res = Coresult(result) + try { + function(call, res) + } catch (e: Exception) { + res.error("safe-exception", e.message, e.stackTraceToString()) + } + } + + suspend fun safesus(call: MethodCall, result: MethodChannel.Result, function: KSuspendFunction2) { + val res = Coresult(result) + try { + function(call, res) + } catch (e: Exception) { + res.error("safe-exception", e.message, e.stackTraceToString()) + } + } + } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt index 22e4c84ca..6b4401073 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt @@ -12,10 +12,11 @@ import android.util.Log import androidx.exifinterface.media.ExifInterface import com.drew.imaging.ImageMetadataReader import com.drew.metadata.file.FileTypeDirectory +import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.metadata.ExifInterfaceHelper import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper import deckers.thibault.aves.metadata.Metadata -import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface @@ -37,12 +38,12 @@ class DebugHandler(private val context: Context) : MethodCallHandler { when (call.method) { "getContextDirs" -> result.success(getContextDirs()) "getEnv" -> result.success(System.getenv()) - "getBitmapFactoryInfo" -> GlobalScope.launch(Dispatchers.IO) { getBitmapFactoryInfo(call, Coresult(result)) } - "getContentResolverMetadata" -> GlobalScope.launch(Dispatchers.IO) { getContentResolverMetadata(call, Coresult(result)) } - "getExifInterfaceMetadata" -> GlobalScope.launch(Dispatchers.IO) { getExifInterfaceMetadata(call, Coresult(result)) } - "getMediaMetadataRetrieverMetadata" -> GlobalScope.launch(Dispatchers.IO) { getMediaMetadataRetrieverMetadata(call, Coresult(result)) } - "getMetadataExtractorSummary" -> GlobalScope.launch(Dispatchers.IO) { getMetadataExtractorSummary(call, Coresult(result)) } - "getTiffStructure" -> GlobalScope.launch(Dispatchers.IO) { getTiffStructure(call, Coresult(result)) } + "getBitmapFactoryInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getBitmapFactoryInfo) } + "getContentResolverMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverMetadata) } + "getExifInterfaceMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifInterfaceMetadata) } + "getMediaMetadataRetrieverMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMediaMetadataRetrieverMetadata) } + "getMetadataExtractorSummary" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMetadataExtractorSummary) } + "getTiffStructure" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getTiffStructure) } else -> result.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 index de0ee5245..a9c831565 100644 --- 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 @@ -5,8 +5,13 @@ import android.graphics.Rect import android.net.Uri import android.util.Size import com.bumptech.glide.Glide +import deckers.thibault.aves.channel.calls.Coresult.Companion.safe +import deckers.thibault.aves.channel.calls.Coresult.Companion.safesus +import deckers.thibault.aves.channel.calls.fetchers.RegionFetcher +import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher +import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher import deckers.thibault.aves.model.ExifOrientationOp -import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider import deckers.thibault.aves.model.provider.MediaStoreImageProvider @@ -26,17 +31,14 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getObsoleteEntries" -> GlobalScope.launch(Dispatchers.IO) { getObsoleteEntries(call, Coresult(result)) } - "getImageEntry" -> GlobalScope.launch(Dispatchers.IO) { getImageEntry(call, Coresult(result)) } - "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { getThumbnail(call, Coresult(result)) } - "getRegion" -> GlobalScope.launch(Dispatchers.IO) { getRegion(call, Coresult(result)) } - "clearSizedThumbnailDiskCache" -> { - GlobalScope.launch(Dispatchers.IO) { Glide.get(activity).clearDiskCache() } - result.success(null) - } - "rename" -> GlobalScope.launch(Dispatchers.IO) { rename(call, Coresult(result)) } - "rotate" -> GlobalScope.launch(Dispatchers.IO) { rotate(call, Coresult(result)) } - "flip" -> GlobalScope.launch(Dispatchers.IO) { flip(call, Coresult(result)) } + "getObsoleteEntries" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getObsoleteEntries) } + "getEntry" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getEntry) } + "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getThumbnail) } + "getRegion" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getRegion) } + "clearSizedThumbnailDiskCache" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::clearSizedThumbnailDiskCache) } + "rename" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::rename) } + "rotate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::rotate) } + "flip" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::flip) } else -> result.notImplemented() } } @@ -58,7 +60,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { val isFlipped = call.argument("isFlipped") val widthDip = call.argument("widthDip") val heightDip = call.argument("heightDip") - val page = call.argument("page") + val pageId = call.argument("pageId") val defaultSizeDip = call.argument("defaultSizeDip") if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) { @@ -76,7 +78,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { isFlipped, width = (widthDip * density).roundToInt(), height = (heightDip * density).roundToInt(), - page = page, + pageId = pageId, defaultSize = (defaultSizeDip * density).roundToInt(), result, ).fetch() @@ -85,7 +87,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { private fun getRegion(call: MethodCall, result: MethodChannel.Result) { val uri = call.argument("uri")?.let { Uri.parse(it) } val mimeType = call.argument("mimeType") - val page = call.argument("page") + val pageId = call.argument("pageId") val sampleSize = call.argument("sampleSize") val x = call.argument("regionX") val y = call.argument("regionY") @@ -102,43 +104,49 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { val regionRect = Rect(x, y, x + width, y + height) when (mimeType) { MimeTypes.TIFF -> TiffRegionFetcher(activity).fetch( - uri, - sampleSize, - regionRect, - page = page ?: 0, - result, + uri = uri, + page = pageId ?: 0, + sampleSize = sampleSize, + regionRect = regionRect, + result = result, ) else -> regionFetcher.fetch( - uri, - mimeType, - sampleSize, - regionRect, - Size(imageWidth, imageHeight), - result, + uri = uri, + mimeType = mimeType, + pageId = pageId, + sampleSize = sampleSize, + regionRect = regionRect, + imageSize = Size(imageWidth, imageHeight), + result = result, ) } } - private suspend fun getImageEntry(call: MethodCall, result: MethodChannel.Result) { + private suspend fun getEntry(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) + result.error("getEntry-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) + result.error("getEntry-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) + override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri", throwable.message) }) } + private fun clearSizedThumbnailDiskCache(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { + Glide.get(activity).clearDiskCache() + result.success(null) + } + private suspend fun rename(call: MethodCall, result: MethodChannel.Result) { val entryMap = call.argument("entry") val newName = call.argument("newName") 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 f0a95b2d6..940ab0cba 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 @@ -1,8 +1,15 @@ package deckers.thibault.aves.channel.calls +import android.content.ContentResolver +import android.content.ContentUris import android.content.Context +import android.database.Cursor +import android.media.MediaExtractor +import android.media.MediaFormat import android.media.MediaMetadataRetriever import android.net.Uri +import android.os.Build +import android.provider.MediaStore import android.util.Log import androidx.exifinterface.media.ExifInterface import com.adobe.internal.xmp.XMPException @@ -18,6 +25,7 @@ import com.drew.metadata.iptc.IptcDirectory import com.drew.metadata.mp4.media.Mp4UuidBoxDirectory import com.drew.metadata.webp.WebpDirectory import com.drew.metadata.xmp.XmpDirectory +import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.metadata.* import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis @@ -38,13 +46,14 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.isGeoTiff import deckers.thibault.aves.metadata.XMP.getSafeDateMillis import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText import deckers.thibault.aves.metadata.XMP.isPanorama -import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.provider.FileImageProvider import deckers.thibault.aves.model.provider.ImageProvider import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.MimeTypes.isHeifLike import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes.isMultimedia import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface @@ -66,14 +75,15 @@ import kotlin.math.roundToLong class MetadataHandler(private val context: Context) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getAllMetadata" -> GlobalScope.launch(Dispatchers.IO) { getAllMetadata(call, Coresult(result)) } - "getCatalogMetadata" -> GlobalScope.launch(Dispatchers.IO) { getCatalogMetadata(call, Coresult(result)) } - "getOverlayMetadata" -> GlobalScope.launch(Dispatchers.IO) { getOverlayMetadata(call, Coresult(result)) } - "getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { getMultiPageInfo(call, Coresult(result)) } - "getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { getPanoramaInfo(call, Coresult(result)) } - "getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { getEmbeddedPictures(call, Coresult(result)) } - "getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { getExifThumbnails(call, Coresult(result)) } - "extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { extractXmpDataProp(call, Coresult(result)) } + "getAllMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAllMetadata) } + "getCatalogMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getCatalogMetadata) } + "getOverlayMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getOverlayMetadata) } + "getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMultiPageInfo) } + "getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPanoramaInfo) } + "getContentResolverProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverProp) } + "getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEmbeddedPictures) } + "getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifThumbnails) } + "extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) } else -> result.notImplemented() } } @@ -430,7 +440,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { } } - if (mimeType == MimeTypes.HEIC || mimeType == MimeTypes.HEIF) { + if (isHeifLike(mimeType)) { retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS) { if (it > 1) flags = flags or MASK_IS_MULTIPAGE } @@ -521,21 +531,57 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { return } - val pages = HashMap() + val pages = ArrayList>() if (mimeType == MimeTypes.TIFF) { - fun toMap(options: TiffBitmapFactory.Options): Map { + fun toMap(page: Int, options: TiffBitmapFactory.Options): HashMap { return hashMapOf( - "width" to options.outWidth, - "height" to options.outHeight, + KEY_PAGE to page, + KEY_MIME_TYPE to mimeType, + KEY_WIDTH to options.outWidth, + KEY_HEIGHT to options.outHeight, ) } getTiffPageInfo(uri, 0)?.let { first -> - pages[0] = toMap(first) + pages.add(toMap(0, first)) val pageCount = first.outDirectoryCount for (i in 1 until pageCount) { - getTiffPageInfo(uri, i)?.let { pages[i] = toMap(it) } + getTiffPageInfo(uri, i)?.let { pages.add(toMap(i, it)) } } } + } else if (isHeifLike(mimeType)) { + fun MediaFormat.getSafeInt(key: String, save: (value: Int) -> Unit) { + if (this.containsKey(key)) save(this.getInteger(key)) + } + + fun MediaFormat.getSafeLong(key: String, save: (value: Long) -> Unit) { + if (this.containsKey(key)) save(this.getLong(key)) + } + + val extractor = MediaExtractor() + extractor.setDataSource(context, uri, null) + for (i in 0 until extractor.trackCount) { + try { + val format = extractor.getTrackFormat(i) + format.getString(MediaFormat.KEY_MIME)?.let { mime -> + val trackMime = if (mime == MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC) MimeTypes.HEIC else mime + val page = hashMapOf( + KEY_PAGE to i, + KEY_MIME_TYPE to trackMime, + ) + format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { page[KEY_IS_DEFAULT] = it != 0 } + format.getSafeInt(MediaFormat.KEY_TRACK_ID) { page[KEY_TRACK_ID] = it } + format.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it } + format.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it } + if (isVideo(trackMime)) { + format.getSafeLong(MediaFormat.KEY_DURATION) { page[KEY_DURATION] = it / 1000 } + } + pages.add(page) + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get track information for uri=$uri, track num=$i", e) + } + } + extractor.release() } result.success(pages) } @@ -555,14 +601,16 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { val metadata = ImageMetadataReader.readMetadata(input) val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java) try { - fun getProp(propName: String): Int? = xmpDirs.map { it.xmpMeta.getPropertyInteger(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null } + fun getIntProp(propName: String): Int? = xmpDirs.map { it.xmpMeta.getPropertyInteger(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null } + fun getStringProp(propName: String): String? = xmpDirs.map { it.xmpMeta.getPropertyString(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null } val fields: FieldMap = hashMapOf( - "croppedAreaLeft" to getProp(XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME), - "croppedAreaTop" to getProp(XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME), - "croppedAreaWidth" to getProp(XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME), - "croppedAreaHeight" to getProp(XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME), - "fullPanoWidth" to getProp(XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME), - "fullPanoHeight" to getProp(XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME), + "croppedAreaLeft" to getIntProp(XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME), + "croppedAreaTop" to getIntProp(XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME), + "croppedAreaWidth" to getIntProp(XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME), + "croppedAreaHeight" to getIntProp(XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME), + "fullPanoWidth" to getIntProp(XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME), + "fullPanoHeight" to getIntProp(XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME), + "projectionType" to (getStringProp(XMP.GPANO_PROJECTION_TYPE_PROP_NAME) ?: XMP.GPANO_PROJECTION_TYPE_DEFAULT), ) result.success(fields) return @@ -580,6 +628,55 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { result.error("getPanoramaInfo-empty", "failed to read XMP from uri=$uri", null) } + private fun getContentResolverProp(call: MethodCall, result: MethodChannel.Result) { + val mimeType = call.argument("mimeType") + val uri = call.argument("uri")?.let { Uri.parse(it) } + val prop = call.argument("prop") + if (mimeType == null || uri == null || prop == null) { + result.error("getContentResolverProp-args", "failed because of missing arguments", null) + return + } + + var contentUri: Uri = uri + if (uri.scheme == ContentResolver.SCHEME_CONTENT && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)) { + try { + val id = ContentUris.parseId(uri) + contentUri = when { + isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) + isVideo(mimeType) -> ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id) + else -> uri + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + contentUri = MediaStore.setRequireOriginal(contentUri) + } + } catch (e: NumberFormatException) { + // ignore + } + } + + val projection = arrayOf(prop) + val cursor = context.contentResolver.query(contentUri, projection, null, null, null) + if (cursor != null && cursor.moveToFirst()) { + var value: Any? = null + try { + value = when (cursor.getType(0)) { + Cursor.FIELD_TYPE_NULL -> null + Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0) + Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0) + Cursor.FIELD_TYPE_STRING -> cursor.getString(0) + Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0) + else -> null + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get value for key=$prop", e) + } + cursor.close() + result.success(value?.toString()) + } else { + result.error("getContentResolverProp-null", "failed to get cursor for contentUri=$contentUri", null) + } + } + private fun getEmbeddedPictures(call: MethodCall, result: MethodChannel.Result) { val uri = call.argument("uri")?.let { Uri.parse(it) } if (uri == null) { @@ -619,7 +716,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) exif.thumbnailBitmap?.let { bitmap -> TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let { - it.getBytes(canHaveAlpha = false, recycle = false)?.let { thumbnails.add(it) } + it.getBytes(canHaveAlpha = false, recycle = false)?.let { bytes -> thumbnails.add(bytes) } } } } @@ -733,7 +830,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { private val LOG_TAG = LogUtils.createTag(MetadataHandler::class.java) const val CHANNEL = "deckers.thibault/aves/metadata" - // catalog metadata + // catalog metadata & page info private const val KEY_MIME_TYPE = "mimeType" private const val KEY_DATE_MILLIS = "dateMillis" private const val KEY_FLAGS = "flags" @@ -742,6 +839,12 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { private const val KEY_LONGITUDE = "longitude" private const val KEY_XMP_SUBJECTS = "xmpSubjects" private const val KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription" + private const val KEY_HEIGHT = "height" + private const val KEY_WIDTH = "width" + private const val KEY_PAGE = "page" + private const val KEY_TRACK_ID = "trackId" + private const val KEY_IS_DEFAULT = "isDefault" + private const val KEY_DURATION = "durationMillis" private const val MASK_IS_ANIMATED = 1 shl 0 private const val MASK_IS_FLIPPED = 1 shl 1 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 index 6ff8babbb..248db4ff6 100644 --- 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 @@ -5,7 +5,7 @@ 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.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.utils.PermissionManager import deckers.thibault.aves.utils.StorageUtils.getVolumePaths import io.flutter.plugin.common.MethodCall @@ -20,27 +20,18 @@ 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 getFreeSpace(call, result) - "getGrantedDirectories" -> result.success(ArrayList(PermissionManager.getGrantedDirs(context))) - "getInaccessibleDirectories" -> getInaccessibleDirectories(call, result) - "revokeDirectoryAccess" -> revokeDirectoryAccess(call, result) - "scanFile" -> GlobalScope.launch(Dispatchers.IO) { scanFile(call, Coresult(result)) } + "getStorageVolumes" -> safe(call, result, ::getStorageVolumes) + "getFreeSpace" -> safe(call, result, ::getFreeSpace) + "getGrantedDirectories" -> safe(call, result, ::getGrantedDirectories) + "getInaccessibleDirectories" -> safe(call, result, ::getInaccessibleDirectories) + "revokeDirectoryAccess" -> safe(call, result, ::revokeDirectoryAccess) + "scanFile" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::scanFile) } else -> result.notImplemented() } } - private val storageVolumes: List> - @RequiresApi(api = Build.VERSION_CODES.N) - get() { + private fun getStorageVolumes(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { + val volumes: List> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val volumes = ArrayList>() val sm = context.getSystemService(StorageManager::class.java) if (sm != null) { @@ -61,8 +52,13 @@ class StorageHandler(private val context: Context) : MethodCallHandler { } } } - return volumes + volumes + } else { + // TODO TLAD find alternative for Android ("path") @@ -93,6 +89,10 @@ class StorageHandler(private val context: Context) : MethodCallHandler { } } + private fun getGrantedDirectories(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { + result.success(ArrayList(PermissionManager.getGrantedDirs(context))) + } + private fun getInaccessibleDirectories(call: MethodCall, result: MethodChannel.Result) { val dirPaths = call.argument>("dirPaths") if (dirPaths == null) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/RegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt similarity index 62% rename from android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/RegionFetcher.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt index 7b9689a7e..9b168e565 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/RegionFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt @@ -1,15 +1,22 @@ -package deckers.thibault.aves.channel.calls +package deckers.thibault.aves.channel.calls.fetchers import android.content.Context +import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.BitmapRegionDecoder import android.graphics.Rect import android.net.Uri import android.util.Size +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.MultiTrackImage import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils import io.flutter.plugin.common.MethodChannel +import java.io.File import kotlin.math.roundToInt class RegionFetcher internal constructor( @@ -17,21 +24,42 @@ class RegionFetcher internal constructor( ) { private var lastDecoderRef: LastDecoderRef? = null + private val pageTempUris = HashMap, Uri>() + + private val multiTrackGlideOptions = RequestOptions() + .format(DecodeFormat.PREFER_ARGB_8888) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + fun fetch( uri: Uri, mimeType: String, + pageId: Int?, sampleSize: Int, regionRect: Rect, imageSize: Size, result: MethodChannel.Result, ) { + if (MimeTypes.isHeifLike(mimeType) && pageId != null) { + val id = Pair(uri, pageId) + fetch( + uri = pageTempUris.getOrPut(id) { createJpegForPage(uri, pageId) }, + mimeType = MimeTypes.JPEG, + pageId = null, + sampleSize = sampleSize, + regionRect = regionRect, + imageSize = imageSize, + result = result, + ) + return + } + val options = BitmapFactory.Options().apply { inSampleSize = sampleSize } var currentDecoderRef = lastDecoderRef if (currentDecoderRef != null && currentDecoderRef.uri != uri) { - currentDecoderRef.decoder.recycle() currentDecoderRef = null } @@ -74,6 +102,26 @@ class RegionFetcher internal constructor( result.error("getRegion-read-exception", "failed to initialize region decoder for uri=$uri regionRect=$regionRect", e.message) } } + + private fun createJpegForPage(sourceUri: Uri, pageId: Int): Uri { + val target = Glide.with(context) + .asBitmap() + .apply(multiTrackGlideOptions) + .load(MultiTrackImage(context, sourceUri, pageId)) + .submit() + try { + val bitmap = target.get() + val tempFile = File.createTempFile("aves", null, context.cacheDir).apply { + deleteOnExit() + outputStream().use { outputStream -> + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + } + } + return Uri.fromFile(tempFile) + } finally { + Glide.with(context).clear(target) + } + } } private data class LastDecoderRef( 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/fetchers/ThumbnailFetcher.kt similarity index 87% rename from android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt index 3728fa209..e631af9eb 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channel.calls +package deckers.thibault.aves.channel.calls.fetchers import android.content.ContentUris import android.content.Context @@ -13,11 +13,13 @@ 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.TiffThumbnail +import deckers.thibault.aves.decoder.MultiTrackImage +import deckers.thibault.aves.decoder.TiffImage import deckers.thibault.aves.decoder.VideoThumbnail import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.MimeTypes.isHeifLike import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide @@ -32,14 +34,16 @@ class ThumbnailFetcher internal constructor( private val isFlipped: Boolean, width: Int?, height: Int?, - page: Int?, + private val pageId: Int?, private val defaultSize: Int, private val result: MethodChannel.Result, ) { private val uri: Uri = Uri.parse(uri) private val width: Int = if (width?.takeIf { it > 0 } != null) width else defaultSize private val height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize - private val page = page ?: 0 + private val tiffFetch = mimeType == MimeTypes.TIFF + private val multiTrackFetch = isHeifLike(mimeType) && pageId != null + private val customFetch = tiffFetch || multiTrackFetch fun fetch() { var bitmap: Bitmap? = null @@ -47,7 +51,7 @@ class ThumbnailFetcher internal constructor( var exception: Exception? = null try { - if (mimeType != MimeTypes.TIFF && (width == defaultSize || height == defaultSize) && !isFlipped) { + if (!customFetch && (width == defaultSize || height == defaultSize) && !isFlipped) { // Fetch low quality thumbnails when size is not specified. // As of Android R, the Media Store content resolver may return a thumbnail // that is automatically rotated according to EXIF orientation, but not flipped, @@ -110,7 +114,7 @@ class ThumbnailFetcher internal constructor( // 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-$page")) + .signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$pageId")) .override(width, height) val target = if (isVideo(mimeType)) { @@ -121,7 +125,11 @@ class ThumbnailFetcher internal constructor( .load(VideoThumbnail(context, uri)) .submit(width, height) } else { - val model: Any = if (mimeType == MimeTypes.TIFF) TiffThumbnail(context, uri, page) else uri + val model: Any = when { + tiffFetch -> TiffImage(context, uri, pageId) + multiTrackFetch -> MultiTrackImage(context, uri, pageId) + else -> uri + } Glide.with(context) .asBitmap() .apply(options) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TiffRegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/TiffRegionFetcher.kt similarity index 95% rename from android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TiffRegionFetcher.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/TiffRegionFetcher.kt index 2d17f62d6..502553422 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TiffRegionFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/TiffRegionFetcher.kt @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channel.calls +package deckers.thibault.aves.channel.calls.fetchers import android.content.Context import android.graphics.Rect @@ -13,9 +13,9 @@ class TiffRegionFetcher internal constructor( ) { fun fetch( uri: Uri, + page: Int, sampleSize: Int, regionRect: Rect, - page: Int = 0, result: MethodChannel.Result, ) { try { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ContentChangeStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ContentChangeStreamHandler.kt new file mode 100644 index 000000000..8142c1bb9 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ContentChangeStreamHandler.kt @@ -0,0 +1,61 @@ +package deckers.thibault.aves.channel.streams + +import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.MediaStore +import android.util.Log +import deckers.thibault.aves.utils.LogUtils +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink + +class ContentChangeStreamHandler(private val context: Context) : EventChannel.StreamHandler { + private val contentObserver = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + this.onChange(selfChange, null) + } + + override fun onChange(selfChange: Boolean, uri: Uri?) { + // warning: querying the content resolver right after a change + // sometimes yields obsolete results + success(uri?.toString()) + } + } + private lateinit var eventSink: EventSink + private lateinit var handler: Handler + + init { + context.contentResolver.apply { + registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver) + registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, contentObserver) + } + } + + override fun onListen(arguments: Any?, eventSink: EventSink) { + this.eventSink = eventSink + handler = Handler(Looper.getMainLooper()) + } + + override fun onCancel(arguments: Any?) {} + + fun dispose() { + context.contentResolver.unregisterContentObserver(contentObserver) + } + + private fun success(uri: String?) { + handler.post { + try { + eventSink.success(uri) + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to use event sink", e) + } + } + } + + companion object { + private val LOG_TAG = LogUtils.createTag(ContentChangeStreamHandler::class.java) + const val CHANNEL = "deckers.thibault/aves/contentchange" + } +} \ 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 index 04087ca04..b0e464483 100644 --- 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 @@ -9,11 +9,14 @@ 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.MultiTrackImage +import deckers.thibault.aves.decoder.TiffImage import deckers.thibault.aves.decoder.VideoThumbnail import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.MimeTypes.isHeifLike import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide @@ -23,7 +26,6 @@ import io.flutter.plugin.common.EventChannel.EventSink import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import org.beyka.tiffbitmapfactory.TiffBitmapFactory import java.io.IOException import java.io.InputStream @@ -84,7 +86,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen val uri = (arguments["uri"] as String?)?.let { Uri.parse(it) } val rotationDegrees = arguments["rotationDegrees"] as Int val isFlipped = arguments["isFlipped"] as Boolean - val page = arguments["page"] as Int + val pageId = arguments["pageId"] as Int? if (mimeType == null || uri == null) { error("streamImage-args", "failed because of missing arguments", null) @@ -94,11 +96,9 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen if (isVideo(mimeType)) { streamVideoByGlide(uri) - } else if (mimeType == MimeTypes.TIFF) { - streamTiffImage(uri, page) } 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) + streamImageByGlide(uri, pageId, mimeType, rotationDegrees, isFlipped) } else { // to be decoded by Flutter streamImageAsIs(uri) @@ -114,11 +114,19 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen } } - private fun streamImageByGlide(uri: Uri, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) { + private fun streamImageByGlide(uri: Uri, pageId: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) { + val model: Any = if (isHeifLike(mimeType) && pageId != null) { + MultiTrackImage(activity, uri, pageId) + } else if (mimeType == MimeTypes.TIFF) { + TiffImage(activity, uri, pageId) + } else { + uri + } + val target = Glide.with(activity) .asBitmap() .apply(glideOptions) - .load(uri) + .load(model) .submit() try { var bitmap = target.get() @@ -157,28 +165,6 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen } } - private fun streamTiffImage(uri: Uri, page: Int = 0) { - val resolver = activity.contentResolver - try { - val fd = resolver.openFileDescriptor(uri, "r")?.detachFd() - if (fd == null) { - error("streamImage-tiff-fd", "failed to get file descriptor for uri=$uri", null) - return - } - val options = TiffBitmapFactory.Options().apply { - inDirectoryNumber = page - } - val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options) - if (bitmap != null) { - success(bitmap.getBytes(canHaveAlpha = true, recycle = true)) - } else { - error("streamImage-tiff-null", "failed to get tiff image (dir=$page) from uri=$uri", null) - } - } catch (e: Exception) { - error("streamImage-tiff-exception", "failed to get image from uri=$uri", toErrorDetails(e)) - } - } - private fun toErrorDetails(e: Exception): String? { val errorDetails = e.message return if (errorDetails?.isNotEmpty() == true) { 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 index 503fad11e..86e8d0650 100644 --- 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 @@ -5,8 +5,8 @@ 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.AvesEntry +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider import deckers.thibault.aves.utils.LogUtils @@ -42,6 +42,7 @@ class ImageOpStreamHandler(private val context: Context, private val arguments: when (op) { "delete" -> GlobalScope.launch(Dispatchers.IO) { delete() } + "export" -> GlobalScope.launch(Dispatchers.IO) { export() } "move" -> GlobalScope.launch(Dispatchers.IO) { move() } else -> endOfStream() } @@ -80,36 +81,6 @@ class ImageOpStreamHandler(private val context: Context, private val arguments: } } - private suspend 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 suspend fun delete() { if (entryMapList.isEmpty()) { endOfStream() @@ -144,6 +115,66 @@ class ImageOpStreamHandler(private val context: Context, private val arguments: endOfStream() } + private suspend fun export() { + if (arguments !is Map<*, *> || entryMapList.isEmpty()) { + endOfStream() + return + } + + var destinationDir = arguments["destinationPath"] as String? + val mimeType = arguments["mimeType"] as String? + if (destinationDir == null || mimeType == null) { + error("export-args", "failed because of missing arguments", null) + 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("export-provider", "failed to find provider for entry=$firstEntry", null) + return + } + + destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir) + val entries = entryMapList.map(::AvesEntry) + provider.exportMultiple(context, mimeType, destinationDir, entries, object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = success(fields) + override fun onFailure(throwable: Throwable) = error("export-failure", "failed to export entries", throwable) + }) + endOfStream() + } + + private suspend fun move() { + if (arguments !is Map<*, *> || entryMapList.isEmpty()) { + endOfStream() + 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 + } + + // 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 + } + + destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir) + val entries = entryMapList.map(::AvesEntry) + 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() + } + companion object { private val LOG_TAG = LogUtils.createTag(ImageOpStreamHandler::class.java) const val CHANNEL = "deckers.thibault/aves/imageopstream" 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 abd594c58..c5861f208 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 @@ -18,4 +18,8 @@ class IntentStreamHandler : EventChannel.StreamHandler { fun notifyNewIntent(intentData: MutableMap?) { eventSink?.success(intentData) } + + companion object { + const val CHANNEL = "deckers.thibault/aves/intent" + } } \ 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 f2892cfb2..ac9fec726 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 @@ -4,7 +4,7 @@ import android.content.Context import android.os.Handler import android.os.Looper import android.util.Log -import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.provider.MediaStoreImageProvider import deckers.thibault.aves.utils.LogUtils import io.flutter.plugin.common.EventChannel diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt new file mode 100644 index 000000000..290b7badd --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt @@ -0,0 +1,73 @@ +package deckers.thibault.aves.decoder + +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +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.metadata.MultiTrackMedia + + +@GlideModule +class MultiTrackImageGlideModule : LibraryGlideModule() { + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + registry.append(MultiTrackImage::class.java, Bitmap::class.java, MultiTrackThumbnailLoader.Factory()) + } +} + +class MultiTrackImage(val context: Context, val uri: Uri, val trackId: Int?) + +internal class MultiTrackThumbnailLoader : ModelLoader { + override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData { + return ModelLoader.LoadData(ObjectKey(model.uri), MultiTrackImageFetcher(model, width, height)) + } + + override fun handles(model: MultiTrackImage): Boolean = true + + internal class Factory : ModelLoaderFactory { + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = MultiTrackThumbnailLoader() + + override fun teardown() {} + } +} + +internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int, val height: Int) : DataFetcher { + override fun loadData(priority: Priority, callback: DataCallback) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + callback.onLoadFailed(Exception("unsupported Android version")) + return + } + + val context = model.context + val uri = model.uri + val trackId = model.trackId + + val bitmap = MultiTrackMedia.getImage(context, uri, trackId) + if (bitmap == null) { + callback.onLoadFailed(Exception("null bitmap")) + } else { + callback.onDataReady(bitmap) + } + } + + override fun cleanup() {} + + // cannot cancel + override fun cancel() {} + + override fun getDataClass(): Class = Bitmap::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/decoder/TiffGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffGlideModule.kt new file mode 100644 index 000000000..074f06332 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffGlideModule.kt @@ -0,0 +1,99 @@ +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 org.beyka.tiffbitmapfactory.TiffBitmapFactory + +@GlideModule +class TiffGlideModule : LibraryGlideModule() { + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + registry.append(TiffImage::class.java, Bitmap::class.java, TiffLoader.Factory()) + } +} + +class TiffImage(val context: Context, val uri: Uri, val page: Int?) + +internal class TiffLoader : ModelLoader { + override fun buildLoadData(model: TiffImage, width: Int, height: Int, options: Options): ModelLoader.LoadData { + return ModelLoader.LoadData(ObjectKey(model.uri), TiffFetcher(model, width, height)) + } + + override fun handles(model: TiffImage): Boolean = true + + internal class Factory : ModelLoaderFactory { + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = TiffLoader() + + override fun teardown() {} + } +} + +internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int) : DataFetcher { + override fun loadData(priority: Priority, callback: DataCallback) { + val context = model.context + val uri = model.uri + val page = model.page ?: 0 + + var sampleSize = 1 + if (width > 0 && height > 0) { + // determine sample size + val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() + if (fd == null) { + callback.onLoadFailed(Exception("null file descriptor")) + return + } + val options = TiffBitmapFactory.Options().apply { + inJustDecodeBounds = true + inDirectoryNumber = page + } + TiffBitmapFactory.decodeFileDescriptor(fd, options) + val imageWidth = options.outWidth + val imageHeight = options.outHeight + if (imageHeight > height || imageWidth > width) { + while (imageHeight / (sampleSize * 2) > height && imageWidth / (sampleSize * 2) > width) { + sampleSize *= 2 + } + } + } + + // decode + val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() + if (fd == null) { + callback.onLoadFailed(Exception("null file descriptor")) + return + } + val options = TiffBitmapFactory.Options().apply { + inJustDecodeBounds = false + inDirectoryNumber = page + inSampleSize = sampleSize + } + val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options) + if (bitmap == null) { + callback.onLoadFailed(Exception("null bitmap")) + } else { + callback.onDataReady(bitmap) + } + } + + override fun cleanup() {} + + // cannot cancel + override fun cancel() {} + + override fun getDataClass(): Class = Bitmap::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/decoder/TiffThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffThumbnailGlideModule.kt deleted file mode 100644 index 30c3627c2..000000000 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffThumbnailGlideModule.kt +++ /dev/null @@ -1,99 +0,0 @@ -package deckers.thibault.aves.decoder - -import android.content.Context -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.BitmapUtils.getBytes -import org.beyka.tiffbitmapfactory.TiffBitmapFactory -import java.io.InputStream - -@GlideModule -class TiffThumbnailGlideModule : LibraryGlideModule() { - override fun registerComponents(context: Context, glide: Glide, registry: Registry) { - registry.append(TiffThumbnail::class.java, InputStream::class.java, TiffThumbnailLoader.Factory()) - } -} - -class TiffThumbnail(val context: Context, val uri: Uri, val page: Int) - -internal class TiffThumbnailLoader : ModelLoader { - override fun buildLoadData(model: TiffThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData { - return ModelLoader.LoadData(ObjectKey(model.uri), TiffThumbnailFetcher(model, width, height)) - } - - override fun handles(tiffThumbnail: TiffThumbnail): Boolean = true - - internal class Factory : ModelLoaderFactory { - override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = TiffThumbnailLoader() - - override fun teardown() {} - } -} - -internal class TiffThumbnailFetcher(val model: TiffThumbnail, val width: Int, val height: Int) : DataFetcher { - override fun loadData(priority: Priority, callback: DataCallback) { - val context = model.context - val uri = model.uri - val page = model.page - - // determine sample size - var fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() - if (fd == null) { - callback.onLoadFailed(Exception("null file descriptor")) - return - } - var sampleSize = 1 - var options = TiffBitmapFactory.Options().apply { - inJustDecodeBounds = true - inDirectoryNumber = page - } - TiffBitmapFactory.decodeFileDescriptor(fd, options) - val imageWidth = options.outWidth - val imageHeight = options.outHeight - if (imageHeight > height || imageWidth > width) { - while (imageHeight / (sampleSize * 2) > height && imageWidth / (sampleSize * 2) > width) { - sampleSize *= 2 - } - } - - // decode - fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() - if (fd == null) { - callback.onLoadFailed(Exception("null file descriptor")) - return - } - options = TiffBitmapFactory.Options().apply { - inJustDecodeBounds = false - inDirectoryNumber = page - inSampleSize = sampleSize - } - val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options) - if (bitmap == null) { - callback.onLoadFailed(Exception("null bitmap")) - } else { - callback.onDataReady(bitmap.getBytes()?.inputStream()) - } - } - - // 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/decoder/VideoThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt index a6045c773..278e55cc6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt @@ -1,7 +1,9 @@ package deckers.thibault.aves.decoder import android.content.Context +import android.media.MediaMetadataRetriever import android.net.Uri +import android.os.Build import com.bumptech.glide.Glide import com.bumptech.glide.Priority import com.bumptech.glide.Registry @@ -34,7 +36,7 @@ internal class VideoThumbnailLoader : ModelLoader { return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model)) } - override fun handles(videoThumbnail: VideoThumbnail): Boolean = true + override fun handles(model: VideoThumbnail): Boolean = true internal class Factory : ModelLoaderFactory { override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = VideoThumbnailLoader() @@ -48,9 +50,29 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe val retriever = openMetadataRetriever(model.context, model.uri) if (retriever != null) { try { - val picture = retriever.embeddedPicture ?: retriever.frameAtTime?.getBytes(canHaveAlpha = false, recycle = false) - if (picture != null) { - callback.onDataReady(ByteArrayInputStream(picture)) + var bytes = retriever.embeddedPicture + if (bytes == null) { + // try to match the thumbnails returned by the content resolver / Media Store + // the following strategies are from empirical evidence from a few test devices: + // - API 29: sync frame closest to the middle + // - API 26/27: default representative frame at any time position + var timeMillis: Long? = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val durationMillis = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() + if (durationMillis != null) { + timeMillis = durationMillis / 2 + } + } + val frame = if (timeMillis != null) { + retriever.getFrameAtTime(timeMillis * 1000) + } else { + retriever.frameAtTime + } + bytes = frame?.getBytes(canHaveAlpha = false, recycle = false) + } + + if (bytes != null) { + callback.onDataReady(ByteArrayInputStream(bytes)) } else { callback.onLoadFailed(Exception("failed to get embedded picture or any frame")) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiTrackMedia.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiTrackMedia.kt new file mode 100644 index 000000000..8ac8dad05 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiTrackMedia.kt @@ -0,0 +1,59 @@ +package deckers.thibault.aves.metadata + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaExtractor +import android.media.MediaFormat +import android.net.Uri +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import deckers.thibault.aves.utils.LogUtils +import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.StorageUtils + +object MultiTrackMedia { + private val LOG_TAG = LogUtils.createTag(MultiTrackMedia::class.java) + + @RequiresApi(Build.VERSION_CODES.P) + fun getImage(context: Context, uri: Uri, trackId: Int?): Bitmap? { + val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return null + try { + return if (trackId != null) { + val imageIndex = trackIdToImageIndex(context, uri, trackId) ?: return null + retriever.getImageAtIndex(imageIndex) + } else { + retriever.primaryImage + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to extract image from uri=$uri trackId=$trackId", e) + } finally { + // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs + retriever.release() + } + return null + } + + private fun trackIdToImageIndex(context: Context, uri: Uri, trackId: Int): Int? { + val extractor = MediaExtractor() + try { + extractor.setDataSource(context, uri, null) + val trackCount = extractor.trackCount + var imageIndex = 0 + for (i in 0 until trackCount) { + val trackFormat = extractor.getTrackFormat(i) + if (trackId == trackFormat.getInteger(MediaFormat.KEY_TRACK_ID)) { + return imageIndex + } + if (MimeTypes.isImage(trackFormat.getString(MediaFormat.KEY_MIME))) { + imageIndex++ + } + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackId=$trackId", e) + } finally { + extractor.release() + } + return null + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt index 1e67bdfa4..0ca46696b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt @@ -1,6 +1,7 @@ package deckers.thibault.aves.metadata import android.util.Log +import com.adobe.internal.xmp.XMPError import com.adobe.internal.xmp.XMPException import com.adobe.internal.xmp.XMPMeta import deckers.thibault.aves.utils.LogUtils @@ -9,6 +10,8 @@ import java.util.* object XMP { private val LOG_TAG = LogUtils.createTag(XMP::class.java) + // standard namespaces + // cf com.adobe.internal.xmp.XMPConst const val DC_SCHEMA_NS = "http://purl.org/dc/elements/1.1/" const val PHOTOSHOP_SCHEMA_NS = "http://ns.adobe.com/photoshop/1.0/" const val XMP_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/" @@ -51,27 +54,46 @@ object XMP { const val GPANO_CROPPED_AREA_TOP_PROP_NAME = "GPano:CroppedAreaTopPixels" const val GPANO_FULL_PANO_HEIGHT_PROP_NAME = "GPano:FullPanoHeightPixels" const val GPANO_FULL_PANO_WIDTH_PROP_NAME = "GPano:FullPanoWidthPixels" - private const val GPANO_PROJECTION_TYPE_PROP_NAME = "GPano:ProjectionType" + const val GPANO_PROJECTION_TYPE_PROP_NAME = "GPano:ProjectionType" + const val GPANO_PROJECTION_TYPE_DEFAULT = "equirectangular" private const val PMTM_IS_PANO360 = "pmtm:IsPano360" + // `GPano:ProjectionType` is required by spec but it is sometimes missing, assuming default + // `GPano:FullPanoHeightPixels` is required by spec but it is sometimes missing (e.g. Samsung Camera app panorama mode) private val gpanoRequiredProps = listOf( GPANO_CROPPED_AREA_HEIGHT_PROP_NAME, GPANO_CROPPED_AREA_WIDTH_PROP_NAME, GPANO_CROPPED_AREA_LEFT_PROP_NAME, GPANO_CROPPED_AREA_TOP_PROP_NAME, - GPANO_FULL_PANO_HEIGHT_PROP_NAME, GPANO_FULL_PANO_WIDTH_PROP_NAME, - GPANO_PROJECTION_TYPE_PROP_NAME, ) // extensions fun XMPMeta.isPanorama(): Boolean { // Google - if (gpanoRequiredProps.all { doesPropertyExist(GPANO_SCHEMA_NS, it) }) return true + try { + if (gpanoRequiredProps.all { doesPropertyExist(GPANO_SCHEMA_NS, it) }) return true + } catch (e: XMPException) { + if (e.errorCode != XMPError.BADSCHEMA) { + // `BADSCHEMA` code is reported when we check a property + // from a non standard namespace, and that namespace is not declared in the XMP + Log.w(LOG_TAG, "failed to check Google panorama props from XMP", e) + } + } + // Photomatix - if (getPropertyString(PMTM_SCHEMA_NS, PMTM_IS_PANO360) == "Yes") return true + try { + if (getPropertyString(PMTM_SCHEMA_NS, PMTM_IS_PANO360) == "Yes") return true + } catch (e: XMPException) { + if (e.errorCode != XMPError.BADSCHEMA) { + // `BADSCHEMA` code is reported when we check a property + // from a non standard namespace, and that namespace is not declared in the XMP + Log.w(LOG_TAG, "failed to check Photomatix panorama props from XMP", e) + } + } + return false } @@ -102,7 +124,7 @@ object XMP { } } } catch (e: XMPException) { - Log.w(LOG_TAG, "failed to get text for XMP schema=$schema, propName=$propName", e) + Log.w(LOG_TAG, "failed to get date for XMP schema=$schema, propName=$propName", e) } } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt similarity index 66% rename from android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt index 06adb5d03..872ed6819 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt @@ -1,13 +1,15 @@ package deckers.thibault.aves.model import android.net.Uri -import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.FieldMap -class AvesImageEntry(map: FieldMap) { +class AvesEntry(map: FieldMap) { val uri: Uri = Uri.parse(map["uri"] as String) // content or file URI val path = map["path"] as String? // best effort to get local path + val pageId = map["pageId"] as Int? // null means the main entry val mimeType = map["mimeType"] as String val width = map["width"] as Int val height = map["height"] as Int val rotationDegrees = map["rotationDegrees"] as Int + val isFlipped = map["isFlipped"] as Boolean } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/FieldMap.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/FieldMap.kt new file mode 100644 index 000000000..78592a2b0 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/FieldMap.kt @@ -0,0 +1,3 @@ +package deckers.thibault.aves.model + +typealias FieldMap = MutableMap diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt similarity index 98% rename from android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt index 2e963ae6a..5a51905da 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt @@ -25,13 +25,13 @@ 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.model.FieldMap import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils import org.beyka.tiffbitmapfactory.TiffBitmapFactory import java.io.IOException -class SourceImageEntry { +class SourceEntry { val uri: Uri // content or file URI var path: String? = null // best effort to get local path private val sourceMimeType: String @@ -119,7 +119,7 @@ class SourceImageEntry { // metadata retrieval // expects entry with: uri, mimeType // finds: width, height, orientation/rotation, date, title, duration - fun fillPreCatalogMetadata(context: Context): SourceImageEntry { + fun fillPreCatalogMetadata(context: Context): SourceEntry { if (isSvg) return this if (isVideo) { fillVideoByMediaMetadataRetriever(context) 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 index 006e61e2f..96b49e980 100644 --- 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 @@ -3,7 +3,7 @@ package deckers.thibault.aves.model.provider import android.content.Context import android.net.Uri import android.provider.MediaStore -import deckers.thibault.aves.model.SourceImageEntry +import deckers.thibault.aves.model.SourceEntry internal class ContentImageProvider : ImageProvider() { override suspend fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { @@ -28,7 +28,7 @@ internal class ContentImageProvider : ImageProvider() { return } - val entry = SourceImageEntry(map).fillPreCatalogMetadata(context) + val entry = SourceEntry(map).fillPreCatalogMetadata(context) if (entry.isSized || entry.isSvg) { callback.onSuccess(entry.toMap()) } else { 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 index f47adb072..7a08724bf 100644 --- 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 @@ -2,7 +2,7 @@ package deckers.thibault.aves.model.provider import android.content.Context import android.net.Uri -import deckers.thibault.aves.model.SourceImageEntry +import deckers.thibault.aves.model.SourceEntry import java.io.File internal class FileImageProvider : ImageProvider() { @@ -12,7 +12,7 @@ internal class FileImageProvider : ImageProvider() { return } - val entry = SourceImageEntry(uri, mimeType) + val entry = SourceEntry(uri, mimeType) val path = uri.path if (path != null) { 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 index 8814ee5c7..f92ba728c 100644 --- 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 @@ -2,18 +2,28 @@ package deckers.thibault.aves.model.provider import android.content.ContentUris import android.content.Context +import android.graphics.Bitmap import android.media.MediaScannerConnection import android.net.Uri +import android.os.Build import android.provider.MediaStore import android.util.Log import androidx.exifinterface.media.ExifInterface +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.commonsware.cwac.document.DocumentFileCompat -import deckers.thibault.aves.model.AvesImageEntry +import deckers.thibault.aves.decoder.MultiTrackImage +import deckers.thibault.aves.decoder.TiffImage +import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.ExifOrientationOp +import deckers.thibault.aves.model.FieldMap +import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.LogUtils -import deckers.thibault.aves.utils.MimeTypes.isImage -import deckers.thibault.aves.utils.MimeTypes.isVideo +import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils.copyFileToTemp +import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent import deckers.thibault.aves.utils.StorageUtils.getDocumentFile import java.io.File import java.io.FileNotFoundException @@ -32,10 +42,151 @@ abstract class ImageProvider { throw UnsupportedOperationException() } - open suspend fun moveMultiple(context: Context, copy: Boolean, destinationDir: String, entries: List, callback: ImageOpCallback) { + open suspend fun moveMultiple(context: Context, copy: Boolean, destinationDir: String, entries: List, callback: ImageOpCallback) { callback.onFailure(UnsupportedOperationException()) } + suspend fun exportMultiple( + context: Context, + mimeType: String, + 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 pageId = entry.pageId + + val result = hashMapOf( + "uri" to sourceUri.toString(), + "pageId" to pageId, + "success" to false, + ) + + try { + val newFields = exportSingleByTreeDocAndScan( + context = context, + sourceEntry = entry, + destinationDir = destinationDir, + destinationDirDocFile = destinationDirDocFile, + exportMimeType = mimeType, + ) + result["newFields"] = newFields + result["success"] = true + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to export to destinationDir=$destinationDir entry with sourcePath=$sourcePath pageId=$pageId", e) + } + callback.onSuccess(result) + } + } + + private suspend fun exportSingleByTreeDocAndScan( + context: Context, + sourceEntry: AvesEntry, + destinationDir: String, + destinationDirDocFile: DocumentFileCompat, + exportMimeType: String, + ): FieldMap { + val sourceMimeType = sourceEntry.mimeType + val sourceUri = sourceEntry.uri + val pageId = sourceEntry.pageId + + var desiredNameWithoutExtension = if (sourceEntry.path != null) { + val sourcePath = sourceEntry.path + val sourceFile = File(sourcePath) + val sourceFileName = sourceFile.name + sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "") + } else { + sourceUri.lastPathSegment!! + } + if (pageId != null) { + val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId + desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" + } + val desiredFileName = desiredNameWithoutExtension + when (exportMimeType) { + MimeTypes.JPEG -> ".jpg" + MimeTypes.PNG -> ".png" + MimeTypes.WEBP -> ".webp" + else -> throw Exception("unsupported export MIME type=$exportMimeType") + } + + if (File(destinationDir, desiredFileName).exists()) { + throw Exception("file with name=$desiredFileName already exists in destination directory") + } + + // 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 + @Suppress("BlockingMethodInNonBlockingContext") + val destinationTreeFile = destinationDirDocFile.createFile(exportMimeType, desiredNameWithoutExtension) + val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri) + + val model: Any = if (MimeTypes.isHeifLike(sourceMimeType) && pageId != null) { + MultiTrackImage(context, sourceUri, pageId) + } else if (sourceMimeType == MimeTypes.TIFF) { + TiffImage(context, sourceUri, pageId) + } else { + sourceUri + } + + // request a fresh image with the highest quality format + val glideOptions = RequestOptions() + .format(DecodeFormat.PREFER_ARGB_8888) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + + val target = Glide.with(context) + .asBitmap() + .apply(glideOptions) + .load(model) + .submit() + try { + @Suppress("BlockingMethodInNonBlockingContext") + var bitmap = target.get() + if (MimeTypes.needRotationAfterGlide(sourceMimeType)) { + bitmap = BitmapUtils.applyExifOrientation(context, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped) + } + bitmap ?: throw Exception("failed to get image from uri=$sourceUri page=$pageId") + + val quality = 100 + val format = when (exportMimeType) { + MimeTypes.JPEG -> Bitmap.CompressFormat.JPEG + MimeTypes.PNG -> Bitmap.CompressFormat.PNG + MimeTypes.WEBP -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (quality == 100) { + Bitmap.CompressFormat.WEBP_LOSSLESS + } else { + Bitmap.CompressFormat.WEBP_LOSSY + } + } else { + @Suppress("DEPRECATION") + Bitmap.CompressFormat.WEBP + } + else -> throw Exception("unsupported export MIME type=$exportMimeType") + } + + @Suppress("BlockingMethodInNonBlockingContext") + destinationDocFile.openOutputStream().use { + bitmap.compress(format, quality, it) + } + } finally { + Glide.with(context).clear(target) + } + + val fileName = destinationDocFile.name + val destinationFullPath = destinationDir + fileName + + return scanNewPath(context, destinationFullPath, exportMimeType) + } + suspend fun rename(context: Context, oldPath: String, oldMediaUri: Uri, mimeType: String, newFilename: String, callback: ImageOpCallback) { val oldFile = File(oldPath) val newFile = File(oldFile.parent, newFilename) @@ -147,9 +298,9 @@ abstract class ImageProvider { // `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)) { + if (MimeTypes.isImage(mimeType)) { contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId) - } else if (isVideo(mimeType)) { + } else if (MimeTypes.isVideo(mimeType)) { contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId) } } @@ -198,5 +349,3 @@ abstract class ImageProvider { private val LOG_TAG = LogUtils.createTag(ImageProvider::class.java) } } - -typealias FieldMap = MutableMap 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 index 7329453a0..cf90f1b5d 100644 --- 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 @@ -8,8 +8,9 @@ import android.os.Build import android.provider.MediaStore import android.util.Log import com.commonsware.cwac.document.DocumentFileCompat -import deckers.thibault.aves.model.AvesImageEntry -import deckers.thibault.aves.model.SourceImageEntry +import deckers.thibault.aves.model.AvesEntry +import deckers.thibault.aves.model.FieldMap +import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.isImage @@ -158,7 +159,7 @@ class MediaStoreImageProvider : ImageProvider() { // missing some attributes such as width, height, orientation. // Also, the reported size of raw images is inconsistent across devices // and Android versions (sometimes the raw size, sometimes the decoded size). - val entry = SourceImageEntry(entryMap).fillPreCatalogMetadata(context) + val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context) entryMap = entry.toMap() } @@ -185,7 +186,7 @@ class MediaStoreImageProvider : ImageProvider() { override suspend fun delete(context: Context, uri: Uri, path: String?) { path ?: throw Exception("failed to delete file because path is null") - if (requireAccessPermission(context, path)) { + if (File(path).exists() && 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 val df = getDocumentFile(context, path, uri) @@ -203,7 +204,7 @@ class MediaStoreImageProvider : ImageProvider() { context: Context, copy: Boolean, destinationDir: String, - entries: List, + entries: List, callback: ImageOpCallback, ) { val destinationDirDocFile = createDirectoryIfAbsent(context, destinationDir) 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 cb70f3347..2f71186ee 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 @@ -26,7 +26,7 @@ object BitmapUtils { } catch (e: IllegalStateException) { Log.e(LOG_TAG, "failed to get bytes from bitmap", e) } - return null; + return null } fun applyExifOrientation(context: Context, bitmap: Bitmap?, rotationDegrees: Int?, isFlipped: Boolean?): Bitmap? { 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 9f7766d36..f033dbf22 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 @@ -9,10 +9,10 @@ object MimeTypes { private const val BMP = "image/bmp" const val GIF = "image/gif" const val HEIC = "image/heic" - const val HEIF = "image/heif" + private const val HEIF = "image/heif" private const val ICO = "image/x-icon" - private const val JPEG = "image/jpeg" - private const val PNG = "image/png" + const val JPEG = "image/jpeg" + const val PNG = "image/png" const val TIFF = "image/tiff" private const val WBMP = "image/vnd.wap.wbmp" const val WEBP = "image/webp" @@ -41,10 +41,9 @@ object MimeTypes { fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith(VIDEO) - fun isMultimedia(mimeType: String?) = when (mimeType) { - HEIC, HEIF -> true - else -> isVideo(mimeType) - } + fun isHeifLike(mimeType: String?) = mimeType != null && (mimeType == HEIC || mimeType == HEIF) + + fun isMultimedia(mimeType: String?) = isVideo(mimeType) || isHeifLike(mimeType) fun isRaw(mimeType: String): Boolean { return when (mimeType) { diff --git a/android/build.gradle b/android/build.gradle index 65df263a7..006020e81 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -9,7 +9,7 @@ buildscript { // TODO TLAD upgrade AGP to 4+ when this lands on stable: https://github.com/flutter/flutter/pull/70808 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.gms:google-services:4.3.5' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1' } } diff --git a/lib/image_providers/app_icon_image_provider.dart b/lib/image_providers/app_icon_image_provider.dart index 06d21092c..cbc7490e5 100644 --- a/lib/image_providers/app_icon_image_provider.dart +++ b/lib/image_providers/app_icon_image_provider.dart @@ -2,7 +2,7 @@ import 'dart:ui' as ui show Codec; import 'package:aves/services/android_app_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class AppIconImage extends ImageProvider { const AppIconImage({ diff --git a/lib/image_providers/region_provider.dart b/lib/image_providers/region_provider.dart index ce08b6967..3563d9419 100644 --- a/lib/image_providers/region_provider.dart +++ b/lib/image_providers/region_provider.dart @@ -2,10 +2,9 @@ import 'dart:async'; import 'dart:math'; import 'dart:ui' as ui show Codec; -import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class RegionProvider extends ImageProvider { final RegionProviderKey key; @@ -23,7 +22,7 @@ class RegionProvider extends ImageProvider { codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () sync* { - yield ErrorDescription('uri=${key.uri}, regionRect=${key.regionRect}'); + yield ErrorDescription('uri=${key.uri}, pageId=${key.pageId}, mimeType=${key.mimeType}, region=${key.region}'); }, ); } @@ -31,6 +30,7 @@ class RegionProvider extends ImageProvider { Future _loadAsync(RegionProviderKey key, DecoderCallback decode) async { final uri = key.uri; final mimeType = key.mimeType; + final pageId = key.pageId; try { final bytes = await ImageFileService.getRegion( uri, @@ -38,9 +38,9 @@ class RegionProvider extends ImageProvider { key.rotationDegrees, key.isFlipped, key.sampleSize, - key.regionRect, + key.region, key.imageSize, - page: key.page, + pageId: pageId, taskKey: key, ); if (bytes == null) { @@ -49,7 +49,7 @@ class RegionProvider extends ImageProvider { return await decode(bytes); } catch (error) { debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error'); - throw StateError('$mimeType region decoding failed'); + throw StateError('$mimeType region decoding failed (page $pageId)'); } } @@ -63,21 +63,23 @@ class RegionProvider extends ImageProvider { } class RegionProviderKey { + // do not store the entry as it is, because the key should be constant + // but the entry attributes may change over time final String uri, mimeType; - final int rotationDegrees, sampleSize, page; + final int pageId, rotationDegrees, sampleSize; final bool isFlipped; - final Rectangle regionRect; + final Rectangle region; final Size imageSize; final double scale; const RegionProviderKey({ @required this.uri, @required this.mimeType, + @required this.pageId, @required this.rotationDegrees, @required this.isFlipped, - this.page = 0, @required this.sampleSize, - @required this.regionRect, + @required this.region, @required this.imageSize, this.scale = 1.0, }) : assert(uri != null), @@ -85,49 +87,29 @@ class RegionProviderKey { assert(rotationDegrees != null), assert(isFlipped != null), assert(sampleSize != null), - assert(regionRect != null), + assert(region != null), assert(imageSize != null), assert(scale != null); - // do not store the entry as it is, because the key should be constant - // but the entry attributes may change over time - factory RegionProviderKey.fromEntry( - ImageEntry entry, { - int page = 0, - @required int sampleSize, - @required Rectangle rect, - }) { - return RegionProviderKey( - uri: entry.uri, - mimeType: entry.mimeType, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - page: page, - sampleSize: sampleSize, - regionRect: rect, - imageSize: Size(entry.width.toDouble(), entry.height.toDouble()), - ); - } - @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.sampleSize == sampleSize && other.regionRect == regionRect && other.imageSize == imageSize && other.scale == scale; + return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.pageId == pageId && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.region == region && other.imageSize == imageSize && other.scale == scale; } @override int get hashCode => hashValues( uri, mimeType, + pageId, rotationDegrees, isFlipped, - page, sampleSize, - regionRect, + region, imageSize, scale, ); @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, sampleSize=$sampleSize, regionRect=$regionRect, imageSize=$imageSize, scale=$scale}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, region=$region, imageSize=$imageSize, scale=$scale}'; } diff --git a/lib/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart index 62546bbae..fac117ab4 100644 --- a/lib/image_providers/thumbnail_provider.dart +++ b/lib/image_providers/thumbnail_provider.dart @@ -1,9 +1,8 @@ import 'dart:ui' as ui show Codec; -import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class ThumbnailProvider extends ImageProvider { final ThumbnailProviderKey key; @@ -24,7 +23,7 @@ class ThumbnailProvider extends ImageProvider { codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () sync* { - yield ErrorDescription('uri=${key.uri}, extent=${key.extent}'); + yield ErrorDescription('uri=${key.uri}, pageId=${key.pageId}, mimeType=${key.mimeType}, extent=${key.extent}'); }, ); } @@ -32,16 +31,16 @@ class ThumbnailProvider extends ImageProvider { Future _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async { final uri = key.uri; final mimeType = key.mimeType; + final pageId = key.pageId; try { final bytes = await ImageFileService.getThumbnail( - uri, - mimeType, - key.dateModifiedSecs, - key.rotationDegrees, - key.isFlipped, - key.extent, - key.extent, - page: key.page, + uri: uri, + mimeType: mimeType, + pageId: pageId, + rotationDegrees: key.rotationDegrees, + isFlipped: key.isFlipped, + dateModifiedSecs: key.dateModifiedSecs, + extent: key.extent, taskKey: key, ); if (bytes == null) { @@ -50,7 +49,7 @@ class ThumbnailProvider extends ImageProvider { return await decode(bytes); } catch (error) { debugPrint('$runtimeType _loadAsync failed with uri=$uri, error=$error'); - throw StateError('$mimeType decoding failed'); + throw StateError('$mimeType decoding failed (page $pageId)'); } } @@ -64,61 +63,49 @@ class ThumbnailProvider extends ImageProvider { } class ThumbnailProviderKey { + // do not store the entry as it is, because the key should be constant + // but the entry attributes may change over time final String uri, mimeType; - final int dateModifiedSecs, rotationDegrees, page; + final int pageId, rotationDegrees; final bool isFlipped; + final int dateModifiedSecs; final double extent, scale; const ThumbnailProviderKey({ @required this.uri, @required this.mimeType, - @required this.dateModifiedSecs, + @required this.pageId, @required this.rotationDegrees, @required this.isFlipped, - this.page = 0, + @required this.dateModifiedSecs, this.extent = 0, this.scale = 1, }) : assert(uri != null), assert(mimeType != null), - assert(dateModifiedSecs != null), assert(rotationDegrees != null), assert(isFlipped != null), + assert(dateModifiedSecs != null), assert(extent != null), assert(scale != null); - // do not store the entry as it is, because the key should be constant - // but the entry attributes may change over time - factory ThumbnailProviderKey.fromEntry(ImageEntry entry, {int page = 0, double extent = 0}) { - return ThumbnailProviderKey( - uri: entry.uri, - mimeType: entry.mimeType, - // `dateModifiedSecs` can be missing in viewer mode - dateModifiedSecs: entry.dateModifiedSecs ?? -1, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - page: page, - extent: extent, - ); - } - @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is ThumbnailProviderKey && other.uri == uri && other.extent == extent && other.mimeType == mimeType && other.dateModifiedSecs == dateModifiedSecs && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.scale == scale; + return other is ThumbnailProviderKey && other.uri == uri && other.mimeType == mimeType && other.pageId == pageId && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.dateModifiedSecs == dateModifiedSecs && other.extent == extent && other.scale == scale; } @override int get hashCode => hashValues( uri, mimeType, - dateModifiedSecs, + pageId, rotationDegrees, isFlipped, - page, + dateModifiedSecs, extent, scale, ); @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, dateModifiedSecs=$dateModifiedSecs, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, extent=$extent, scale=$scale}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent, scale=$scale}'; } diff --git a/lib/image_providers/uri_image_provider.dart b/lib/image_providers/uri_image_provider.dart index 5290913f9..6c3f9615e 100644 --- a/lib/image_providers/uri_image_provider.dart +++ b/lib/image_providers/uri_image_provider.dart @@ -3,19 +3,19 @@ import 'dart:ui' as ui show Codec; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:pedantic/pedantic.dart'; class UriImage extends ImageProvider { final String uri, mimeType; - final int page, rotationDegrees, expectedContentLength; + final int pageId, rotationDegrees, expectedContentLength; final bool isFlipped; final double scale; const UriImage({ @required this.uri, @required this.mimeType, - this.page = 0, + @required this.pageId, @required this.rotationDegrees, @required this.isFlipped, this.expectedContentLength, @@ -37,7 +37,7 @@ class UriImage extends ImageProvider { scale: key.scale, chunkEvents: chunkEvents.stream, informationCollector: () sync* { - yield ErrorDescription('uri=$uri, mimeType=$mimeType'); + yield ErrorDescription('uri=$uri, pageId=$pageId, mimeType=$mimeType'); }, ); } @@ -51,7 +51,7 @@ class UriImage extends ImageProvider { mimeType, rotationDegrees, isFlipped, - page: page, + pageId: pageId, expectedContentLength: expectedContentLength, onBytesReceived: (cumulative, total) { chunkEvents.add(ImageChunkEvent( @@ -66,7 +66,7 @@ class UriImage extends ImageProvider { return await decode(bytes); } catch (error) { debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error'); - throw StateError('$mimeType decoding failed'); + throw StateError('$mimeType decoding failed (page $pageId)'); } finally { unawaited(chunkEvents.close()); } @@ -75,7 +75,7 @@ class UriImage extends ImageProvider { @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is UriImage && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.scale == scale; + return other is UriImage && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.pageId == pageId && other.scale == scale; } @override @@ -84,10 +84,10 @@ class UriImage extends ImageProvider { mimeType, rotationDegrees, isFlipped, - page, + pageId, scale, ); @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, scale=$scale}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, pageId=$pageId, scale=$scale}'; } diff --git a/lib/image_providers/uri_picture_provider.dart b/lib/image_providers/uri_picture_provider.dart index 913c78690..f6e8dc9c1 100644 --- a/lib/image_providers/uri_picture_provider.dart +++ b/lib/image_providers/uri_picture_provider.dart @@ -1,6 +1,6 @@ import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:pedantic/pedantic.dart'; @@ -30,7 +30,7 @@ class UriPicture extends PictureProvider { Future _loadAsync(UriPicture key, {PictureErrorListener onError}) async { assert(key == this); - final data = await ImageFileService.getImage(uri, mimeType, 0, false); + final data = await ImageFileService.getSvg(uri, mimeType); if (data == null || data.isEmpty) { return null; } diff --git a/lib/main.dart b/lib/main.dart index 92cd32fbc..02cb28210 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,10 +2,13 @@ import 'dart:isolate'; import 'dart:ui'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/media_store_source.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/common/behaviour/route_tracker.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; -import 'package:aves/widgets/common/providers/settings_provider.dart'; import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/welcome_page.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; @@ -16,6 +19,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:overlay_support/overlay_support.dart'; +import 'package:provider/provider.dart'; void main() { // HttpClient.enableTimelineLogging = true; // enable network traffic logging @@ -43,12 +47,16 @@ class AvesApp extends StatefulWidget { class _AvesAppState extends State { Future _appSetup; + final _mediaStoreSource = MediaStoreSource(); + final Debouncer _contentChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay); + final List changedUris = []; // observers are not registered when using the same list object with different items // the list itself needs to be reassigned List _navigatorObservers = []; - final _newIntentChannel = EventChannel('deckers.thibault/aves/intent'); - final _navigatorKey = GlobalKey(); + final EventChannel _contentChangeChannel = EventChannel('deckers.thibault/aves/contentchange'); + final EventChannel _newIntentChannel = EventChannel('deckers.thibault/aves/intent'); + final GlobalKey _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); static const accentColor = Colors.indigoAccent; @@ -94,9 +102,57 @@ class _AvesAppState extends State { void initState() { super.initState(); _appSetup = _setup(); + _contentChangeChannel.receiveBroadcastStream().listen((event) => _onContentChange(event as String)); _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map)); } + @override + Widget build(BuildContext context) { + // place the settings provider above `MaterialApp` + // so it can be used during navigation transitions + return ChangeNotifierProvider.value( + value: settings, + child: Provider.value( + value: _mediaStoreSource, + child: OverlaySupport( + child: FutureBuilder( + future: _appSetup, + builder: (context, snapshot) { + final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) + ? getFirstPage() + : Scaffold( + body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(), + ); + return MaterialApp( + navigatorKey: _navigatorKey, + home: home, + navigatorObservers: _navigatorObservers, + title: 'Aves', + darkTheme: darkTheme, + themeMode: ThemeMode.dark, + ); + }, + ), + ), + ), + ); + } + + Widget _buildError(Object error) { + return Container( + alignment: Alignment.center, + padding: EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(AIcons.error), + SizedBox(height: 16), + Text(error.toString()), + ], + ), + ); + } + Future _setup() async { await Firebase.initializeApp().then((app) { final crashlytics = FirebaseCrashlytics.instance; @@ -133,46 +189,11 @@ class _AvesAppState extends State { )); } - @override - Widget build(BuildContext context) { - // place the settings provider above `MaterialApp` - // so it can be used during navigation transitions - return SettingsProvider( - child: OverlaySupport( - child: FutureBuilder( - future: _appSetup, - builder: (context, snapshot) { - final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) - ? getFirstPage() - : Scaffold( - body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(), - ); - return MaterialApp( - navigatorKey: _navigatorKey, - home: home, - navigatorObservers: _navigatorObservers, - title: 'Aves', - darkTheme: darkTheme, - themeMode: ThemeMode.dark, - ); - }, - ), - ), - ); - } - - Widget _buildError(Object error) { - return Container( - alignment: Alignment.center, - padding: EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(AIcons.error), - SizedBox(height: 16), - Text(error.toString()), - ], - ), - ); + void _onContentChange(String uri) { + changedUris.add(uri); + _contentChangeDebouncer(() { + _mediaStoreSource.refreshUris(List.of(changedUris)); + changedUris.clear(); + }); } } diff --git a/lib/model/actions/entry_actions.dart b/lib/model/actions/entry_actions.dart index 805df42f0..5cd6f62c3 100644 --- a/lib/model/actions/entry_actions.dart +++ b/lib/model/actions/entry_actions.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; enum EntryAction { delete, edit, + export, flip, info, open, @@ -31,6 +32,7 @@ class EntryActions { EntryAction.share, EntryAction.delete, EntryAction.rename, + EntryAction.export, EntryAction.print, EntryAction.viewSource, ]; @@ -52,6 +54,8 @@ extension ExtraEntryAction on EntryAction { return null; case EntryAction.delete: return 'Delete'; + case EntryAction.export: + return 'Export'; case EntryAction.info: return 'Info'; case EntryAction.rename: @@ -91,6 +95,8 @@ extension ExtraEntryAction on EntryAction { return null; case EntryAction.delete: return AIcons.delete; + case EntryAction.export: + return AIcons.export; case EntryAction.info: return AIcons.info; case EntryAction.rename: diff --git a/lib/model/actions/move_type.dart b/lib/model/actions/move_type.dart new file mode 100644 index 000000000..71b326b70 --- /dev/null +++ b/lib/model/actions/move_type.dart @@ -0,0 +1 @@ +enum MoveType { copy, move, export } diff --git a/lib/model/connectivity.dart b/lib/model/connectivity.dart new file mode 100644 index 000000000..009c0fa1c --- /dev/null +++ b/lib/model/connectivity.dart @@ -0,0 +1,28 @@ +import 'package:connectivity/connectivity.dart'; +import 'package:flutter/foundation.dart'; + +final AvesConnectivity connectivity = AvesConnectivity._private(); + +class AvesConnectivity { + bool _isConnected; + + AvesConnectivity._private() { + Connectivity().onConnectivityChanged.listen(_updateFromResult); + } + + void onResume() => _isConnected = null; + + Future get isConnected async { + if (_isConnected != null) return SynchronousFuture(_isConnected); + final result = await (Connectivity().checkConnectivity()); + _updateFromResult(result); + return _isConnected; + } + + Future get canGeolocate => isConnected; + + void _updateFromResult(ConnectivityResult result) { + _isConnected = result != ConnectivityResult.none; + debugPrint('Device is connected=$_isConnected'); + } +} diff --git a/lib/model/image_entry.dart b/lib/model/entry.dart similarity index 84% rename from lib/model/image_entry.dart rename to lib/model/entry.dart index 1d1e6f697..384b29a4b 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/entry.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:aves/model/entry_cache.dart'; import 'package:aves/model/favourite_repo.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/services/image_file_service.dart'; @@ -21,16 +21,18 @@ import 'package:path/path.dart' as ppath; import '../ref/mime_types.dart'; -class ImageEntry { +class AvesEntry { String uri; String _path, _directory, _filename, _extension; - int contentId; + int pageId, contentId; final String sourceMimeType; int width; int height; int sourceRotationDegrees; final int sizeBytes; String sourceTitle; + + // `dateModifiedSecs` can be missing in viewer mode int _dateModifiedSecs; final int sourceDateTakenMillis; final int durationMillis; @@ -43,10 +45,11 @@ class ImageEntry { // TODO TLAD make it dynamic if it depends on OS/lib versions static const List undecodable = [MimeTypes.crw, MimeTypes.psd]; - ImageEntry({ + AvesEntry({ this.uri, String path, this.contentId, + this.pageId, this.sourceMimeType, @required this.width, @required this.height, @@ -66,14 +69,14 @@ class ImageEntry { bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType); - ImageEntry copyWith({ + AvesEntry copyWith({ @required String uri, @required String path, @required int contentId, @required int dateModifiedSecs, }) { final copyContentId = contentId ?? this.contentId; - final copied = ImageEntry( + final copied = AvesEntry( uri: uri ?? uri, path: path ?? this.path, contentId: copyContentId, @@ -93,9 +96,39 @@ class ImageEntry { return copied; } + AvesEntry getPageEntry(SinglePageInfo pageInfo, {bool eraseDefaultPageId = true}) { + if (pageInfo == null) return this; + + // do not provide the page ID for the default page, + // so that we can treat this page like the main entry + // and retrieve cached images for it + final pageId = eraseDefaultPageId && pageInfo.isDefault ? null : pageInfo.pageId; + + return AvesEntry( + uri: uri, + path: path, + contentId: contentId, + pageId: pageId, + sourceMimeType: pageInfo.mimeType ?? sourceMimeType, + width: pageInfo.width ?? width, + height: pageInfo.height ?? height, + sourceRotationDegrees: sourceRotationDegrees, + sizeBytes: sizeBytes, + sourceTitle: sourceTitle, + dateModifiedSecs: dateModifiedSecs, + sourceDateTakenMillis: sourceDateTakenMillis, + durationMillis: pageInfo.durationMillis ?? durationMillis, + ) + ..catalogMetadata = _catalogMetadata?.copyWith( + mimeType: pageInfo.mimeType, + isMultipage: false, + ) + ..addressDetails = _addressDetails?.copyWith(); + } + // from DB or platform source entry - factory ImageEntry.fromMap(Map map) { - return ImageEntry( + factory AvesEntry.fromMap(Map map) { + return AvesEntry( uri: map['uri'] as String, path: map['path'] as String, contentId: map['contentId'] as int, @@ -136,7 +169,7 @@ class ImageEntry { } @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path, pageId=$pageId}'; set path(String path) { _path = path; @@ -196,7 +229,11 @@ class ImageEntry { ].contains(mimeType) && !isAnimated; - bool get canTile => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff; + bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff; + + // as of panorama v0.3.1, the `Panorama` widget throws on initialization when the image is already resolved + // so we use tiles for panoramas as a workaround to not collide with the `panorama` package resolution + bool get useTiles => supportTiling && (width > 4096 || height > 4096 || is360); bool get isRaw => MimeTypes.rawImages.contains(mimeType); @@ -216,8 +253,6 @@ class ImageEntry { bool get canEdit => path != null; - bool get canPrint => !isVideo; - bool get canRotateAndFlip => canEdit && canEditExif; // support for writing EXIF @@ -233,29 +268,21 @@ class ImageEntry { } } - // The additional comparison of width to height is a workaround for badly registered entries. - // e.g. a portrait FHD video should be registered as width=1920, height=1080, orientation=90, - // but is incorrectly registered in the Media Store as width=1080, height=1920, orientation=0 - // Double-checking the width/height during loading or cataloguing is the proper solution, - // but it would take space and time, so a basic workaround will do. - bool get isPortrait => rotationDegrees % 180 == 90 && (catalogMetadata?.rotationDegrees == null || width > height); + // Media Store size/rotation is inaccurate, e.g. a portrait FHD video is rotated according to its metadata, + // so it should be registered as width=1920, height=1080, orientation=90, + // but is incorrectly registered as width=1080, height=1920, orientation=0. + // Double-checking the width/height during loading or cataloguing is the proper solution, but it would take space and time. + // Comparing width and height can help with the portrait FHD video example, + // but it fails for a portrait screenshot rotated, which is landscape with width=1080, height=1920, orientation=90 + bool get isRotated => rotationDegrees % 180 == 90; static const ratioSeparator = '\u2236'; static const resolutionSeparator = ' \u00D7 '; - String getResolutionText({MultiPageInfo multiPageInfo, int page}) { - int w; - int h; - if (multiPageInfo != null && page != null) { - final pageInfo = multiPageInfo.pages[page]; - w = pageInfo?.width; - h = pageInfo?.height; - } - w ??= width; - h ??= height; - final ws = w ?? '?'; - final hs = h ?? '?'; - return isPortrait ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs'; + String get resolutionText { + final ws = width ?? '?'; + final hs = height ?? '?'; + return isRotated ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs'; } String get aspectRatioText { @@ -263,7 +290,7 @@ class ImageEntry { final gcd = width.gcd(height); final w = width ~/ gcd; final h = height ~/ gcd; - return isPortrait ? '$h$ratioSeparator$w' : '$w$ratioSeparator$h'; + return isRotated ? '$h$ratioSeparator$w' : '$w$ratioSeparator$h'; } else { return '?$ratioSeparator?'; } @@ -271,20 +298,13 @@ class ImageEntry { double get displayAspectRatio { if (width == 0 || height == 0) return 1; - return isPortrait ? height / width : width / height; + return isRotated ? height / width : width / height; } - Size getDisplaySize({MultiPageInfo multiPageInfo, int page}) { - int w; - int h; - if (multiPageInfo != null && page != null) { - final pageInfo = multiPageInfo.pages[page]; - w = pageInfo?.width; - h = pageInfo?.height; - } - w ??= width; - h ??= height; - return isPortrait ? Size(h.toDouble(), w.toDouble()) : Size(w.toDouble(), h.toDouble()); + Size get displaySize { + final w = width.toDouble(); + final h = height.toDouble(); + return isRotated ? Size(h, w) : Size(w, h); } int get megaPixels => width != null && height != null ? (width * height / 1000000).round() : null; @@ -598,7 +618,7 @@ class ImageEntry { // compare by: // 1) title ascending // 2) extension ascending - static int compareByName(ImageEntry a, ImageEntry b) { + static int compareByName(AvesEntry a, AvesEntry b) { final c = compareAsciiUpperCase(a.bestTitle, b.bestTitle); return c != 0 ? c : compareAsciiUpperCase(a.extension, b.extension); } @@ -606,7 +626,7 @@ class ImageEntry { // compare by: // 1) size descending // 2) name ascending - static int compareBySize(ImageEntry a, ImageEntry b) { + static int compareBySize(AvesEntry a, AvesEntry b) { final c = b.sizeBytes.compareTo(a.sizeBytes); return c != 0 ? c : compareByName(a, b); } @@ -615,9 +635,12 @@ class ImageEntry { // compare by: // 1) date descending - // 2) name ascending - static int compareByDate(ImageEntry a, ImageEntry b) { - final c = (b.bestDate ?? _epoch).compareTo(a.bestDate ?? _epoch); - return c != 0 ? c : compareByName(a, b); + // 2) name descending + static int compareByDate(AvesEntry a, AvesEntry b) { + var c = (b.bestDate ?? _epoch).compareTo(a.bestDate ?? _epoch); + if (c != 0) return c; + c = (b.dateModifiedSecs ?? 0).compareTo(a.dateModifiedSecs ?? 0); + if (c != 0) return c; + return -compareByName(a, b); } } diff --git a/lib/model/entry_cache.dart b/lib/model/entry_cache.dart index a940821e0..1e794f202 100644 --- a/lib/model/entry_cache.dart +++ b/lib/model/entry_cache.dart @@ -12,14 +12,14 @@ class EntryCache { int oldRotationDegrees, bool oldIsFlipped, ) async { - // TODO TLAD revisit this for multipage items, if someday image editing features are added for them - const page = 0; + // TODO TLAD provide pageId parameter for multipage items, if someday image editing features are added for them + int pageId; // evict fullscreen image await UriImage( uri: uri, mimeType: mimeType, - page: page, + pageId: pageId, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, ).evict(); @@ -28,10 +28,10 @@ class EntryCache { await ThumbnailProvider(ThumbnailProviderKey( uri: uri, mimeType: mimeType, + pageId: pageId, dateModifiedSecs: dateModifiedSecs, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, - page: page, )).evict(); // evict higher quality thumbnails (with powers of 2 from 32 to 1024 as specified extents) @@ -41,10 +41,10 @@ class EntryCache { (extent) => ThumbnailProvider(ThumbnailProviderKey( uri: uri, mimeType: mimeType, + pageId: pageId, dateModifiedSecs: dateModifiedSecs, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, - page: page, extent: extent, )).evict()); } diff --git a/lib/model/entry_images.dart b/lib/model/entry_images.dart new file mode 100644 index 000000000..cc8c28374 --- /dev/null +++ b/lib/model/entry_images.dart @@ -0,0 +1,67 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:aves/image_providers/region_provider.dart'; +import 'package:aves/image_providers/thumbnail_provider.dart'; +import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/entry.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraAvesEntry on AvesEntry { + ThumbnailProvider getThumbnail({double extent = 0}) { + return ThumbnailProvider(_getThumbnailProviderKey(extent)); + } + + ThumbnailProviderKey _getThumbnailProviderKey(double extent) { + // we standardize the thumbnail loading dimension by taking the nearest larger power of 2 + // so that there are less variants of the thumbnails to load and cache + // it increases the chance of cache hit when loading similarly sized columns (e.g. on orientation change) + final requestExtent = extent == 0 ? .0 : pow(2, (log(extent) / log(2)).ceil()).toDouble(); + + return ThumbnailProviderKey( + uri: uri, + mimeType: mimeType, + pageId: pageId, + rotationDegrees: rotationDegrees, + isFlipped: isFlipped, + dateModifiedSecs: dateModifiedSecs ?? -1, + extent: requestExtent, + ); + } + + RegionProvider getRegion({@required int sampleSize, Rectangle region}) { + return RegionProvider(_getRegionProviderKey(sampleSize, region)); + } + + RegionProviderKey _getRegionProviderKey(int sampleSize, Rectangle region) { + return RegionProviderKey( + uri: uri, + mimeType: mimeType, + pageId: pageId, + rotationDegrees: rotationDegrees, + isFlipped: isFlipped, + sampleSize: sampleSize, + region: region ?? Rectangle(0, 0, width, height), + imageSize: Size(width.toDouble(), height.toDouble()), + ); + } + + UriImage get uriImage => UriImage( + uri: uri, + mimeType: mimeType, + pageId: pageId, + rotationDegrees: rotationDegrees, + isFlipped: isFlipped, + expectedContentLength: sizeBytes, + ); + + bool _isReady(Object providerKey) => imageCache.statusForKey(providerKey).keepAlive; + + ImageProvider getBestThumbnail(double extent) { + final sizedThumbnailKey = _getThumbnailProviderKey(extent); + if (_isReady(sizedThumbnailKey)) return ThumbnailProvider(sizedThumbnailKey); + + return getThumbnail(); + } +} diff --git a/lib/model/favourite_repo.dart b/lib/model/favourite_repo.dart index d138de273..78daf8883 100644 --- a/lib/model/favourite_repo.dart +++ b/lib/model/favourite_repo.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/utils/change_notifier.dart'; @@ -18,25 +18,25 @@ class FavouriteRepo { int get count => _rows.length; - bool isFavourite(ImageEntry entry) => _rows.any((row) => row.contentId == entry.contentId); + bool isFavourite(AvesEntry entry) => _rows.any((row) => row.contentId == entry.contentId); - FavouriteRow _entryToRow(ImageEntry entry) => FavouriteRow(contentId: entry.contentId, path: entry.path); + FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId, path: entry.path); - Future add(Iterable entries) async { + Future add(Iterable entries) async { final newRows = entries.map(_entryToRow); await metadataDb.addFavourites(newRows); _rows.addAll(newRows); changeNotifier.notifyListeners(); } - Future remove(Iterable entries) async { + Future remove(Iterable entries) async { final removedRows = entries.map(_entryToRow); await metadataDb.removeFavourites(removedRows); removedRows.forEach(_rows.remove); changeNotifier.notifyListeners(); } - Future move(int oldContentId, ImageEntry entry) async { + Future move(int oldContentId, AvesEntry entry) async { final oldRow = _rows.firstWhere((row) => row.contentId == oldContentId, orElse: () => null); if (oldRow != null) { _rows.remove(oldRow); diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index 82f3bcc56..1ab6ce062 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -1,6 +1,6 @@ import 'package:aves/image_providers/app_icon_image_provider.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; @@ -33,7 +33,7 @@ class AlbumFilter extends CollectionFilter { }; @override - bool filter(ImageEntry entry) => entry.directory == album; + bool filter(AvesEntry entry) => entry.directory == album; @override String get label => uniqueName ?? album.split(separator).last; diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart index d4e9716c1..c64326959 100644 --- a/lib/model/filters/favourite.dart +++ b/lib/model/filters/favourite.dart @@ -1,5 +1,5 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -13,7 +13,7 @@ class FavouriteFilter extends CollectionFilter { }; @override - bool filter(ImageEntry entry) => entry.isFavourite; + bool filter(AvesEntry entry) => entry.isFavourite; @override String get label => 'Favourite'; diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index e70f39dd8..a3e985302 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -1,12 +1,12 @@ import 'dart:convert'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/tag.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -49,7 +49,7 @@ abstract class CollectionFilter implements Comparable { String toJson() => jsonEncode(toMap()); - bool filter(ImageEntry entry); + bool filter(AvesEntry entry); bool get isUnique => true; @@ -78,7 +78,7 @@ abstract class CollectionFilter implements Comparable { // TODO TLAD replace this by adding getters to CollectionFilter, with cached entry/count coming from Source class FilterGridItem { final T filter; - final ImageEntry entry; + final AvesEntry entry; const FilterGridItem(this.filter, this.entry); diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index cf46da1b2..c702cf2c4 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -1,5 +1,5 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -35,7 +35,7 @@ class LocationFilter extends CollectionFilter { String get countryNameAndCode => '$_location$locationSeparator$_countryCode'; @override - bool filter(ImageEntry entry) => _location.isEmpty ? !entry.isLocated : entry.isLocated && ((level == LocationLevel.country && entry.addressDetails.countryCode == _countryCode) || (level == LocationLevel.place && entry.addressDetails.place == _location)); + bool filter(AvesEntry entry) => _location.isEmpty ? !entry.isLocated : entry.isLocated && ((level == LocationLevel.country && entry.addressDetails.countryCode == _countryCode) || (level == LocationLevel.place && entry.addressDetails.place == _location)); @override String get label => _location.isEmpty ? emptyLabel : _location; diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 2b3342140..5944c27df 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -1,5 +1,5 @@ import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/mime_utils.dart'; import 'package:flutter/foundation.dart'; @@ -15,7 +15,7 @@ class MimeFilter extends CollectionFilter { static const geotiff = 'aves/geotiff'; // subset of `image/tiff` final String mime; - bool Function(ImageEntry) _filter; + bool Function(AvesEntry) _filter; String _label; IconData _icon; @@ -67,7 +67,7 @@ class MimeFilter extends CollectionFilter { }; @override - bool filter(ImageEntry entry) => _filter(entry); + bool filter(AvesEntry entry) => _filter(entry); @override String get label => _label; diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index bb880cb2f..33fe221e4 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -1,5 +1,5 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -12,7 +12,7 @@ class QueryFilter extends CollectionFilter { final String query; final bool colorful; - bool Function(ImageEntry) _filter; + bool Function(AvesEntry) _filter; QueryFilter(this.query, {this.colorful = true}) { var upQuery = query.toUpperCase(); @@ -44,7 +44,7 @@ class QueryFilter extends CollectionFilter { }; @override - bool filter(ImageEntry entry) => _filter(entry); + bool filter(AvesEntry entry) => _filter(entry); @override bool get isUnique => false; diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index ff9e94611..5d21f0b7f 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -1,5 +1,5 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -24,7 +24,7 @@ class TagFilter extends CollectionFilter { }; @override - bool filter(ImageEntry entry) => tag.isEmpty ? entry.xmpSubjects.isEmpty : entry.xmpSubjects.contains(tag); + bool filter(AvesEntry entry) => tag.isEmpty ? entry.xmpSubjects.isEmpty : entry.xmpSubjects.contains(tag); @override bool get isUnique => false; diff --git a/lib/model/image_metadata.dart b/lib/model/metadata.dart similarity index 97% rename from lib/model/image_metadata.dart rename to lib/model/metadata.dart index ccbbad3c0..e750fbbb0 100644 --- a/lib/model/image_metadata.dart +++ b/lib/model/metadata.dart @@ -68,17 +68,19 @@ class CatalogMetadata { } CatalogMetadata copyWith({ - @required int contentId, + int contentId, + String mimeType, + bool isMultipage, }) { return CatalogMetadata( contentId: contentId ?? this.contentId, - mimeType: mimeType, + mimeType: mimeType ?? this.mimeType, dateMillis: dateMillis, isAnimated: isAnimated, isFlipped: isFlipped, isGeotiff: isGeotiff, is360: is360, - isMultipage: isMultipage, + isMultipage: isMultipage ?? this.isMultipage, rotationDegrees: rotationDegrees, xmpSubjects: xmpSubjects, xmpTitleDescription: xmpTitleDescription, @@ -169,7 +171,7 @@ class AddressDetails { }); AddressDetails copyWith({ - @required int contentId, + int contentId, }) { return AddressDetails( contentId: contentId ?? this.contentId, diff --git a/lib/model/metadata_db.dart b/lib/model/metadata_db.dart index 48a8b5a3b..3630f5a27 100644 --- a/lib/model/metadata_db.dart +++ b/lib/model/metadata_db.dart @@ -1,7 +1,7 @@ import 'dart:io'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db_upgrade.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; @@ -116,16 +116,16 @@ class MetadataDb { debugPrint('$runtimeType clearEntries deleted $count entries'); } - Future> loadEntries() async { + Future> loadEntries() async { final stopwatch = Stopwatch()..start(); final db = await _database; final maps = await db.query(entryTable); - final entries = maps.map((map) => ImageEntry.fromMap(map)).toList(); + final entries = maps.map((map) => AvesEntry.fromMap(map)).toList(); debugPrint('$runtimeType loadEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries'); return entries; } - Future saveEntries(Iterable entries) async { + Future saveEntries(Iterable entries) async { if (entries == null || entries.isEmpty) return; final stopwatch = Stopwatch()..start(); final db = await _database; @@ -135,7 +135,7 @@ class MetadataDb { debugPrint('$runtimeType saveEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries'); } - Future updateEntryId(int oldId, ImageEntry entry) async { + Future updateEntryId(int oldId, AvesEntry entry) async { final db = await _database; final batch = db.batch(); batch.delete(entryTable, where: 'contentId = ?', whereArgs: [oldId]); @@ -143,7 +143,7 @@ class MetadataDb { await batch.commit(noResult: true); } - void _batchInsertEntry(Batch batch, ImageEntry entry) { + void _batchInsertEntry(Batch batch, AvesEntry entry) { if (entry == null) return; batch.insert( entryTable, diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart index 7ca616792..9400c1beb 100644 --- a/lib/model/multipage.dart +++ b/lib/model/multipage.dart @@ -1,42 +1,83 @@ import 'package:flutter/foundation.dart'; -class SinglePageInfo { - final int width, height; - - SinglePageInfo({ - this.width, - this.height, - }); - - factory SinglePageInfo.fromMap(Map map) { - return SinglePageInfo( - width: map['width'] as int, - height: map['height'] as int, - ); - } - - @override - String toString() => '$runtimeType#${shortHash(this)}{width=$width, height=$height}'; -} - class MultiPageInfo { - final Map pages; + final List pages; int get pageCount => pages.length; MultiPageInfo({ this.pages, - }); - - factory MultiPageInfo.fromMap(Map map) { - final pages = {}; - map.keys.forEach((key) { - final index = key as int; - pages.putIfAbsent(index, () => SinglePageInfo.fromMap(map[key])); - }); - return MultiPageInfo(pages: pages); + }) { + if (pages.isNotEmpty) { + pages.sort(); + // make sure there is a page marked as default + if (defaultPage == null) { + final firstPage = pages.removeAt(0); + pages.insert(0, firstPage.copyWith(isDefault: true)); + } + } } + factory MultiPageInfo.fromPageMaps(List pageMaps) { + return MultiPageInfo(pages: pageMaps.map((page) => SinglePageInfo.fromMap(page)).toList()); + } + + SinglePageInfo get defaultPage => pages.firstWhere((page) => page.isDefault, orElse: () => null); + + SinglePageInfo getByIndex(int index) => pages.firstWhere((page) => page.index == index, orElse: () => null); + + SinglePageInfo getById(int pageId) => pages.firstWhere((page) => page.pageId == pageId, orElse: () => null); + @override String toString() => '$runtimeType#${shortHash(this)}{pages=$pages}'; } + +class SinglePageInfo implements Comparable { + final int index, pageId; + final String mimeType; + final bool isDefault; + final int width, height, durationMillis; + + const SinglePageInfo({ + this.index, + this.pageId, + this.mimeType, + this.isDefault, + this.width, + this.height, + this.durationMillis, + }); + + SinglePageInfo copyWith({ + bool isDefault, + }) { + return SinglePageInfo( + index: index, + pageId: pageId, + mimeType: mimeType, + isDefault: isDefault ?? this.isDefault, + width: width, + height: height, + durationMillis: durationMillis, + ); + } + + factory SinglePageInfo.fromMap(Map map) { + final index = map['page'] as int; + return SinglePageInfo( + index: index, + pageId: map['trackId'] as int ?? index, + mimeType: map['mimeType'] as String, + isDefault: map['isDefault'] as bool ?? false, + width: map['width'] as int ?? 0, + height: map['height'] as int ?? 0, + durationMillis: map['durationMillis'] as int, + ); + } + + @override + String toString() => '$runtimeType#${shortHash(this)}{index=$index, pageId=$pageId, mimeType=$mimeType, isDefault=$isDefault, width=$width, height=$height, durationMillis=$durationMillis}'; + + @override + int compareTo(SinglePageInfo other) => index.compareTo(other.index); +} diff --git a/lib/model/panorama.dart b/lib/model/panorama.dart index 0bebe9501..99d1ff318 100644 --- a/lib/model/panorama.dart +++ b/lib/model/panorama.dart @@ -4,24 +4,38 @@ import 'package:flutter/widgets.dart'; class PanoramaInfo { final Rect croppedAreaRect; final Size fullPanoSize; + final String projectionType; PanoramaInfo({ this.croppedAreaRect, this.fullPanoSize, + this.projectionType, }); factory PanoramaInfo.fromMap(Map map) { - final cLeft = map['croppedAreaLeft'] as int; - final cTop = map['croppedAreaTop'] as int; + var cLeft = map['croppedAreaLeft'] as int; + var cTop = map['croppedAreaTop'] as int; final cWidth = map['croppedAreaWidth'] as int; final cHeight = map['croppedAreaHeight'] as int; + var fWidth = map['fullPanoWidth'] as int; + var fHeight = map['fullPanoHeight'] as int; + final projectionType = map['projectionType'] as String; + + // handle missing `fullPanoHeight` (e.g. Samsung camera app panorama mode) + if (fHeight == null && cWidth != null && cHeight != null) { + // assume the cropped area is actually covering 360 degrees horizontally + // even when `croppedAreaLeft` is non zero + fWidth = cWidth; + fHeight = (fWidth / 2).round(); + cTop = ((fHeight - cHeight) / 2).round(); + cLeft = 0; + } + Rect croppedAreaRect; if (cLeft != null && cTop != null && cWidth != null && cHeight != null) { croppedAreaRect = Rect.fromLTWH(cLeft.toDouble(), cTop.toDouble(), cWidth.toDouble(), cHeight.toDouble()); } - final fWidth = map['fullPanoWidth'] as int; - final fHeight = map['fullPanoHeight'] as int; Size fullPanoSize; if (fWidth != null && fHeight != null) { fullPanoSize = Size(fWidth.toDouble(), fHeight.toDouble()); @@ -30,11 +44,12 @@ class PanoramaInfo { return PanoramaInfo( croppedAreaRect: croppedAreaRect, fullPanoSize: fullPanoSize, + projectionType: projectionType, ); } bool get hasCroppedArea => croppedAreaRect != null && fullPanoSize != null; @override - String toString() => '$runtimeType#${shortHash(this)}{croppedAreaRect=$croppedAreaRect, fullPanoSize=$fullPanoSize}'; + String toString() => '$runtimeType#${shortHash(this)}{croppedAreaRect=$croppedAreaRect, fullPanoSize=$fullPanoSize, projectionType=$projectionType}'; } diff --git a/lib/model/settings/map_style.dart b/lib/model/settings/map_style.dart new file mode 100644 index 000000000..25559107a --- /dev/null +++ b/lib/model/settings/map_style.dart @@ -0,0 +1,34 @@ +// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ +enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor } + +extension ExtraEntryMapStyle on EntryMapStyle { + String get name { + switch (this) { + case EntryMapStyle.googleNormal: + return 'Google Maps'; + case EntryMapStyle.googleHybrid: + return 'Google Maps (Hybrid)'; + case EntryMapStyle.googleTerrain: + return 'Google Maps (Terrain)'; + case EntryMapStyle.osmHot: + return 'Humanitarian OSM'; + case EntryMapStyle.stamenToner: + return 'Stamen Toner'; + case EntryMapStyle.stamenWatercolor: + return 'Stamen Watercolor'; + default: + return toString(); + } + } + + bool get isGoogleMaps { + switch (this) { + case EntryMapStyle.googleNormal: + case EntryMapStyle.googleHybrid: + case EntryMapStyle.googleTerrain: + return true; + default: + return false; + } + } +} diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 3e5fe5246..fcc10a444 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -2,8 +2,8 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/coordinate_format.dart'; import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/home_page.dart'; +import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/screen_on.dart'; -import 'package:aves/widgets/viewer/info/location_section.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index 0dce9f30f..44b2789c1 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -1,5 +1,5 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/utils/android_file_utils.dart'; @@ -50,10 +50,10 @@ mixin AlbumMixin on SourceBase { } } - Map getAlbumEntries() { + Map getAlbumEntries() { final entries = sortedEntriesForFilterList; final regularAlbums = [], appAlbums = [], specialAlbums = []; - for (var album in sortedAlbums) { + for (final album in sortedAlbums) { switch (androidFileUtils.getAlbumType(album)) { case AlbumType.regular: regularAlbums.add(album); diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index f56893da9..a151de471 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:collection'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/tag.dart'; @@ -19,24 +19,28 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel EntryGroupFactor groupFactor; EntrySortFactor sortFactor; final AChangeNotifier filterChangeNotifier = AChangeNotifier(); + bool listenToSource; - List _filteredEntries; + List _filteredEntries; List _subscriptions = []; - Map> sections = Map.unmodifiable({}); + Map> sections = Map.unmodifiable({}); CollectionLens({ @required this.source, Iterable filters, @required EntryGroupFactor groupFactor, @required EntrySortFactor sortFactor, + this.listenToSource = true, }) : filters = {if (filters != null) ...filters.where((f) => f != null)}, groupFactor = groupFactor ?? EntryGroupFactor.month, sortFactor = sortFactor ?? EntrySortFactor.date { - _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); - _subscriptions.add(source.eventBus.on().listen((e) => onEntryRemoved(e.entries))); - _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); - _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); + if (listenToSource) { + _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); + _subscriptions.add(source.eventBus.on().listen((e) => onEntryRemoved(e.entries))); + _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); + _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); + } _refresh(); } @@ -49,23 +53,14 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel super.dispose(); } - CollectionLens derive(CollectionFilter filter) { - return CollectionLens( - source: source, - filters: filters, - groupFactor: groupFactor, - sortFactor: sortFactor, - )..addFilter(filter); - } - bool get isEmpty => _filteredEntries.isEmpty; int get entryCount => _filteredEntries.length; // sorted as displayed to the user, i.e. sorted then grouped, not an absolute order on all entries - List _sortedEntries; + List _sortedEntries; - List get sortedEntries { + List get sortedEntries { _sortedEntries ??= List.of(sections.entries.expand((e) => e.value)); return _sortedEntries; } @@ -82,7 +77,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel return true; } - Object heroTag(ImageEntry entry) => '$hashCode${entry.uri}'; + Object heroTag(AvesEntry entry) => entry.uri; void addFilter(CollectionFilter filter) { if (filter == null || filters.contains(filter)) return; @@ -123,13 +118,13 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel void _applySort() { switch (sortFactor) { case EntrySortFactor.date: - _filteredEntries.sort(ImageEntry.compareByDate); + _filteredEntries.sort(AvesEntry.compareByDate); break; case EntrySortFactor.size: - _filteredEntries.sort(ImageEntry.compareBySize); + _filteredEntries.sort(AvesEntry.compareBySize); break; case EntrySortFactor.name: - _filteredEntries.sort(ImageEntry.compareByName); + _filteredEntries.sort(AvesEntry.compareByName); break; } } @@ -139,13 +134,13 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel case EntrySortFactor.date: switch (groupFactor) { case EntryGroupFactor.album: - sections = groupBy(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory)); + sections = groupBy(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory)); break; case EntryGroupFactor.month: - sections = groupBy(_filteredEntries, (entry) => EntryDateSectionKey(entry.monthTaken)); + sections = groupBy(_filteredEntries, (entry) => EntryDateSectionKey(entry.monthTaken)); break; case EntryGroupFactor.day: - sections = groupBy(_filteredEntries, (entry) => EntryDateSectionKey(entry.dayTaken)); + sections = groupBy(_filteredEntries, (entry) => EntryDateSectionKey(entry.dayTaken)); break; case EntryGroupFactor.none: sections = Map.fromEntries([ @@ -160,8 +155,8 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel ]); break; case EntrySortFactor.name: - final byAlbum = groupBy(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory)); - sections = SplayTreeMap>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.folderPath, b.folderPath)); + final byAlbum = groupBy(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory)); + sections = SplayTreeMap>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.folderPath, b.folderPath)); break; } sections = Map.unmodifiable(sections); @@ -177,7 +172,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel _applyGroup(); } - void onEntryRemoved(Iterable entries) { + void onEntryRemoved(Iterable entries) { // we should remove obsolete entries and sections // but do not apply sort/group // as section order change would surprise the user while browsing @@ -207,18 +202,18 @@ mixin CollectionActivityMixin { mixin CollectionSelectionMixin on CollectionActivityMixin { final AChangeNotifier selectionChangeNotifier = AChangeNotifier(); - final Set _selection = {}; + final Set _selection = {}; - Set get selection => _selection; + Set get selection => _selection; - bool isSelected(Iterable entries) => entries.every(selection.contains); + bool isSelected(Iterable entries) => entries.every(selection.contains); - void addToSelection(Iterable entries) { + void addToSelection(Iterable entries) { _selection.addAll(entries); selectionChangeNotifier.notifyListeners(); } - void removeFromSelection(Iterable entries) { + void removeFromSelection(Iterable entries) { _selection.removeAll(entries); selectionChangeNotifier.notifyListeners(); } @@ -228,7 +223,7 @@ mixin CollectionSelectionMixin on CollectionActivityMixin { selectionChangeNotifier.notifyListeners(); } - void toggleSelection(ImageEntry entry) { + void toggleSelection(AvesEntry entry) { if (_selection.isEmpty) select(); if (!_selection.remove(entry)) _selection.add(entry); selectionChangeNotifier.notifyListeners(); diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index d3e669e18..137ca9786 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -1,30 +1,30 @@ import 'dart:async'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; -import 'package:aves/services/image_file_service.dart'; +import 'package:aves/services/image_op_events.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; import 'enums.dart'; mixin SourceBase { - final List _rawEntries = []; + final List _rawEntries = []; - List get rawEntries => List.unmodifiable(_rawEntries); + List get rawEntries => List.unmodifiable(_rawEntries); final EventBus _eventBus = EventBus(); EventBus get eventBus => _eventBus; - List get sortedEntriesForFilterList; + List get sortedEntriesForFilterList; final Map _filterEntryCountMap = {}; @@ -39,7 +39,7 @@ mixin SourceBase { abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { @override - List get sortedEntriesForFilterList => CollectionLens( + List get sortedEntriesForFilterList => CollectionLens( source: this, groupFactor: EntryGroupFactor.none, sortFactor: EntrySortFactor.date, @@ -55,7 +55,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM debugPrint('$runtimeType loadDates complete in ${stopwatch.elapsed.inMilliseconds}ms for ${_savedDates.length} entries'); } - void addAll(Iterable entries) { + void addAll(Iterable entries) { + if (entries.isEmpty) return; if (_rawEntries.isNotEmpty) { final newContentIds = entries.map((entry) => entry.contentId).toList(); _rawEntries.removeWhere((entry) => newContentIds.contains(entry.contentId)); @@ -70,7 +71,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM eventBus.fire(EntryAddedEvent()); } - void removeEntries(List entries) { + void removeEntries(List entries) { entries.forEach((entry) => entry.removeFromFavourites()); _rawEntries.removeWhere(entries.contains); cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet()); @@ -91,7 +92,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM // `dateModifiedSecs` changes when moving entries to another directory, // but it does not change when renaming the containing directory - Future moveEntry(ImageEntry entry, Map newFields) async { + Future moveEntry(AvesEntry entry, Map newFields) async { final oldContentId = entry.contentId; final newContentId = newFields['contentId'] as int; final newDateModifiedSecs = newFields['dateModifiedSecs'] as int; @@ -109,7 +110,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } void updateAfterMove({ - @required Set selection, + @required Set selection, @required bool copy, @required String destinationAlbum, @required Iterable movedOps, @@ -117,7 +118,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM if (movedOps.isEmpty) return; final fromAlbums = {}; - final movedEntries = []; + final movedEntries = []; if (copy) { movedOps.forEach((movedOp) { final sourceUri = movedOp.uri; @@ -164,27 +165,31 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length); } + bool get initialized => false; + + Future init(); + Future refresh(); - Future refreshMetadata(Set entries); + Future refreshMetadata(Set entries); } enum SourceState { loading, cataloguing, locating, ready } class EntryAddedEvent { - final ImageEntry entry; + final AvesEntry entry; const EntryAddedEvent([this.entry]); } class EntryRemovedEvent { - final Iterable entries; + final Iterable entries; const EntryRemovedEvent(this.entries); } class EntryMovedEvent { - final Iterable entries; + final Iterable entries; const EntryMovedEvent(this.entries); } diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index 6312bffeb..37d6c0af8 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -1,8 +1,9 @@ import 'dart:math'; +import 'package:aves/model/connectivity.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/location.dart'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:collection/collection.dart'; @@ -27,8 +28,10 @@ mixin LocationMixin on SourceBase { } Future locateEntries() async { + if (!(await connectivity.canGeolocate)) return; + // final stopwatch = Stopwatch()..start(); - final byLocated = groupBy(rawEntries.where((entry) => entry.hasGps), (entry) => entry.isLocated); + final byLocated = groupBy(rawEntries.where((entry) => entry.hasGps), (entry) => entry.isLocated); final todo = byLocated[false] ?? []; if (todo.isEmpty) return; @@ -42,7 +45,7 @@ mixin LocationMixin on SourceBase { // - 652 calls (22%) when approximating to 2 decimal places (~1km - town or village) // cf https://en.wikipedia.org/wiki/Decimal_degrees#Precision final latLngFactor = pow(10, 2); - Tuple2 approximateLatLng(ImageEntry entry) { + Tuple2 approximateLatLng(AvesEntry entry) { final lat = entry.catalogMetadata?.latitude; final lng = entry.catalogMetadata?.longitude; if (lat == null || lng == null) return null; @@ -57,7 +60,7 @@ mixin LocationMixin on SourceBase { setProgress(done: progressDone, total: progressTotal); final newAddresses = []; - await Future.forEach(todo, (entry) async { + await Future.forEach(todo, (entry) async { final latLng = approximateLatLng(entry); if (knownLocations.containsKey(latLng)) { entry.addressDetails = knownLocations[latLng]?.copyWith(contentId: entry.contentId); diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index c0afae1aa..ea0cd40f2 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -1,7 +1,8 @@ +import 'dart:async'; import 'dart:math'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/favourite_repo.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -13,6 +14,12 @@ import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:pedantic/pedantic.dart'; class MediaStoreSource extends CollectionSource { + bool _initialized = false; + + @override + bool get initialized => _initialized; + + @override Future init() async { final stopwatch = Stopwatch()..start(); stateNotifier.value = SourceState.loading; @@ -28,11 +35,13 @@ class MediaStoreSource extends CollectionSource { settings.catalogTimeZone = currentTimeZone; } await loadDates(); // 100ms for 5400 entries + _initialized = true; debugPrint('$runtimeType init done, elapsed=${stopwatch.elapsed}'); } @override Future refresh() async { + assert(_initialized); debugPrint('$runtimeType refresh start'); final stopwatch = Stopwatch()..start(); stateNotifier.value = SourceState.loading; @@ -40,8 +49,8 @@ class MediaStoreSource extends CollectionSource { final oldEntries = await metadataDb.loadEntries(); // 400ms for 5500 entries final knownEntryMap = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedSecs))); - final obsoleteEntries = (await ImageFileService.getObsoleteEntries(knownEntryMap.keys.toList())).toSet(); - oldEntries.removeWhere((entry) => obsoleteEntries.contains(entry.contentId)); + final obsoleteContentIds = (await ImageFileService.getObsoleteEntries(knownEntryMap.keys.toList())).toSet(); + oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId)); // show known entries addAll(oldEntries); @@ -50,19 +59,20 @@ class MediaStoreSource extends CollectionSource { debugPrint('$runtimeType refresh loaded ${oldEntries.length} known entries, elapsed=${stopwatch.elapsed}'); // clean up obsolete entries - metadataDb.removeIds(obsoleteEntries, updateFavourites: true); + metadataDb.removeIds(obsoleteContentIds, updateFavourites: true); // fetch new entries + // refresh after the first 10 entries, then after 100 more, then every 1000 entries var refreshCount = 10; const refreshCountMax = 1000; - final allNewEntries = [], pendingNewEntries = []; + final allNewEntries = [], pendingNewEntries = []; void addPendingEntries() { allNewEntries.addAll(pendingNewEntries); addAll(pendingNewEntries); pendingNewEntries.clear(); } - ImageFileService.getImageEntries(knownEntryMap).listen( + ImageFileService.getEntries(knownEntryMap).listen( (entry) { pendingNewEntries.add(entry); if (pendingNewEntries.length >= refreshCount) { @@ -95,8 +105,48 @@ class MediaStoreSource extends CollectionSource { ); } + Future refreshUris(List changedUris) async { + if (!_initialized) return; + + final uriByContentId = Map.fromEntries(changedUris.map((uri) { + if (uri == null) return null; + final idString = Uri.parse(uri).pathSegments.last; + final contentId = int.tryParse(idString); + if (contentId == null) return null; + return MapEntry(contentId, uri); + }).where((kv) => kv != null)); + + // clean up obsolete entries + final obsoleteContentIds = (await ImageFileService.getObsoleteEntries(uriByContentId.keys.toList())).toSet(); + uriByContentId.removeWhere((contentId, _) => obsoleteContentIds.contains(contentId)); + metadataDb.removeIds(obsoleteContentIds, updateFavourites: true); + + // add new entries + final newEntries = []; + for (final kv in uriByContentId.entries) { + final contentId = kv.key; + final uri = kv.value; + final sourceEntry = await ImageFileService.getEntry(uri, null); + final existingEntry = rawEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null); + if (existingEntry == null || sourceEntry.dateModifiedSecs > existingEntry.dateModifiedSecs) { + newEntries.add(sourceEntry); + } + } + addAll(newEntries); + await metadataDb.saveEntries(newEntries); + updateAlbums(); + + stateNotifier.value = SourceState.cataloguing; + await catalogEntries(); + + stateNotifier.value = SourceState.locating; + await locateEntries(); + + stateNotifier.value = SourceState.ready; + } + @override - Future refreshMetadata(Set entries) { + Future refreshMetadata(Set entries) { final contentIds = entries.map((entry) => entry.contentId).toSet(); metadataDb.removeIds(contentIds, updateFavourites: false); return refresh(); diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index 55a1053ca..d5b1ca3c0 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:collection/collection.dart'; @@ -31,7 +31,7 @@ mixin TagMixin on SourceBase { setProgress(done: progressDone, total: progressTotal); final newMetadata = []; - await Future.forEach(todo, (entry) async { + await Future.forEach(todo, (entry) async { await entry.catalog(background: true); if (entry.isCatalogued) { newMetadata.add(entry.catalogMetadata); diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index 016cffb27..9a379334d 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -8,12 +9,18 @@ import 'package:flutter/services.dart'; class AndroidAppService { static const platform = MethodChannel('deckers.thibault/aves/app'); - static Future getAppNames() async { + static Future> getPackages() async { try { - final result = await platform.invokeMethod('getAppNames'); - return result as Map; + final result = await platform.invokeMethod('getPackages'); + final packages = (result as List).cast().map((map) => Package.fromMap(map)).toSet(); + // additional info for known directories + final kakaoTalk = packages.firstWhere((package) => package.packageName == 'com.kakao.talk', orElse: () => null); + if (kakaoTalk != null) { + kakaoTalk.ownedDirs.add('KakaoTalkDownload'); + } + return packages; } on PlatformException catch (e) { - debugPrint('getAppNames failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); + debugPrint('getPackages failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } return {}; } @@ -81,10 +88,10 @@ class AndroidAppService { return false; } - static Future shareEntries(Iterable entries) async { + static Future shareEntries(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())); + final urisByMimeType = groupBy(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); try { return await platform.invokeMethod('share', { 'title': 'Share via:', diff --git a/lib/services/android_debug_service.dart b/lib/services/android_debug_service.dart index 31392df08..3815c5df8 100644 --- a/lib/services/android_debug_service.dart +++ b/lib/services/android_debug_service.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -26,7 +26,7 @@ class AndroidDebugService { return {}; } - static Future getBitmapFactoryInfo(ImageEntry entry) async { + static Future getBitmapFactoryInfo(AvesEntry entry) async { try { // return map with all data available when decoding image bounds with `BitmapFactory` final result = await platform.invokeMethod('getBitmapFactoryInfo', { @@ -39,7 +39,7 @@ class AndroidDebugService { return {}; } - static Future getContentResolverMetadata(ImageEntry entry) async { + static Future getContentResolverMetadata(AvesEntry entry) async { try { // return map with all data available from the content resolver final result = await platform.invokeMethod('getContentResolverMetadata', { @@ -53,7 +53,7 @@ class AndroidDebugService { return {}; } - static Future getExifInterfaceMetadata(ImageEntry entry) async { + static Future getExifInterfaceMetadata(AvesEntry entry) async { try { // return map with all data available from the `ExifInterface` library final result = await platform.invokeMethod('getExifInterfaceMetadata', { @@ -68,7 +68,7 @@ class AndroidDebugService { return {}; } - static Future getMediaMetadataRetrieverMetadata(ImageEntry entry) async { + static Future getMediaMetadataRetrieverMetadata(AvesEntry entry) async { try { // return map with all data available from `MediaMetadataRetriever` final result = await platform.invokeMethod('getMediaMetadataRetrieverMetadata', { @@ -81,7 +81,7 @@ class AndroidDebugService { return {}; } - static Future getMetadataExtractorSummary(ImageEntry entry) async { + static Future getMetadataExtractorSummary(AvesEntry entry) async { try { // return map with the mime type and tag count for each directory found by `metadata-extractor` final result = await platform.invokeMethod('getMetadataExtractorSummary', { @@ -96,7 +96,7 @@ class AndroidDebugService { return {}; } - static Future getTiffStructure(ImageEntry entry) async { + static Future getTiffStructure(AvesEntry entry) async { if (entry.mimeType != MimeTypes.tiff) return {}; try { diff --git a/lib/services/android_file_service.dart b/lib/services/android_file_service.dart index 0a0bb2f12..7b8668315 100644 --- a/lib/services/android_file_service.dart +++ b/lib/services/android_file_service.dart @@ -9,14 +9,14 @@ class AndroidFileService { static const platform = MethodChannel('deckers.thibault/aves/storage'); static final StreamsChannel storageAccessChannel = StreamsChannel('deckers.thibault/aves/storageaccessstream'); - static Future> getStorageVolumes() async { + static Future> getStorageVolumes() async { try { final result = await platform.invokeMethod('getStorageVolumes'); - return (result as List).cast(); + return (result as List).cast().map((map) => StorageVolume.fromMap(map)).toSet(); } on PlatformException catch (e) { debugPrint('getStorageVolumes failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } - return []; + return {}; } static Future getFreeSpace(StorageVolume volume) async { diff --git a/lib/services/app_shortcut_service.dart b/lib/services/app_shortcut_service.dart index 53b6c8f0c..02a3f76f6 100644 --- a/lib/services/app_shortcut_service.dart +++ b/lib/services/app_shortcut_service.dart @@ -1,7 +1,7 @@ import 'dart:typed_data'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -26,18 +26,18 @@ class AppShortcutService { return false; } - static Future pin(String label, ImageEntry iconEntry, Set filters) async { + static Future pin(String label, AvesEntry entry, Set filters) async { Uint8List iconBytes; - if (iconEntry != null) { - final size = iconEntry.isVideo ? 0.0 : 256.0; + if (entry != null) { + final size = entry.isVideo ? 0.0 : 256.0; iconBytes = await ImageFileService.getThumbnail( - iconEntry.uri, - iconEntry.mimeType, - iconEntry.dateModifiedSecs, - iconEntry.rotationDegrees, - iconEntry.isFlipped, - size, - size, + uri: entry.uri, + mimeType: entry.mimeType, + pageId: entry.pageId, + rotationDegrees: entry.rotationDegrees, + isFlipped: entry.isFlipped, + dateModifiedSecs: entry.dateModifiedSecs, + extent: size, ); } try { diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 5ad41ffb2..723d1312e 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -3,12 +3,12 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; +import 'package:aves/services/image_op_events.dart'; import 'package:aves/services/service_policy.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:streams_channel/streams_channel.dart'; class ImageFileService { @@ -18,10 +18,11 @@ class ImageFileService { static final StreamsChannel opChannel = StreamsChannel('deckers.thibault/aves/imageopstream'); static const double thumbnailDefaultSize = 64.0; - static Map _toPlatformEntryMap(ImageEntry entry) { + static Map _toPlatformEntryMap(AvesEntry entry) { return { 'uri': entry.uri, 'path': entry.path, + 'pageId': entry.pageId, 'mimeType': entry.mimeType, 'width': entry.width, 'height': entry.height, @@ -32,13 +33,13 @@ class ImageFileService { } // knownEntries: map of contentId -> dateModifiedSecs - static Stream getImageEntries(Map knownEntries) { + static Stream getEntries(Map knownEntries) { try { return mediaStoreChannel.receiveBroadcastStream({ 'knownEntries': knownEntries, - }).map((event) => ImageEntry.fromMap(event)); + }).map((event) => AvesEntry.fromMap(event)); } on PlatformException catch (e) { - debugPrint('getImageEntries failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + debugPrint('getEntries failed with code=${e.code}, exception=${e.message}, details=${e.details}'); return Stream.error(e); } } @@ -55,26 +56,40 @@ class ImageFileService { return []; } - static Future getImageEntry(String uri, String mimeType) async { - debugPrint('getImageEntry for uri=$uri, mimeType=$mimeType'); + static Future getEntry(String uri, String mimeType) async { try { - final result = await platform.invokeMethod('getImageEntry', { + final result = await platform.invokeMethod('getEntry', { 'uri': uri, 'mimeType': mimeType, }) as Map; - return ImageEntry.fromMap(result); + return AvesEntry.fromMap(result); } on PlatformException catch (e) { - debugPrint('getImageEntry failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + debugPrint('getEntry failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } return null; } + static Future getSvg( + String uri, + String mimeType, { + int expectedContentLength, + BytesReceivedCallback onBytesReceived, + }) => + getImage( + uri, + mimeType, + 0, + false, + expectedContentLength: expectedContentLength, + onBytesReceived: onBytesReceived, + ); + static Future getImage( String uri, String mimeType, int rotationDegrees, bool isFlipped, { - int page = 0, + int pageId, int expectedContentLength, BytesReceivedCallback onBytesReceived, }) { @@ -87,7 +102,7 @@ class ImageFileService { 'mimeType': mimeType, 'rotationDegrees': rotationDegrees ?? 0, 'isFlipped': isFlipped ?? false, - 'page': page ?? 0, + 'pageId': pageId, }).listen( (data) { final chunk = data as Uint8List; @@ -125,7 +140,7 @@ class ImageFileService { int sampleSize, Rectangle regionRect, Size imageSize, { - int page = 0, + int pageId, Object taskKey, int priority, }) { @@ -135,7 +150,7 @@ class ImageFileService { final result = await platform.invokeMethod('getRegion', { 'uri': uri, 'mimeType': mimeType, - 'page': page, + 'pageId': pageId, 'sampleSize': sampleSize, 'regionX': regionRect.left, 'regionY': regionRect.top, @@ -155,15 +170,14 @@ class ImageFileService { ); } - static Future getThumbnail( - String uri, - String mimeType, - int dateModifiedSecs, - int rotationDegrees, - bool isFlipped, - double width, - double height, { - int page, + static Future getThumbnail({ + @required String uri, + @required String mimeType, + @required int rotationDegrees, + @required int pageId, + @required bool isFlipped, + @required int dateModifiedSecs, + @required double extent, Object taskKey, int priority, }) { @@ -179,9 +193,9 @@ class ImageFileService { 'dateModifiedSecs': dateModifiedSecs, 'rotationDegrees': rotationDegrees, 'isFlipped': isFlipped, - 'widthDip': width, - 'heightDip': height, - 'page': page, + 'widthDip': extent, + 'heightDip': extent, + 'pageId': pageId, 'defaultSizeDip': thumbnailDefaultSize, }); return result as Uint8List; @@ -191,7 +205,7 @@ class ImageFileService { return null; }, // debugLabel: 'getThumbnail width=$width, height=$height entry=${entry.filenameWithoutExtension}', - priority: priority ?? (width == 0 || height == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), + priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), key: taskKey, ); } @@ -210,7 +224,7 @@ class ImageFileService { static Future resumeLoading(Object taskKey) => servicePolicy.resume(taskKey); - static Stream delete(Iterable entries) { + static Stream delete(Iterable entries) { try { return opChannel.receiveBroadcastStream({ 'op': 'delete', @@ -222,7 +236,11 @@ class ImageFileService { } } - static Stream move(Iterable entries, {@required bool copy, @required String destinationAlbum}) { + static Stream move( + Iterable entries, { + @required bool copy, + @required String destinationAlbum, + }) { try { return opChannel.receiveBroadcastStream({ 'op': 'move', @@ -236,7 +254,25 @@ class ImageFileService { } } - static Future rename(ImageEntry entry, String newName) async { + static Stream export( + Iterable entries, { + String mimeType = MimeTypes.jpeg, + @required String destinationAlbum, + }) { + try { + return opChannel.receiveBroadcastStream({ + 'op': 'export', + 'entries': entries.map(_toPlatformEntryMap).toList(), + 'mimeType': mimeType, + 'destinationPath': destinationAlbum, + }).map((event) => ExportOpEvent.fromMap(event)); + } on PlatformException catch (e) { + debugPrint('export failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + return Stream.error(e); + } + } + + static Future rename(AvesEntry entry, String newName) async { try { // return map with: 'contentId' 'path' 'title' 'uri' (all optional) final result = await platform.invokeMethod('rename', { @@ -250,7 +286,7 @@ class ImageFileService { return {}; } - static Future rotate(ImageEntry entry, {@required bool clockwise}) async { + static Future rotate(AvesEntry entry, {@required bool clockwise}) async { try { // return map with: 'rotationDegrees' 'isFlipped' final result = await platform.invokeMethod('rotate', { @@ -264,7 +300,7 @@ class ImageFileService { return {}; } - static Future flip(ImageEntry entry) async { + static Future flip(AvesEntry entry) async { try { // return map with: 'rotationDegrees' 'isFlipped' final result = await platform.invokeMethod('flip', { @@ -278,57 +314,6 @@ class ImageFileService { } } -@immutable -class ImageOpEvent { - final bool success; - final String uri; - - const ImageOpEvent({ - this.success, - this.uri, - }); - - factory ImageOpEvent.fromMap(Map map) { - return ImageOpEvent( - success: map['success'] ?? false, - uri: map['uri'], - ); - } - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) return false; - return other is ImageOpEvent && other.success == success && other.uri == uri; - } - - @override - int get hashCode => hashValues(success, uri); - - @override - String toString() => '$runtimeType#${shortHash(this)}{success=$success, uri=$uri}'; -} - -class MoveOpEvent extends ImageOpEvent { - final Map newFields; - - const MoveOpEvent({bool success, String uri, this.newFields}) - : super( - success: success, - uri: uri, - ); - - factory MoveOpEvent.fromMap(Map map) { - return MoveOpEvent( - success: map['success'] ?? false, - uri: map['uri'], - newFields: map['newFields'], - ); - } - - @override - String toString() => '$runtimeType#${shortHash(this)}{success=$success, uri=$uri, newFields=$newFields}'; -} - // cf flutter/foundation `consolidateHttpClientResponseBytes` typedef BytesReceivedCallback = void Function(int cumulative, int total); diff --git a/lib/services/image_op_events.dart b/lib/services/image_op_events.dart new file mode 100644 index 000000000..2f30d8fe7 --- /dev/null +++ b/lib/services/image_op_events.dart @@ -0,0 +1,85 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +@immutable +class ImageOpEvent { + final bool success; + final String uri; + + const ImageOpEvent({ + this.success, + this.uri, + }); + + factory ImageOpEvent.fromMap(Map map) { + return ImageOpEvent( + success: map['success'] ?? false, + uri: map['uri'], + ); + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is ImageOpEvent && other.success == success && other.uri == uri; + } + + @override + int get hashCode => hashValues(success, uri); + + @override + String toString() => '$runtimeType#${shortHash(this)}{success=$success, uri=$uri}'; +} + +class MoveOpEvent extends ImageOpEvent { + final Map newFields; + + const MoveOpEvent({bool success, String uri, this.newFields}) + : super( + success: success, + uri: uri, + ); + + factory MoveOpEvent.fromMap(Map map) { + return MoveOpEvent( + success: map['success'] ?? false, + uri: map['uri'], + newFields: map['newFields'], + ); + } + + @override + String toString() => '$runtimeType#${shortHash(this)}{success=$success, uri=$uri, newFields=$newFields}'; +} + +class ExportOpEvent extends MoveOpEvent { + final int pageId; + + const ExportOpEvent({bool success, String uri, this.pageId, Map newFields}) + : super( + success: success, + uri: uri, + newFields: newFields, + ); + + factory ExportOpEvent.fromMap(Map map) { + return ExportOpEvent( + success: map['success'] ?? false, + uri: map['uri'], + pageId: map['pageId'], + newFields: map['newFields'], + ); + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is ExportOpEvent && other.success == success && other.uri == uri && other.pageId == pageId; + } + + @override + int get hashCode => hashValues(success, uri, pageId); + + @override + String toString() => '$runtimeType#${shortHash(this)}{success=$success, uri=$uri, pageId=$pageId, newFields=$newFields}'; +} diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index 9a53587c6..d55799255 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -1,7 +1,7 @@ import 'dart:typed_data'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/model/panorama.dart'; import 'package:aves/services/service_policy.dart'; @@ -12,7 +12,7 @@ class MetadataService { static const platform = MethodChannel('deckers.thibault/aves/metadata'); // return Map> (map of directories, each directory being a map of metadata label and value description) - static Future getAllMetadata(ImageEntry entry) async { + static Future getAllMetadata(AvesEntry entry) async { if (entry.isSvg) return null; try { @@ -28,7 +28,7 @@ class MetadataService { return {}; } - static Future getCatalogMetadata(ImageEntry entry, {bool background = false}) async { + static Future getCatalogMetadata(AvesEntry entry, {bool background = false}) async { if (entry.isSvg) return null; Future call() async { @@ -65,7 +65,7 @@ class MetadataService { : call(); } - static Future getOverlayMetadata(ImageEntry entry) async { + static Future getOverlayMetadata(AvesEntry entry) async { if (entry.isSvg) return null; try { @@ -82,20 +82,21 @@ class MetadataService { return null; } - static Future getMultiPageInfo(ImageEntry entry) async { + static Future getMultiPageInfo(AvesEntry entry) async { try { final result = await platform.invokeMethod('getMultiPageInfo', { 'mimeType': entry.mimeType, 'uri': entry.uri, - }) as Map; - return MultiPageInfo.fromMap(result); + }); + final pageMaps = (result as List).cast(); + return MultiPageInfo.fromPageMaps(pageMaps); } on PlatformException catch (e) { debugPrint('getMultiPageInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } return null; } - static Future getPanoramaInfo(ImageEntry entry) async { + static Future getPanoramaInfo(AvesEntry entry) async { try { // return map with values for: // 'croppedAreaLeft' (int), 'croppedAreaTop' (int), 'croppedAreaWidth' (int), 'croppedAreaHeight' (int), @@ -112,6 +113,19 @@ class MetadataService { return null; } + static Future getContentResolverProp(AvesEntry entry, String prop) async { + try { + return await platform.invokeMethod('getContentResolverProp', { + 'mimeType': entry.mimeType, + 'uri': entry.uri, + 'prop': prop, + }); + } on PlatformException catch (e) { + debugPrint('getContentResolverProp failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + } + return null; + } + static Future> getEmbeddedPictures(String uri) async { try { final result = await platform.invokeMethod('getEmbeddedPictures', { @@ -124,7 +138,7 @@ class MetadataService { return []; } - static Future> getExifThumbnails(ImageEntry entry) async { + static Future> getExifThumbnails(AvesEntry entry) async { try { final result = await platform.invokeMethod('getExifThumbnails', { 'mimeType': entry.mimeType, @@ -138,7 +152,7 @@ class MetadataService { return []; } - static Future extractXmpDataProp(ImageEntry entry, String propPath, String propMimeType) async { + static Future extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType) async { try { final result = await platform.invokeMethod('extractXmpDataProp', { 'mimeType': entry.mimeType, diff --git a/lib/services/svg_metadata_service.dart b/lib/services/svg_metadata_service.dart index 8d09750bc..b5823e7ff 100644 --- a/lib/services/svg_metadata_service.dart +++ b/lib/services/svg_metadata_service.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/utils/string_utils.dart'; import 'package:flutter/foundation.dart'; @@ -15,9 +15,9 @@ class SvgMetadataService { static const _textElements = ['title', 'desc']; static const _metadataElement = 'metadata'; - static Future getSize(ImageEntry entry) async { + static Future getSize(AvesEntry entry) async { try { - final data = await ImageFileService.getImage(entry.uri, entry.mimeType, 0, false); + final data = await ImageFileService.getSvg(entry.uri, entry.mimeType); final document = XmlDocument.parse(utf8.decode(data)); final root = document.rootElement; @@ -48,7 +48,7 @@ class SvgMetadataService { return null; } - static Future>> getAllMetadata(ImageEntry entry) async { + static Future>> getAllMetadata(AvesEntry entry) async { String formatKey(String key) { switch (key) { case 'desc': @@ -59,7 +59,7 @@ class SvgMetadataService { } try { - final data = await ImageFileService.getImage(entry.uri, entry.mimeType, 0, false); + final data = await ImageFileService.getSvg(entry.uri, entry.mimeType); final document = XmlDocument.parse(utf8.decode(data)); final root = document.rootElement; diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 59a0f2a64..9cc883fda 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -48,4 +48,11 @@ class Durations { static const doubleBackTimerDelay = Duration(milliseconds: 1000); static const softKeyboardDisplayDelay = Duration(milliseconds: 300); static const searchDebounceDelay = Duration(milliseconds: 250); + + // Content change monitoring delay should be large enough, + // so that querying the Media Store yields final entries. + // For example, when taking a picture with a Galaxy S10e default camera app, + // querying the Media Store just 1 second after sometimes yields an entry with + // its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg` + static const contentChangeDebounceDelay = Duration(milliseconds: 1500); } diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 5cdf7f2b5..2ff083f0e 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -31,6 +31,7 @@ class AIcons { static const IconData createAlbum = Icons.add_circle_outline; static const IconData debug = Icons.whatshot_outlined; static const IconData delete = Icons.delete_outlined; + static const IconData export = Icons.save_alt_outlined; static const IconData flip = Icons.flip_outlined; static const IconData favourite = Icons.favorite_border; static const IconData favouriteActive = Icons.favorite; @@ -38,7 +39,7 @@ class AIcons { static const IconData group = Icons.group_work_outlined; static const IconData info = Icons.info_outlined; static const IconData layers = Icons.layers_outlined; - static const IconData openInNew = Icons.open_in_new_outlined; + static const IconData openOutside = Icons.open_in_new_outlined; static const IconData pin = Icons.push_pin_outlined; static const IconData print = Icons.print_outlined; static const IconData refresh = Icons.refresh_outlined; diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index eec38cddf..949e3424a 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -1,6 +1,7 @@ import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:aves/utils/change_notifier.dart'; +import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); @@ -8,14 +9,17 @@ final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); class AndroidFileUtils { String primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath; Set storageVolumes = {}; - Map appNameMap = {}; + Set _packages = {}; + List _potentialAppDirs = []; AChangeNotifier appNameChangeNotifier = AChangeNotifier(); + Iterable get _launcherPackages => _packages.where((package) => package.categoryLauncher); + AndroidFileUtils._private(); Future init() async { - storageVolumes = (await AndroidFileService.getStorageVolumes()).map((map) => StorageVolume.fromMap(map)).toSet(); + storageVolumes = await AndroidFileService.getStorageVolumes(); // path_provider getExternalStorageDirectory() gives '/storage/emulated/0/Android/data/deckers.thibault.aves/files' primaryStorage = storageVolumes.firstWhere((volume) => volume.isPrimary).path; dcimPath = join(primaryStorage, 'DCIM'); @@ -25,8 +29,8 @@ class AndroidFileUtils { } Future initAppNames() async { - appNameMap = await AndroidAppService.getAppNames() - ..addAll({'KakaoTalkDownload': 'com.kakao.talk'}); + _packages = await AndroidAppService.getPackages(); + _potentialAppDirs = _launcherPackages.expand((package) => package.potentialDirs).toList(); appNameChangeNotifier.notifyListeners(); } @@ -42,27 +46,67 @@ class AndroidFileUtils { bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false; - AlbumType getAlbumType(String albumDirectory) { - if (albumDirectory != null) { - if (isCameraPath(albumDirectory)) return AlbumType.camera; - if (isDownloadPath(albumDirectory)) return AlbumType.download; - if (isScreenRecordingsPath(albumDirectory)) return AlbumType.screenRecordings; - if (isScreenshotsPath(albumDirectory)) return AlbumType.screenshots; + AlbumType getAlbumType(String albumPath) { + if (albumPath != null) { + if (isCameraPath(albumPath)) return AlbumType.camera; + if (isDownloadPath(albumPath)) return AlbumType.download; + if (isScreenRecordingsPath(albumPath)) return AlbumType.screenRecordings; + if (isScreenshotsPath(albumPath)) return AlbumType.screenshots; - final parts = albumDirectory.split(separator); - if (albumDirectory.startsWith(primaryStorage) && appNameMap.keys.contains(parts.last)) return AlbumType.app; + final dir = albumPath.split(separator).last; + if (albumPath.startsWith(primaryStorage) && _potentialAppDirs.contains(dir)) return AlbumType.app; } return AlbumType.regular; } - String getAlbumAppPackageName(String albumDirectory) { - final parts = albumDirectory.split(separator); - return appNameMap[parts.last]; + String getAlbumAppPackageName(String albumPath) { + if (albumPath == null) return null; + final dir = albumPath.split(separator).last; + final package = _launcherPackages.firstWhere((package) => package.potentialDirs.contains(dir), orElse: () => null); + return package?.packageName; + } + + String getCurrentAppName(String packageName) { + final package = _packages.firstWhere((package) => package.packageName == packageName, orElse: () => null); + return package?.currentLabel; } } enum AlbumType { regular, app, camera, download, screenRecordings, screenshots } +class Package { + final String packageName, currentLabel, englishLabel; + final bool categoryLauncher, isSystem; + final Set ownedDirs = {}; + + Package({ + this.packageName, + this.currentLabel, + this.englishLabel, + this.categoryLauncher, + this.isSystem, + }); + + factory Package.fromMap(Map map) { + return Package( + packageName: map['packageName'], + currentLabel: map['currentLabel'], + englishLabel: map['englishLabel'], + categoryLauncher: map['categoryLauncher'], + isSystem: map['isSystem'], + ); + } + + Set get potentialDirs => [ + currentLabel, + englishLabel, + ...ownedDirs, + ].where((dir) => dir != null).toSet(); + + @override + String toString() => '$runtimeType#${shortHash(this)}{packageName=$packageName, categoryLauncher=$categoryLauncher, isSystem=$isSystem, currentLabel=$currentLabel, englishLabel=$englishLabel, ownedDirs=$ownedDirs}'; +} + class StorageVolume { final String description, path, state; final bool isEmulated, isPrimary, isRemovable; diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index b31208632..87453882c 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -29,14 +29,14 @@ class Constants { Dependency( name: 'AndroidX Core-KTX', license: 'Apache 2.0', - licenseUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/LICENSE.txt', - sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/core/core-ktx', + licenseUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt', + sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/core/core-ktx', ), Dependency( name: 'AndroidX Exifinterface', license: 'Apache 2.0', - licenseUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/LICENSE.txt', - sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/exifinterface/exifinterface', + licenseUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt', + sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/exifinterface/exifinterface', ), Dependency( name: 'Android-TiffBitmapFactory', @@ -83,18 +83,18 @@ class Constants { licenseUrl: 'https://github.com/dart-lang/collection/blob/master/LICENSE', sourceUrl: 'https://github.com/dart-lang/collection', ), + Dependency( + name: 'Connectivity', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity/LICENSE', + sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity', + ), Dependency( name: 'Decorated Icon', license: 'MIT', licenseUrl: 'https://github.com/benPesso/flutter_decorated_icon/blob/master/LICENSE', sourceUrl: 'https://github.com/benPesso/flutter_decorated_icon', ), - Dependency( - name: 'Draggable Scrollbar', - license: 'MIT', - licenseUrl: 'https://github.com/fluttercommunity/flutter-draggable-scrollbar/blob/master/LICENSE', - sourceUrl: 'https://github.com/fluttercommunity/flutter-draggable-scrollbar', - ), Dependency( name: 'Event Bus', license: 'MIT', diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index ba71b34c9..34fc10f68 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -1,7 +1,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/collection/thumbnail_collection.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; -import 'package:aves/widgets/common/gesture_area_protector.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/drawer/app_drawer.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 2d132398d..6c3276361 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'package:aves/model/actions/collection_actions.dart'; import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/actions/move_type.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; +import 'package:aves/services/image_op_events.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; @@ -22,7 +24,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware CollectionSource get source => collection.source; - Set get selection => collection.selection; + Set get selection => collection.selection; EntrySetActionDelegate({ @required this.collection, @@ -46,10 +48,10 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware void onCollectionActionSelected(BuildContext context, CollectionAction action) { switch (action) { case CollectionAction.copy: - _moveSelection(context, copy: true); + _moveSelection(context, moveType: MoveType.copy); break; case CollectionAction.move: - _moveSelection(context, copy: false); + _moveSelection(context, moveType: MoveType.move); break; case CollectionAction.refreshMetadata: source.refreshMetadata(selection); @@ -61,12 +63,12 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware } } - Future _moveSelection(BuildContext context, {@required bool copy}) async { + Future _moveSelection(BuildContext context, {@required MoveType moveType}) async { final destinationAlbum = await Navigator.push( context, MaterialPageRoute( settings: RouteSettings(name: AlbumPickPage.routeName), - builder: (context) => AlbumPickPage(source: source, copy: copy), + builder: (context) => AlbumPickPage(source: source, moveType: moveType), ), ); if (destinationAlbum == null || destinationAlbum.isEmpty) return; @@ -74,16 +76,17 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware if (!await checkStoragePermission(context, selection)) return; - if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, copy)) return; + if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, moveType)) return; + final copy = moveType == MoveType.copy; + final selectionCount = selection.length; showOpReport( context: context, - selection: selection, opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: destinationAlbum), + itemCount: selectionCount, onDone: (processed) async { final movedOps = processed.where((e) => e.success); final movedCount = movedOps.length; - final selectionCount = selection.length; if (movedCount < selectionCount) { final count = selectionCount - movedCount; showFeedback(context, 'Failed to ${copy ? 'copy' : 'move'} ${Intl.plural(count, one: '$count item', other: '$count items')}'); @@ -129,14 +132,14 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware if (!await checkStoragePermission(context, selection)) return; + final selectionCount = selection.length; showOpReport( context: context, - selection: selection, opStream: ImageFileService.delete(selection), + itemCount: selectionCount, onDone: (processed) { final deletedUris = processed.where((e) => e.success).map((e) => e.uri).toList(); final deletedCount = deletedUris.length; - final selectionCount = selection.length; if (deletedCount < selectionCount) { final count = selectionCount - deletedCount; showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}'); diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index 230e1cfea..10e922443 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -25,7 +25,7 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget { } class _FilterBarState extends State { - final GlobalKey _animatedListKey = GlobalKey(); + final GlobalKey _animatedListKey = GlobalKey(debugLabel: 'filter-bar-animated-list'); CollectionFilter _userRemovedFilter; @override diff --git a/lib/widgets/collection/grid/section_layout.dart b/lib/widgets/collection/grid/section_layout.dart index 3dad0d735..c481e2ce1 100644 --- a/lib/widgets/collection/grid/section_layout.dart +++ b/lib/widgets/collection/grid/section_layout.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/widgets/collection/grid/headers/any.dart'; @@ -6,7 +6,7 @@ import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider { +class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider { final CollectionLens collection; const SectionedEntryListLayoutProvider({ @@ -14,7 +14,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider collection.showHeaders; @override - Map> get sections => collection.sections; + Map> get sections => collection.sections; @override double getHeaderExtent(BuildContext context, SectionKey sectionKey) { diff --git a/lib/widgets/collection/grid/selector.dart b/lib/widgets/collection/grid/selector.dart index 83c38e506..be1f48d66 100644 --- a/lib/widgets/collection/grid/selector.dart +++ b/lib/widgets/collection/grid/selector.dart @@ -1,9 +1,10 @@ import 'dart:async'; import 'dart:math'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/utils/math_utils.dart'; +import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -38,7 +39,7 @@ class _GridSelectionGestureDetectorState extends State widget.collection; - List get entries => collection.sortedEntries; + List get entries => collection.sortedEntries; ScrollController get scrollController => widget.scrollController; @@ -62,7 +63,7 @@ class _GridSelectionGestureDetectorState extends State().viewInsets.bottom, + bottom: context.read().effectiveBottomPadding, ); _scrollSpeedFactor = 0; _pressing = true; @@ -130,12 +131,12 @@ class _GridSelectionGestureDetectorState extends State>(); + final sectionedListLayout = context.read>(); return sectionedListLayout.getItemAt(offset); } diff --git a/lib/widgets/collection/grid/thumbnail.dart b/lib/widgets/collection/grid/thumbnail.dart index 64866df0b..3eabb767c 100644 --- a/lib/widgets/collection/grid/thumbnail.dart +++ b/lib/widgets/collection/grid/thumbnail.dart @@ -1,5 +1,5 @@ import 'package:aves/main.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/viewer_service.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; class InteractiveThumbnail extends StatelessWidget { final CollectionLens collection; - final ImageEntry entry; + final AvesEntry entry; final double tileExtent; final ValueNotifier isScrollingNotifier; @@ -53,9 +53,15 @@ class InteractiveThumbnail extends StatelessWidget { Navigator.push( context, TransparentMaterialPageRoute( - settings: RouteSettings(name: MultiEntryViewerPage.routeName), - pageBuilder: (c, a, sa) => MultiEntryViewerPage( - collection: collection, + settings: RouteSettings(name: EntryViewerPage.routeName), + pageBuilder: (c, a, sa) => EntryViewerPage( + collection: CollectionLens( + source: collection.source, + filters: collection.filters, + groupFactor: collection.groupFactor, + sortFactor: collection.sortFactor, + listenToSource: false, + ), initialEntry: entry, ), ), diff --git a/lib/widgets/collection/thumbnail/decorated.dart b/lib/widgets/collection/thumbnail/decorated.dart index 724000503..40720893f 100644 --- a/lib/widgets/collection/thumbnail/decorated.dart +++ b/lib/widgets/collection/thumbnail/decorated.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/collection/thumbnail/overlay.dart'; import 'package:aves/widgets/collection/thumbnail/raster.dart'; @@ -6,7 +6,7 @@ import 'package:aves/widgets/collection/thumbnail/vector.dart'; import 'package:flutter/material.dart'; class DecoratedThumbnail extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; final CollectionLens collection; final ValueNotifier isScrollingNotifier; diff --git a/lib/widgets/collection/thumbnail/error.dart b/lib/widgets/collection/thumbnail/error.dart index f66274fde..6b531adeb 100644 --- a/lib/widgets/collection/thumbnail/error.dart +++ b/lib/widgets/collection/thumbnail/error.dart @@ -1,9 +1,9 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/utils/mime_utils.dart'; import 'package:flutter/material.dart'; class ErrorThumbnail extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; final String tooltip; diff --git a/lib/widgets/collection/thumbnail/overlay.dart b/lib/widgets/collection/thumbnail/overlay.dart index 771faf505..4858f28b3 100644 --- a/lib/widgets/collection/thumbnail/overlay.dart +++ b/lib/widgets/collection/thumbnail/overlay.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:aves/model/highlight.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/enums.dart'; @@ -14,7 +14,7 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class ThumbnailEntryOverlay extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; const ThumbnailEntryOverlay({ @@ -61,7 +61,7 @@ class ThumbnailEntryOverlay extends StatelessWidget { } class ThumbnailSelectionOverlay extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; const ThumbnailSelectionOverlay({ @@ -121,7 +121,7 @@ class ThumbnailSelectionOverlay extends StatelessWidget { } class ThumbnailHighlightOverlay extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; const ThumbnailHighlightOverlay({ @@ -137,7 +137,7 @@ class ThumbnailHighlightOverlay extends StatefulWidget { class _ThumbnailHighlightOverlayState extends State { final ValueNotifier _highlightedNotifier = ValueNotifier(false); - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override Widget build(BuildContext context) { diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index 660a1159d..5a37c8367 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -1,17 +1,14 @@ -import 'dart:math'; - import 'package:aves/image_providers/thumbnail_provider.dart'; -import 'package:aves/image_providers/uri_image_provider.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/thumbnail/error.dart'; import 'package:aves/widgets/common/fx/transition_image.dart'; import 'package:flutter/material.dart'; class RasterImageThumbnail extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; - final int page; final ValueNotifier isScrollingNotifier; final Object heroTag; @@ -19,7 +16,6 @@ class RasterImageThumbnail extends StatefulWidget { Key key, @required this.entry, @required this.extent, - this.page = 0, this.isScrollingNotifier, this.heroTag, }) : super(key: key); @@ -31,19 +27,12 @@ class RasterImageThumbnail extends StatefulWidget { class _RasterImageThumbnailState extends State { ThumbnailProvider _fastThumbnailProvider, _sizedThumbnailProvider; - ImageEntry get entry => widget.entry; - - int get page => widget.page; + AvesEntry get entry => widget.entry; double get extent => widget.extent; Object get heroTag => widget.heroTag; - // we standardize the thumbnail loading dimension by taking the nearest larger power of 2 - // so that there are less variants of the thumbnails to load and cache - // it increases the chance of cache hit when loading similarly sized columns (e.g. on orientation change) - double get requestExtent => pow(2, (log(extent) / log(2)).ceil()).toDouble(); - @override void initState() { super.initState(); @@ -78,14 +67,8 @@ class _RasterImageThumbnailState extends State { void _initProvider() { if (!entry.canDecode) return; - _fastThumbnailProvider = ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry, page: page), - ); - if (!entry.isVideo) { - _sizedThumbnailProvider = ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry, page: page, extent: requestExtent), - ); - } + _fastThumbnailProvider = entry.getThumbnail(); + _sizedThumbnailProvider = entry.getThumbnail(extent: extent); } void _pauseProvider() { @@ -148,22 +131,8 @@ class _RasterImageThumbnailState extends State { : Hero( tag: heroTag, flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { - ImageProvider heroImageProvider = _fastThumbnailProvider; - if (!entry.isVideo) { - final imageProvider = UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - page: page, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ); - if (imageCache.statusForKey(imageProvider).keepAlive) { - heroImageProvider = imageProvider; - } - } return TransitionImage( - image: heroImageProvider, + image: entry.getBestThumbnail(extent), animation: animation, ); }, diff --git a/lib/widgets/collection/thumbnail/vector.dart b/lib/widgets/collection/thumbnail/vector.dart index 54cb811b9..c930d9afa 100644 --- a/lib/widgets/collection/thumbnail/vector.dart +++ b/lib/widgets/collection/thumbnail/vector.dart @@ -1,5 +1,5 @@ import 'package:aves/image_providers/uri_picture_provider.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; @@ -8,7 +8,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; class VectorImageThumbnail extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; final Object heroTag; @@ -29,10 +29,10 @@ class VectorImageThumbnail extends StatelessWidget { return LayoutBuilder( builder: (context, constraints) { final availableSize = constraints.biggest; - final fitSize = applyBoxFit(fit, entry.getDisplaySize(), availableSize).destination; + final fitSize = applyBoxFit(fit, entry.displaySize, availableSize).destination; final offset = fitSize / 2 - availableSize / 2; - final child = DecoratedBox( - decoration: CheckeredDecoration(checkSize: extent / 8, offset: offset), + final child = CustomPaint( + painter: CheckeredPainter(checkSize: extent / 8, offset: offset), child: SvgPicture( UriPicture( uri: entry.uri, diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index f5530347b..aa2c848c9 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -4,7 +4,7 @@ import 'package:aves/main.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/highlight.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/ref/mime_types.dart'; @@ -16,15 +16,17 @@ import 'package:aves/widgets/collection/grid/section_layout.dart'; import 'package:aves/widgets/collection/grid/selector.dart'; import 'package:aves/widgets/collection/grid/thumbnail.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; -import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/sliver.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/scaling.dart'; import 'package:aves/widgets/common/tile_extent_manager.dart'; -import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -34,7 +36,7 @@ class ThumbnailCollection extends StatelessWidget { final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); final ValueNotifier _tileExtentNotifier = ValueNotifier(0); final ValueNotifier _isScrollingNotifier = ValueNotifier(false); - final GlobalKey _scrollableKey = GlobalKey(); + final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable'); static const columnCountDefault = 4; static const extentMin = 46.0; @@ -44,6 +46,7 @@ class ThumbnailCollection extends StatelessWidget { Widget build(BuildContext context) { return HighlightInfoProvider( child: SafeArea( + bottom: false, child: LayoutBuilder( builder: (context, constraints) { final viewportSize = constraints.biggest; @@ -79,7 +82,7 @@ class ThumbnailCollection extends StatelessWidget { ), ); - final scaler = GridScaleGestureDetector( + final scaler = GridScaleGestureDetector( tileExtentManager: tileExtentManager, scrollableKey: _scrollableKey, appBarHeightNotifier: _appBarHeightNotifier, @@ -103,7 +106,7 @@ class ThumbnailCollection extends StatelessWidget { highlightable: false, ), getScaledItemTileRect: (context, entry) { - final sectionedListLayout = context.read>(); + final sectionedListLayout = context.read>(); return sectionedListLayout.getTileRect(entry) ?? Rect.zero; }, onScaled: (entry) => Provider.of(context, listen: false).add(entry), @@ -192,10 +195,12 @@ class _CollectionScrollViewState extends State { } void _registerWidget(CollectionScrollView widget) { + widget.collection.filterChangeNotifier.addListener(_onFilterChange); widget.scrollController.addListener(_onScrollChange); } void _unregisterWidget(CollectionScrollView widget) { + widget.collection.filterChangeNotifier.removeListener(_onFilterChange); widget.scrollController.removeListener(_onScrollChange); } @@ -220,15 +225,8 @@ class _CollectionScrollViewState extends State { child: _buildEmptyCollectionPlaceholder(collection), hasScrollBody: false, ) - : SectionedListSliver(), - SliverToBoxAdapter( - child: Selector( - selector: (context, mq) => mq.viewInsets.bottom, - builder: (context, mqViewInsetsBottom, child) { - return SizedBox(height: mqViewInsetsBottom); - }, - ), - ), + : SectionedListSliver(), + BottomPaddingSliver(), ], ); } @@ -237,10 +235,10 @@ class _CollectionScrollViewState extends State { return ValueListenableBuilder( valueListenable: widget.appBarHeightNotifier, builder: (context, appBarHeight, child) => Selector( - selector: (context, mq) => mq.viewInsets.bottom, - builder: (context, mqViewInsetsBottom, child) => DraggableScrollbar( - heightScrollThumb: avesScrollThumbHeight, + selector: (context, mq) => mq.effectiveBottomPadding, + builder: (context, mqPaddingBottom, child) => DraggableScrollbar( backgroundColor: Colors.white, + scrollThumbHeight: avesScrollThumbHeight, scrollThumbBuilder: avesScrollThumbBuilder( height: avesScrollThumbHeight, backgroundColor: Colors.white, @@ -249,7 +247,7 @@ class _CollectionScrollViewState extends State { padding: EdgeInsets.only( // padding to keep scroll thumb between app bar above and nav bar below top: appBarHeight, - bottom: mqViewInsetsBottom, + bottom: mqPaddingBottom, ), child: scrollView, ), @@ -285,6 +283,8 @@ class _CollectionScrollViewState extends State { ); } + void _onFilterChange() => widget.scrollController.jumpTo(0); + void _onScrollChange() { widget.isScrollingNotifier.value = true; _stopScrollMonitoringTimer(); diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index ae69e3a7b..4a1743e68 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -1,5 +1,3 @@ -import 'package:aves/model/image_entry.dart'; -import 'package:aves/services/image_file_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:flushbar/flushbar.dart'; import 'package:flutter/material.dart'; @@ -27,19 +25,66 @@ mixin FeedbackMixin { // report overlay for multiple operations - OverlayEntry _opReportOverlayEntry; - - void showOpReport({ + void showOpReport({ @required BuildContext context, - @required Set selection, @required Stream opStream, - @required void Function(Set processed) onDone, + @required int itemCount, + void Function(Set processed) onDone, }) { - final processed = {}; + OverlayEntry _opReportOverlayEntry; + _opReportOverlayEntry = OverlayEntry( + builder: (context) => ReportOverlay( + opStream: opStream, + itemCount: itemCount, + onDone: (processed) { + _opReportOverlayEntry.remove(); + onDone?.call(processed); + }, + ), + ); + Overlay.of(context).insert(_opReportOverlayEntry); + } +} + +class ReportOverlay extends StatefulWidget { + final Stream opStream; + final int itemCount; + final void Function(Set processed) onDone; + + const ReportOverlay({ + @required this.opStream, + @required this.itemCount, + @required this.onDone, + }); + + @override + _ReportOverlayState createState() => _ReportOverlayState(); +} + +class _ReportOverlayState extends State> with SingleTickerProviderStateMixin { + final processed = {}; + AnimationController _animationController; + Animation _animation; + + Stream get opStream => widget.opStream; + + @override + void initState() { + super.initState(); + + _animationController = AnimationController( + duration: Durations.collectionOpOverlayAnimation, + vsync: this, + ); + _animation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutQuad, + ); + _animationController.forward(); // do not handle completion inside `StreamBuilder` // as it could be called multiple times - Future onComplete() => _hideOpReportOverlay().then((_) => onDone(processed)); + Future onComplete() => _animationController.reverse().then((_) => widget.onDone(processed)); opStream.listen( processed.add, onError: (error) { @@ -48,17 +93,34 @@ mixin FeedbackMixin { }, onDone: onComplete, ); + } - _opReportOverlayEntry = OverlayEntry( - builder: (context) { - return AbsorbPointer( - child: StreamBuilder( - stream: opStream, - builder: (context, snapshot) { - Widget child = SizedBox.shrink(); - if (!snapshot.hasError) { - final percent = processed.length.toDouble() / selection.length; - child = CircularPercentIndicator( + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AbsorbPointer( + child: StreamBuilder( + stream: opStream, + builder: (context, snapshot) { + final percent = processed.length.toDouble() / widget.itemCount; + return FadeTransition( + opacity: _animation, + child: Container( + decoration: BoxDecoration( + gradient: RadialGradient( + colors: [ + Colors.black, + Colors.black54, + ], + ), + ), + child: Center( + child: CircularPercentIndicator( percent: percent, lineWidth: 16, radius: 160, @@ -67,22 +129,11 @@ mixin FeedbackMixin { animation: true, center: Text(NumberFormat.percentPattern().format(percent)), animateFromLastPercent: true, - ); - } - return AnimatedSwitcher( - duration: Durations.collectionOpOverlayAnimation, - child: child, - ); - }), - ); - }, + ), + ), + ), + ); + }), ); - Overlay.of(context).insert(_opReportOverlayEntry); - } - - Future _hideOpReportOverlay() async { - await Future.delayed(Durations.collectionOpOverlayAnimation * timeDilation); - _opReportOverlayEntry.remove(); - _opReportOverlayEntry = null; } } diff --git a/lib/widgets/common/action_mixins/permission_aware.dart b/lib/widgets/common/action_mixins/permission_aware.dart index 58b896830..5d3ac5255 100644 --- a/lib/widgets/common/action_mixins/permission_aware.dart +++ b/lib/widgets/common/action_mixins/permission_aware.dart @@ -1,10 +1,10 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:flutter/material.dart'; mixin PermissionAwareMixin { - Future checkStoragePermission(BuildContext context, Set entries) { + Future checkStoragePermission(BuildContext context, Set entries) { return checkStoragePermissionForAlbums(context, entries.where((e) => e.path != null).map((e) => e.directory).toSet()); } diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index d3d7bd18c..bf6e34b44 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:math'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/actions/move_type.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; @@ -11,21 +12,30 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; mixin SizeAwareMixin { - Future checkFreeSpaceForMove(BuildContext context, Set selection, String destinationAlbum, bool copy) async { + Future checkFreeSpaceForMove( + BuildContext context, + Set selection, + String destinationAlbum, + MoveType moveType, + ) async { final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum); final free = await AndroidFileService.getFreeSpace(destinationVolume); int needed; int sumSize(sum, entry) => sum + entry.sizeBytes; - if (copy) { - needed = selection.fold(0, sumSize); - } else { - // when moving, we only need space for the entries that are not already on the destination volume - final byVolume = groupBy(selection, (entry) => androidFileUtils.getStorageVolume(entry.path)); - final otherVolumes = byVolume.keys.where((volume) => volume != destinationVolume); - final fromOtherVolumes = otherVolumes.fold(0, (sum, volume) => sum + byVolume[volume].fold(0, sumSize)); - // and we need at least as much space as the largest entry because individual entries are copied then deleted - final largestSingle = selection.fold(0, (largest, entry) => max(largest, entry.sizeBytes)); - needed = max(fromOtherVolumes, largestSingle); + switch (moveType) { + case MoveType.copy: + case MoveType.export: + needed = selection.fold(0, sumSize); + break; + case MoveType.move: + // when moving, we only need space for the entries that are not already on the destination volume + final byVolume = groupBy(selection, (entry) => androidFileUtils.getStorageVolume(entry.path)); + final otherVolumes = byVolume.keys.where((volume) => volume != destinationVolume); + final fromOtherVolumes = otherVolumes.fold(0, (sum, volume) => sum + byVolume[volume].fold(0, sumSize)); + // and we need at least as much space as the largest entry because individual entries are copied then deleted + final largestSingle = selection.fold(0, (largest, entry) => max(largest, entry.sizeBytes)); + needed = max(fromOtherVolumes, largestSingle); + break; } final hasEnoughSpace = needed < free; diff --git a/lib/widgets/common/basic/draggable_scrollbar.dart b/lib/widgets/common/basic/draggable_scrollbar.dart new file mode 100644 index 000000000..c2e8d3292 --- /dev/null +++ b/lib/widgets/common/basic/draggable_scrollbar.dart @@ -0,0 +1,388 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/* + This is derived from `draggable_scrollbar` package v0.0.4: + - removed default thumb builders + - allow any `ScrollView` as child + - allow any `Widget` as label content + - moved out constraints responsibility + - various extent & thumb positioning fixes + */ + +/// Build the Scroll Thumb and label using the current configuration +typedef ScrollThumbBuilder = Widget Function( + Color backgroundColor, + Animation thumbAnimation, + Animation labelAnimation, + double height, { + Widget labelText, +}); + +/// Build a Text widget using the current scroll offset +typedef LabelTextBuilder = Widget Function(double offsetY); + +/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged +/// for quick navigation of the BoxScrollView. +class DraggableScrollbar extends StatefulWidget { + /// The background color of the label and thumb + final Color backgroundColor; + + /// The height of the scroll thumb + final double scrollThumbHeight; + + /// A function that builds a thumb using the current configuration + final ScrollThumbBuilder scrollThumbBuilder; + + /// The amount of padding that should surround the thumb + final EdgeInsetsGeometry padding; + + /// Determines how quickly the scrollbar will animate in and out + final Duration scrollbarAnimationDuration; + + /// How long should the thumb be visible before fading out + final Duration scrollbarTimeToFade; + + /// Build a Text widget from the current offset in the BoxScrollView + final LabelTextBuilder labelTextBuilder; + + /// The ScrollController for the BoxScrollView + final ScrollController controller; + + /// The view that will be scrolled with the scroll thumb + final ScrollView child; + + DraggableScrollbar({ + Key key, + @required this.backgroundColor, + @required this.scrollThumbHeight, + @required this.scrollThumbBuilder, + @required this.controller, + this.padding, + this.scrollbarAnimationDuration = const Duration(milliseconds: 300), + this.scrollbarTimeToFade = const Duration(milliseconds: 600), + this.labelTextBuilder, + @required this.child, + }) : assert(controller != null), + assert(scrollThumbBuilder != null), + assert(child.scrollDirection == Axis.vertical), + super(key: key); + + @override + _DraggableScrollbarState createState() => _DraggableScrollbarState(); + + static Widget buildScrollThumbAndLabel({ + @required Widget scrollThumb, + @required Color backgroundColor, + @required Animation thumbAnimation, + @required Animation labelAnimation, + @required Widget labelText, + }) { + final scrollThumbAndLabel = labelText == null + ? scrollThumb + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ScrollLabel( + animation: labelAnimation, + child: labelText, + backgroundColor: backgroundColor, + ), + scrollThumb, + ], + ); + return SlideFadeTransition( + animation: thumbAnimation, + child: scrollThumbAndLabel, + ); + } +} + +class ScrollLabel extends StatelessWidget { + final Animation animation; + final Color backgroundColor; + final Widget child; + + const ScrollLabel({ + Key key, + @required this.child, + @required this.animation, + @required this.backgroundColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: animation, + child: Container( + margin: EdgeInsets.only(right: 12.0), + child: Material( + elevation: 4.0, + color: backgroundColor, + borderRadius: BorderRadius.all(Radius.circular(16.0)), + child: child, + ), + ), + ); + } +} + +class _DraggableScrollbarState extends State with TickerProviderStateMixin { + final ValueNotifier _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0); + bool _isDragInProcess = false; + + AnimationController _thumbAnimationController; + Animation _thumbAnimation; + AnimationController _labelAnimationController; + Animation _labelAnimation; + Timer _fadeoutTimer; + + @override + void initState() { + super.initState(); + + _thumbAnimationController = AnimationController( + vsync: this, + duration: widget.scrollbarAnimationDuration, + ); + + _thumbAnimation = CurvedAnimation( + parent: _thumbAnimationController, + curve: Curves.fastOutSlowIn, + ); + + _labelAnimationController = AnimationController( + vsync: this, + duration: widget.scrollbarAnimationDuration, + ); + + _labelAnimation = CurvedAnimation( + parent: _labelAnimationController, + curve: Curves.fastOutSlowIn, + ); + } + + @override + void dispose() { + _thumbAnimationController.dispose(); + _labelAnimationController.dispose(); + _fadeoutTimer?.cancel(); + super.dispose(); + } + + ScrollController get controller => widget.controller; + + double get thumbMaxScrollExtent => context.size.height - widget.scrollThumbHeight - (widget.padding?.vertical ?? 0.0); + + double get thumbMinScrollExtent => 0.0; + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (notification) { + _onScrollNotification(notification); + return false; + }, + child: Stack( + children: [ + RepaintBoundary( + child: widget.child, + ), + RepaintBoundary( + child: GestureDetector( + onVerticalDragStart: _onVerticalDragStart, + onVerticalDragUpdate: _onVerticalDragUpdate, + onVerticalDragEnd: _onVerticalDragEnd, + child: ValueListenableBuilder( + valueListenable: _thumbOffsetNotifier, + builder: (context, thumbOffset, child) => Container( + alignment: AlignmentDirectional.topEnd, + padding: EdgeInsets.only(top: thumbOffset) + widget.padding, + child: widget.scrollThumbBuilder( + widget.backgroundColor, + _thumbAnimation, + _labelAnimation, + widget.scrollThumbHeight, + labelText: (widget.labelTextBuilder != null && _isDragInProcess) + ? ValueListenableBuilder( + valueListenable: _viewOffsetNotifier, + builder: (context, viewOffset, child) => widget.labelTextBuilder(viewOffset + thumbOffset), + ) + : null, + ), + ), + ), + ), + ), + ], + ), + ); + } + + void _onScrollNotification(ScrollNotification notification) { + final scrollMetrics = notification.metrics; + + // do not update the thumb if we cannot actually scroll + if (scrollMetrics.minScrollExtent >= scrollMetrics.maxScrollExtent) return; + + _viewOffsetNotifier.value = scrollMetrics.pixels; + + // we update the thumb position from the scrolled offset + // when the user is not dragging the thumb + if (!_isDragInProcess) { + if (notification is ScrollUpdateNotification) { + _thumbOffsetNotifier.value = (scrollMetrics.pixels / scrollMetrics.maxScrollExtent * thumbMaxScrollExtent).clamp(thumbMinScrollExtent, thumbMaxScrollExtent); + } + + if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { + _showThumb(); + _scheduleFadeout(); + } + } + } + + void _onVerticalDragStart(DragStartDetails details) { + _labelAnimationController.forward(); + _fadeoutTimer?.cancel(); + setState(() => _isDragInProcess = true); + } + + void _onVerticalDragUpdate(DragUpdateDetails details) { + _showThumb(); + if (_isDragInProcess) { + // thumb offset + _thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + details.delta.dy).clamp(thumbMinScrollExtent, thumbMaxScrollExtent); + + // scroll offset + final min = controller.position.minScrollExtent; + final max = controller.position.maxScrollExtent; + controller.jumpTo((_thumbOffsetNotifier.value / thumbMaxScrollExtent * max).clamp(min, max)); + } + } + + void _onVerticalDragEnd(DragEndDetails details) { + _scheduleFadeout(); + setState(() => _isDragInProcess = false); + } + + void _showThumb() { + if (_thumbAnimationController.status != AnimationStatus.forward) { + _thumbAnimationController.forward(); + } + } + + void _scheduleFadeout() { + _fadeoutTimer?.cancel(); + _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { + _thumbAnimationController.reverse(); + _labelAnimationController.reverse(); + _fadeoutTimer = null; + }); + } +} + +/// Draws 2 triangles like arrow up and arrow down +class ArrowCustomPainter extends CustomPainter { + Color color; + + ArrowCustomPainter(this.color); + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + const width = 12.0; + const height = 8.0; + final baseX = size.width / 2; + final baseY = size.height / 2; + + canvas.drawPath( + _trianglePath(Offset(baseX, baseY - 2.0), width, height, true), + paint, + ); + canvas.drawPath( + _trianglePath(Offset(baseX, baseY + 2.0), width, height, false), + paint, + ); + } + + static Path _trianglePath(Offset o, double width, double height, bool isUp) { + return Path() + ..moveTo(o.dx, o.dy) + ..lineTo(o.dx + width, o.dy) + ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) + ..close(); + } +} + +///This cut 2 lines in arrow shape +class ArrowClipper extends CustomClipper { + @override + Path getClip(Size size) { + final path = Path(); + path.lineTo(0.0, size.height); + path.lineTo(size.width, size.height); + path.lineTo(size.width, 0.0); + path.lineTo(0.0, 0.0); + path.close(); + + const arrowWidth = 8.0; + final startPointX = (size.width - arrowWidth) / 2; + var startPointY = size.height / 2 - arrowWidth / 2; + path.moveTo(startPointX, startPointY); + path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); + path.lineTo(startPointX + arrowWidth, startPointY); + path.lineTo(startPointX + arrowWidth, startPointY + 1.0); + path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); + path.lineTo(startPointX, startPointY + 1.0); + path.close(); + + startPointY = size.height / 2 + arrowWidth / 2; + path.moveTo(startPointX + arrowWidth, startPointY); + path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); + path.lineTo(startPointX, startPointY); + path.lineTo(startPointX, startPointY - 1.0); + path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); + path.lineTo(startPointX + arrowWidth, startPointY - 1.0); + path.close(); + + return path; + } + + @override + bool shouldReclip(CustomClipper oldClipper) => false; +} + +class SlideFadeTransition extends StatelessWidget { + final Animation animation; + final Widget child; + + const SlideFadeTransition({ + Key key, + @required this.animation, + @required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: animation, + builder: (context, child) => animation.value == 0.0 ? Container() : child, + child: SlideTransition( + position: Tween( + begin: Offset(0.3, 0.0), + end: Offset(0.0, 0.0), + ).animate(animation), + child: FadeTransition( + opacity: animation, + child: child, + ), + ), + ); + } +} diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart new file mode 100644 index 000000000..50d2fd56c --- /dev/null +++ b/lib/widgets/common/basic/insets.dart @@ -0,0 +1,62 @@ +import 'package:aves/widgets/common/extensions/media_query.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +// This widget should be added on top of Scaffolds with: +// - `resizeToAvoidBottomInset` set to false, +// - a vertically scrollable body. +// It will prevent the body from scrolling when a user swipe from bottom to use Android Q style navigation gestures. +class BottomGestureAreaProtector extends StatelessWidget { + // as of Flutter v1.22.5, `systemGestureInsets` from `MediaQuery` mistakenly reports no bottom inset, + // so we use an empirical measurement instead + static const double systemGestureInsetsBottom = 32; + + @override + Widget build(BuildContext context) { + return Selector( + selector: (c, mq) => mq.effectiveBottomPadding, + builder: (c, mqPaddingBottom, child) { + // devices with physical navigation buttons have no bottom insets + // we assume these devices do not use gesture navigation + if (mqPaddingBottom == 0) return SizedBox(); + return Positioned( + left: 0, + right: 0, + bottom: 0, + height: systemGestureInsetsBottom, + child: AbsorbPointer(), + ); + }, + ); + } +} + +class GestureAreaProtectorStack extends StatelessWidget { + final Widget child; + + const GestureAreaProtectorStack({@required this.child}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + child, + BottomGestureAreaProtector(), + ], + ); + } +} + +class BottomPaddingSliver extends StatelessWidget { + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: Selector( + selector: (context, mq) => mq.effectiveBottomPadding, + builder: (context, mqPaddingBottom, child) { + return SizedBox(height: mqPaddingBottom); + }, + ), + ); + } +} diff --git a/lib/widgets/common/basic/link_chip.dart b/lib/widgets/common/basic/link_chip.dart index c0f54a506..b04c0b2ae 100644 --- a/lib/widgets/common/basic/link_chip.dart +++ b/lib/widgets/common/basic/link_chip.dart @@ -44,7 +44,7 @@ class LinkChip extends StatelessWidget { SizedBox(width: 8), Builder( builder: (context) => Icon( - AIcons.openInNew, + AIcons.openOutside, size: DefaultTextStyle.of(context).style.fontSize, color: color, ), diff --git a/lib/widgets/common/behaviour/routes.dart b/lib/widgets/common/behaviour/routes.dart index 03e460480..a5ac9d079 100644 --- a/lib/widgets/common/behaviour/routes.dart +++ b/lib/widgets/common/behaviour/routes.dart @@ -1,9 +1,5 @@ import 'package:flutter/material.dart'; -extension ExtraContext on BuildContext { - String get currentRouteName => ModalRoute.of(this)?.settings?.name; -} - class DirectMaterialPageRoute extends PageRouteBuilder { DirectMaterialPageRoute({ RouteSettings settings, diff --git a/lib/widgets/common/extensions/build_context.dart b/lib/widgets/common/extensions/build_context.dart new file mode 100644 index 000000000..3f06767b8 --- /dev/null +++ b/lib/widgets/common/extensions/build_context.dart @@ -0,0 +1,5 @@ +import 'package:flutter/widgets.dart'; + +extension ExtraContext on BuildContext { + String get currentRouteName => ModalRoute.of(this)?.settings?.name; +} diff --git a/lib/widgets/common/extensions/media_query.dart b/lib/widgets/common/extensions/media_query.dart new file mode 100644 index 000000000..e0e15959c --- /dev/null +++ b/lib/widgets/common/extensions/media_query.dart @@ -0,0 +1,41 @@ +import 'dart:math'; + +import 'package:flutter/widgets.dart'; + +extension ExtraMediaQueryData on MediaQueryData { + /* + examples of MediaQuery props in practice, as of Flutter v1.22.5 + + S20, Android 11, portrait, notch top, button nav bar bottom + padding EdgeInsets(0.0, 26.0, 0.0, 48.0) + viewPadding EdgeInsets(0.0, 26.0, 0.0, 48.0) + viewInsets EdgeInsets.zero + + S20, Android 11, landscape, notch left, button nav bar right + padding EdgeInsets(26.0, 24.0, 0.0, 0.0) + viewPadding EdgeInsets(26.0, 24.0, 0.0, 0.0) + viewInsets EdgeInsets.zero + + S10e, Android 10, portrait, notch top, button nav bar bottom + padding EdgeInsets(0.0, 39.0, 0.0, 0.0) + viewPadding EdgeInsets(0.0, 39.0, 0.0, 0.0) + viewInsets EdgeInsets(0.0, 0.0, 0.0, 48.0) + + S10e, Android 10, portrait, notch top, gesture nav bar bottom + padding EdgeInsets(0.0, 39.0, 0.0, 0.0) + viewPadding EdgeInsets(0.0, 39.0, 0.0, 0.0) + viewInsets EdgeInsets(0.0, 0.0, 0.0, 15.0) + + S10e, Android 10, landscape, notch left, button nav bar right + padding EdgeInsets(38.7, 24.0, 0.0, 0.0) + viewPadding EdgeInsets(38.7, 24.0, 0.0, 0.0) + viewInsets EdgeInsets.zero + + S7, portrait/landscape, no notch, no nav bar + padding EdgeInsets(0.0, 24.0, 0.0, 0.0) + viewPadding EdgeInsets(0.0, 24.0, 0.0, 0.0) + viewInsets EdgeInsets.zero + */ + + double get effectiveBottomPadding => max(viewPadding.bottom, viewInsets.bottom); +} diff --git a/lib/widgets/common/fx/checkered_decoration.dart b/lib/widgets/common/fx/checkered_decoration.dart index 4d541eaee..c5bc26520 100644 --- a/lib/widgets/common/fx/checkered_decoration.dart +++ b/lib/widgets/common/fx/checkered_decoration.dart @@ -1,57 +1,40 @@ import 'package:flutter/material.dart'; -class CheckeredDecoration extends Decoration { - final Color light, dark; +class CheckeredPainter extends CustomPainter { + final Paint lightPaint, darkPaint; final double checkSize; final Offset offset; - const CheckeredDecoration({ - this.light = const Color(0xFF999999), - this.dark = const Color(0xFF666666), + CheckeredPainter({ + Color light = const Color(0xFF999999), + Color dark = const Color(0xFF666666), this.checkSize = 20, this.offset = Offset.zero, - }); + }) : lightPaint = Paint()..color = light, + darkPaint = Paint()..color = dark; @override - _CheckeredDecorationPainter createBoxPainter([VoidCallback onChanged]) { - return _CheckeredDecorationPainter(this, onChanged); - } -} + void paint(Canvas canvas, Size size) { + final background = Rect.fromLTWH(0, 0, size.width, size.height); + canvas.drawRect(background, lightPaint); -class _CheckeredDecorationPainter extends BoxPainter { - final CheckeredDecoration decoration; - - const _CheckeredDecorationPainter(this.decoration, VoidCallback onChanged) : super(onChanged); - - @override - void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { - final size = configuration.size; - var dx = offset.dx; - var dy = offset.dy; - - final lightPaint = Paint()..color = decoration.light; - final darkPaint = Paint()..color = decoration.dark; - final checkSize = decoration.checkSize; - - // save/restore because of the clip - canvas.save(); - canvas.clipRect(Rect.fromLTWH(dx, dy, size.width, size.height)); - - canvas.drawPaint(lightPaint); - - dx += decoration.offset.dx % (decoration.checkSize * 2); - dy += decoration.offset.dy % (decoration.checkSize * 2); + final dx = offset.dx % (checkSize * 2); + final dy = offset.dy % (checkSize * 2); final xMax = size.width / checkSize; final yMax = size.height / checkSize; for (var x = -2; x < xMax; x++) { for (var y = -2; y < yMax; y++) { if ((x + y) % 2 == 0) { - final rect = Rect.fromLTWH(dx + x * checkSize, dy + y * checkSize, checkSize, checkSize); - canvas.drawRect(rect, darkPaint); + final check = Rect.fromLTWH(dx + x * checkSize, dy + y * checkSize, checkSize, checkSize); + if (check.overlaps(background)) { + canvas.drawRect(check.intersect(background), darkPaint); + } } } } - canvas.restore(); } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; } diff --git a/lib/widgets/common/gesture_area_protector.dart b/lib/widgets/common/gesture_area_protector.dart deleted file mode 100644 index 33a736d6b..000000000 --- a/lib/widgets/common/gesture_area_protector.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; - -// This widget should be added on top of Scaffolds with: -// - `resizeToAvoidBottomInset` set to false, -// - a vertically scrollable body. -// It will prevent the body from scrolling when a user swipe from bottom to use Android Q style navigation gestures. -class BottomGestureAreaProtector extends StatelessWidget { - // as of Flutter v1.22.5, `systemGestureInsets` from `MediaQuery` mistakenly reports no bottom inset, - // so we use an empirical measurement instead - static const double systemGestureInsetsBottom = 32; - - @override - Widget build(BuildContext context) { - return Positioned( - left: 0, - right: 0, - bottom: 0, - height: systemGestureInsetsBottom, - child: AbsorbPointer(), - ); - } -} - -class GestureAreaProtectorStack extends StatelessWidget { - final Widget child; - - const GestureAreaProtectorStack({@required this.child}); - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - child, - BottomGestureAreaProtector(), - ], - ); - } -} diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index 0bc1db5ad..3acf4c2af 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -36,7 +36,7 @@ class SectionHeader extends StatelessWidget { padding: padding, constraints: BoxConstraints(minHeight: leadingDimension), child: GestureDetector( - onTap: () => _toggleSectionSelection(context), + onTap: selectable ? () => _toggleSectionSelection(context) : null, child: Text.rich( TextSpan( children: [ @@ -53,7 +53,7 @@ class SectionHeader extends StatelessWidget { child: leading, ) : null, - onPressed: () => _toggleSectionSelection(context), + onPressed: selectable ? () => _toggleSectionSelection(context) : null, ), ), TextSpan( diff --git a/lib/widgets/common/grid/section_layout.dart b/lib/widgets/common/grid/section_layout.dart index a49a2e241..70be8fd0c 100644 --- a/lib/widgets/common/grid/section_layout.dart +++ b/lib/widgets/common/grid/section_layout.dart @@ -167,7 +167,7 @@ class SectionedListLayout { final sectionItemIndex = section.value.indexOf(item); final column = sectionItemIndex % columnCount; final row = (sectionItemIndex / columnCount).floor(); - final listIndex = sectionLayout.firstIndex + (showHeaders ? 1 : 0) + row; + final listIndex = sectionLayout.firstIndex + 1 + row; final left = tileExtent * column + spacing * (column - 1); final top = sectionLayout.indexToLayoutOffset(listIndex); diff --git a/lib/widgets/common/grid/sliver.dart b/lib/widgets/common/grid/sliver.dart index d5effa5f8..85ba6b917 100644 --- a/lib/widgets/common/grid/sliver.dart +++ b/lib/widgets/common/grid/sliver.dart @@ -215,7 +215,7 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { if (child == null) { // We have run out of children. final layout = sectionAtIndex(index) ?? sectionLayouts.last; - estimatedMaxScrollOffset = layout.indexToLayoutOffset(index); + estimatedMaxScrollOffset = layout.maxOffset; break; } } else { @@ -264,7 +264,7 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { final targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite ? getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint) : null; geometry = SliverGeometry( scrollExtent: estimatedMaxScrollOffset, - paintExtent: paintExtent, + paintExtent: math.min(paintExtent, estimatedMaxScrollOffset), cacheExtent: cacheExtent, maxPaintExtent: estimatedMaxScrollOffset, // Conservative to avoid flickering away the clip during scroll. diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index adbf3fa8e..4fd080246 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -1,7 +1,7 @@ import 'dart:ui'; import 'package:aves/image_providers/app_icon_image_provider.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; @@ -9,7 +9,7 @@ import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; class VideoIcon extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double iconSize; final bool showDuration; diff --git a/lib/widgets/common/identity/scroll_thumb.dart b/lib/widgets/common/identity/scroll_thumb.dart index de907a8b3..268874983 100644 --- a/lib/widgets/common/identity/scroll_thumb.dart +++ b/lib/widgets/common/identity/scroll_thumb.dart @@ -1,4 +1,4 @@ -import 'package:draggable_scrollbar/draggable_scrollbar.dart'; +import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; const double avesScrollThumbHeight = 48; @@ -35,7 +35,6 @@ ScrollThumbBuilder avesScrollThumbBuilder({ thumbAnimation: thumbAnimation, labelAnimation: labelAnimation, labelText: labelText, - alwaysVisibleScrollThumb: false, ); }; } diff --git a/lib/widgets/common/magnifier/core/core.dart b/lib/widgets/common/magnifier/core/core.dart index 047a1a6eb..a22e2c1ba 100644 --- a/lib/widgets/common/magnifier/core/core.dart +++ b/lib/widgets/common/magnifier/core/core.dart @@ -15,7 +15,6 @@ class MagnifierCore extends StatefulWidget { Key key, @required this.child, @required this.onTap, - @required this.gestureDetectorBehavior, @required this.controller, @required this.scaleStateCycle, @required this.applyScale, @@ -29,7 +28,6 @@ class MagnifierCore extends StatefulWidget { final MagnifierTapCallback onTap; - final HitTestBehavior gestureDetectorBehavior; final bool applyScale; final double panInertia; diff --git a/lib/widgets/common/magnifier/magnifier.dart b/lib/widgets/common/magnifier/magnifier.dart index e73bebbde..61fe5a27c 100644 --- a/lib/widgets/common/magnifier/magnifier.dart +++ b/lib/widgets/common/magnifier/magnifier.dart @@ -6,116 +6,76 @@ import 'package:aves/widgets/common/magnifier/scale/scale_level.dart'; import 'package:aves/widgets/common/magnifier/scale/state.dart'; import 'package:flutter/material.dart'; -/// `Magnifier` is derived from `photo_view` package v0.9.2: -/// - removed image related aspects to focus on a general purpose pan/scale viewer (à la `InteractiveViewer`) -/// - removed rotation and many customization parameters -/// - removed ignorable/ignoring partial notifiers -/// - formatted, renamed and reorganized -/// - fixed gesture recognizers when used inside a scrollable widget like `PageView` -/// - fixed corner hit detection when in containers scrollable in both axes -/// - fixed corner hit detection issues due to imprecise double comparisons -/// - added single & double tap position feedback -/// - fixed focus when scaling by double-tap/pinch -class Magnifier extends StatefulWidget { +/* + `Magnifier` is derived from `photo_view` package v0.9.2: + - removed image related aspects to focus on a general purpose pan/scale viewer (à la `InteractiveViewer`) + - removed rotation and many customization parameters + - removed ignorable/ignoring partial notifiers + - formatted, renamed and reorganized + - fixed gesture recognizers when used inside a scrollable widget like `PageView` + - fixed corner hit detection when in containers scrollable in both axes + - fixed corner hit detection issues due to imprecise double comparisons + - added single & double tap position feedback + - fixed focus when scaling by double-tap/pinch + */ +class Magnifier extends StatelessWidget { const Magnifier({ Key key, - @required this.child, - this.childSize, - this.controller, - this.maxScale, - this.minScale, - this.initialScale, - this.scaleStateCycle, + @required this.controller, + @required this.childSize, + this.minScale = const ScaleLevel(factor: .0), + this.maxScale = const ScaleLevel(factor: double.infinity), + this.initialScale = const ScaleLevel(ref: ScaleReference.contained), + this.scaleStateCycle = defaultScaleStateCycle, + this.applyScale = true, this.onTap, - this.gestureDetectorBehavior, - this.applyScale, - }) : super(key: key); - - final Widget child; - - /// The size of the custom [child]. This value is used to compute the relation between the child and the container's size to calculate the scale value. - final Size childSize; - - /// Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size. - final ScaleLevel maxScale; - - /// Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size. - final ScaleLevel minScale; - - /// Defines the size the image will assume when the component is initialized, it is proportional to the original image size. - final ScaleLevel initialScale; + @required this.child, + }) : assert(controller != null), + assert(childSize != null), + assert(minScale != null), + assert(maxScale != null), + assert(initialScale != null), + assert(scaleStateCycle != null), + assert(applyScale != null), + super(key: key); final MagnifierController controller; + + // The size of the custom [child]. This value is used to compute the relation between the child and the container's size to calculate the scale value. + final Size childSize; + + // Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size. + final ScaleLevel minScale; + + // Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size. + final ScaleLevel maxScale; + + // Defines the size the image will assume when the component is initialized, it is proportional to the original image size. + final ScaleLevel initialScale; + final ScaleStateCycle scaleStateCycle; - final MagnifierTapCallback onTap; - final HitTestBehavior gestureDetectorBehavior; final bool applyScale; - - @override - State createState() { - return _MagnifierState(); - } -} - -class _MagnifierState extends State { - bool _controlledController; - MagnifierController _controller; - - Size get childSize => widget.childSize; - - @override - void initState() { - super.initState(); - if (widget.controller == null) { - _controlledController = true; - _controller = MagnifierController(); - } else { - _controlledController = false; - _controller = widget.controller; - } - } - - @override - void didUpdateWidget(covariant Magnifier oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.controller == null) { - if (!_controlledController) { - _controlledController = true; - _controller = MagnifierController(); - } - } else { - _controlledController = false; - _controller = widget.controller; - } - } - - @override - void dispose() { - if (_controlledController) { - _controller.dispose(); - } - super.dispose(); - } + final MagnifierTapCallback onTap; + final Widget child; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - _controller.setScaleBoundaries(ScaleBoundaries( - widget.minScale ?? 0.0, - widget.maxScale ?? ScaleLevel(factor: double.infinity), - widget.initialScale ?? ScaleLevel(ref: ScaleReference.contained), - constraints.biggest, - widget.childSize?.isEmpty == true ? constraints.biggest : widget.childSize, + controller.setScaleBoundaries(ScaleBoundaries( + minScale: minScale, + maxScale: maxScale, + initialScale: initialScale, + viewportSize: constraints.biggest, + childSize: childSize?.isEmpty == false ? childSize : constraints.biggest, )); return MagnifierCore( - child: widget.child, - controller: _controller, - scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle, - onTap: widget.onTap, - gestureDetectorBehavior: widget.gestureDetectorBehavior, - applyScale: widget.applyScale ?? true, + child: child, + controller: controller, + scaleStateCycle: scaleStateCycle, + onTap: onTap, + applyScale: applyScale, ); }, ); diff --git a/lib/widgets/common/magnifier/scale/scale_boundaries.dart b/lib/widgets/common/magnifier/scale/scale_boundaries.dart index b5f565fb4..30615777a 100644 --- a/lib/widgets/common/magnifier/scale/scale_boundaries.dart +++ b/lib/widgets/common/magnifier/scale/scale_boundaries.dart @@ -7,20 +7,22 @@ import 'package:flutter/foundation.dart'; /// Internal class to wrap custom scale boundaries (min, max and initial) /// Also, stores values regarding the two sizes: the container and the child. class ScaleBoundaries { - const ScaleBoundaries( - this._minScale, - this._maxScale, - this._initialScale, - this.viewportSize, - this.childSize, - ); - final ScaleLevel _minScale; final ScaleLevel _maxScale; final ScaleLevel _initialScale; final Size viewportSize; final Size childSize; + const ScaleBoundaries({ + @required ScaleLevel minScale, + @required ScaleLevel maxScale, + @required ScaleLevel initialScale, + @required this.viewportSize, + @required this.childSize, + }) : _minScale = minScale, + _maxScale = maxScale, + _initialScale = initialScale; + double _scaleForLevel(ScaleLevel level) { final factor = level.factor; switch (level.ref) { diff --git a/lib/widgets/common/providers/settings_provider.dart b/lib/widgets/common/providers/settings_provider.dart deleted file mode 100644 index a47b5329b..000000000 --- a/lib/widgets/common/providers/settings_provider.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:aves/model/settings/settings.dart'; -import 'package:flutter/widgets.dart'; -import 'package:provider/provider.dart'; - -class SettingsProvider extends StatelessWidget { - final Widget child; - - const SettingsProvider({@required this.child}); - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: settings, - child: child, - ); - } -} diff --git a/lib/widgets/common/scaling.dart b/lib/widgets/common/scaling.dart index 6e74ed431..10b479fc1 100644 --- a/lib/widgets/common/scaling.dart +++ b/lib/widgets/common/scaling.dart @@ -1,4 +1,3 @@ -import 'dart:math'; import 'dart:ui' as ui; import 'package:aves/theme/durations.dart'; @@ -153,7 +152,11 @@ class _GridScaleGestureDetectorState extends State _DebugAndroidAppSectionState(); +} + +class _DebugAndroidAppSectionState extends State with AutomaticKeepAliveClientMixin { + Future> _loader; + + static const iconSize = 20.0; + + @override + void initState() { + super.initState(); + _loader = AndroidAppService.getPackages(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + return AvesExpansionTile( + title: 'Android Apps', + children: [ + Padding( + padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: FutureBuilder>( + future: _loader, + builder: (context, snapshot) { + if (snapshot.hasError) return Text(snapshot.error.toString()); + if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + final packages = snapshot.data.toList()..sort((a, b) => compareAsciiUpperCase(a.packageName, b.packageName)); + final enabledTheme = IconTheme.of(context); + final disabledTheme = enabledTheme.merge(IconThemeData(opacity: .2)); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: packages.map((package) { + return Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image( + image: AppIconImage( + packageName: package.packageName, + size: iconSize, + ), + width: iconSize, + height: iconSize, + ), + ), + TextSpan( + text: ' ${package.packageName}\n', + style: InfoRowGroup.keyStyle, + ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: IconTheme( + data: package.categoryLauncher ? enabledTheme : disabledTheme, + child: Icon( + Icons.launch_outlined, + size: iconSize, + ), + ), + ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: IconTheme( + data: package.isSystem ? enabledTheme : disabledTheme, + child: Icon( + Icons.android, + size: iconSize, + ), + ), + ), + TextSpan( + text: ' ${package.potentialDirs.join(', ')}\n', + style: InfoRowGroup.baseStyle, + ), + ], + ), + ); + }).toList(), + ); + }, + ), + ), + ], + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/widgets/debug/android_dirs.dart b/lib/widgets/debug/android_dirs.dart index 3731170fe..92e71bdf7 100644 --- a/lib/widgets/debug/android_dirs.dart +++ b/lib/widgets/debug/android_dirs.dart @@ -24,7 +24,7 @@ class _DebugAndroidDirSectionState extends State with Au super.build(context); return AvesExpansionTile( - title: 'Android Dir', + title: 'Android Dirs', children: [ Padding( padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 09626c721..b600f0380 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -1,7 +1,8 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/debug/android_apps.dart'; import 'package:aves/widgets/debug/android_dirs.dart'; import 'package:aves/widgets/debug/android_env.dart'; import 'package:aves/widgets/debug/cache.dart'; @@ -26,7 +27,7 @@ class AppDebugPage extends StatefulWidget { } class _AppDebugPageState extends State { - List get entries => widget.source.rawEntries; + List get entries => widget.source.rawEntries; static OverlayEntry _taskQueueOverlayEntry; @@ -42,6 +43,7 @@ class _AppDebugPageState extends State { padding: EdgeInsets.all(8), children: [ _buildGeneralTabView(), + DebugAndroidAppSection(), DebugAndroidDirSection(), DebugAndroidEnvironmentSection(), DebugCacheSection(), diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index bcf80693b..1e261e43f 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -1,6 +1,6 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/favourite_repo.dart'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; @@ -13,7 +13,7 @@ class DebugAppDatabaseSection extends StatefulWidget { class _DebugAppDatabaseSectionState extends State with AutomaticKeepAliveClientMixin { Future _dbFileSizeLoader; - Future> _dbEntryLoader; + Future> _dbEntryLoader; Future> _dbDateLoader; Future> _dbMetadataLoader; Future> _dbAddressLoader; diff --git a/lib/widgets/debug/overlay.dart b/lib/widgets/debug/overlay.dart index 9c0081c0e..6fc169a14 100644 --- a/lib/widgets/debug/overlay.dart +++ b/lib/widgets/debug/overlay.dart @@ -1,4 +1,5 @@ import 'package:aves/services/service_policy.dart'; +import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:flutter/material.dart'; class DebugTaskQueueOverlay extends StatelessWidget { @@ -13,7 +14,7 @@ class DebugTaskQueueOverlay extends StatelessWidget { child: Container( color: Colors.indigo[900].withAlpha(0xCC), margin: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, + bottom: MediaQuery.of(context).effectiveBottomPadding, ), padding: EdgeInsets.all(8), child: StreamBuilder( diff --git a/lib/widgets/dialogs/rename_entry_dialog.dart b/lib/widgets/dialogs/rename_entry_dialog.dart index b28e25730..d73410ba9 100644 --- a/lib/widgets/dialogs/rename_entry_dialog.dart +++ b/lib/widgets/dialogs/rename_entry_dialog.dart @@ -1,13 +1,13 @@ import 'dart:io'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as path; import 'aves_dialog.dart'; class RenameEntryDialog extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; const RenameEntryDialog(this.entry); @@ -19,7 +19,7 @@ class _RenameEntryDialogState extends State { final TextEditingController _nameController = TextEditingController(); final ValueNotifier _isValidNotifier = ValueNotifier(false); - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override void initState() { diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index 1daea1536..112fc28ae 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -11,6 +11,7 @@ import 'package:aves/ref/mime_types.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/about/about_page.dart'; +import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/debug/app_debug_page.dart'; @@ -38,10 +39,52 @@ class _AppDrawerState extends State { @override Widget build(BuildContext context) { - final header = Container( + final drawerItems = [ + _buildHeader(context), + allCollectionTile, + videoTile, + favouriteTile, + _buildSpecialAlbumSection(), + Divider(), + albumListTile, + countryListTile, + tagListTile, + Divider(), + settingsTile, + aboutTile, + if (kDebugMode) ...[ + Divider(), + debugTile, + ], + ]; + + return Drawer( + child: Selector( + selector: (c, mq) => mq.effectiveBottomPadding, + builder: (c, mqPaddingBottom, child) { + return SingleChildScrollView( + padding: EdgeInsets.only(bottom: mqPaddingBottom), + child: Theme( + data: Theme.of(context).copyWith( + // color used by `ExpansionTile` for leading icon + unselectedWidgetColor: Colors.white, + ), + child: Column( + children: drawerItems, + ), + ), + ); + }, + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return Container( padding: EdgeInsets.all(16), color: Theme.of(context).accentColor, child: SafeArea( + bottom: false, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -66,45 +109,6 @@ class _AppDrawerState extends State { ), ), ); - - final drawerItems = [ - header, - allCollectionTile, - videoTile, - favouriteTile, - _buildSpecialAlbumSection(), - Divider(), - albumListTile, - countryListTile, - tagListTile, - Divider(), - settingsTile, - aboutTile, - if (kDebugMode) ...[ - Divider(), - debugTile, - ], - ]; - - return Drawer( - child: Selector( - selector: (c, mq) => mq.viewInsets.bottom, - builder: (c, mqViewInsetsBottom, child) { - return SingleChildScrollView( - padding: EdgeInsets.only(bottom: mqViewInsetsBottom), - child: Theme( - data: Theme.of(context).copyWith( - // color used by `ExpansionTile` for leading icon - unselectedWidgetColor: Colors.white, - ), - child: Column( - children: drawerItems, - ), - ), - ); - }, - ), - ); } Widget _buildAlbumTile(String album) { diff --git a/lib/widgets/drawer/tile.dart b/lib/widgets/drawer/tile.dart index b2bbb7614..be6b4f8b6 100644 --- a/lib/widgets/drawer/tile.dart +++ b/lib/widgets/drawer/tile.dart @@ -1,6 +1,6 @@ import 'dart:ui'; -import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index 6b33d36d9..edb98101d 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -1,10 +1,15 @@ import 'package:aves/model/actions/chip_actions.dart'; +import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/app_bar_subtitle.dart'; +import 'package:aves/widgets/common/basic/menu_row.dart'; import 'package:aves/widgets/common/basic/query_bar.dart'; import 'package:aves/widgets/dialogs/create_album_dialog.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; @@ -12,18 +17,20 @@ import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; class AlbumPickPage extends StatefulWidget { static const routeName = '/album_pick'; final CollectionSource source; - final bool copy; + final MoveType moveType; const AlbumPickPage({ @required this.source, - @required this.copy, + @required this.moveType, }); @override @@ -38,32 +45,36 @@ class _AlbumPickPageState extends State { @override Widget build(BuildContext context) { Widget appBar = AlbumPickAppBar( - copy: widget.copy, + source: source, + moveType: widget.moveType, actionDelegate: AlbumChipSetActionDelegate(source: source), queryNotifier: _queryNotifier, ); - return Selector( - selector: (context, s) => s.albumSortFactor, - builder: (context, sortFactor, child) { - return FilterGridPage( - source: source, - appBar: appBar, - filterSections: AlbumListPage.getAlbumEntries(source), - showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, - applyQuery: (filters, query) { - if (query == null || query.isEmpty) return filters; - query = query.toUpperCase(); - return filters.where((item) => item.filter.uniqueName.toUpperCase().contains(query)).toList(); - }, - queryNotifier: _queryNotifier, - emptyBuilder: () => EmptyContent( - icon: AIcons.album, - text: 'No albums', + return Selector>( + selector: (context, s) => Tuple2(s.albumGroupFactor, s.albumSortFactor), + builder: (context, s, child) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) => FilterGridPage( + source: source, + appBar: appBar, + filterSections: AlbumListPage.getAlbumEntries(source), + showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, + applyQuery: (filters, query) { + if (query == null || query.isEmpty) return filters; + query = query.toUpperCase(); + return filters.where((item) => item.filter.uniqueName.toUpperCase().contains(query)).toList(); + }, + queryNotifier: _queryNotifier, + emptyBuilder: () => EmptyContent( + icon: AIcons.album, + text: 'No albums', + ), + settingsRouteKey: AlbumListPage.routeName, + appBarHeight: AlbumPickAppBar.preferredHeight, + onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), ), - settingsRouteKey: AlbumListPage.routeName, - appBarHeight: AlbumPickAppBar.preferredHeight, - onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), ); }, ); @@ -71,23 +82,41 @@ class _AlbumPickPageState extends State { } class AlbumPickAppBar extends StatelessWidget { - final bool copy; + final CollectionSource source; + final MoveType moveType; final AlbumChipSetActionDelegate actionDelegate; final ValueNotifier queryNotifier; static const preferredHeight = kToolbarHeight + AlbumFilterBar.preferredHeight; const AlbumPickAppBar({ - @required this.copy, + @required this.source, + @required this.moveType, @required this.actionDelegate, @required this.queryNotifier, }); @override Widget build(BuildContext context) { + String title() { + switch (moveType) { + case MoveType.copy: + return 'Copy to Album'; + case MoveType.export: + return 'Export to Album'; + case MoveType.move: + return 'Move to Album'; + default: + return null; + } + } + return SliverAppBar( leading: BackButton(), - title: Text(copy ? 'Copy to Album' : 'Move to Album'), + title: SourceStateAwareAppBarTitle( + title: Text(title()), + source: source, + ), bottom: AlbumFilterBar( filterNotifier: queryNotifier, ), @@ -105,10 +134,23 @@ class AlbumPickAppBar extends StatelessWidget { }, tooltip: 'Create album', ), - IconButton( - icon: Icon(AIcons.sort), - onPressed: () => actionDelegate.onActionSelected(context, ChipSetAction.sort), - tooltip: 'Sort…', + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + value: ChipSetAction.sort, + child: MenuRow(text: 'Sort…', icon: AIcons.sort), + ), + PopupMenuItem( + value: ChipSetAction.group, + child: MenuRow(text: 'Group…', icon: AIcons.group), + ), + ]; + }, + onSelected: (action) { + // wait for the popup menu to hide before proceeding with the action + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => actionDelegate.onActionSelected(context, action)); + }, ), ], floating: true, diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 15fec7a8c..3d220401b 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -1,9 +1,11 @@ import 'package:aves/model/actions/chip_actions.dart'; +import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/image_file_service.dart'; +import 'package:aves/services/image_op_events.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; @@ -77,14 +79,14 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per if (!await checkStoragePermission(context, selection)) return; + final selectionCount = selection.length; showOpReport( context: context, - selection: selection, opStream: ImageFileService.delete(selection), + itemCount: selectionCount, onDone: (processed) { final deletedUris = processed.where((e) => e.success).map((e) => e.uri).toList(); final deletedCount = deletedUris.length; - final selectionCount = selection.length; if (deletedCount < selectionCount) { final count = selectionCount - deletedCount; showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}'); @@ -109,16 +111,16 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per final selection = source.rawEntries.where(filter.filter).toSet(); final destinationAlbum = path.join(path.dirname(album), newName); - if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, false)) return; + if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, MoveType.move)) return; + final selectionCount = selection.length; showOpReport( context: context, - selection: selection, opStream: ImageFileService.move(selection, copy: false, destinationAlbum: destinationAlbum), + itemCount: selectionCount, onDone: (processed) async { final movedOps = processed.where((e) => e.success); final movedCount = movedOps.length; - 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')}'); diff --git a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart index 08e9e32d0..2625b42cb 100644 --- a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart @@ -92,7 +92,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate { builder: (context) => AvesSelectionDialog( initialValue: settings.albumGroupFactor, options: { - AlbumChipGroupFactor.importance: 'By importance', + AlbumChipGroupFactor.importance: 'By tier', AlbumChipGroupFactor.volume: 'By storage volume', AlbumChipGroupFactor.none: 'Do not group', }, diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index 5842967ee..1801a2454 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -3,7 +3,7 @@ import 'dart:ui'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -20,7 +20,7 @@ import 'package:flutter/material.dart'; class DecoratedFilterChip extends StatelessWidget { final CollectionSource source; final CollectionFilter filter; - final ImageEntry entry; + final AvesEntry entry; final double extent; final bool pinned, highlightable; final FilterCallback onTap; diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index b03997e9b..fcdb2e61f 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -4,9 +4,11 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; -import 'package:aves/widgets/common/behaviour/routes.dart'; -import 'package:aves/widgets/common/gesture_area_protector.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/sliver.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -19,7 +21,6 @@ import 'package:aves/widgets/drawer/app_drawer.dart'; import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart'; import 'package:aves/widgets/filter_grids/common/section_layout.dart'; -import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -39,7 +40,7 @@ class FilterGridPage extends StatelessWidget { final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); final ValueNotifier _tileExtentNotifier = ValueNotifier(0); - final GlobalKey _scrollableKey = GlobalKey(); + final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable'); static const columnCountDefault = 2; static const extentMin = 60.0; @@ -72,6 +73,7 @@ class FilterGridPage extends StatelessWidget { child: HighlightInfoProvider( child: GestureAreaProtectorStack( child: SafeArea( + bottom: false, child: LayoutBuilder( builder: (context, constraints) { final viewportSize = constraints.biggest; @@ -188,10 +190,10 @@ class FilterGridPage extends StatelessWidget { Widget _buildDraggableScrollView(ScrollView scrollView) { return Selector( - selector: (context, mq) => mq.viewInsets.bottom, - builder: (context, mqViewInsetsBottom, child) => DraggableScrollbar( - heightScrollThumb: avesScrollThumbHeight, + selector: (context, mq) => mq.effectiveBottomPadding, + builder: (context, mqPaddingBottom, child) => DraggableScrollbar( backgroundColor: Colors.white, + scrollThumbHeight: avesScrollThumbHeight, scrollThumbBuilder: avesScrollThumbBuilder( height: avesScrollThumbHeight, backgroundColor: Colors.white, @@ -200,7 +202,7 @@ class FilterGridPage extends StatelessWidget { padding: EdgeInsets.only( // padding to keep scroll thumb between app bar above and nav bar below top: _appBarHeightNotifier.value, - bottom: mqViewInsetsBottom, + bottom: mqPaddingBottom, ), child: scrollView, ), @@ -212,10 +214,10 @@ class FilterGridPage extends StatelessWidget { if (empty) { content = SliverFillRemaining( child: Selector( - selector: (context, mq) => mq.viewInsets.bottom, - builder: (context, mqViewInsetsBottom, child) { + selector: (context, mq) => mq.effectiveBottomPadding, + builder: (context, mqPaddingBottom, child) { return Padding( - padding: EdgeInsets.only(bottom: mqViewInsetsBottom), + padding: EdgeInsets.only(bottom: mqPaddingBottom), child: emptyBuilder(), ); }, @@ -226,22 +228,13 @@ class FilterGridPage extends StatelessWidget { content = SectionedListSliver>(); } - final padding = SliverToBoxAdapter( - child: Selector( - selector: (context, mq) => mq.viewInsets.bottom, - builder: (context, mqViewInsetsBottom, child) { - return SizedBox(height: mqViewInsetsBottom); - }, - ), - ); - return CustomScrollView( key: _scrollableKey, controller: PrimaryScrollController.of(context), slivers: [ appBar, content, - padding, + BottomPaddingSliver(), ], ); } diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index e7ba1d8ef..0cff14049 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -1,25 +1,27 @@ import 'package:aves/main.dart'; +import 'package:aves/model/connectivity.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/home_page.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/model/source/media_store_source.dart'; +import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/viewer_service.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; -import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/search/search_page.dart'; +import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pedantic/pedantic.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:provider/provider.dart'; class HomePage extends StatefulWidget { static const routeName = '/'; @@ -34,8 +36,7 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - MediaStoreSource _mediaStore; - ImageEntry _viewerEntry; + AvesEntry _viewerEntry; String _shortcutRouteName; List _shortcutFilters; @@ -100,20 +101,25 @@ class _HomePageState extends State { unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', AvesApp.mode.toString())); if (AvesApp.mode != AppMode.view) { - _mediaStore = MediaStoreSource(); - await _mediaStore.init(); - unawaited(_mediaStore.refresh()); + final source = context.read(); + await source.init(); + unawaited(source.refresh()); } unawaited(Navigator.pushReplacement(context, _getRedirectRoute())); } - Future _initViewerEntry({@required String uri, @required String mimeType}) async { - final entry = await ImageFileService.getImageEntry(uri, mimeType); + Future _initViewerEntry({@required String uri, @required String mimeType}) async { + final entry = await ImageFileService.getEntry(uri, mimeType); if (entry != null) { - // cataloguing is essential for geolocation and video rotation + // cataloguing is essential for coordinates and video rotation await entry.catalog(); - unawaited(entry.locate()); + // locating is fine in the background + unawaited(connectivity.canGeolocate.then((connected) { + if (connected) { + entry.locate(); + } + })); } return entry; } @@ -121,8 +127,10 @@ class _HomePageState extends State { Route _getRedirectRoute() { if (AvesApp.mode == AppMode.view) { return DirectMaterialPageRoute( - settings: RouteSettings(name: SingleEntryViewerPage.routeName), - builder: (_) => SingleEntryViewerPage(entry: _viewerEntry), + settings: RouteSettings(name: EntryViewerPage.routeName), + builder: (_) => EntryViewerPage( + initialEntry: _viewerEntry, + ), ); } @@ -134,15 +142,16 @@ class _HomePageState extends State { routeName = _shortcutRouteName ?? settings.homePage.routeName; filters = (_shortcutFilters ?? []).map(CollectionFilter.fromJson); } + final source = context.read(); switch (routeName) { case AlbumListPage.routeName: return DirectMaterialPageRoute( settings: RouteSettings(name: AlbumListPage.routeName), - builder: (_) => AlbumListPage(source: _mediaStore), + builder: (_) => AlbumListPage(source: source), ); case SearchPage.routeName: return SearchPageRoute( - delegate: CollectionSearchDelegate(source: _mediaStore), + delegate: CollectionSearchDelegate(source: source), ); case CollectionPage.routeName: default: @@ -150,7 +159,7 @@ class _HomePageState extends State { settings: RouteSettings(name: CollectionPage.routeName), builder: (_) => CollectionPage( CollectionLens( - source: _mediaStore, + source: source, filters: filters, groupFactor: settings.collectionGroupFactor, sortFactor: settings.collectionSortFactor, diff --git a/lib/widgets/search/expandable_filter_row.dart b/lib/widgets/search/expandable_filter_row.dart index 5702fc9fb..a838d3901 100644 --- a/lib/widgets/search/expandable_filter_row.dart +++ b/lib/widgets/search/expandable_filter_row.dart @@ -105,7 +105,8 @@ class ExpandableFilterRow extends StatelessWidget { Widget _buildFilterChip(CollectionFilter filter) { return AvesFilterChip( - key: ValueKey(filter), + // key `album-...` is expected by test driver + key: Key(filter.key), filter: filter, heroType: heroTypeBuilder?.call(filter) ?? HeroType.onTap, onTap: onTap, diff --git a/lib/widgets/search/search_page.dart b/lib/widgets/search/search_page.dart index ef3c3e933..e85ced83d 100644 --- a/lib/widgets/search/search_page.dart +++ b/lib/widgets/search/search_page.dart @@ -71,9 +71,12 @@ class _SearchPageState extends State { } void _onQueryChanged() { - _debouncer(() => setState(() { - // rebuild ourselves because query changed. - })); + _debouncer(() { + if (mounted) { + // rebuild ourselves because query changed. + setState(() {}); + } + }); } void _onSearchBodyChanged() { diff --git a/lib/widgets/settings/entry_background.dart b/lib/widgets/settings/entry_background.dart index ade54d894..51b0a9326 100644 --- a/lib/widgets/settings/entry_background.dart +++ b/lib/widgets/settings/entry_background.dart @@ -50,8 +50,8 @@ class _EntryBackgroundSelectorState extends State { break; case EntryBackground.checkered: child = ClipOval( - child: DecoratedBox( - decoration: CheckeredDecoration( + child: CustomPaint( + painter: CheckeredPainter( checkSize: radius, ), ), diff --git a/lib/widgets/stats/stats.dart b/lib/widgets/stats/stats.dart index 2907bcb96..0292f1a0a 100644 --- a/lib/widgets/stats/stats.dart +++ b/lib/widgets/stats/stats.dart @@ -4,7 +4,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/tag.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -30,7 +30,7 @@ class StatsPage extends StatelessWidget { final CollectionLens parentCollection; final Map entryCountPerCountry = {}, entryCountPerPlace = {}, entryCountPerTag = {}; - List get entries => parentCollection?.sortedEntries ?? source.rawEntries; + List get entries => parentCollection?.sortedEntries ?? source.rawEntries; static const mimeDonutMinWidth = 124.0; @@ -66,7 +66,7 @@ class StatsPage extends StatelessWidget { text: 'No images', ); } else { - final byMimeTypes = groupBy(entries, (entry) => entry.mimeType).map((k, v) => MapEntry(k, v.length)); + final byMimeTypes = groupBy(entries, (entry) => entry.mimeType).map((k, v) => MapEntry(k, v.length)); final imagesByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('image/'))); final videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video/'))); final mimeDonuts = Wrap( diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart index 641ee2709..ad5df9358 100644 --- a/lib/widgets/viewer/debug/db.dart +++ b/lib/widgets/viewer/debug/db.dart @@ -1,11 +1,11 @@ -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; class DbTab extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; const DbTab({@required this.entry}); @@ -15,11 +15,11 @@ class DbTab extends StatefulWidget { class _DbTabState extends State { Future _dbDateLoader; - Future _dbEntryLoader; + Future _dbEntryLoader; Future _dbMetadataLoader; Future _dbAddressLoader; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override void initState() { @@ -60,7 +60,7 @@ class _DbTabState extends State { }, ), SizedBox(height: 16), - FutureBuilder( + FutureBuilder( future: _dbEntryLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); diff --git a/lib/widgets/viewer/debug/metadata.dart b/lib/widgets/viewer/debug/metadata.dart index 075cde487..63bb3b178 100644 --- a/lib/widgets/viewer/debug/metadata.dart +++ b/lib/widgets/viewer/debug/metadata.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'dart:typed_data'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/android_debug_service.dart'; import 'package:aves/utils/constants.dart'; @@ -10,7 +10,7 @@ import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; class MetadataTab extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; const MetadataTab({@required this.entry}); @@ -25,7 +25,7 @@ class _MetadataTabState extends State { static const secondTimestampKeys = ['date_added', 'date_modified', 'date_expires', 'isPlayed']; static const millisecondTimestampKeys = ['datetaken', 'datetime']; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override void initState() { diff --git a/lib/widgets/viewer/debug_page.dart b/lib/widgets/viewer/debug_page.dart index 21b2aa728..bd1f7ce21 100644 --- a/lib/widgets/viewer/debug_page.dart +++ b/lib/widgets/viewer/debug_page.dart @@ -1,7 +1,7 @@ -import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/main.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/viewer/debug/db.dart'; import 'package:aves/widgets/viewer/debug/metadata.dart'; @@ -13,7 +13,7 @@ import 'package:tuple/tuple.dart'; class ViewerDebugPage extends StatelessWidget { static const routeName = '/viewer/debug'; - final ImageEntry entry; + final AvesEntry entry; const ViewerDebugPage({@required this.entry}); @@ -77,10 +77,10 @@ class ViewerDebugPage extends StatelessWidget { 'height': '${entry.height}', 'sourceRotationDegrees': '${entry.sourceRotationDegrees}', 'rotationDegrees': '${entry.rotationDegrees}', + 'isRotated': '${entry.isRotated}', 'isFlipped': '${entry.isFlipped}', - 'portrait': '${entry.isPortrait}', 'displayAspectRatio': '${entry.displayAspectRatio}', - 'displaySize': '${entry.getDisplaySize()}', + 'displaySize': '${entry.displaySize}', }), Divider(), InfoRowGroup({ @@ -100,7 +100,6 @@ class ViewerDebugPage extends StatelessWidget { 'is360': '${entry.is360}', 'canEdit': '${entry.canEdit}', 'canEditExif': '${entry.canEditExif}', - 'canPrint': '${entry.canPrint}', 'canRotateAndFlip': '${entry.canRotateAndFlip}', 'xmpSubjects': '${entry.xmpSubjects}', }), @@ -135,18 +134,14 @@ class ViewerDebugPage extends StatelessWidget { Text('Raster (fast)'), Center( child: Image( - image: ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry), - ), + image: entry.getThumbnail(), ), ), SizedBox(height: 16), Text('Raster ($extent)'), Center( child: Image( - image: ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry, extent: extent), - ), + image: entry.getThumbnail(extent: extent), ), ), ], diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index 1b95e87bd..566405e0c 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -1,23 +1,31 @@ import 'dart:convert'; import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/actions/move_type.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; +import 'package:aves/services/image_op_events.dart'; +import 'package:aves/services/metadata_service.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; +import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/rename_entry_dialog.dart'; +import 'package:aves/widgets/filter_grids/album_pick.dart'; import 'package:aves/widgets/viewer/debug_page.dart'; +import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:aves/widgets/viewer/printer.dart'; import 'package:aves/widgets/viewer/source_viewer_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; import 'package:pedantic/pedantic.dart'; +import 'package:provider/provider.dart'; -class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { +class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final CollectionLens collection; final VoidCallback showInfo; @@ -28,7 +36,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { bool get hasCollection => collection != null; - void onActionSelected(BuildContext context, ImageEntry entry, EntryAction action) { + void onActionSelected(BuildContext context, AvesEntry entry, EntryAction action) { switch (action) { case EntryAction.toggleFavourite: entry.toggleFavourite(); @@ -36,6 +44,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { case EntryAction.delete: _showDeleteDialog(context, entry); break; + case EntryAction.export: + _showExportDialog(context, entry); + break; case EntryAction.info: showInfo(); break; @@ -43,7 +54,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { _showRenameDialog(context, entry); break; case EntryAction.print: - EntryPrinter(entry).print(); + EntryPrinter(entry).print(context); break; case EntryAction.rotateCCW: _rotate(context, entry, clockwise: false); @@ -88,21 +99,21 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { } } - Future _flip(BuildContext context, ImageEntry entry) async { + Future _flip(BuildContext context, AvesEntry entry) async { if (!await checkStoragePermission(context, {entry})) return; final success = await entry.flip(); if (!success) showFeedback(context, 'Failed'); } - Future _rotate(BuildContext context, ImageEntry entry, {@required bool clockwise}) async { + Future _rotate(BuildContext context, AvesEntry entry, {@required bool clockwise}) async { if (!await checkStoragePermission(context, {entry})) return; final success = await entry.rotate(clockwise: clockwise); if (!success) showFeedback(context, 'Failed'); } - Future _showDeleteDialog(BuildContext context, ImageEntry entry) async { + Future _showDeleteDialog(BuildContext context, AvesEntry entry) async { final confirmed = await showDialog( context: context, builder: (context) { @@ -128,19 +139,67 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { if (!await entry.delete()) { showFeedback(context, 'Failed'); - } else if (hasCollection) { - // update collection - collection.source.removeEntries([entry]); - if (collection.sortedEntries.isEmpty) { - Navigator.pop(context); - } } else { - // leave viewer - unawaited(SystemNavigator.pop()); + if (hasCollection) { + collection.source.removeEntries([entry]); + } + EntryDeletedNotification(entry).dispatch(context); } } - Future _showRenameDialog(BuildContext context, ImageEntry entry) async { + Future _showExportDialog(BuildContext context, AvesEntry entry) async { + final source = context.read(); + if (!source.initialized) { + await source.init(); + unawaited(source.refresh()); + } + final destinationAlbum = await Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: AlbumPickPage.routeName), + builder: (context) => AlbumPickPage(source: source, moveType: MoveType.export), + ), + ); + + if (destinationAlbum == null || destinationAlbum.isEmpty) return; + if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; + + if (!await checkStoragePermission(context, {entry})) return; + + if (!await checkFreeSpaceForMove(context, {entry}, destinationAlbum, MoveType.export)) return; + + final selection = {}; + if (entry.isMultipage) { + final multiPageInfo = await MetadataService.getMultiPageInfo(entry); + if (multiPageInfo.pageCount > 1) { + for (final page in multiPageInfo.pages) { + final pageEntry = entry.getPageEntry(page, eraseDefaultPageId: false); + selection.add(pageEntry); + } + } + } else { + selection.add(entry); + } + + final selectionCount = selection.length; + showOpReport( + context: context, + opStream: ImageFileService.export(selection, destinationAlbum: destinationAlbum), + itemCount: selectionCount, + onDone: (processed) { + final movedOps = processed.where((e) => e.success); + final movedCount = movedOps.length; + if (movedCount < selectionCount) { + final count = selectionCount - movedCount; + showFeedback(context, 'Failed to export ${Intl.plural(count, one: '$count page', other: '$count pages')}'); + } else { + showFeedback(context, 'Done!'); + } + }, + ); + } + + Future _showRenameDialog(BuildContext context, AvesEntry entry) async { final newName = await showDialog( context: context, builder: (context) => RenameEntryDialog(entry), @@ -152,19 +211,19 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { showFeedback(context, await entry.rename(newName) ? 'Done!' : 'Failed'); } - void _goToSourceViewer(BuildContext context, ImageEntry entry) { + void _goToSourceViewer(BuildContext context, AvesEntry entry) { Navigator.push( context, MaterialPageRoute( settings: RouteSettings(name: SourceViewerPage.routeName), builder: (context) => SourceViewerPage( - loader: () => ImageFileService.getImage(entry.uri, entry.mimeType, 0, false).then(utf8.decode), + loader: () => ImageFileService.getSvg(entry.uri, entry.mimeType).then(utf8.decode), ), ), ); } - void _goToDebug(BuildContext context, ImageEntry entry) { + void _goToDebug(BuildContext context, AvesEntry entry) { Navigator.push( context, MaterialPageRoute( diff --git a/lib/widgets/viewer/entry_scroller.dart b/lib/widgets/viewer/entry_horizontal_pager.dart similarity index 74% rename from lib/widgets/viewer/entry_scroller.dart rename to lib/widgets/viewer/entry_horizontal_pager.dart index 58b799a9d..7927597a4 100644 --- a/lib/widgets/viewer/entry_scroller.dart +++ b/lib/widgets/viewer/entry_horizontal_pager.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/magnifier/pan/gesture_detector_scope.dart'; @@ -7,6 +7,7 @@ import 'package:aves/widgets/viewer/multipage.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; +import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class MultiEntryScroller extends StatefulWidget { @@ -33,7 +34,7 @@ class MultiEntryScroller extends StatefulWidget { } class _MultiEntryScrollerState extends State with AutomaticKeepAliveClientMixin { - List get entries => widget.collection.sortedEntries; + List get entries => widget.collection.sortedEntries; @override Widget build(BuildContext context) { @@ -61,7 +62,7 @@ class _MultiEntryScrollerState extends State with AutomaticK return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { - return _buildViewer(entry, multiPageInfo: multiPageInfo, page: page); + return _buildViewer(entry, page: multiPageInfo?.getByIndex(page)); }, ); }, @@ -79,20 +80,25 @@ class _MultiEntryScrollerState extends State with AutomaticK ); } - EntryPageView _buildViewer(ImageEntry entry, {MultiPageInfo multiPageInfo, int page = 0}) { - return EntryPageView( - key: Key('imageview'), - entry: entry, - multiPageInfo: multiPageInfo, - page: page, - heroTag: widget.collection.heroTag(entry), - onTap: (_) => widget.onTap?.call(), - videoControllers: widget.videoControllers, - onDisposed: () => widget.onViewDisposed?.call(entry.uri), + Widget _buildViewer(AvesEntry entry, {SinglePageInfo page}) { + return Selector( + selector: (c, mq) => mq.size, + builder: (c, mqSize, child) { + return EntryPageView( + key: Key('imageview'), + mainEntry: entry, + page: page, + viewportSize: mqSize, + heroTag: widget.collection.heroTag(entry), + onTap: (_) => widget.onTap?.call(), + videoControllers: widget.videoControllers, + onDisposed: () => widget.onViewDisposed?.call(entry.uri), + ); + }, ); } - MultiPageController _getMultiPageController(ImageEntry entry) { + MultiPageController _getMultiPageController(AvesEntry entry) { return widget.multiPageControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; } @@ -101,7 +107,7 @@ class _MultiEntryScrollerState extends State with AutomaticK } class SingleEntryScroller extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final VoidCallback onTap; final List> videoControllers; final List> multiPageControllers; @@ -118,7 +124,7 @@ class SingleEntryScroller extends StatefulWidget { } class _SingleEntryScrollerState extends State with AutomaticKeepAliveClientMixin { - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override Widget build(BuildContext context) { @@ -135,7 +141,7 @@ class _SingleEntryScrollerState extends State with Automati return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { - return _buildViewer(multiPageInfo: multiPageInfo, page: page); + return _buildViewer(page: multiPageInfo?.getByIndex(page)); }, ); }, @@ -150,17 +156,22 @@ class _SingleEntryScrollerState extends State with Automati ); } - EntryPageView _buildViewer({MultiPageInfo multiPageInfo, int page = 0}) { - return EntryPageView( - entry: entry, - multiPageInfo: multiPageInfo, - page: page, - onTap: (_) => widget.onTap?.call(), - videoControllers: widget.videoControllers, + Widget _buildViewer({SinglePageInfo page}) { + return Selector( + selector: (c, mq) => mq.size, + builder: (c, mqSize, child) { + return EntryPageView( + mainEntry: entry, + page: page, + viewportSize: mqSize, + onTap: (_) => widget.onTap?.call(), + videoControllers: widget.videoControllers, + ); + }, ); } - MultiPageController _getMultiPageController(ImageEntry entry) { + MultiPageController _getMultiPageController(AvesEntry entry) { return widget.multiPageControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; } diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart new file mode 100644 index 000000000..2abf301a7 --- /dev/null +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -0,0 +1,170 @@ +import 'dart:math'; + +import 'package:aves/model/connectivity.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart'; +import 'package:aves/widgets/viewer/entry_horizontal_pager.dart'; +import 'package:aves/widgets/viewer/info/info_page.dart'; +import 'package:aves/widgets/viewer/info/notifications.dart'; +import 'package:aves/widgets/viewer/multipage.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; +import 'package:tuple/tuple.dart'; + +class ViewerVerticalPageView extends StatefulWidget { + final CollectionLens collection; + final ValueNotifier entryNotifier; + final List> videoControllers; + final List> multiPageControllers; + final PageController horizontalPager, verticalPager; + final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; + final VoidCallback onImageTap, onImagePageRequested; + final void Function(String uri) onViewDisposed; + + const ViewerVerticalPageView({ + @required this.collection, + @required this.entryNotifier, + @required this.videoControllers, + @required this.multiPageControllers, + @required this.verticalPager, + @required this.horizontalPager, + @required this.onVerticalPageChanged, + @required this.onHorizontalPageChanged, + @required this.onImageTap, + @required this.onImagePageRequested, + @required this.onViewDisposed, + }); + + @override + _ViewerVerticalPageViewState createState() => _ViewerVerticalPageViewState(); +} + +class _ViewerVerticalPageViewState extends State { + final ValueNotifier _backgroundColorNotifier = ValueNotifier(Colors.black); + final ValueNotifier _infoPageVisibleNotifier = ValueNotifier(false); + AvesEntry _oldEntry; + + CollectionLens get collection => widget.collection; + + bool get hasCollection => collection != null; + + AvesEntry get entry => widget.entryNotifier.value; + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant ViewerVerticalPageView oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(ViewerVerticalPageView widget) { + widget.verticalPager.addListener(_onVerticalPageControllerChanged); + widget.entryNotifier.addListener(_onEntryChanged); + if (_oldEntry != entry) _onEntryChanged(); + } + + void _unregisterWidget(ViewerVerticalPageView widget) { + widget.verticalPager.removeListener(_onVerticalPageControllerChanged); + widget.entryNotifier.removeListener(_onEntryChanged); + _oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged); + } + + @override + Widget build(BuildContext context) { + final pages = [ + // fake page for opacity transition between collection and viewer + SizedBox(), + hasCollection + ? MultiEntryScroller( + collection: collection, + pageController: widget.horizontalPager, + onTap: widget.onImageTap, + onPageChanged: widget.onHorizontalPageChanged, + videoControllers: widget.videoControllers, + multiPageControllers: widget.multiPageControllers, + onViewDisposed: widget.onViewDisposed, + ) + : SingleEntryScroller( + entry: entry, + onTap: widget.onImageTap, + videoControllers: widget.videoControllers, + multiPageControllers: widget.multiPageControllers, + ), + NotificationListener( + onNotification: (notification) { + if (notification is BackUpNotification) widget.onImagePageRequested(); + return false; + }, + child: InfoPage( + collection: collection, + entryNotifier: widget.entryNotifier, + visibleNotifier: _infoPageVisibleNotifier, + ), + ), + ]; + return ValueListenableBuilder( + valueListenable: _backgroundColorNotifier, + builder: (context, backgroundColor, child) => Container( + color: backgroundColor, + child: child, + ), + child: PageView( + key: Key('vertical-pageview'), + scrollDirection: Axis.vertical, + controller: widget.verticalPager, + physics: MagnifierScrollerPhysics(parent: PageScrollPhysics()), + onPageChanged: (page) { + widget.onVerticalPageChanged(page); + _infoPageVisibleNotifier.value = page == pages.length - 1; + }, + children: pages, + ), + ); + } + + void _onVerticalPageControllerChanged() { + final opacity = min(1.0, widget.verticalPager.page); + _backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity); + } + + // when the entry changed (e.g. by scrolling through the PageView, or if the entry got deleted) + void _onEntryChanged() { + _oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged); + _oldEntry = entry; + + if (entry != null) { + entry.imageChangeNotifier.addListener(_onImageChanged); + // make sure to locate the entry, + // so that we can display the address instead of coordinates + // even when initial collection locating has not reached this entry yet + connectivity.canGeolocate.then((connected) { + if (connected) { + entry.locate(); + } + }); + } else { + Navigator.pop(context); + } + } + + // when the entry image itself changed (e.g. after rotation) + void _onImageChanged() async { + // rebuild to refresh the Image inside ImagePage + setState(() {}); + } +} diff --git a/lib/widgets/viewer/entry_viewer_page.dart b/lib/widgets/viewer/entry_viewer_page.dart index 65d35279b..f285d8292 100644 --- a/lib/widgets/viewer/entry_viewer_page.dart +++ b/lib/widgets/viewer/entry_viewer_page.dart @@ -1,20 +1,20 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/viewer/entry_viewer_stack.dart'; import 'package:flutter/material.dart'; -class MultiEntryViewerPage extends AnimatedWidget { +class EntryViewerPage extends StatelessWidget { static const routeName = '/viewer'; final CollectionLens collection; - final ImageEntry initialEntry; + final AvesEntry initialEntry; - const MultiEntryViewerPage({ + const EntryViewerPage({ Key key, this.collection, this.initialEntry, - }) : super(key: key, listenable: collection); + }) : super(key: key); @override Widget build(BuildContext context) { @@ -24,30 +24,6 @@ class MultiEntryViewerPage extends AnimatedWidget { collection: collection, initialEntry: initialEntry, ), - backgroundColor: Colors.transparent, - resizeToAvoidBottomInset: false, - ), - ); - } -} - -class SingleEntryViewerPage extends StatelessWidget { - static const routeName = '/viewer'; - - final ImageEntry entry; - - const SingleEntryViewerPage({ - Key key, - this.entry, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return MediaQueryDataProvider( - child: Scaffold( - body: EntryViewerStack( - initialEntry: entry, - ), backgroundColor: Navigator.canPop(context) ? Colors.transparent : Colors.black, resizeToAvoidBottomInset: false, ), diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 116befdc2..f9eec2f2d 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -1,18 +1,17 @@ import 'dart:math'; +import 'package:aves/model/connectivity.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/collection/collection_page.dart'; -import 'package:aves/widgets/common/gesture_area_protector.dart'; -import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/viewer/entry_action_delegate.dart'; -import 'package:aves/widgets/viewer/entry_scroller.dart'; -import 'package:aves/widgets/viewer/info/info_page.dart'; +import 'package:aves/widgets/viewer/entry_vertical_pager.dart'; import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:aves/widgets/viewer/multipage.dart'; import 'package:aves/widgets/viewer/overlay/bottom.dart'; @@ -31,12 +30,12 @@ import 'package:tuple/tuple.dart'; class EntryViewerStack extends StatefulWidget { final CollectionLens collection; - final ImageEntry initialEntry; + final AvesEntry initialEntry; const EntryViewerStack({ Key key, this.collection, - this.initialEntry, + @required this.initialEntry, }) : super(key: key); @override @@ -44,7 +43,7 @@ class EntryViewerStack extends StatefulWidget { } class _EntryViewerStackState extends State with SingleTickerProviderStateMixin, WidgetsBindingObserver { - final ValueNotifier _entryNotifier = ValueNotifier(null); + final ValueNotifier _entryNotifier = ValueNotifier(null); int _currentHorizontalPage; ValueNotifier _currentVerticalPage; PageController _horizontalPager, _verticalPager; @@ -63,7 +62,7 @@ class _EntryViewerStackState extends State with SingleTickerPr bool get hasCollection => collection != null; - List get entries => hasCollection ? collection.sortedEntries : [widget.initialEntry]; + List get entries => hasCollection ? collection.sortedEntries : [widget.initialEntry]; static const int transitionPage = 0; @@ -143,8 +142,15 @@ class _EntryViewerStackState extends State with SingleTickerPr @override void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.paused) { - _pauseVideoControllers(); + switch (state) { + case AppLifecycleState.paused: + _pauseVideoControllers(); + break; + case AppLifecycleState.resumed: + connectivity.onResume(); + break; + default: + break; } } @@ -166,6 +172,8 @@ class _EntryViewerStackState extends State with SingleTickerPr _goToCollection(notification.filter); } else if (notification is ViewStateNotification) { _updateViewState(notification.uri, notification.viewState); + } else if (notification is EntryDeletedNotification) { + _onEntryDeleted(context, notification.entry); } return false; }, @@ -199,7 +207,7 @@ class _EntryViewerStackState extends State with SingleTickerPr } Widget _buildTopOverlay() { - final child = ValueListenableBuilder( + final child = ValueListenableBuilder( valueListenable: _entryNotifier, builder: (context, entry, child) { if (entry == null) return SizedBox.shrink(); @@ -232,7 +240,7 @@ class _EntryViewerStackState extends State with SingleTickerPr } Widget _buildBottomOverlay() { - Widget bottomOverlay = ValueListenableBuilder( + Widget bottomOverlay = ValueListenableBuilder( valueListenable: _entryNotifier, builder: (context, entry, child) { if (entry == null) return SizedBox.shrink(); @@ -312,7 +320,7 @@ class _EntryViewerStackState extends State with SingleTickerPr return bottomOverlay; } - MultiPageController _getMultiPageController(ImageEntry entry) { + MultiPageController _getMultiPageController(AvesEntry entry) { return entry.isMultipage ? _multiPageControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2 : null; } @@ -326,7 +334,14 @@ class _EntryViewerStackState extends State with SingleTickerPr context, MaterialPageRoute( settings: RouteSettings(name: CollectionPage.routeName), - builder: (context) => CollectionPage(collection.derive(filter)), + builder: (context) => CollectionPage( + CollectionLens( + source: collection.source, + filters: collection.filters, + groupFactor: collection.groupFactor, + sortFactor: collection.sortFactor, + )..addFilter(filter), + ), ), (route) => false, ); @@ -358,6 +373,21 @@ class _EntryViewerStackState extends State with SingleTickerPr _updateEntry(); } + void _onEntryDeleted(BuildContext context, AvesEntry entry) { + if (hasCollection) { + final entries = collection.sortedEntries; + entries.remove(entry); + if (entries.isEmpty) { + Navigator.pop(context); + } else { + _onCollectionChange(); + } + } else { + // leave viewer + SystemNavigator.pop(); + } + } + void _updateEntry() { if (_currentHorizontalPage != null && entries.isNotEmpty && _currentHorizontalPage >= entries.length) { // as of Flutter v1.22.2, `PageView` does not call `onPageChanged` when the last page is deleted @@ -475,154 +505,3 @@ class _EntryViewerStackState extends State with SingleTickerPr void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause()); } - -class ViewerVerticalPageView extends StatefulWidget { - final CollectionLens collection; - final ValueNotifier entryNotifier; - final List> videoControllers; - final List> multiPageControllers; - final PageController horizontalPager, verticalPager; - final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; - final VoidCallback onImageTap, onImagePageRequested; - final void Function(String uri) onViewDisposed; - - const ViewerVerticalPageView({ - @required this.collection, - @required this.entryNotifier, - @required this.videoControllers, - @required this.multiPageControllers, - @required this.verticalPager, - @required this.horizontalPager, - @required this.onVerticalPageChanged, - @required this.onHorizontalPageChanged, - @required this.onImageTap, - @required this.onImagePageRequested, - @required this.onViewDisposed, - }); - - @override - _ViewerVerticalPageViewState createState() => _ViewerVerticalPageViewState(); -} - -class _ViewerVerticalPageViewState extends State { - final ValueNotifier _backgroundColorNotifier = ValueNotifier(Colors.black); - final ValueNotifier _infoPageVisibleNotifier = ValueNotifier(false); - ImageEntry _oldEntry; - - CollectionLens get collection => widget.collection; - - bool get hasCollection => collection != null; - - ImageEntry get entry => widget.entryNotifier.value; - - @override - void initState() { - super.initState(); - _registerWidget(widget); - } - - @override - void didUpdateWidget(covariant ViewerVerticalPageView oldWidget) { - super.didUpdateWidget(oldWidget); - _unregisterWidget(oldWidget); - _registerWidget(widget); - } - - @override - void dispose() { - _unregisterWidget(widget); - super.dispose(); - } - - void _registerWidget(ViewerVerticalPageView widget) { - widget.verticalPager.addListener(_onVerticalPageControllerChanged); - widget.entryNotifier.addListener(_onEntryChanged); - if (_oldEntry != entry) _onEntryChanged(); - } - - void _unregisterWidget(ViewerVerticalPageView widget) { - widget.verticalPager.removeListener(_onVerticalPageControllerChanged); - widget.entryNotifier.removeListener(_onEntryChanged); - _oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged); - } - - @override - Widget build(BuildContext context) { - final pages = [ - // fake page for opacity transition between collection and viewer - SizedBox(), - hasCollection - ? MultiEntryScroller( - collection: collection, - pageController: widget.horizontalPager, - onTap: widget.onImageTap, - onPageChanged: widget.onHorizontalPageChanged, - videoControllers: widget.videoControllers, - multiPageControllers: widget.multiPageControllers, - onViewDisposed: widget.onViewDisposed, - ) - : SingleEntryScroller( - entry: entry, - onTap: widget.onImageTap, - videoControllers: widget.videoControllers, - multiPageControllers: widget.multiPageControllers, - ), - NotificationListener( - onNotification: (notification) { - if (notification is BackUpNotification) widget.onImagePageRequested(); - return false; - }, - child: InfoPage( - collection: collection, - entryNotifier: widget.entryNotifier, - visibleNotifier: _infoPageVisibleNotifier, - ), - ), - ]; - return ValueListenableBuilder( - valueListenable: _backgroundColorNotifier, - builder: (context, backgroundColor, child) => Container( - color: backgroundColor, - child: child, - ), - child: PageView( - key: Key('vertical-pageview'), - scrollDirection: Axis.vertical, - controller: widget.verticalPager, - physics: MagnifierScrollerPhysics(parent: PageScrollPhysics()), - onPageChanged: (page) { - widget.onVerticalPageChanged(page); - _infoPageVisibleNotifier.value = page == pages.length - 1; - }, - children: pages, - ), - ); - } - - void _onVerticalPageControllerChanged() { - final opacity = min(1.0, widget.verticalPager.page); - _backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity); - } - - // when the entry changed (e.g. by scrolling through the PageView, or if the entry got deleted) - void _onEntryChanged() { - _oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged); - _oldEntry = entry; - - if (entry != null) { - entry.imageChangeNotifier.addListener(_onImageChanged); - // make sure to locate the entry, - // so that we can display the address instead of coordinates - // even when background locating has not reached this entry yet - entry.locate(); - } else { - Navigator.pop(context); - } - } - - // when the entry image itself changed (e.g. after rotation) - void _onImageChanged() async { - // rebuild to refresh the Image inside ImagePage - setState(() {}); - } -} diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index bab656c2b..acfd8119d 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -1,11 +1,14 @@ +import 'package:aves/image_providers/app_icon_image_provider.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/tag.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/ref/mime_types.dart'; +import 'package:aves/services/metadata_service.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -15,14 +18,16 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class BasicSection extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final CollectionLens collection; + final ValueNotifier visibleNotifier; final FilterCallback onFilter; const BasicSection({ Key key, @required this.entry, this.collection, + @required this.visibleNotifier, @required this.onFilter, }) : super(key: key); @@ -30,7 +35,7 @@ class BasicSection extends StatelessWidget { bool get showMegaPixels => entry.isPhoto && megaPixels != null && megaPixels > 0; - String get rasterResolutionText => '${entry.getResolutionText()}${showMegaPixels ? ' ($megaPixels MP)' : ''}'; + String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? ' ($megaPixels MP)' : ''}'; @override Widget build(BuildContext context) { @@ -55,6 +60,10 @@ class BasicSection extends StatelessWidget { 'URI': uri, if (path != null) 'Path': path, }), + OwnerProp( + entry: entry, + visibleNotifier: visibleNotifier, + ), _buildChips(), ], ); @@ -102,3 +111,109 @@ class BasicSection extends StatelessWidget { }; } } + +class OwnerProp extends StatefulWidget { + final AvesEntry entry; + final ValueNotifier visibleNotifier; + + const OwnerProp({ + @required this.entry, + @required this.visibleNotifier, + }); + + @override + _OwnerPropState createState() => _OwnerPropState(); +} + +class _OwnerPropState extends State { + final ValueNotifier _loadedUri = ValueNotifier(null); + String _ownerPackage; + + AvesEntry get entry => widget.entry; + + bool get isVisible => widget.visibleNotifier.value; + + static const iconSize = 20.0; + + @override + void initState() { + super.initState(); + _registerWidget(widget); + _getOwner(); + } + + @override + void didUpdateWidget(covariant OwnerProp oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + _getOwner(); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(OwnerProp widget) { + widget.visibleNotifier.addListener(_getOwner); + } + + void _unregisterWidget(OwnerProp widget) { + widget.visibleNotifier.removeListener(_getOwner); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _loadedUri, + builder: (context, uri, child) { + if (_ownerPackage == null) return SizedBox(); + final appName = androidFileUtils.getCurrentAppName(_ownerPackage) ?? _ownerPackage; + // as of Flutter v1.22.6, `SelectableText` cannot contain `WidgetSpan` + // so be use a basic `Text` instead + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Owned by', + style: InfoRowGroup.keyStyle, + ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Image( + image: AppIconImage( + packageName: _ownerPackage, + size: iconSize, + ), + width: iconSize, + height: iconSize, + ), + ), + ), + TextSpan( + text: appName, + style: InfoRowGroup.baseStyle, + ), + ], + ), + ); + }, + ); + } + + Future _getOwner() async { + if (entry == null) return; + if (_loadedUri.value == entry.uri) return; + if (isVisible) { + _ownerPackage = await MetadataService.getContentResolverProp(widget.entry, 'owner_package_name'); + _loadedUri.value = entry.uri; + } else { + _ownerPackage = null; + _loadedUri.value = null; + } + } +} diff --git a/lib/widgets/viewer/info/common.dart b/lib/widgets/viewer/info/common.dart index 4002d1571..5d99e65f5 100644 --- a/lib/widgets/viewer/info/common.dart +++ b/lib/widgets/viewer/info/common.dart @@ -42,6 +42,12 @@ class InfoRowGroup extends StatefulWidget { final int maxValueLength; final Map linkHandlers; + static const keyValuePadding = 16; + static const linkColor = Colors.blue; + static final baseStyle = TextStyle(fontFamily: 'Concourse'); + static final keyStyle = baseStyle.copyWith(color: Colors.white70, height: 1.7); + static final linkStyle = baseStyle.copyWith(color: linkColor, decoration: TextDecoration.underline); + const InfoRowGroup( this.keyValues, { this.maxValueLength = 0, @@ -61,20 +67,14 @@ class _InfoRowGroupState extends State { Map get linkHandlers => widget.linkHandlers; - static const keyValuePadding = 16; - static const linkColor = Colors.blue; - static final baseStyle = TextStyle(fontFamily: 'Concourse'); - static final keyStyle = baseStyle.copyWith(color: Colors.white70, height: 1.7); - static final linkStyle = baseStyle.copyWith(color: linkColor, decoration: TextDecoration.underline); - @override Widget build(BuildContext context) { if (keyValues.isEmpty) return SizedBox.shrink(); // compute the size of keys and space in order to align values final textScaleFactor = MediaQuery.textScaleFactorOf(context); - final keySizes = Map.fromEntries(keyValues.keys.map((key) => MapEntry(key, _getSpanWidth(TextSpan(text: '$key', style: keyStyle), textScaleFactor)))); - final baseSpaceWidth = _getSpanWidth(TextSpan(text: '\u200A' * 100, style: baseStyle), textScaleFactor); + final keySizes = Map.fromEntries(keyValues.keys.map((key) => MapEntry(key, _getSpanWidth(TextSpan(text: '$key', style: InfoRowGroup.keyStyle), textScaleFactor)))); + final baseSpaceWidth = _getSpanWidth(TextSpan(text: '\u200A' * 100, style: InfoRowGroup.baseStyle), textScaleFactor); final lastKey = keyValues.keys.last; return LayoutBuilder( @@ -100,7 +100,7 @@ class _InfoRowGroupState extends State { value = handler.linkText; // open link on tap recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context); - style = linkStyle; + style = InfoRowGroup.linkStyle; } else { value = kv.value; // long values are clipped, and made expandable by tapping them @@ -116,20 +116,20 @@ class _InfoRowGroupState extends State { value = '$value\n'; } - // as of Flutter v1.22.4, `SelectableText` cannot contain `WidgetSpan` + // as of Flutter v1.22.6, `SelectableText` cannot contain `WidgetSpan` // so we add padding using multiple hair spaces instead - final thisSpaceSize = max(0.0, (baseValueX - keySizes[key])) + keyValuePadding; + final thisSpaceSize = max(0.0, (baseValueX - keySizes[key])) + InfoRowGroup.keyValuePadding; final spaceCount = (100 * thisSpaceSize / baseSpaceWidth).round(); return [ - TextSpan(text: key, style: keyStyle), + TextSpan(text: key, style: InfoRowGroup.keyStyle), TextSpan(text: '\u200A' * spaceCount), TextSpan(text: value, style: style, recognizer: recognizer), ]; }, ).toList(), ), - style: baseStyle, + style: InfoRowGroup.baseStyle, ), ], ); diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index df0eee399..34e2aebe2 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/viewer/info/info_search.dart'; @@ -6,7 +6,7 @@ import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; import 'package:flutter/material.dart'; class InfoAppBar extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final ValueNotifier> metadataNotifier; final VoidCallback onBackPressed; diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 7f6bf741c..112bb6310 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -1,9 +1,11 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/common/gesture_area_protector.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/info/basic_section.dart'; import 'package:aves/widgets/viewer/info/info_app_bar.dart'; import 'package:aves/widgets/viewer/info/location_section.dart'; @@ -11,11 +13,10 @@ import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:tuple/tuple.dart'; class InfoPage extends StatefulWidget { final CollectionLens collection; - final ValueNotifier entryNotifier; + final ValueNotifier entryNotifier; final ValueNotifier visibleNotifier; const InfoPage({ @@ -35,7 +36,7 @@ class _InfoPageState extends State { CollectionLens get collection => widget.collection; - ImageEntry get entry => widget.entryNotifier.value; + AvesEntry get entry => widget.entryNotifier.value; @override Widget build(BuildContext context) { @@ -43,30 +44,34 @@ class _InfoPageState extends State { child: Scaffold( body: GestureAreaProtectorStack( child: SafeArea( + bottom: false, child: NotificationListener( onNotification: _handleTopScroll, - child: Selector>( - selector: (c, mq) => Tuple2(mq.size.width, mq.viewInsets.bottom), - builder: (c, mq, child) { - final mqWidth = mq.item1; - final mqViewInsetsBottom = mq.item2; - return ValueListenableBuilder( - valueListenable: widget.entryNotifier, - builder: (context, entry, child) { - return entry != null - ? _InfoPageContent( - collection: collection, - entry: entry, - visibleNotifier: widget.visibleNotifier, - scrollController: _scrollController, - split: mqWidth > 400, - mqViewInsetsBottom: mqViewInsetsBottom, - goToViewer: _goToViewer, - ) - : SizedBox.shrink(); - }, - ); + child: NotificationListener( + onNotification: (notification) { + _openTempEntry(notification.entry); + return true; }, + child: Selector( + selector: (c, mq) => mq.size.width, + builder: (c, mqWidth, child) { + return ValueListenableBuilder( + valueListenable: widget.entryNotifier, + builder: (context, entry, child) { + return entry != null + ? _InfoPageContent( + collection: collection, + entry: entry, + visibleNotifier: widget.visibleNotifier, + scrollController: _scrollController, + split: mqWidth > 600, + goToViewer: _goToViewer, + ) + : SizedBox.shrink(); + }, + ); + }, + ), ), ), ), @@ -106,15 +111,26 @@ class _InfoPageState extends State { curve: Curves.easeInOut, ); } + + void _openTempEntry(AvesEntry tempEntry) { + Navigator.push( + context, + TransparentMaterialPageRoute( + settings: RouteSettings(name: EntryViewerPage.routeName), + pageBuilder: (c, a, sa) => EntryViewerPage( + initialEntry: tempEntry, + ), + ), + ); + } } class _InfoPageContent extends StatefulWidget { final CollectionLens collection; - final ImageEntry entry; + final AvesEntry entry; final ValueNotifier visibleNotifier; final ScrollController scrollController; final bool split; - final double mqViewInsetsBottom; final VoidCallback goToViewer; const _InfoPageContent({ @@ -124,7 +140,6 @@ class _InfoPageContent extends StatefulWidget { @required this.visibleNotifier, @required this.scrollController, @required this.split, - @required this.mqViewInsetsBottom, @required this.goToViewer, }) : super(key: key); @@ -139,16 +154,24 @@ class _InfoPageContentState extends State<_InfoPageContent> { CollectionLens get collection => widget.collection; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; + + ValueNotifier get visibleNotifier => widget.visibleNotifier; @override Widget build(BuildContext context) { + final basicSection = BasicSection( + entry: entry, + collection: collection, + visibleNotifier: visibleNotifier, + onFilter: _goToCollection, + ); final locationAtTop = widget.split && entry.hasGps; final locationSection = LocationSection( collection: collection, entry: entry, showTitle: !locationAtTop, - visibleNotifier: widget.visibleNotifier, + visibleNotifier: visibleNotifier, onFilter: _goToCollection, ); final basicAndLocationSliver = locationAtTop @@ -156,7 +179,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(child: BasicSection(entry: entry, collection: collection, onFilter: _goToCollection)), + Expanded(child: basicSection), SizedBox(width: 8), Expanded(child: locationSection), ], @@ -165,7 +188,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { : SliverList( delegate: SliverChildListDelegate.fixed( [ - BasicSection(entry: entry, collection: collection, onFilter: _goToCollection), + basicSection, locationSection, ], ), @@ -173,7 +196,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { final metadataSliver = MetadataSectionSliver( entry: entry, metadataNotifier: _metadataNotifier, - visibleNotifier: widget.visibleNotifier, + visibleNotifier: visibleNotifier, ); return CustomScrollView( @@ -189,9 +212,10 @@ class _InfoPageContentState extends State<_InfoPageContent> { sliver: basicAndLocationSliver, ), SliverPadding( - padding: horizontalPadding + EdgeInsets.only(bottom: 8 + widget.mqViewInsetsBottom), + padding: horizontalPadding + EdgeInsets.only(bottom: 8), sliver: metadataSliver, ), + BottomPaddingSliver(), ], ); } diff --git a/lib/widgets/viewer/info/info_search.dart b/lib/widgets/viewer/info/info_search.dart index 0e12b4276..601f6b70c 100644 --- a/lib/widgets/viewer/info/info_search.dart +++ b/lib/widgets/viewer/info/info_search.dart @@ -1,13 +1,16 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; +import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; class InfoSearchDelegate extends SearchDelegate { - final ImageEntry entry; + final AvesEntry entry; final ValueNotifier> metadataNotifier; Map get metadata => metadataNotifier.value; @@ -109,10 +112,28 @@ class InfoSearchDelegate extends SearchDelegate { icon: AIcons.info, text: 'No matching keys', ) - : ListView.builder( - padding: EdgeInsets.all(8), - itemBuilder: (context, index) => tiles[index], - itemCount: tiles.length, + : NotificationListener( + onNotification: (notification) { + _openTempEntry(context, notification.entry); + return true; + }, + child: ListView.builder( + padding: EdgeInsets.all(8), + itemBuilder: (context, index) => tiles[index], + itemCount: tiles.length, + ), ); } + + void _openTempEntry(BuildContext context, AvesEntry tempEntry) { + Navigator.push( + context, + TransparentMaterialPageRoute( + settings: RouteSettings(name: EntryViewerPage.routeName), + pageBuilder: (c, a, sa) => EntryViewerPage( + initialEntry: tempEntry, + ), + ), + ); + } } diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index 3341d3d54..1dd7a3e0c 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -1,6 +1,8 @@ +import 'package:aves/model/connectivity.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/location.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/coordinate_format.dart'; +import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; @@ -16,7 +18,7 @@ import 'package:tuple/tuple.dart'; class LocationSection extends StatefulWidget { final CollectionLens collection; - final ImageEntry entry; + final AvesEntry entry; final bool showTitle; final ValueNotifier visibleNotifier; final FilterCallback onFilter; @@ -42,7 +44,7 @@ class _LocationSectionState extends State with TickerProviderSt CollectionLens get collection => widget.collection; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override void initState() { @@ -101,34 +103,40 @@ class _LocationSectionState extends State with TickerProviderSt crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.showTitle) SectionRow(AIcons.location), - NotificationListener( - onNotification: (notification) { - if (notification is MapStyleChangedNotification) setState(() {}); - return false; + FutureBuilder( + future: connectivity.isConnected, + builder: (context, snapshot) { + if (snapshot.data != true) return SizedBox(); + return NotificationListener( + onNotification: (notification) { + if (notification is MapStyleChangedNotification) setState(() {}); + return false; + }, + child: AnimatedSize( + alignment: Alignment.topCenter, + curve: Curves.easeInOutCubic, + duration: Durations.mapStyleSwitchAnimation, + vsync: this, + child: settings.infoMapStyle.isGoogleMaps + ? EntryGoogleMap( + // `LatLng` used by `google_maps_flutter` is not the one from `latlong` package + latLng: Tuple2(entry.latLng.latitude, entry.latLng.longitude), + geoUri: entry.geoUri, + initialZoom: settings.infoMapZoom, + markerId: entry.uri ?? entry.path, + markerBuilder: buildMarker, + ) + : EntryLeafletMap( + latLng: entry.latLng, + geoUri: entry.geoUri, + initialZoom: settings.infoMapZoom, + style: settings.infoMapStyle, + markerSize: Size(extent, extent + pointerSize.height), + markerBuilder: buildMarker, + ), + ), + ); }, - child: AnimatedSize( - alignment: Alignment.topCenter, - curve: Curves.easeInOutCubic, - duration: Durations.mapStyleSwitchAnimation, - vsync: this, - child: settings.infoMapStyle.isGoogleMaps - ? EntryGoogleMap( - // `LatLng` used by `google_maps_flutter` is not the one from `latlong` package - latLng: Tuple2(entry.latLng.latitude, entry.latLng.longitude), - geoUri: entry.geoUri, - initialZoom: settings.infoMapZoom, - markerId: entry.uri ?? entry.path, - markerBuilder: buildMarker, - ) - : EntryLeafletMap( - latLng: entry.latLng, - geoUri: entry.geoUri, - initialZoom: settings.infoMapZoom, - style: settings.infoMapStyle, - markerSize: Size(extent, extent + pointerSize.height), - markerBuilder: buildMarker, - ), - ), ), if (entry.hasGps) _AddressInfoGroup(entry: entry), if (filters.isNotEmpty) @@ -157,7 +165,7 @@ class _LocationSectionState extends State with TickerProviderSt } class _AddressInfoGroup extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; const _AddressInfoGroup({@required this.entry}); @@ -168,12 +176,17 @@ class _AddressInfoGroup extends StatefulWidget { class _AddressInfoGroupState extends State<_AddressInfoGroup> { Future _addressLineLoader; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override void initState() { super.initState(); - _addressLineLoader = entry.findAddressLine(); + _addressLineLoader = connectivity.canGeolocate.then((connected) { + if (connected) { + return entry.findAddressLine(); + } + return null; + }); } @override @@ -190,38 +203,3 @@ class _AddressInfoGroupState extends State<_AddressInfoGroup> { ); } } - -// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ -enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor } - -extension ExtraEntryMapStyle on EntryMapStyle { - String get name { - switch (this) { - case EntryMapStyle.googleNormal: - return 'Google Maps'; - case EntryMapStyle.googleHybrid: - return 'Google Maps (Hybrid)'; - case EntryMapStyle.googleTerrain: - return 'Google Maps (Terrain)'; - case EntryMapStyle.osmHot: - return 'Humanitarian OSM'; - case EntryMapStyle.stamenToner: - return 'Stamen Toner'; - case EntryMapStyle.stamenWatercolor: - return 'Stamen Watercolor'; - default: - return toString(); - } - } - - bool get isGoogleMaps { - switch (this) { - case EntryMapStyle.googleNormal: - case EntryMapStyle.googleHybrid: - case EntryMapStyle.googleTerrain: - return true; - default: - return false; - } - } -} diff --git a/lib/widgets/viewer/info/maps/common.dart b/lib/widgets/viewer/info/maps/common.dart index fb8fc89c1..ee28ed748 100644 --- a/lib/widgets/viewer/info/maps/common.dart +++ b/lib/widgets/viewer/info/maps/common.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/theme/durations.dart'; @@ -6,7 +7,6 @@ import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; -import 'package:aves/widgets/viewer/info/location_section.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -63,7 +63,7 @@ class MapButtonPanel extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ MapOverlayButton( - icon: AIcons.openInNew, + icon: AIcons.openOutside, onPressed: () => AndroidAppService.openMap(geoUri).then((success) { if (!success) showNoMatchingAppDialog(context); }), diff --git a/lib/widgets/viewer/info/maps/google_map.dart b/lib/widgets/viewer/info/maps/google_map.dart index 0c6e05bd7..25781b1d1 100644 --- a/lib/widgets/viewer/info/maps/google_map.dart +++ b/lib/widgets/viewer/info/maps/google_map.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/widgets/viewer/info/location_section.dart'; import 'package:aves/widgets/viewer/info/maps/common.dart'; import 'package:aves/widgets/viewer/info/maps/marker.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/viewer/info/maps/leaflet_map.dart b/lib/widgets/viewer/info/maps/leaflet_map.dart index 02f53dbd8..d5d2ecaf6 100644 --- a/lib/widgets/viewer/info/maps/leaflet_map.dart +++ b/lib/widgets/viewer/info/maps/leaflet_map.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/viewer/info/maps/common.dart'; import 'package:aves/widgets/viewer/info/maps/scale_layer.dart'; @@ -7,8 +8,6 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:latlong/latlong.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../location_section.dart'; - class EntryLeafletMap extends StatefulWidget { final LatLng latLng; final String geoUri; diff --git a/lib/widgets/viewer/info/maps/marker.dart b/lib/widgets/viewer/info/maps/marker.dart index 9e3a42629..c40806c22 100644 --- a/lib/widgets/viewer/info/maps/marker.dart +++ b/lib/widgets/viewer/info/maps/marker.dart @@ -1,14 +1,14 @@ import 'dart:typed_data'; import 'dart:ui' as ui; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/widgets/collection/thumbnail/raster.dart'; import 'package:aves/widgets/collection/thumbnail/vector.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class ImageMarker extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; final Size pointerSize; @@ -158,7 +158,7 @@ class _MarkerGeneratorWidgetState extends State { type: MaterialType.transparency, child: Stack( children: widget.markers.map((i) { - final key = GlobalKey(); + final key = GlobalKey(debugLabel: 'map-marker-$i'); _globalKeys.add(key); return RepaintBoundary( key: key, diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index 0ea17ab08..3bc2f74b6 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -1,6 +1,6 @@ import 'dart:collection'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/ref/brand_colors.dart'; import 'package:aves/services/svg_metadata_service.dart'; import 'package:aves/theme/icons.dart'; @@ -16,7 +16,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class MetadataDirTile extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final String title; final MetadataDirectory dir; final ValueNotifier expandedDirectoryNotifier; @@ -61,13 +61,7 @@ class MetadataDirTile extends StatelessWidget { ); if (tags['Has Video'] == 'yes') prefixChildren.add(builder(AIcons.video)); if (tags['Has Audio'] == 'yes') prefixChildren.add(builder(AIcons.audio)); - if (tags['Has Image'] == 'yes') { - int count; - if (tags.containsKey('Image Count')) { - count = int.tryParse(tags['Image Count']); - } - prefixChildren.addAll(List.generate(count ?? 1, (i) => builder(AIcons.image))); - } + if (tags['Has Image'] == 'yes') prefixChildren.add(builder(AIcons.image)); break; } } diff --git a/lib/widgets/viewer/info/metadata/metadata_section.dart b/lib/widgets/viewer/info/metadata/metadata_section.dart index 6cba39d94..d148be209 100644 --- a/lib/widgets/viewer/info/metadata/metadata_section.dart +++ b/lib/widgets/viewer/info/metadata/metadata_section.dart @@ -1,6 +1,6 @@ import 'dart:collection'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:aves/services/svg_metadata_service.dart'; import 'package:aves/theme/durations.dart'; @@ -13,7 +13,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; class MetadataSectionSliver extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final ValueNotifier visibleNotifier; final ValueNotifier> metadataNotifier; @@ -31,7 +31,7 @@ class _MetadataSectionSliverState extends State with Auto final ValueNotifier _loadedMetadataUri = ValueNotifier(null); final ValueNotifier _expandedDirectoryNotifier = ValueNotifier(null); - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; bool get isVisible => widget.visibleNotifier.value; diff --git a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart index 7072ac53e..646bf9736 100644 --- a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart +++ b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:flutter/material.dart'; @@ -9,7 +9,7 @@ enum MetadataThumbnailSource { embedded, exif } class MetadataThumbnails extends StatefulWidget { final MetadataThumbnailSource source; - final ImageEntry entry; + final AvesEntry entry; const MetadataThumbnails({ Key key, @@ -24,7 +24,7 @@ class MetadataThumbnails extends StatefulWidget { class _MetadataThumbnailsState extends State { Future> _loader; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; String get uri => entry.uri; diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index cbd1c5c1a..731d49867 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -1,15 +1,13 @@ import 'dart:collection'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/xmp.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; -import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; -import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/exif.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/google.dart'; @@ -17,12 +15,13 @@ import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/photoshop.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/tiff.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/xmp.dart'; +import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:pedantic/pedantic.dart'; class XmpDirTile extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final SplayTreeMap tags; final ValueNotifier expandedNotifier; final bool initiallyExpanded; @@ -39,7 +38,7 @@ class XmpDirTile extends StatefulWidget { } class _XmpDirTileState extends State with FeedbackMixin { - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override Widget build(BuildContext context) { @@ -123,13 +122,6 @@ class _XmpDirTileState extends State with FeedbackMixin { return; } - final embedEntry = ImageEntry.fromMap(fields); - unawaited(Navigator.push( - context, - TransparentMaterialPageRoute( - settings: RouteSettings(name: SingleEntryViewerPage.routeName), - pageBuilder: (c, a, sa) => SingleEntryViewerPage(entry: embedEntry), - ), - )); + OpenTempEntryNotification(entry: AvesEntry.fromMap(fields)).dispatch(context); } } diff --git a/lib/widgets/viewer/info/notifications.dart b/lib/widgets/viewer/info/notifications.dart index 0f46e5aac..ed2da66ce 100644 --- a/lib/widgets/viewer/info/notifications.dart +++ b/lib/widgets/viewer/info/notifications.dart @@ -1,4 +1,6 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class BackUpNotification extends Notification {} @@ -8,3 +10,20 @@ class FilterNotification extends Notification { const FilterNotification(this.filter); } + +class EntryDeletedNotification extends Notification { + final AvesEntry entry; + + const EntryDeletedNotification(this.entry); +} + +class OpenTempEntryNotification extends Notification { + final AvesEntry entry; + + const OpenTempEntryNotification({ + @required this.entry, + }); + + @override + String toString() => '$runtimeType#${shortHash(this)}{entry=$entry}'; +} diff --git a/lib/widgets/viewer/multipage.dart b/lib/widgets/viewer/multipage.dart index 004ff7a49..91de82859 100644 --- a/lib/widgets/viewer/multipage.dart +++ b/lib/widgets/viewer/multipage.dart @@ -1,16 +1,21 @@ import 'dart:async'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class MultiPageController extends ChangeNotifier { - final Future info; - final ValueNotifier pageNotifier = ValueNotifier(0); + Future info; + final ValueNotifier pageNotifier = ValueNotifier(null); - MultiPageController(ImageEntry entry) : info = MetadataService.getMultiPageInfo(entry); + MultiPageController(AvesEntry entry) { + info = MetadataService.getMultiPageInfo(entry).then((value) { + pageNotifier.value = value.defaultPage.index; + return value; + }); + } int get page => pageNotifier.value; diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 9863c3793..2696d5199 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/model/settings/coordinate_format.dart'; import 'package:aves/model/settings/settings.dart'; @@ -20,7 +20,7 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class ViewerBottomOverlay extends StatefulWidget { - final List entries; + final List entries; final int index; final bool showPosition; final EdgeInsets viewInsets, viewPadding; @@ -42,10 +42,10 @@ class ViewerBottomOverlay extends StatefulWidget { class _ViewerBottomOverlayState extends State { Future _detailLoader; - ImageEntry _lastEntry; + AvesEntry _lastEntry; OverlayMetadata _lastDetails; - ImageEntry get entry { + AvesEntry get entry { final entries = widget.entries; final index = widget.index; return index < entries.length ? entries[index] : null; @@ -97,15 +97,31 @@ class _ViewerBottomOverlayState extends State { _lastDetails = snapshot.data; _lastEntry = entry; } - return _lastEntry == null - ? SizedBox.shrink() - : _BottomOverlayContent( - entry: _lastEntry, - details: _lastDetails, - position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null, - availableWidth: availableWidth, - multiPageController: multiPageController, - ); + if (_lastEntry == null) return SizedBox.shrink(); + + Widget _buildContent({MultiPageInfo multiPageInfo, int page}) => _BottomOverlayContent( + mainEntry: _lastEntry, + page: multiPageInfo?.getByIndex(page), + details: _lastDetails, + position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null, + availableWidth: availableWidth, + multiPageController: multiPageController, + ); + + if (multiPageController == null) return _buildContent(); + + return FutureBuilder( + future: multiPageController.info, + builder: (context, snapshot) { + final multiPageInfo = snapshot.data; + return ValueListenableBuilder( + valueListenable: multiPageController.pageNotifier, + builder: (context, page, child) { + return _buildContent(multiPageInfo: multiPageInfo, page: page); + }, + ); + }, + ); }, ), ); @@ -121,7 +137,8 @@ const double _interRowPadding = 2.0; const double _subRowMinWidth = 300.0; class _BottomOverlayContent extends AnimatedWidget { - final ImageEntry entry; + final AvesEntry mainEntry, entry; + final SinglePageInfo page; final OverlayMetadata details; final String position; final double availableWidth; @@ -131,12 +148,14 @@ class _BottomOverlayContent extends AnimatedWidget { _BottomOverlayContent({ Key key, - this.entry, + this.mainEntry, + this.page, this.details, this.position, this.availableWidth, this.multiPageController, - }) : super(key: key, listenable: entry.metadataChangeNotifier); + }) : entry = mainEntry.getPageEntry(page), + super(key: key, listenable: mainEntry.metadataChangeNotifier); @override Widget build(BuildContext context) { @@ -158,13 +177,13 @@ class _BottomOverlayContent extends AnimatedWidget { infoColumn = _buildInfoColumn(orientation); } - if (multiPageController != null) { + if (mainEntry.isMultipage && multiPageController != null) { infoColumn = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ MultiPageOverlay( - entry: entry, + mainEntry: mainEntry, controller: multiPageController, availableWidth: availableWidth, ), @@ -280,7 +299,7 @@ class _BottomOverlayContent extends AnimatedWidget { } class _LocationRow extends AnimatedWidget { - final ImageEntry entry; + final AvesEntry entry; _LocationRow({ Key key, @@ -306,7 +325,7 @@ class _LocationRow extends AnimatedWidget { } class _PositionTitleRow extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final String collectionPosition; final MultiPageController multiPageController; @@ -320,6 +339,8 @@ class _PositionTitleRow extends StatelessWidget { bool get isNotEmpty => collectionPosition != null || multiPageController != null || title != null; + static const separator = ' • '; + @override Widget build(BuildContext context) { Text toText({String pagePosition}) => Text( @@ -327,7 +348,7 @@ class _PositionTitleRow extends StatelessWidget { if (collectionPosition != null) collectionPosition, if (pagePosition != null) pagePosition, if (title != null) title, - ].join(' • '), + ].join(separator), strutStyle: Constants.overflowStrutStyle); if (multiPageController == null) return toText(); @@ -336,23 +357,24 @@ class _PositionTitleRow extends StatelessWidget { future: multiPageController.info, builder: (context, snapshot) { final multiPageInfo = snapshot.data; - final pageCount = multiPageInfo?.pageCount; - // page count may be 0 when we know an entry to have multiple pages - // but fail to get information about these pages - final missingInfo = pageCount == 0; - return ValueListenableBuilder( - valueListenable: multiPageController.pageNotifier, - builder: (context, page, child) { - return toText(pagePosition: missingInfo ? null : '${page + 1}/${pageCount ?? '?'}'); - }, - ); + String pagePosition; + if (multiPageInfo != null) { + // page count may be 0 when we know an entry to have multiple pages + // but fail to get information about these pages + final pageCount = multiPageInfo.pageCount; + if (pageCount > 0) { + final page = multiPageInfo.getById(entry.pageId) ?? multiPageInfo.defaultPage; + pagePosition = '${(page?.index ?? 0) + 1}/$pageCount'; + } + } + return toText(pagePosition: pagePosition); }, ); } } class _DateRow extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final MultiPageController multiPageController; const _DateRow({ @@ -364,40 +386,14 @@ class _DateRow extends StatelessWidget { Widget build(BuildContext context) { final date = entry.bestDate; final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.overlayUnknown; + final resolutionText = entry.isSvg ? entry.aspectRatioText : entry.resolutionText; - Text toText({MultiPageInfo multiPageInfo, int page}) => Text( - entry.isSvg - ? entry.aspectRatioText - : entry.getResolutionText( - multiPageInfo: multiPageInfo, - page: page, - ), - strutStyle: Constants.overflowStrutStyle, - ); - - Widget resolutionText; - if (multiPageController != null) { - resolutionText = FutureBuilder( - future: multiPageController.info, - builder: (context, snapshot) { - final multiPageInfo = snapshot.data; - return ValueListenableBuilder( - valueListenable: multiPageController.pageNotifier, - builder: (context, page, child) { - return toText(multiPageInfo: multiPageInfo, page: page); - }, - ); - }, - ); - } else { - resolutionText = toText(); - } return Row( children: [ DecoratedIcon(AIcons.date, shadows: [Constants.embossShadow], size: _iconSize), SizedBox(width: _iconPadding), Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)), - Expanded(flex: 2, child: resolutionText), + Expanded(flex: 2, child: Text(resolutionText, strutStyle: Constants.overflowStrutStyle)), ], ); } diff --git a/lib/widgets/viewer/overlay/minimap.dart b/lib/widgets/viewer/overlay/minimap.dart index ce9c6b101..3aea62b92 100644 --- a/lib/widgets/viewer/overlay/minimap.dart +++ b/lib/widgets/viewer/overlay/minimap.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/widgets/viewer/multipage.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; @@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class Minimap extends StatelessWidget { - final ImageEntry entry; + final AvesEntry mainEntry; final ValueNotifier viewStateNotifier; final MultiPageController multiPageController; final Size size; @@ -16,7 +16,7 @@ class Minimap extends StatelessWidget { static const defaultSize = Size(96, 96); const Minimap({ - @required this.entry, + @required this.mainEntry, @required this.viewStateNotifier, @required this.multiPageController, this.size = defaultSize, @@ -34,29 +34,33 @@ class Minimap extends StatelessWidget { return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { - return _buildForEntrySize(entry.getDisplaySize(multiPageInfo: multiPageInfo, page: page)); + final pageEntry = mainEntry.getPageEntry(multiPageInfo?.getByIndex(page)); + return _buildForEntrySize(pageEntry); }, ); }) - : _buildForEntrySize(entry.getDisplaySize()), + : _buildForEntrySize(mainEntry), ); } - Widget _buildForEntrySize(Size entrySize) { + Widget _buildForEntrySize(AvesEntry entry) { return ValueListenableBuilder( valueListenable: viewStateNotifier, builder: (context, viewState, child) { final viewportSize = viewState.viewportSize; if (viewportSize == null) return SizedBox.shrink(); - return CustomPaint( - painter: MinimapPainter( - viewportSize: viewportSize, - entrySize: entrySize, - viewCenterOffset: viewState.position, - viewScale: viewState.scale, - minimapBorderColor: Colors.white30, + return AnimatedBuilder( + animation: entry.imageChangeNotifier, + builder: (context, child) => CustomPaint( + painter: MinimapPainter( + viewportSize: viewportSize, + entrySize: entry.displaySize, + viewCenterOffset: viewState.position, + viewScale: viewState.scale, + minimapBorderColor: Colors.white30, + ), + size: size, ), - size: size, ); }); } diff --git a/lib/widgets/viewer/overlay/multipage.dart b/lib/widgets/viewer/overlay/multipage.dart index a5569967d..b1568b837 100644 --- a/lib/widgets/viewer/overlay/multipage.dart +++ b/lib/widgets/viewer/overlay/multipage.dart @@ -1,24 +1,26 @@ import 'dart:math'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/collection/thumbnail/raster.dart'; +import 'package:aves/widgets/collection/thumbnail/decorated.dart'; import 'package:aves/widgets/viewer/multipage.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class MultiPageOverlay extends StatefulWidget { - final ImageEntry entry; + final AvesEntry mainEntry; final MultiPageController controller; final double availableWidth; - const MultiPageOverlay({ + MultiPageOverlay({ Key key, - @required this.entry, + @required this.mainEntry, @required this.controller, @required this.availableWidth, - }) : super(key: key); + }) : assert(mainEntry.isMultipage), + assert(controller != null), + super(key: key); @override _MultiPageOverlayState createState() => _MultiPageOverlayState(); @@ -31,7 +33,7 @@ class _MultiPageOverlayState extends State { static const double extent = 48; static const double separatorWidth = 2; - ImageEntry get entry => widget.entry; + AvesEntry get mainEntry => widget.mainEntry; MultiPageController get controller => widget.controller; @@ -60,7 +62,8 @@ class _MultiPageOverlayState extends State { } void _registerWidget() { - final scrollOffset = pageToScrollOffset(controller.page); + final page = controller.page ?? 0; + final scrollOffset = pageToScrollOffset(page); _scrollController = ScrollController(initialScrollOffset: scrollOffset); _scrollController.addListener(_onScrollChange); } @@ -97,7 +100,7 @@ class _MultiPageOverlayState extends State { width: availableWidth, height: extent, child: ListView.separated( - key: ValueKey(entry), + key: ValueKey(mainEntry), scrollDirection: Axis.horizontal, controller: _scrollController, // default padding in scroll direction matches `MediaQuery.viewPadding`, @@ -106,6 +109,8 @@ class _MultiPageOverlayState extends State { itemBuilder: (context, index) { if (index == 0 || index == multiPageInfo.pageCount + 1) return horizontalMargin; final page = index - 1; + final pageEntry = mainEntry.getPageEntry(multiPageInfo.getByIndex(page)); + return GestureDetector( onTap: () async { _syncScroll = false; @@ -117,14 +122,11 @@ class _MultiPageOverlayState extends State { ); _syncScroll = true; }, - child: Container( - width: extent, - height: extent, - child: RasterImageThumbnail( - entry: entry, - extent: extent, - page: page, - ), + child: DecoratedThumbnail( + entry: pageEntry, + extent: extent, + selectable: false, + highlightable: false, ), ); }, diff --git a/lib/widgets/viewer/overlay/panorama.dart b/lib/widgets/viewer/overlay/panorama.dart index 9356a8206..0d142edeb 100644 --- a/lib/widgets/viewer/overlay/panorama.dart +++ b/lib/widgets/viewer/overlay/panorama.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/panorama_page.dart'; @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:pedantic/pedantic.dart'; class PanoramaOverlay extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final Animation scale; const PanoramaOverlay({ diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index a3effb3cd..44fc7a3f1 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -1,8 +1,8 @@ import 'dart:math'; import 'package:aves/model/actions/entry_actions.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/favourite_repo.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -19,7 +19,7 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class ViewerTopOverlay extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final Animation scale; final EdgeInsets viewInsets, viewPadding; final Function(EntryAction value) onActionSelected; @@ -86,7 +86,7 @@ class ViewerTopOverlay extends StatelessWidget { FadeTransition( opacity: scale, child: Minimap( - entry: entry, + mainEntry: entry, viewStateNotifier: viewStateNotifier, multiPageController: multiPageController, ), @@ -111,8 +111,9 @@ class ViewerTopOverlay extends StatelessWidget { case EntryAction.rotateCW: case EntryAction.flip: return entry.canRotateAndFlip; + case EntryAction.export: case EntryAction.print: - return entry.canPrint; + return !entry.isVideo; case EntryAction.openMap: return entry.hasGps; case EntryAction.viewSource: @@ -135,7 +136,7 @@ class _TopOverlayRow extends StatelessWidget { final List inAppActions; final List externalAppActions; final Animation scale; - final ImageEntry entry; + final AvesEntry entry; final Function(EntryAction value) onActionSelected; const _TopOverlayRow({ @@ -194,14 +195,15 @@ class _TopOverlayRow extends StatelessWidget { onPressed: onPressed, ); break; - case EntryAction.info: - case EntryAction.share: case EntryAction.delete: + case EntryAction.export: + case EntryAction.flip: + case EntryAction.info: + case EntryAction.print: case EntryAction.rename: case EntryAction.rotateCCW: case EntryAction.rotateCW: - case EntryAction.flip: - case EntryAction.print: + case EntryAction.share: case EntryAction.viewSource: child = IconButton( icon: Icon(action.getIcon()), @@ -237,14 +239,15 @@ class _TopOverlayRow extends StatelessWidget { isMenuItem: true, ); break; - case EntryAction.info: - case EntryAction.share: case EntryAction.delete: + case EntryAction.export: + case EntryAction.flip: + case EntryAction.info: + case EntryAction.print: case EntryAction.rename: case EntryAction.rotateCCW: case EntryAction.rotateCW: - case EntryAction.flip: - case EntryAction.print: + case EntryAction.share: case EntryAction.viewSource: case EntryAction.debug: child = MenuRow(text: action.getText(), icon: action.getIcon()); @@ -299,7 +302,7 @@ class _TopOverlayRow extends StatelessWidget { } class _FavouriteToggler extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final bool isMenuItem; final VoidCallback onPressed; diff --git a/lib/widgets/viewer/overlay/video.dart b/lib/widgets/viewer/overlay/video.dart index eee35e659..59c1cd9d5 100644 --- a/lib/widgets/viewer/overlay/video.dart +++ b/lib/widgets/viewer/overlay/video.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -12,7 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; class VideoControlOverlay extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final IjkMediaController controller; final Animation scale; @@ -28,7 +28,7 @@ class VideoControlOverlay extends StatefulWidget { } class _VideoControlOverlayState extends State with SingleTickerProviderStateMixin { - final GlobalKey _progressBarKey = GlobalKey(); + final GlobalKey _progressBarKey = GlobalKey(debugLabel: 'video-progress-bar'); bool _playingOnDragStart = false; AnimationController _playPauseAnimation; final List _subscriptions = []; @@ -37,7 +37,7 @@ class _VideoControlOverlayState extends State with SingleTi // video info is not refreshed by default, so we use a timer to do so Timer _progressTimer; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; Animation get scale => widget.scale; @@ -110,7 +110,7 @@ class _VideoControlOverlayState extends State with SingleTi OverlayButton( scale: scale, child: IconButton( - icon: Icon(AIcons.openInNew), + icon: Icon(AIcons.openOutside), onPressed: () => AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype), tooltip: 'Open', ), diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index d1e44385b..979971d58 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -1,8 +1,8 @@ -import 'package:aves/image_providers/uri_image_provider.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/panorama.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/gesture_area_protector.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/foundation.dart'; @@ -15,13 +15,11 @@ import 'package:provider/provider.dart'; class PanoramaPage extends StatefulWidget { static const routeName = '/viewer/panorama'; - final ImageEntry entry; - final int page; + final AvesEntry entry; final PanoramaInfo info; const PanoramaPage({ @required this.entry, - this.page = 0, @required this.info, }); @@ -33,7 +31,7 @@ class _PanoramaPageState extends State { final ValueNotifier _overlayVisible = ValueNotifier(true); final ValueNotifier _sensorControl = ValueNotifier(SensorControl.None); - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; PanoramaInfo get info => widget.info; @@ -74,14 +72,7 @@ class _PanoramaPageState extends State { ); }, child: Image( - image: UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - page: widget.page, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ), + image: entry.uriImage, ), ), Positioned( @@ -97,10 +88,10 @@ class _PanoramaPageState extends State { return Visibility( visible: overlayVisible, child: Selector( - selector: (c, mq) => mq.padding + mq.viewInsets, - builder: (c, mqViewInsets, child) { + selector: (c, mq) => mq.viewPadding + mq.viewInsets, + builder: (c, mqPadding, child) { return Padding( - padding: EdgeInsets.all(8) + EdgeInsets.only(right: mqViewInsets.right, bottom: mqViewInsets.bottom), + padding: EdgeInsets.all(8) + EdgeInsets.only(right: mqPadding.right, bottom: mqPadding.bottom), child: OverlayButton( scale: kAlwaysCompleteAnimation, child: ValueListenableBuilder( diff --git a/lib/widgets/viewer/printer.dart b/lib/widgets/viewer/printer.dart index 00824a099..dee300e6f 100644 --- a/lib/widgets/viewer/printer.dart +++ b/lib/widgets/viewer/printer.dart @@ -1,23 +1,26 @@ +import 'dart:async'; import 'dart:convert'; -import 'package:aves/image_providers/uri_image_provider.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/metadata_service.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:flutter/widgets.dart'; import 'package:pdf/widgets.dart' as pdf; import 'package:pedantic/pedantic.dart'; import 'package:printing/printing.dart'; -class EntryPrinter { - final ImageEntry entry; +class EntryPrinter with FeedbackMixin { + final AvesEntry entry; - const EntryPrinter(this.entry); + EntryPrinter(this.entry); - Future print() async { + Future print(BuildContext context) async { final documentName = entry.bestTitle ?? 'Aves'; final doc = pdf.Document(title: documentName); - final pages = await _buildPages(); + final pages = await _buildPages(context); if (pages.isNotEmpty) { pages.forEach(doc.addPage); // Page unawaited(Printing.layoutPdf( @@ -27,13 +30,14 @@ class EntryPrinter { } } - Future> _buildPages() async { + Future> _buildPages(BuildContext context) async { final pages = []; void _addPdfPage(pdf.Widget pdfChild) { if (pdfChild == null) return; + final displaySize = entry.displaySize; pages.add(pdf.Page( - orientation: entry.isPortrait ? pdf.PageOrientation.portrait : pdf.PageOrientation.landscape, + orientation: displaySize.height > displaySize.width ? pdf.PageOrientation.portrait : pdf.PageOrientation.landscape, build: (context) => pdf.FullPage( ignoreMargins: true, child: pdf.Center( @@ -46,38 +50,34 @@ class EntryPrinter { if (entry.isMultipage) { final multiPageInfo = await MetadataService.getMultiPageInfo(entry); if (multiPageInfo.pageCount > 1) { - for (final kv in multiPageInfo.pages.entries) { - _addPdfPage(await _buildPageImage(page: kv.key)); + final streamController = StreamController.broadcast(); + showOpReport( + context: context, + opStream: streamController.stream, + itemCount: multiPageInfo.pageCount, + ); + for (final page in multiPageInfo.pages) { + final pageEntry = entry.getPageEntry(page); + _addPdfPage(await _buildPageImage(pageEntry)); + streamController.sink.add(pageEntry); } + await streamController.close(); } } if (pages.isEmpty) { - _addPdfPage(await _buildPageImage()); + _addPdfPage(await _buildPageImage(entry)); } return pages; } - Future _buildPageImage({page = 0}) async { - final uri = entry.uri; - final mimeType = entry.mimeType; - final rotationDegrees = entry.rotationDegrees; - final isFlipped = entry.isFlipped; - + Future _buildPageImage(AvesEntry entry) async { if (entry.isSvg) { - final bytes = await ImageFileService.getImage(uri, mimeType, rotationDegrees, isFlipped); + final bytes = await ImageFileService.getSvg(entry.uri, entry.mimeType); if (bytes != null && bytes.isNotEmpty) { return pdf.SvgImage(svg: utf8.decode(bytes)); } } else { - return pdf.Image(await flutterImageProvider( - UriImage( - uri: uri, - mimeType: mimeType, - page: page, - rotationDegrees: rotationDegrees, - isFlipped: isFlipped, - ), - )); + return pdf.Image(await flutterImageProvider(entry.uriImage)); } return null; } diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index ae4638de8..ba18f7473 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:aves/image_providers/uri_picture_provider.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/settings.dart'; @@ -23,81 +23,124 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:tuple/tuple.dart'; class EntryPageView extends StatefulWidget { - final ImageEntry entry; - final MultiPageInfo multiPageInfo; - final int page; + final AvesEntry entry; + final SinglePageInfo page; + final Size viewportSize; final Object heroTag; final MagnifierTapCallback onTap; final List> videoControllers; final VoidCallback onDisposed; static const decorationCheckSize = 20.0; - static const initialScale = ScaleLevel(ref: ScaleReference.contained); - static const minScale = ScaleLevel(ref: ScaleReference.contained); - static const maxScale = ScaleLevel(factor: 2.0); - const EntryPageView({ + EntryPageView({ Key key, - @required this.entry, - this.multiPageInfo, - this.page = 0, + AvesEntry mainEntry, + this.page, + this.viewportSize, this.heroTag, @required this.onTap, @required this.videoControllers, this.onDisposed, - }) : super(key: key); + }) : entry = mainEntry.getPageEntry(page) ?? mainEntry, + super(key: key); @override _EntryPageViewState createState() => _EntryPageViewState(); } class _EntryPageViewState extends State { - final MagnifierController _magnifierController = MagnifierController(); + MagnifierController _magnifierController; final ValueNotifier _viewStateNotifier = ValueNotifier(ViewState.zero); final List _subscriptions = []; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; - MultiPageInfo get multiPageInfo => widget.multiPageInfo; - - int get page => widget.page; + Size get viewportSize => widget.viewportSize; MagnifierTapCallback get onTap => widget.onTap; - Size get pageDisplaySize => entry.getDisplaySize(multiPageInfo: multiPageInfo, page: page); + static const initialScale = ScaleLevel(ref: ScaleReference.contained); + static const minScale = ScaleLevel(ref: ScaleReference.contained); + static const maxScale = ScaleLevel(factor: 2.0); @override void initState() { super.initState(); - _subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged)); - _subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged)); + _registerWidget(); + } + + @override + void didUpdateWidget(covariant EntryPageView oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.entry.displaySize != entry.displaySize) { + // do not reset the magnifier view state unless page dimensions change, + // in effect locking the zoom & position when browsing entry pages of the same size + _unregisterWidget(); + _registerWidget(); + } } @override void dispose() { - _subscriptions - ..forEach((sub) => sub.cancel()) - ..clear(); + _unregisterWidget(); widget.onDisposed?.call(); super.dispose(); } + void _registerWidget() { + // try to initialize the view state to match magnifier initial state + _viewStateNotifier.value = viewportSize != null + ? ViewState( + Offset.zero, + ScaleBoundaries( + minScale: minScale, + maxScale: maxScale, + initialScale: initialScale, + viewportSize: viewportSize, + childSize: entry.displaySize, + ).initialScale, + viewportSize, + ) + : ViewState.zero; + + _magnifierController = MagnifierController(); + _subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged)); + _subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged)); + } + + void _unregisterWidget() { + _magnifierController?.dispose(); + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + } + @override Widget build(BuildContext context) { - Widget child; - if (entry.isVideo) { - if (entry.width > 0 && entry.height > 0) { - child = _buildVideoView(); - } - } else if (entry.isSvg) { - child = _buildSvgView(); - } else if (entry.canDecode) { - child = _buildRasterView(); - } - child ??= ErrorView(onTap: () => onTap?.call(null)); + final child = AnimatedBuilder( + animation: entry.imageChangeNotifier, + builder: (context, child) { + Widget child; + if (entry.isVideo) { + if (!entry.displaySize.isEmpty) { + child = _buildVideoView(); + } + } else if (entry.isSvg) { + child = _buildSvgView(); + } else if (entry.canDecode) { + child = _buildRasterView(); + } + child ??= ErrorView( + entry: entry, + onTap: () => onTap?.call(null), + ); + return child; + }, + ); - // no hero for videos, as a typical video first frame is different from its thumbnail - return widget.heroTag != null && !entry.isVideo + return widget.heroTag != null ? Hero( tag: widget.heroTag, transitionOnUserGestures: true, @@ -107,23 +150,16 @@ class _EntryPageViewState extends State { } Widget _buildRasterView() { - return Magnifier( - // key includes size and orientation to refresh when the image is rotated - key: ValueKey('${page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), - child: TiledImageView( - entry: entry, - multiPageInfo: multiPageInfo, - page: page, - viewStateNotifier: _viewStateNotifier, - errorBuilder: (context, error, stackTrace) => ErrorView(onTap: () => onTap?.call(null)), - ), - childSize: pageDisplaySize, - controller: _magnifierController, - maxScale: EntryPageView.maxScale, - minScale: EntryPageView.minScale, - initialScale: EntryPageView.initialScale, - onTap: (c, d, s, childPosition) => onTap?.call(childPosition), + return _buildMagnifier( applyScale: false, + child: RasterImageView( + entry: entry, + viewStateNotifier: _viewStateNotifier, + errorBuilder: (context, error, stackTrace) => ErrorView( + entry: entry, + onTap: () => onTap?.call(null), + ), + ), ); } @@ -131,7 +167,9 @@ class _EntryPageViewState extends State { final background = settings.vectorBackground; final colorFilter = background.isColor ? ColorFilter.mode(background.color, BlendMode.dstOver) : null; - Widget child = Magnifier( + var child = _buildMagnifier( + maxScale: ScaleLevel(factor: double.infinity), + scaleStateCycle: _vectorScaleStateCycle, child: SvgPicture( UriPicture( uri: entry.uri, @@ -139,17 +177,11 @@ class _EntryPageViewState extends State { colorFilter: colorFilter, ), ), - childSize: pageDisplaySize, - controller: _magnifierController, - minScale: EntryPageView.minScale, - initialScale: EntryPageView.initialScale, - scaleStateCycle: _vectorScaleStateCycle, - onTap: (c, d, s, childPosition) => onTap?.call(childPosition), ); if (background == EntryBackground.checkered) { child = VectorViewCheckeredBackground( - displaySize: pageDisplaySize, + displaySize: entry.displaySize, viewStateNotifier: _viewStateNotifier, child: child, ); @@ -159,19 +191,33 @@ class _EntryPageViewState extends State { Widget _buildVideoView() { final videoController = widget.videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; + if (videoController == null) return SizedBox(); + return _buildMagnifier( + child: VideoView( + entry: entry, + controller: videoController, + ), + ); + } + + Widget _buildMagnifier({ + ScaleLevel maxScale = maxScale, + ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, + bool applyScale = true, + @required Widget child, + }) { return Magnifier( - child: videoController != null - ? AvesVideo( - entry: entry, - controller: videoController, - ) - : SizedBox(), - childSize: pageDisplaySize, + // key includes modified date to refresh when the image is modified by metadata (e.g. rotated) + key: ValueKey('${entry.pageId}_${entry.dateModifiedSecs}'), controller: _magnifierController, - maxScale: EntryPageView.maxScale, - minScale: EntryPageView.minScale, - initialScale: EntryPageView.initialScale, + childSize: entry.displaySize, + minScale: minScale, + maxScale: maxScale, + initialScale: initialScale, + scaleStateCycle: scaleStateCycle, + applyScale: applyScale, onTap: (c, d, s, childPosition) => onTap?.call(childPosition), + child: child, ); } diff --git a/lib/widgets/viewer/visual/error.dart b/lib/widgets/viewer/visual/error.dart index fbee7fb3b..f16192aed 100644 --- a/lib/widgets/viewer/visual/error.dart +++ b/lib/widgets/viewer/visual/error.dart @@ -1,26 +1,54 @@ +import 'dart:io'; + +import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/empty.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -class ErrorView extends StatelessWidget { +class ErrorView extends StatefulWidget { + final AvesEntry entry; final VoidCallback onTap; - const ErrorView({@required this.onTap}); + const ErrorView({ + @required this.entry, + @required this.onTap, + }); + + @override + _ErrorViewState createState() => _ErrorViewState(); +} + +class _ErrorViewState extends State { + Future _exists; + + AvesEntry get entry => widget.entry; + + @override + void initState() { + super.initState(); + _exists = entry.path != null ? File(entry.path).exists() : SynchronousFuture(true); + } @override Widget build(BuildContext context) { return 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` + onTap: () => widget.onTap?.call(), + // use container to expand constraints, so that the user can tap anywhere child: Container( - color: Colors.transparent, - child: EmptyContent( - icon: AIcons.error, - text: 'Oops!', - alignment: Alignment.center, - ), + // opaque to cover potential lower quality layer below + color: Colors.black, + child: FutureBuilder( + future: _exists, + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) return SizedBox(); + final exists = snapshot.data; + return EmptyContent( + icon: AIcons.error, + text: exists ? 'Oops!' : 'The file no longer exists.', + alignment: Alignment.center, + ); + }), ), ); } diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index cba13a263..72098fb99 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -1,13 +1,12 @@ import 'dart:math'; import 'package:aves/image_providers/region_provider.dart'; -import 'package:aves/image_providers/thumbnail_provider.dart'; -import 'package:aves/image_providers/uri_image_provider.dart'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/multipage.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/math_utils.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; @@ -15,26 +14,22 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; -class TiledImageView extends StatefulWidget { - final ImageEntry entry; - final MultiPageInfo multiPageInfo; - final int page; +class RasterImageView extends StatefulWidget { + final AvesEntry entry; final ValueNotifier viewStateNotifier; final ImageErrorWidgetBuilder errorBuilder; - const TiledImageView({ + const RasterImageView({ @required this.entry, - this.multiPageInfo, - this.page = 0, @required this.viewStateNotifier, @required this.errorBuilder, }); @override - _TiledImageViewState createState() => _TiledImageViewState(); + _RasterImageViewState createState() => _RasterImageViewState(); } -class _TiledImageViewState extends State { +class _RasterImageViewState extends State { Size _displaySize; bool _isTilingInitialized = false; int _maxSampleSize; @@ -44,51 +39,22 @@ class _TiledImageViewState extends State { ImageStreamListener _fullImageListener; final ValueNotifier _fullImageLoaded = ValueNotifier(false); - ImageEntry get entry => widget.entry; - - int get page => widget.page; + AvesEntry get entry => widget.entry; ValueNotifier get viewStateNotifier => widget.viewStateNotifier; bool get useBackground => entry.canHaveAlpha && settings.rasterBackground != EntryBackground.transparent; - // as of panorama v0.3.1, the `Panorama` widget throws on initialization when the image is already resolved - // so we use tiles for panoramas as a workaround to not collide with the `panorama` package resolution - bool get useTiles => entry.canTile && (entry.getDisplaySize(multiPageInfo: widget.multiPageInfo, page: page).longestSide > 4096 || entry.is360); + ViewState get viewState => viewStateNotifier.value; - ImageProvider get thumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry, page: page)); + ImageProvider get thumbnailProvider => entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)); ImageProvider get fullImageProvider { - if (useTiles) { + if (entry.useTiles) { assert(_isTilingInitialized); - final displayWidth = _displaySize.width.round(); - final displayHeight = _displaySize.height.round(); - final viewState = viewStateNotifier.value; - final regionRect = _getTileRects( - x: 0, - y: 0, - layerRegionWidth: displayWidth, - layerRegionHeight: displayHeight, - displayWidth: displayWidth, - displayHeight: displayHeight, - scale: viewState.scale, - viewRect: _getViewRect(viewState, displayWidth, displayHeight), - )?.item2; - return RegionProvider(RegionProviderKey.fromEntry( - entry, - page: page, - sampleSize: _maxSampleSize, - rect: regionRect, - )); + return entry.getRegion(sampleSize: _maxSampleSize); } else { - return UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - page: page, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ); + return entry.uriImage; } } @@ -98,18 +64,18 @@ class _TiledImageViewState extends State { @override void initState() { super.initState(); - _displaySize = entry.getDisplaySize(multiPageInfo: widget.multiPageInfo, page: page); + _displaySize = entry.displaySize; _fullImageListener = ImageStreamListener(_onFullImageCompleted); - if (!useTiles) _registerFullImage(); + if (!entry.useTiles) _registerFullImage(); } @override - void didUpdateWidget(covariant TiledImageView oldWidget) { + void didUpdateWidget(covariant RasterImageView oldWidget) { super.didUpdateWidget(oldWidget); final oldViewState = oldWidget.viewStateNotifier.value; final viewState = widget.viewStateNotifier.value; - if (oldWidget.entry != widget.entry || oldViewState.viewportSize != viewState.viewportSize || oldWidget.page != page) { + if (oldWidget.entry != widget.entry || oldViewState.viewportSize != viewState.viewportSize) { _isTilingInitialized = false; _fullImageLoaded.value = false; _unregisterFullImage(); @@ -141,11 +107,12 @@ class _TiledImageViewState extends State { Widget build(BuildContext context) { if (viewStateNotifier == null) return SizedBox.shrink(); + final useTiles = entry.useTiles; return ValueListenableBuilder( valueListenable: viewStateNotifier, builder: (context, viewState, child) { final viewportSize = viewState.viewportSize; - final viewportSized = viewportSize != null; + final viewportSized = viewportSize?.isEmpty == false; if (viewportSized && useTiles && !_isTilingInitialized) _initTiling(viewportSize); return SizedBox.fromSize( @@ -153,9 +120,9 @@ class _TiledImageViewState extends State { child: Stack( alignment: Alignment.center, children: [ - if (useBackground && viewportSized) _buildBackground(viewState), - _buildLoading(viewState), - if (useTiles) ..._getTiles(viewState), + if (useBackground && viewportSized) _buildBackground(), + _buildLoading(), + if (useTiles) ..._getTiles(), if (!useTiles) Image( image: fullImageProvider, @@ -192,7 +159,7 @@ class _TiledImageViewState extends State { _registerFullImage(); } - Widget _buildLoading(ViewState viewState) { + Widget _buildLoading() { return ValueListenableBuilder( valueListenable: _fullImageLoaded, builder: (context, fullImageLoaded, child) { @@ -212,27 +179,39 @@ class _TiledImageViewState extends State { ); } - Widget _buildBackground(ViewState viewState) { + Widget _buildBackground() { final viewportSize = viewState.viewportSize; assert(viewportSize != null); final viewSize = _displaySize * viewState.scale; final decorationOffset = ((viewSize - viewportSize) as Offset) / 2 - viewState.position; - final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source; + // deflate as a quick way to prevent background bleed + final decorationSize = (applyBoxFit(BoxFit.none, viewSize, viewportSize).source - Offset(.5, .5)) as Size; - Decoration decoration; + Widget child; final background = settings.rasterBackground; if (background == EntryBackground.checkered) { final side = viewportSize.shortestSide; final checkSize = side / ((side / EntryPageView.decorationCheckSize).round()); final offset = ((decorationSize - viewportSize) as Offset) / 2; - decoration = CheckeredDecoration( - checkSize: checkSize, - offset: offset, + child = ValueListenableBuilder( + valueListenable: _fullImageLoaded, + builder: (context, fullImageLoaded, child) { + if (!fullImageLoaded) return SizedBox.shrink(); + + return CustomPaint( + painter: CheckeredPainter( + checkSize: checkSize, + offset: offset, + ), + ); + }, ); } else { - decoration = BoxDecoration( - color: background.color, + child = DecoratedBox( + decoration: BoxDecoration( + color: background.color, + ), ); } return Positioned( @@ -240,36 +219,37 @@ class _TiledImageViewState extends State { top: decorationOffset.dy >= 0 ? decorationOffset.dy : null, width: decorationSize.width, height: decorationSize.height, - child: DecoratedBox( - decoration: decoration, - ), + child: child, ); } - List _getTiles(ViewState viewState) { + List _getTiles() { if (!_isTilingInitialized) return []; final displayWidth = _displaySize.width.round(); final displayHeight = _displaySize.height.round(); - final viewRect = _getViewRect(viewState, displayWidth, displayHeight); + final viewRect = _getViewRect(displayWidth, displayHeight); final scale = viewState.scale; - final tiles = []; + // for the largest sample size (matching the initial scale), the whole image is in view + // so we subsample the whole image without tiling + final fullImageRegionTile = RegionTile( + entry: entry, + tileRect: Rect.fromLTWH(0, 0, displayWidth * scale, displayHeight * scale), + sampleSize: _maxSampleSize, + ); + final tiles = [fullImageRegionTile]; + var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize); - for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) { - // for the largest sample size (matching the initial scale), the whole image is in view - // so we subsample the whole image without tiling - final fullImageRegion = sampleSize == _maxSampleSize; + int nextSampleSize(int sampleSize) => (sampleSize / 2).floor(); + for (var sampleSize = nextSampleSize(_maxSampleSize); sampleSize >= minSampleSize; sampleSize = nextSampleSize(sampleSize)) { final regionSide = (_tileSide * sampleSize).round(); - final layerRegionWidth = fullImageRegion ? displayWidth : regionSide; - final layerRegionHeight = fullImageRegion ? displayHeight : regionSide; - for (var x = 0; x < displayWidth; x += layerRegionWidth) { - for (var y = 0; y < displayHeight; y += layerRegionHeight) { + for (var x = 0; x < displayWidth; x += regionSide) { + for (var y = 0; y < displayHeight; y += regionSide) { final rects = _getTileRects( x: x, y: y, - layerRegionWidth: layerRegionWidth, - layerRegionHeight: layerRegionHeight, + regionSide: regionSide, displayWidth: displayWidth, displayHeight: displayHeight, scale: scale, @@ -278,7 +258,6 @@ class _TiledImageViewState extends State { if (rects != null) { tiles.add(RegionTile( entry: entry, - page: page, tileRect: rects.item1, regionRect: rects.item2, sampleSize: sampleSize, @@ -290,7 +269,7 @@ class _TiledImageViewState extends State { return tiles; } - Rect _getViewRect(ViewState viewState, int displayWidth, int displayHeight) { + Rect _getViewRect(int displayWidth, int displayHeight) { final scale = viewState.scale; final centerOffset = viewState.position; final viewportSize = viewState.viewportSize; @@ -304,17 +283,16 @@ class _TiledImageViewState extends State { Tuple2> _getTileRects({ @required int x, @required int y, - @required int layerRegionWidth, - @required int layerRegionHeight, + @required int regionSide, @required int displayWidth, @required int displayHeight, @required double scale, @required Rect viewRect, }) { - final nextX = x + layerRegionWidth; - final nextY = y + layerRegionHeight; - final thisRegionWidth = layerRegionWidth - (nextX >= displayWidth ? nextX - displayWidth : 0); - final thisRegionHeight = layerRegionHeight - (nextY >= displayHeight ? nextY - displayHeight : 0); + final nextX = x + regionSide; + final nextY = y + regionSide; + final thisRegionWidth = regionSide - (nextX >= displayWidth ? nextX - displayWidth : 0); + final thisRegionHeight = regionSide - (nextY >= displayHeight ? nextY - displayHeight : 0); final tileRect = Rect.fromLTWH(x * scale, y * scale, thisRegionWidth * scale, thisRegionHeight * scale); // only build visible tiles @@ -346,8 +324,7 @@ class _TiledImageViewState extends State { } class RegionTile extends StatefulWidget { - final ImageEntry entry; - final int page; + final AvesEntry entry; // `tileRect` uses Flutter view coordinates // `regionRect` uses the raw image pixel coordinates @@ -357,20 +334,28 @@ class RegionTile extends StatefulWidget { const RegionTile({ @required this.entry, - @required this.page, @required this.tileRect, - @required this.regionRect, + this.regionRect, @required this.sampleSize, }); @override _RegionTileState createState() => _RegionTileState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IntProperty('contentId', entry.contentId)); + properties.add(DiagnosticsProperty('tileRect', tileRect)); + properties.add(DiagnosticsProperty>('regionRect', regionRect)); + properties.add(IntProperty('sampleSize', sampleSize)); + } } class _RegionTileState extends State { RegionProvider _provider; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override void initState() { @@ -404,12 +389,10 @@ class _RegionTileState extends State { void _initProvider() { if (!entry.canDecode) return; - _provider = RegionProvider(RegionProviderKey.fromEntry( - entry, - page: widget.page, + _provider = entry.getRegion( sampleSize: widget.sampleSize, - rect: widget.regionRect, - )); + region: widget.regionRect, + ); } void _pauseProvider() => _provider?.pause(); @@ -452,12 +435,4 @@ class _RegionTileState extends State { child: child, ); } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(IntProperty('contentId', widget.entry.contentId)); - properties.add(IntProperty('sampleSize', widget.sampleSize)); - properties.add(DiagnosticsProperty>('regionRect', widget.regionRect)); - } } diff --git a/lib/widgets/viewer/visual/vector.dart b/lib/widgets/viewer/visual/vector.dart index 443b01602..1fe9d3e28 100644 --- a/lib/widgets/viewer/visual/vector.dart +++ b/lib/widgets/viewer/visual/vector.dart @@ -36,8 +36,8 @@ class VectorViewCheckeredBackground extends StatelessWidget { Positioned( width: decorationSize.width, height: decorationSize.height, - child: DecoratedBox( - decoration: CheckeredDecoration( + child: CustomPaint( + painter: CheckeredPainter( checkSize: checkSize, offset: offset, ), diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index 51c257072..aac61c5e4 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -1,29 +1,31 @@ import 'dart:async'; import 'dart:ui'; -import 'package:aves/image_providers/uri_image_provider.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; -class AvesVideo extends StatefulWidget { - final ImageEntry entry; +class VideoView extends StatefulWidget { + final AvesEntry entry; final IjkMediaController controller; - const AvesVideo({ + const VideoView({ Key key, @required this.entry, @required this.controller, }) : super(key: key); @override - State createState() => _AvesVideoState(); + State createState() => _VideoViewState(); } -class _AvesVideoState extends State { +class _VideoViewState extends State { final List _subscriptions = []; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; IjkMediaController get controller => widget.controller; @@ -34,7 +36,7 @@ class _AvesVideoState extends State { } @override - void didUpdateWidget(covariant AvesVideo oldWidget) { + void didUpdateWidget(covariant VideoView oldWidget) { super.didUpdateWidget(oldWidget); _unregisterWidget(oldWidget); _registerWidget(widget); @@ -46,11 +48,11 @@ class _AvesVideoState extends State { super.dispose(); } - void _registerWidget(AvesVideo widget) { + void _registerWidget(VideoView widget) { _subscriptions.add(widget.controller.playFinishStream.listen(_onPlayFinish)); } - void _unregisterWidget(AvesVideo widget) { + void _unregisterWidget(VideoView widget) { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); @@ -98,16 +100,8 @@ class _AvesVideoState extends State { backgroundColor: Colors.transparent, ) : Image( - image: UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - page: 0, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ), - width: entry.width.toDouble(), - height: entry.height.toDouble(), + image: entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)), + fit: BoxFit.contain, ); }); } diff --git a/pubspec.lock b/pubspec.lock index fdff631b9..514b21954 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,13 +57,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0-nullsafety.1" - cached_network_image: - dependency: transitive - description: - name: cached_network_image - url: "https://pub.dartlang.org" - source: hosted - version: "2.5.0" characters: dependency: transitive description: @@ -113,6 +106,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0-nullsafety.3" + connectivity: + dependency: "direct main" + description: + name: connectivity + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + connectivity_for_web: + dependency: transitive + description: + name: connectivity_for_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1+4" + connectivity_macos: + dependency: transitive + description: + name: connectivity_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0+7" + connectivity_platform_interface: + dependency: transitive + description: + name: connectivity_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.6" console_log_handler: dependency: transitive description: @@ -148,15 +169,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" - draggable_scrollbar: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "3b823ae0a9def4edec62771f18e6348312bfce15" - url: "git://github.com/deckerst/flutter-draggable-scrollbar.git" - source: git - version: "0.0.4" event_bus: dependency: "direct main" description: @@ -269,20 +281,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_blurhash: - dependency: transitive - description: - name: flutter_blurhash - url: "https://pub.dartlang.org" - source: hosted - version: "0.5.0" - flutter_cache_manager: - dependency: transitive - description: - name: flutter_cache_manager - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" flutter_cube: dependency: transitive description: @@ -307,7 +305,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: d5a827929882f06a30cb82de29feec545d8f00da + resolved-ref: c1b7f25e2a3bc67ab7b30561af49a62ae9a8c409 url: "git://github.com/deckerst/flutter_ijkplayer.git" source: git version: "0.3.7" @@ -324,7 +322,7 @@ packages: name: flutter_map url: "https://pub.dartlang.org" source: hosted - version: "0.10.1+1" + version: "0.11.0" flutter_markdown: dependency: "direct main" description: @@ -563,14 +561,7 @@ packages: name: node_preamble url: "https://pub.dartlang.org" source: hosted - version: "1.4.12" - octo_image: - dependency: transitive - description: - name: octo_image - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0" + version: "1.4.13" overlay_support: dependency: "direct main" description: @@ -591,7 +582,7 @@ packages: name: package_info url: "https://pub.dartlang.org" source: hosted - version: "0.4.3+2" + version: "0.4.3+4" palette_generator: dependency: "direct main" description: @@ -627,13 +618,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.4" - path_provider: - dependency: transitive - description: - name: path_provider - url: "https://pub.dartlang.org" - source: hosted - version: "1.6.27" path_provider_linux: dependency: transitive description: @@ -641,13 +625,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1+2" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.4+8" path_provider_platform_interface: dependency: transitive description: @@ -759,7 +736,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "4.3.2+4" + version: "4.3.3" pub_semver: dependency: transitive description: @@ -781,13 +758,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.5" - rxdart: - dependency: transitive - description: - name: rxdart - url: "https://pub.dartlang.org" - source: hosted - version: "0.25.0" screen: dependency: "direct main" description: @@ -836,7 +806,7 @@ packages: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+3" + version: "0.0.2+2" shelf: dependency: transitive description: @@ -850,21 +820,21 @@ packages: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" shelf_static: dependency: transitive description: name: shelf_static url: "https://pub.dartlang.org" source: hosted - version: "0.2.9+1" + version: "0.2.9+2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.3" + version: "0.2.4" sky_engine: dependency: transitive description: flutter @@ -904,7 +874,7 @@ packages: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.3+1" stack_trace: dependency: transitive description: @@ -1044,7 +1014,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.5+1" + version: "0.1.5+3" url_launcher_windows: dependency: transitive description: @@ -1052,13 +1022,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1+3" - uuid: - dependency: transitive - description: - name: uuid - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.2" validate: dependency: transitive description: @@ -1121,7 +1084,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "1.7.4" + version: "1.7.4+1" wkt_parser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 60130769b..19b833434 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Aves is a gallery and metadata explorer app, built for Android. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.3.2+38 +version: 1.3.3+39 # brendan-duncan/image (as of v2.1.19): # - does not support TIFF with JPEG compression (issue #184) @@ -33,11 +33,8 @@ dependencies: sdk: flutter charts_flutter: collection: + connectivity: decorated_icon: - draggable_scrollbar: -# path: ../flutter-draggable-scrollbar - git: - url: git://github.com/deckerst/flutter-draggable-scrollbar.git event_bus: expansion_tile_card: # path: ../expansion_tile_card diff --git a/shaders_1.22.5.sksl.json b/shaders_1.22.5.sksl.json deleted file mode 100644 index a7927453a..000000000 --- a/shaders_1.22.5.sksl.json +++ /dev/null @@ -1 +0,0 @@ -{"platform":"android","name":"SM G970N","engineRevision":"ae90085a8437c0ae94d6b5ad2741739ebc742cb4","data":{"CAZAAAICBIAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEANwQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjdWxhclJSZWN0CgkJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCQlmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxLlJCOwoJCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTEueCAtIGxlbmd0aChkeHkpKSk7CgkJb3V0cHV0X1N0YWdlMSA9IG91dHB1dENvdmVyYWdlX1N0YWdlMCAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECA4AAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABMAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAECA4AAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAWwEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAICBIAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAoAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAICCEAABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAABQAAAAEAAFEAAFAAAAAKAAIUAAQAAAAAYAAPAAAQAAAAAAAAAAAAYAAAAEAABAAACAAAAAAAAAAAAAHQADYAB4AA6AAAAAAABQAAAALQAEAAAEAAAAAAAAAAAACAAAABWAAFYAAQAAAAAAAAAAAAQAAAAHYACYAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAEBAAAAAAAAAQEA3w8AAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVyZWN0VW5pZm9ybV9TdGFnZTI7CmluIGhhbGYgdmluQ292ZXJhZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpOwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoMyA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDMgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICgzIDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICgzIDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoMyA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCWhhbGYgYWxwaGEgPSAxLjA7CgkJYWxwaGEgPSB2aW5Db3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoYWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UyOwoJewoJCS8vIFN0YWdlIDIsIEFBUmVjdEVmZmVjdAoJCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJCWhhbGYgYWxwaGE7CgkJQHN3aXRjaCAoMSkgCgkJewoJCQljYXNlIDA6ICAgIGNhc2UgMjogICAgICAgIGFscGhhID0gaGFsZihhbGwoZ3JlYXRlclRoYW4oZmxvYXQ0KHNrX0ZyYWdDb29yZC54eSwgdXJlY3RVbmlmb3JtX1N0YWdlMi56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fU3RhZ2UyLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7CgkJCWJyZWFrOwoJCQlkZWZhdWx0OiAgICAgICAgaGFsZiB4U3ViLCB5U3ViOwoJCQl4U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnggLSB1cmVjdFVuaWZvcm1fU3RhZ2UyLngpLCAwLjApOwoJCQl4U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTIueiAtIHNrX0ZyYWdDb29yZC54KSwgMC4wKTsKCQkJeVN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC55IC0gdXJlY3RVbmlmb3JtX1N0YWdlMi55KSwgMC4wKTsKCQkJeVN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UyLncgLSBza19GcmFnQ29vcmQueSksIDAuMCk7CgkJCWFscGhhID0gKDEuMCArIG1heCh4U3ViLCAtMS4wKSkgKiAoMS4wICsgbWF4KHlTdWIsIC0xLjApKTsKCQl9CgkJQGlmICgxID09IDIgfHwgMSA9PSAzKSAKCQl7CgkJCWFscGhhID0gMS4wIC0gYWxwaGE7CgkJfQoJCWhhbGY0IGlucHV0Q29sb3IgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0X1N0YWdlMiA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0X1N0YWdlMjsKCX0KfQoAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZACAICCEAABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAABQAAAAEAAFEAAFAAAAAKAAIUAAQAAAAAYAAPAAAQAAAAABAAAAAAYAAAAEAABAAACAAAAAAAAAAAAAHQADYAB4AA6ACAAAAABQAAAALQAEAAAEAAAAAAAAAAAACAAAABWAAFYAAQAAAAAAAAAAAAQAAAAHYACYAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAEBAAAAAAAAAQEA3g8AAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVyZWN0VW5pZm9ybV9TdGFnZTI7CmluIGhhbGYgdmluQ292ZXJhZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpOwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoMyA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDMgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICgzIDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICgzIDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoMyA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBhbHBoYSA9IDEuMDsKCQlhbHBoYSA9IHZpbkNvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChhbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTI7Cgl7CgkJLy8gU3RhZ2UgMiwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UyLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTIueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTIueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMi56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UyLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTIudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRfU3RhZ2UyID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRfU3RhZ2UyOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZAAAICBIAAAAIAAAABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1M6CQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqIChhcmNjb29yZC9yYWRpaSAqIDIpOwoJfQoJc2tfUG9zaXRpb24gPSBmbG9hdDQoZGV2Y29vcmQueCAsIGRldmNvb3JkLnksIDAsIDEpOwp9CgAAAAEBAAAAAAAAAQEAdQQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZ3g9dmFyY2Nvb3JkX1N0YWdlMC56LCBneT12YXJjY29vcmRfU3RhZ2UwLnc7CgkJCWZsb2F0IGZud2lkdGggPSBhYnMoZ3gpICsgYWJzKGd5KTsKCQkJaGFsZiBkID0gaGFsZihmbi9mbndpZHRoKTsKCQkJY292ZXJhZ2UgPSBjbGFtcCguNSAtIGQsIDAsIDEpOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZACAECBMAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAEAAAIIDAAWAATYABAAAAAABAAAQAABBAMADYAB4AACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1NdAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTE7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMSkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAACcEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1N0YWdlMV9jMC54LCB1Y2xhbXBfU3RhZ2UxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfU3RhZ2UxX2MwLnksIHVjbGFtcF9TdGFnZTFfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJX291dHB1dCA9IHRleHR1cmVDb2xvcjsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE1hdHJpeEVmZmVjdAoJCW91dHB1dF9TdGFnZTEgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAACBAAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAArQIAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAECAUAAAEYAAEABYAARAANQAAQAAAAAAAAMABEQAAAAAAAAAAAAAABAAAAAEAAFQAA":"AgAAAExTS1OFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFJSZWN0U2hhZG93Cgl2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAB1AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBoYWxmMyB2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUlJlY3RTaGFkb3cKCQloYWxmMyBzaGFkb3dQYXJhbXM7CgkJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CgkJZmxvYXQyIHV2ID0gZmxvYXQyKHNoYWRvd1BhcmFtcy56ICogKDEuMCAtIGQpLCAwLjUpOwoJCWhhbGYgZmFjdG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdXYpLnJycnIuYTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChmYWN0b3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","CAZAAAECAYAAABAAAAAACAAAAAJQAAIA777777YPAAKAAABBAMABIAA2AAAAAAAAAAAAAAACAAAAAKAALAAA":"AgAAAExTS1P9AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoaW5Qb3NpdGlvbi54ICwgaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABLAgAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gaW50IHZUZXhJbmRleF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZjQgdGV4Q29sb3I7CgkJewoJCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHZUZXh0dXJlQ29vcmRzX1N0YWdlMCk7CgkJfQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IG91dHB1dENvbG9yX1N0YWdlMCAqIHRleENvbG9yOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZAAAACAYAAAAAIAAABGAABAD7777777777777777776FAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1PkAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAWgEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAEAAAAKAAAAaW5Qb3NpdGlvbgAAAQAAAAAAAAA=","CAZAAAMCBEAAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAAAAAAAAAQAAAAEAACYAA":"AgAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAEBAAAAAAAAAQEA5QIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAAAAAIIDAAWAATYABEAAAAABAAAAAABBAMADYAB4AACQAAAABQAAAAABAAAAAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAfRQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TdGFnZTFfYzBfYzAueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC53KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAECA4AAAAAAAAABKAADAAKQAAYACUAAGAATAAAQAFIAAMABKAADAAOAAEIAEAADEAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MXBAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgcmFkaWk7CglyYWRpaS54ID0gZG90KHJhZGlpX3NlbGVjdG9yLCByYWRpaV94KTsKCXJhZGlpLnkgPSBkb3QocmFkaWlfc2VsZWN0b3IsIHJhZGlpX3kpOwoJYm9vbCBpc19hcmNfc2VjdGlvbiA9IChyYWRpaS54ID4gMCk7CglyYWRpaSA9IGFicyhyYWRpaSk7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpOwoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZTsKCWlmIChpc19hcmNfc2VjdGlvbikgCgl7CgkJdmFyY2Nvb3JkX1N0YWdlMC54eSA9IDEgLSBhYnMocmFkaXVzX291dHNldCk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqICh2YXJjY29vcmRfU3RhZ2UwLnh5L3JhZGlpICogY29ybmVyICogMik7Cgl9CgllbHNlIAoJewoJCXZhcmNjb29yZF9TdGFnZTAgPSBmbG9hdDQoMCk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAACgCAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0NCB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgkJaWYgKGZsb2F0MigwKSAhPSB2YXJjY29vcmRfU3RhZ2UwLnh5KSAKCQl7CgkJCWZsb2F0IGZuID0gZG90KHZhcmNjb29yZF9TdGFnZTAueHksIHZhcmNjb29yZF9TdGFnZTAueHkpIC0gMTsKCQkJaWYgKGZuID4gMCkgCgkJCXsKCQkJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDApOwoJCQl9CgkJfQoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAHAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECAYAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAA2AAAAAAAAAAAAAAACAAAAAKAALAAA":"AgAAAExTS1NGAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAZAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKS5ycnJyOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZAAAACBAAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAA4AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAEAAAIIDAAWAATYABEAAAAAAAAAQAABBAMADYAB4AACQAAAABQAAAAAAAAAQAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAfRQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gc3Vic2V0Q29vcmQueTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAACA4AAAAYAAAAAAAAAAAAQAAAACMAACAA4AAIQADYACQAAAAAAAAMAALQAAAAAAAAAAAAAAAQAAAACYACYAA":"AgAAAExTS1PjAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNEaW1lbnNpb25zSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSAodGV4SWR4KTsKCXZJbnRUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAXAwAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBmbG9hdDIgdkludFRleHR1cmVDb29yZHNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGlzdGFuY2VGaWVsZFBhdGgKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfU3RhZ2UwOwoJCWhhbGY0IHRleENvbG9yOwoJCXsKCQkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB1dikucnJycjsKCQl9CgkJaGFsZiBkaXN0YW5jZSA9IDcuOTY4NzUqKHRleENvbG9yLnIgLSAwLjUwMTk2MDc4NDMxKTsKCQloYWxmIGFmd2lkdGg7CgkJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeSh2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTAueSkpKTsKCQloYWxmIHZhbCA9IHNtb290aHN0ZXAoLWFmd2lkdGgsIGFmd2lkdGgsIGRpc3RhbmNlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh2YWwpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZACAACB4AAAAAAAAAGOAIAAAJQAAIACIAAAAA4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAACAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAQAAAALQAEAAAEAAAAAAAAAAAAKAAAABWAAWAA":"AgAAAExTS1McAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0IHZjb3ZlcmFnZV9TdGFnZTA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBwb3NpdGlvbiA9IHBvc2l0aW9uLnh5OwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJdmNvdmVyYWdlX1N0YWdlMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBsb2NhbENvb3JkLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAvQcAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gKGhhbGY0KDEuMCkgLSBvdXRwdXRfU3RhZ2UxKSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAICCAAAAEIBAAABGAABAAOAAAYABQAEIAAAAAAAAAYAAAABQACSAACQAAAAEAAEKAAIAAAAAKAAHQAAIAAAAAAQAAAAAMAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPABAAAAAAYAAAAFIACAAACAAAAAAAAAAAABAAAAAZAAC4AAIAAAAAAAAAAAAIAAAADUABMAA":"AgAAAExTS1NbAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZAoJdkhhaXJRdWFkRWRnZV9TdGFnZTAgPSBpbkhhaXJRdWFkRWRnZTsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAABAQAAAAAAAAEBAGMRAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UyOwppbiBoYWxmNCB2SGFpclF1YWRFZGdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKGxlbmd0aCh2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApKTsKCV9vdXRwdXQgPSBoYWxmNCh0LCAxLjAsIDAuMCwgMC4wKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpOwoJZmxvYXQ0IHNjYWxlLCBiaWFzOwoJaWYgKDMgPD0gNCB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLncpIAoJewoJCWlmICgzIDw9IDIgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS55KSAKCQl7CgkJCWlmICgzIDw9IDEgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSAzIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueikgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoMyA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDMgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAodHJ1ZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCQloYWxmIGVkZ2VBbHBoYTsKCQloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBnRiA9IGhhbGYyKDIuMCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiBkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHkueCAtIGR1dmR5LnkpOwoJCWVkZ2VBbHBoYSA9IGhhbGYodkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggLSB2SGFpclF1YWRFZGdlX1N0YWdlMC55KTsKCQllZGdlQWxwaGEgPSBzcXJ0KGVkZ2VBbHBoYSAqIGVkZ2VBbHBoYSAvIGRvdChnRiwgZ0YpKTsKCQllZGdlQWxwaGEgPSBtYXgoMS4wIC0gZWRnZUFscGhhLCAwLjApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTI7Cgl7CgkJLy8gU3RhZ2UgMiwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UyLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTIueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTIueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMi56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UyLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTIudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRfU3RhZ2UyID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRfU3RhZ2UyOwoJfQp9CgAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAA4AAABpbkhhaXJRdWFkRWRnZQAAAQAAAAAAAAA=","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAIIDAAWAATYABEAAAAAAAAAAAABBAMADYAB4AACQAAAABQAAAAAAAAAAAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAOxMAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgX2Nvb3Jkcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgR2F1c3NpYW5Db252b2x1dGlvbgoJCWZsb2F0MiBfY29vcmRzID0gdkxvY2FsQ29vcmRfU3RhZ2UwLnh5OwoJCW91dHB1dF9TdGFnZTEgPSBoYWxmNCgwLCAwLCAwLCAwKTsKCQlmbG9hdDIgY29vcmQgPSBfY29vcmRzIC0gMTIuMCAqIHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWZsb2F0MiBjb29yZFNhbXBsZWQgPSBoYWxmMigwLCAwKTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAACAYAABEAJAAABGAABAAOAAEIA777777YZAAAAAFAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCWNvbG9yID0gY29sb3IgKiBpbkNvdmVyYWdlOwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAABVAQAAaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","CAZAAAECA4AAAAAAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAEwCAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZAAAMCBAAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAA2AAAAAAAAAAAAAAABAAAAAKAAC4AAIAAAAAAAAAAAAIAAAABYABMAA":"AgAAAExTS1NGAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAQEAAAAAAAABAQCCBQAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKS5ycnJyOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","CAZACAACB4AABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAABQAAAAEAAFEAAFAAAAAKAAIUAAQAAAAAYAAPAAAQAAAAABAAAAAAYAAAAEAABAAACAAAAAAAAAAAAAHQADYAB4AA6ACAAAAABQAAAALQAEAAAEAAAAAAAAAAAAEAAAABWAAWAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAAAAdQwAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzE7CmluIGhhbGYgdmluQ292ZXJhZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpOwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoMyA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDMgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICgzIDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICgzIDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoMyA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBhbHBoYSA9IDEuMDsKCQlhbHBoYSA9IHZpbkNvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChhbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","CAZACAICCAAAAEIBAAABGAABAAOAAAYABQAEIAAAAAAAAAYAAAABQACSAACQAAAAEAAEKAAIAAAAAKAAHQAAIAAAAAAAAAAAAMAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAAAYAAAAFIACAAACAAAAAAAAAAAABAAAAAZAAC4AAIAAAAAAAAAAAAIAAAADUABMAA":"AgAAAExTS1NbAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZAoJdkhhaXJRdWFkRWRnZV9TdGFnZTAgPSBpbkhhaXJRdWFkRWRnZTsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAABAQAAAAAAAAEBAGQRAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UyOwppbiBoYWxmNCB2SGFpclF1YWRFZGdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKGxlbmd0aCh2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApKTsKCV9vdXRwdXQgPSBoYWxmNCh0LCAxLjAsIDAuMCwgMC4wKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpOwoJZmxvYXQ0IHNjYWxlLCBiaWFzOwoJaWYgKDMgPD0gNCB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLncpIAoJewoJCWlmICgzIDw9IDIgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS55KSAKCQl7CgkJCWlmICgzIDw9IDEgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSAzIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueikgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoMyA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDMgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAoZmFsc2UpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UyOwoJewoJCS8vIFN0YWdlIDIsIEFBUmVjdEVmZmVjdAoJCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJCWhhbGYgYWxwaGE7CgkJQHN3aXRjaCAoMSkgCgkJewoJCQljYXNlIDA6ICAgIGNhc2UgMjogICAgICAgIGFscGhhID0gaGFsZihhbGwoZ3JlYXRlclRoYW4oZmxvYXQ0KHNrX0ZyYWdDb29yZC54eSwgdXJlY3RVbmlmb3JtX1N0YWdlMi56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fU3RhZ2UyLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7CgkJCWJyZWFrOwoJCQlkZWZhdWx0OiAgICAgICAgaGFsZiB4U3ViLCB5U3ViOwoJCQl4U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnggLSB1cmVjdFVuaWZvcm1fU3RhZ2UyLngpLCAwLjApOwoJCQl4U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTIueiAtIHNrX0ZyYWdDb29yZC54KSwgMC4wKTsKCQkJeVN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC55IC0gdXJlY3RVbmlmb3JtX1N0YWdlMi55KSwgMC4wKTsKCQkJeVN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UyLncgLSBza19GcmFnQ29vcmQueSksIDAuMCk7CgkJCWFscGhhID0gKDEuMCArIG1heCh4U3ViLCAtMS4wKSkgKiAoMS4wICsgbWF4KHlTdWIsIC0xLjApKTsKCQl9CgkJQGlmICgxID09IDIgfHwgMSA9PSAzKSAKCQl7CgkJCWFscGhhID0gMS4wIC0gYWxwaGE7CgkJfQoJCWhhbGY0IGlucHV0Q29sb3IgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0X1N0YWdlMiA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0X1N0YWdlMjsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAA4AAABpbkhhaXJRdWFkRWRnZQAAAQAAAAAAAAA=","CAZACAECBUAABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAAOAASQAAUAAAABEAA4AACAAAAACYAB4AACAAAAAAAAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAAUAAIAAAIAAAAAAAAAAAAIAAAADAABMAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAAAArgcAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXN0YXJ0X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1ZW5kX1N0YWdlMV9jMF9jMTsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7Cglfb3V0cHV0ID0gbWl4KHVzdGFydF9TdGFnZTFfYzBfYzEsIHVlbmRfU3RhZ2UxX2MwX2MxLCB0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCWhhbGYgYWxwaGEgPSAxLjA7CgkJYWxwaGEgPSB2aW5Db3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoYWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","CAZACAECBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAIIDAAWAATYABAAAAAAAAAAAAABBAMADYAB4AACAAAAAAAAAAAANAAAAAAAAAAAAAIIDABKAAAQAAQAAAAAAAAAAAAQAAAAGQACYAA":"AgAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAACIAwAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKENvbXBvc2UtT25lIGJlaGF2aW9yKQoJCW91dHB1dF9TdGFnZTEgPSBibGVuZF9tb2R1bGF0ZShNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpKSwgb3V0cHV0Q29sb3JfU3RhZ2UwKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZACAACCAAAAAYAAEAAAAAAAAAQAAAACMAACAA4AAIQADYACQAAAAAAAAMAALQAAAAAAAAEAAAAAKAAKIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAAAAAAAACAAAAAJAACAAAEAAAAAAAAAAAAAPAAHQADYAB4AAAAAAAEAAAAAZAAIAAAIAAAAAAAAAAAAIAAAADUABMAA":"AgAAAExTS1PlAwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfU3RhZ2UwOwp1bmlmb3JtIGZsb2F0NCB1bG9jYWxNYXRyaXhfU3RhZ2UwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEaXN0YW5jZUZpZWxkUGF0aAoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzRGltZW5zaW9uc0ludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBfdG1wXzFfaW5Qb3NpdGlvbi54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAAFoOAAB1bmlmb3JtIGhhbGY0IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlNl83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM2XzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gaW50IHZUZXhJbmRleF9TdGFnZTA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBVbnJvbGxlZEJpbmFyeUdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCWZsb2F0NCBzY2FsZSwgYmlhczsKCWlmICg0IDw9IDQgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS53KSAKCXsKCQlpZiAoNCA8PSAyIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoNCA8PSAxIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueCkgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMF8xX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDQgPD0gMyB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNl83X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczZfN19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICg0IDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoNCA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoNCA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdXYgPSB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CgkJaGFsZjQgdGV4Q29sb3I7CgkJewoJCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHV2KS5ycnJyOwoJCX0KCQloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJCWhhbGYgYWZ3aWR0aDsKCQlhZndpZHRoID0gYWJzKDAuNjUqaGFsZihkRmR5KHZJbnRUZXh0dXJlQ29vcmRzX1N0YWdlMC55KSkpOwoJCWhhbGYgdmFsID0gc21vb3Roc3RlcCgtYWZ3aWR0aCwgYWZ3aWR0aCwgZGlzdGFuY2UpOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KHZhbCk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAEAAAIIDAAWAATYABEAAAAABAAAQAABBAMADYAB4AACQAAAABQAAAAABAAAQAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAshQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gY2xhbXAoc3Vic2V0Q29vcmQueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC55LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgY2xhbXBlZENvb3JkKTsKCV9vdXRwdXQgPSB0ZXh0dXJlQ29sb3I7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgR2F1c3NpYW5Db252b2x1dGlvbgoJCWZsb2F0MiBfY29vcmRzID0gdkxvY2FsQ29vcmRfU3RhZ2UwLnh5OwoJCW91dHB1dF9TdGFnZTEgPSBoYWxmNCgwLCAwLCAwLCAwKTsKCQlmbG9hdDIgY29vcmQgPSBfY29vcmRzIC0gMTIuMCAqIHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWZsb2F0MiBjb29yZFNhbXBsZWQgPSBoYWxmMigwLCAwKTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZACAACB4AABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAABQAAAAEAAFEAAFAAAAAKAAIUAAQAAAAAYAAPAAAQAAAAAAAAAAAAYAAAAEAABAAACAAAAAAAAAAAAAHQADYAB4AA6AAAAAAABQAAAALQAEAAAEAAAAAAAAAAAAEAAAABWAAWAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAAAAdgwAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzE7CmluIGhhbGYgdmluQ292ZXJhZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpOwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoMyA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDMgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICgzIDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICgzIDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoMyA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCWhhbGYgYWxwaGEgPSAxLjA7CgkJYWxwaGEgPSB2aW5Db3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoYWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","CAZAAAECAUAAAAAAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAAAAAAAAABAAAAAEAAFQAA":"AgAAAExTS1NuAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TdGFnZTAgPSBpblF1YWRFZGdlOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAB+AwAAaW4gaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWhhbGYgZWRnZUFscGhhOwoJCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQlpZiAodlF1YWRFZGdlX1N0YWdlMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TdGFnZTAudyA+IDAuMCkgCgkJewoJCQllZGdlQWxwaGEgPSBtaW4obWluKHZRdWFkRWRnZV9TdGFnZTAueiwgdlF1YWRFZGdlX1N0YWdlMC53KSArIDAuNSwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWhhbGYyIGdGID0gaGFsZjIoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KTsKCQkJZWRnZUFscGhhID0gKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","CAZAAAACBAAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAFgIAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZACAACBYAAAEIBAAABGAABAAOAAAYABQAEIAAAAAAAAAYAAAABQACSAACQAAAAEAAEKAAIAAAAAKAAHQAAIAAAAAAQAAAAAMAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPABAAAAAAYAAAAFIACAAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1NbAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZAoJdkhhaXJRdWFkRWRnZV9TdGFnZTAgPSBpbkhhaXJRdWFkRWRnZTsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAPoNAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxOwppbiBoYWxmNCB2SGFpclF1YWRFZGdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKGxlbmd0aCh2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApKTsKCV9vdXRwdXQgPSBoYWxmNCh0LCAxLjAsIDAuMCwgMC4wKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpOwoJZmxvYXQ0IHNjYWxlLCBiaWFzOwoJaWYgKDMgPD0gNCB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLncpIAoJewoJCWlmICgzIDw9IDIgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS55KSAKCQl7CgkJCWlmICgzIDw9IDEgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSAzIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueikgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoMyA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDMgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAodHJ1ZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCQloYWxmIGVkZ2VBbHBoYTsKCQloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBnRiA9IGhhbGYyKDIuMCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiBkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHkueCAtIGR1dmR5LnkpOwoJCWVkZ2VBbHBoYSA9IGhhbGYodkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggLSB2SGFpclF1YWRFZGdlX1N0YWdlMC55KTsKCQllZGdlQWxwaGEgPSBzcXJ0KGVkZ2VBbHBoYSAqIGVkZ2VBbHBoYSAvIGRvdChnRiwgZ0YpKTsKCQllZGdlQWxwaGEgPSBtYXgoMS4wIC0gZWRnZUFscGhhLCAwLjApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAOAAAAaW5IYWlyUXVhZEVkZ2UAAAEAAAAAAAAA","CAZAAAACAYAAAEAYAAABGAABAAOAAEIA7777777777776FAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1NnAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBjb2xvciA9IGluQ29sb3I7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAVQEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgABAAAAAAAAAA==","CAZAAAECA4AAAAIAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAADgAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJCWVkZ2VBbHBoYSAqPSBpbm5lckFscGhhOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","CAZAAAMCBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAGAAAAAYAAFYAAQAAAADZAAAAAAYAAAAEAAAGAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAHoGAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBhbHBoYTsKCUBzd2l0Y2ggKDMpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZiB4U3ViLCB5U3ViOwoJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueCksIDAuMCk7CgkJeFN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnogLSBza19GcmFnQ29vcmQueCksIDAuMCk7CgkJeVN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC55IC0gdXJlY3RVbmlmb3JtX1N0YWdlMV9jMC55KSwgMC4wKTsKCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQlhbHBoYSA9ICgxLjAgKyBtYXgoeFN1YiwgLTEuMCkpICogKDEuMCArIG1heCh5U3ViLCAtMS4wKSk7Cgl9CglAaWYgKDMgPT0gMiB8fCAzID09IDMpIAoJewoJCWFscGhhID0gMS4wIC0gYWxwaGE7Cgl9CgloYWxmNCBpbnB1dENvbG9yID0gX2lucHV0OwoJX291dHB1dCA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCkgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECA4AAAAIAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAOACAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQloYWxmIGRpc3RhbmNlVG9Jbm5lckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqIChkIC0gY2lyY2xlRWRnZS53KSk7CgkJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgkJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZACAACBYAAAEIBAAABGAABAAOAAAYABQAEIAAAAAAAAAYAAAABQACSAACQAAAAEAAEKAAIAAAAAKAAHQAAIAAAAAAAAAAAAMAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAAAYAAAAFIACAAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1NbAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZAoJdkhhaXJRdWFkRWRnZV9TdGFnZTAgPSBpbkhhaXJRdWFkRWRnZTsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAPsNAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxOwppbiBoYWxmNCB2SGFpclF1YWRFZGdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKGxlbmd0aCh2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApKTsKCV9vdXRwdXQgPSBoYWxmNCh0LCAxLjAsIDAuMCwgMC4wKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpOwoJZmxvYXQ0IHNjYWxlLCBiaWFzOwoJaWYgKDMgPD0gNCB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLncpIAoJewoJCWlmICgzIDw9IDIgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS55KSAKCQl7CgkJCWlmICgzIDw9IDEgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSAzIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueikgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoMyA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDMgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAoZmFsc2UpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAOAAAAaW5IYWlyUXVhZEVkZ2UAAAEAAAAAAAAA","CAZAAAECA4AAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAGABAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZACAACBUAAAAYAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAABQACKAACQAAAAEAADQAAIAAAAAKAAHQAAIAAAAAAAAAAAGQACAAAEAAAAAAAAAAAAAPAAHQADYAB4AAAAAACMABAAABAAAAAAAAAAAABAAAAALQAFQAA":"AgAAAExTS1OYAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRFZGdlCgl2UXVhZEVkZ2VfU3RhZ2UwID0gaW5RdWFkRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBfdG1wXzFfaW5Qb3NpdGlvbi54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAIAJAAB1bmlmb3JtIGhhbGY0IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1c3RhcnRfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHVlbmRfU3RhZ2UxX2MwX2MxOwppbiBoYWxmNCB2UXVhZEVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7Cglfb3V0cHV0ID0gbWl4KHVzdGFydF9TdGFnZTFfYzBfYzEsIHVlbmRfU3RhZ2UxX2MwX2MxLCB0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkRWRnZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmIGVkZ2VBbHBoYTsKCQloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJCWhhbGYyIGR1dmR5ID0gaGFsZjIoZEZkeSh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaWYgKHZRdWFkRWRnZV9TdGFnZTAueiA+IDAuMCAmJiB2UXVhZEVkZ2VfU3RhZ2UwLncgPiAwLjApIAoJCXsKCQkJZWRnZUFscGhhID0gbWluKG1pbih2UXVhZEVkZ2VfU3RhZ2UwLnosIHZRdWFkRWRnZV9TdGFnZTAudykgKyAwLjUsIDEuMCk7CgkJfQoJCWVsc2UgCgkJewoJCQloYWxmMiBnRiA9IGhhbGYyKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeS54IC0gZHV2ZHkueSk7CgkJCWVkZ2VBbHBoYSA9ICh2UXVhZEVkZ2VfU3RhZ2UwLngqdlF1YWRFZGdlX1N0YWdlMC54IC0gdlF1YWRFZGdlX1N0YWdlMC55KTsKCQkJZWRnZUFscGhhID0gc2F0dXJhdGUoMC41IC0gZWRnZUFscGhhIC8gbGVuZ3RoKGdGKSk7CgkJfQoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","CAZACAECBUAABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAAOAAMAAAUAAAABEABCQACAAAAACYAB4AACAAAAAAAAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAAUAAIAAAIAAAAAAAAAAAAIAAAADAABMAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAAAAyggAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTAxX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMwMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTIzX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZiB1dGhyZXNob2xkX1N0YWdlMV9jMF9jMTsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihsZW5ndGgodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwKSk7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBEdWFsSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAodCA8IHV0aHJlc2hvbGRfU3RhZ2UxX2MwX2MxKSAKCXsKCQlzY2FsZSA9IHVzY2FsZTAxX1N0YWdlMV9jMF9jMTsKCQliaWFzID0gdWJpYXMwMV9TdGFnZTFfYzBfYzE7Cgl9CgllbHNlIAoJewoJCXNjYWxlID0gdXNjYWxlMjNfU3RhZ2UxX2MwX2MxOwoJCWJpYXMgPSB1YmlhczIzX1N0YWdlMV9jMF9jMTsKCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IER1YWxJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKGZhbHNlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBhbHBoYSA9IDEuMDsKCQlhbHBoYSA9IHZpbkNvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChhbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZACAECCAAAAAAAAAACOAIAAAJQAAIACIAAAAH777776EYAAEAP777777777777EAAFWAAAAAAAAAYAAAACYACSAACQAAAAGQAEKAAIAAAAAPAAHQAAIAAAAAAAAAAAAMAAAACMAAQAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAAAYAAAAGQACAAACAAAAAAAAAAAACAAAAA6AALAAA":"AgAAAExTS1PWAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y292ZXJhZ2VfU3RhZ2UwID0gY292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAAFUMAAB1bmlmb3JtIGhhbGY0IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMTsKaW4gZmxvYXQgdmNvdmVyYWdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKGxlbmd0aCh2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApKTsKCV9vdXRwdXQgPSBoYWxmNCh0LCAxLjAsIDAuMCwgMC4wKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpOwoJZmxvYXQ0IHNjYWxlLCBiaWFzOwoJaWYgKDMgPD0gNCB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLncpIAoJewoJCWlmICgzIDw9IDIgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS55KSAKCQl7CgkJCWlmICgzIDw9IDEgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSAzIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueikgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoMyA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDMgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAoZmFsc2UpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJZmxvYXQgY292ZXJhZ2UgPSB2Y292ZXJhZ2VfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGhhbGYoY292ZXJhZ2UpKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yCgkJaGFsZjQgY29uc3RDb2xvcjsKCQlAaWYgKGZhbHNlKSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgwKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCk7CgkJfQoJCW91dHB1dF9TdGFnZTEgPSBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGNvbnN0Q29sb3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZACAACB4AABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAACAAAAAEAAFEAAFAAAAAKAAIUAAQAAAAAYAAPAAAQAAAAAAAAAAABAAAAAEAABAAACAAAAAAAAAAAAAHQADYAB4AA6AAAAAAACAAAAALQAEAAAEAAAAAAAAAAAAEAAAABWAAWAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAAAA3AwAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTZfN19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzNl83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMTsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihsZW5ndGgodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwKSk7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBVbnJvbGxlZEJpbmFyeUdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCWZsb2F0NCBzY2FsZSwgYmlhczsKCWlmICg0IDw9IDQgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS53KSAKCXsKCQlpZiAoNCA8PSAyIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoNCA8PSAxIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueCkgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMF8xX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDQgPD0gMyB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNl83X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczZfN19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICg0IDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoNCA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoNCA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCWhhbGYgYWxwaGEgPSAxLjA7CgkJYWxwaGEgPSB2aW5Db3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoYWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","CAZAAAMCBEAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAAAAAAAAAQAAAAEAACYAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAOoCAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZACAECBYAAAAYAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAAAIAAAAAOAAUQAAUAAAABEAA4AACAAAAACYAB4AACAAAAAAAAAAAAEAAAAAPAAEAAAIAAAAAAAAAAAAA6AAPAAHQADYAAAAAAAIAAAABMAAQAAAQAAAAAAAAAAAAQAAAAGQACYAA":"AgAAAExTS1OYAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRFZGdlCgl2UXVhZEVkZ2VfU3RhZ2UwID0gaW5RdWFkRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBfdG1wXzFfaW5Qb3NpdGlvbi54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAMEOAAB1bmlmb3JtIGhhbGY0IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlNl83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM2XzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxOwppbiBoYWxmNCB2UXVhZEVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoNCA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDQgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDQgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICg0IDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTZfN19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXM2XzdfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoNCA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDQgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDQgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAoZmFsc2UpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJCWlmICh2UXVhZEVkZ2VfU3RhZ2UwLnogPiAwLjAgJiYgdlF1YWRFZGdlX1N0YWdlMC53ID4gMC4wKSAKCQl7CgkJCWVkZ2VBbHBoYSA9IG1pbihtaW4odlF1YWRFZGdlX1N0YWdlMC56LCB2UXVhZEVkZ2VfU3RhZ2UwLncpICsgMC41LCAxLjApOwoJCX0KCQllbHNlIAoJCXsKCQkJaGFsZjIgZ0YgPSBoYWxmMigyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR4LnggLSBkdXZkeC55LCAgICAgICAgICAgICAgIDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHkueCAtIGR1dmR5LnkpOwoJCQllZGdlQWxwaGEgPSAodlF1YWRFZGdlX1N0YWdlMC54KnZRdWFkRWRnZV9TdGFnZTAueCAtIHZRdWFkRWRnZV9TdGFnZTAueSk7CgkJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","CAZAAAACAUAABEAAAAABGAABAAOAAAYABQAEIAAAAAAAAAAAAAAAEAAAAAOAAWAA":"AgAAAExTS1MxAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkCgl2SGFpclF1YWRFZGdlX1N0YWdlMCA9IGluSGFpclF1YWRFZGdlOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABjAwAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwp1bmlmb3JtIGhhbGYgdUNvdmVyYWdlX1N0YWdlMDsKaW4gaGFsZjQgdkhhaXJRdWFkRWRnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh1Q292ZXJhZ2VfU3RhZ2UwICogZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADgAAAGluSGFpclF1YWRFZGdlAAABAAAAAAAAAA==","CAZACAMCCIAAAAAAAAAGOAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAAAAAAACYACKAACQAAAAGQAEKAAIAAAAAPAAHQAAIAAAAAAQAAAAJAACAAAEAAAAABQAAAAACAAAABMAAAQAAQAAAAAAAAAAAAAAAAAGIACPAAEAAAAAAAAAAAAAAAAAA5AAHQAAIAAAAAAAAAAAAIAAAAEIABMAA":"AgAAAExTS1NIAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTI7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMV9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBsb2NhbENvb3JkLnh5MSkueHk7Cgl9Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzFfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTIpKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAB1CAAAdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXN0YXJ0X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1ZW5kX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMjsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UyOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihsZW5ndGgodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwKSk7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UyX2MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMiwgdlRyYW5zZm9ybWVkQ29vcmRzXzFfU3RhZ2UwKS5ycnJyOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBCbGVuZAoJCS8vIEJsZW5kIG1vZGU6IERzdEluIChDb21wb3NlLU9uZSBiZWhhdmlvcikKCQlvdXRwdXRfU3RhZ2UxID0gYmxlbmRfZHN0X2luKG91dHB1dENvbG9yX1N0YWdlMCwgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSkpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMjsKCXsKCQkvLyBTdGFnZSAyLCBNYXRyaXhFZmZlY3QKCQlvdXRwdXRfU3RhZ2UyID0gVGV4dHVyZUVmZmVjdF9TdGFnZTJfYzEob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0X1N0YWdlMjsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAICBIAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAQEAAAAAAAABAQDCAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAICBIAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAAAQAAAAGQAB6AAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAcQQAAHVuaWZvcm0gZmxvYXQ0IHVjaXJjbGVfU3RhZ2UxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJZmxvYXQyIHRleENvb3JkOwoJCXRleENvb3JkID0gdmxvY2FsQ29vcmRfU3RhZ2UwOwoJCW91dHB1dENvbG9yX1N0YWdlMCA9IChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB0ZXhDb29yZCkgKiBvdXRwdXRDb2xvcl9TdGFnZTApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjbGVFZmZlY3QKCQlmbG9hdDIgcHJldkNlbnRlcjsKCQlmbG9hdCBwcmV2UmFkaXVzID0gLTEuMDAwMDAwOwoJCWhhbGYgZDsKCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TdGFnZTEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TdGFnZTEudykgLSAxLjApICogdWNpcmNsZV9TdGFnZTEueik7CgkJfQoJCWVsc2UgCgkJewoJCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1N0YWdlMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1N0YWdlMS53KSkgKiB1Y2lyY2xlX1N0YWdlMS56KTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlAaWYgKDEgPT0gMSB8fCAxID09IDMpIAoJCXsKCQkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBjbGFtcChkLCAwLjAsIDEuMCk7CgkJfQoJCWVsc2UgCgkJewoJCQlvdXRwdXRfU3RhZ2UxID0gZCA+IDAuNSA/IGlucHV0Q29sb3IgOiBoYWxmNCgwLjApOwoJCX0KCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAMCBMAAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAABAAAAA6IAAAAEAAAXAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQBvBwAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMTsKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCV9vdXRwdXQgPSBfaW5wdXQgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7CgkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZAAAACAYAABAAIAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1M3AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJdmluQ292ZXJhZ2VfU3RhZ2UwID0gaW5Db3ZlcmFnZTsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAArAEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCQloYWxmIGFscGhhID0gMS4wOwoJCWFscGhhID0gdmluQ292ZXJhZ2VfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZAAAMCBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHWAAAAAYAABQAAQAAAADZAAAAA6YAAAAEAAAGAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBALoEAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7Cglfb3V0cHV0ID0gX2lucHV0ICogYWxwaGE7CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKSAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","CAZACAACCAAAAAAAAAAGOAQAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAGAAAAAYAAUQAAUAAAABYAA4AACAAAAAEAAB4AACAAAAAAEAAAAADAAAAAUAAEAAAIAAAAADAAAAAAEAAAAADAAAAAZAAAIAAIAAAAAAAAAAAAIAAAADUABMAA":"AgAAAExTS1OqAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBsb2NhbENvb3JkLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAADYCwAAdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzE7CmluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoMyA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDMgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICgzIDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICgzIDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoMyA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogRHN0SW4gKENvbXBvc2UtT25lIGJlaGF2aW9yKQoJCW91dHB1dF9TdGFnZTEgPSBibGVuZF9kc3RfaW4ob3V0cHV0Q29sb3JfU3RhZ2UwLCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpKSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAICCMAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAAAAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAAAAAALQAEAAAEAAAAAAAAAAAAAAAAABUAATYABAAAAAAAAAAAAAAAAAAHQAB4AACAAAAAAAAAAAACAAAABDAALAAA":"AgAAAExTS1MCAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTI7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18xX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMV9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMikpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAxggAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTI7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMjsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18xX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKGZhbHNlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMl9jMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTIsIHZUcmFuc2Zvcm1lZENvb3Jkc18xX1N0YWdlMCkucnJycjsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTI7Cgl7CgkJLy8gU3RhZ2UgMiwgTWF0cml4RWZmZWN0CgkJb3V0cHV0X1N0YWdlMiA9IFRleHR1cmVFZmZlY3RfU3RhZ2UyX2MxKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dF9TdGFnZTI7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAECBQAAAEIBAAABGAABAAOAAAYABQAEIAAAAAAAAFAAGAAAKAAAAAOAARIABAAAAABEAA6AABAAAAAAAAAAAAYAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAAAAAAJAAEAAAEAAAAAAAAAAAAEAAAABMAAWAA":"AgAAAExTS1NbAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZAoJdkhhaXJRdWFkRWRnZV9TdGFnZTAgPSBpbkhhaXJRdWFkRWRnZTsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAE8KAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUwMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMDFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUyM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMjNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGYgdXRocmVzaG9sZF9TdGFnZTFfYzBfYzE7CmluIGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpOwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgRHVhbEludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpOwoJZmxvYXQ0IHNjYWxlLCBiaWFzOwoJaWYgKHQgPCB1dGhyZXNob2xkX1N0YWdlMV9jMF9jMSkgCgl7CgkJc2NhbGUgPSB1c2NhbGUwMV9TdGFnZTFfYzBfYzE7CgkJYmlhcyA9IHViaWFzMDFfU3RhZ2UxX2MwX2MxOwoJfQoJZWxzZSAKCXsKCQlzY2FsZSA9IHVzY2FsZTIzX1N0YWdlMV9jMF9jMTsKCQliaWFzID0gdWJpYXMyM19TdGFnZTFfYzBfYzE7Cgl9Cglfb3V0cHV0ID0gaGFsZjQoZmxvYXQodCkgKiBzY2FsZSArIGJpYXMpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBEdWFsSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCQloYWxmIGVkZ2VBbHBoYTsKCQloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBnRiA9IGhhbGYyKDIuMCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiBkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHkueCAtIGR1dmR5LnkpOwoJCWVkZ2VBbHBoYSA9IGhhbGYodkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggLSB2SGFpclF1YWRFZGdlX1N0YWdlMC55KTsKCQllZGdlQWxwaGEgPSBzcXJ0KGVkZ2VBbHBoYSAqIGVkZ2VBbHBoYSAvIGRvdChnRiwgZ0YpKTsKCQllZGdlQWxwaGEgPSBtYXgoMS4wIC0gZWRnZUFscGhhLCAwLjApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAA4AAABpbkhhaXJRdWFkRWRnZQAAAQAAAAAAAAA=","CAZAAAMCBMAAAAAAAAAGOAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAAAAAAWAATYABAAAAAAAAAAAAAAAAAADYAB4AACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1OjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTE7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTEpKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAA5gIAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkucnJycjsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgTWF0cml4RWZmZWN0CgkJb3V0cHV0X1N0YWdlMSA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZACAACBYAAAEIBAAABGAABAAOAAAYABQAEIAAAAAAAABAAAAABQACSAACQAAAAEAAEKAAIAAAAAKAAHQAAIAAAAAAAAAAAAQAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAABAAAAAFIACAAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1NbAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZAoJdkhhaXJRdWFkRWRnZV9TdGFnZTAgPSBpbkhhaXJRdWFkRWRnZTsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAGEOAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGU2XzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczZfN19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzE7CmluIGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpOwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoNCA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDQgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDQgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICg0IDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTZfN19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXM2XzdfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoNCA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDQgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDQgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAoZmFsc2UpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAA4AAABpbkhhaXJRdWFkRWRnZQAAAQAAAAAAAAA=","CAZACAACBUAAAAYAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAABQACKAACQAAAAEAADQAAIAAAAAKAAHQAAIAAAAAAQAAAAGQACAAAEAAAAAAAAAAAAAPAAHQADYAB4AEAAAACMABAAABAAAAAAAAAAAABAAAAALQAFQAA":"AgAAAExTS1OYAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRFZGdlCgl2UXVhZEVkZ2VfU3RhZ2UwID0gaW5RdWFkRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBfdG1wXzFfaW5Qb3NpdGlvbi54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAH8JAAB1bmlmb3JtIGhhbGY0IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1c3RhcnRfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHVlbmRfU3RhZ2UxX2MwX2MxOwppbiBoYWxmNCB2UXVhZEVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7Cglfb3V0cHV0ID0gbWl4KHVzdGFydF9TdGFnZTFfYzBfYzEsIHVlbmRfU3RhZ2UxX2MwX2MxLCB0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWhhbGYgZWRnZUFscGhhOwoJCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQlpZiAodlF1YWRFZGdlX1N0YWdlMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TdGFnZTAudyA+IDAuMCkgCgkJewoJCQllZGdlQWxwaGEgPSBtaW4obWluKHZRdWFkRWRnZV9TdGFnZTAueiwgdlF1YWRFZGdlX1N0YWdlMC53KSArIDAuNSwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWhhbGYyIGdGID0gaGFsZjIoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KTsKCQkJZWRnZUFscGhhID0gKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yCgkJaGFsZjQgY29uc3RDb2xvcjsKCQlAaWYgKGZhbHNlKSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgwKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCk7CgkJfQoJCW91dHB1dF9TdGFnZTEgPSBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGNvbnN0Q29sb3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","CAZACAACB4AAAAAAAAAGOAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAACAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAQAAAALQAEAAAEAAAAAAAAAAAAEAAAABWAAWAA":"AgAAAExTS1OvAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAYQcAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yCgkJaGFsZjQgY29uc3RDb2xvcjsKCQlAaWYgKGZhbHNlKSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgwKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCk7CgkJfQoJCW91dHB1dF9TdGFnZTEgPSBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGNvbnN0Q29sb3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA=="}} \ No newline at end of file diff --git a/shaders_1.22.6.sksl.json b/shaders_1.22.6.sksl.json new file mode 100644 index 000000000..fa507ac3f --- /dev/null +++ b/shaders_1.22.6.sksl.json @@ -0,0 +1 @@ +{"platform":"android","name":"SM G970N","engineRevision":"2f0af3715217a0c2ada72c717d4ed9178d68f6ed","data":{"CAZACAECBMAAAAIACAAAAAAAAAAAAAAACMAACAA4AAIQB777777RQADSAAAAAAAAEAACOAAEAAAAAAAAAAAAAAAAAAYAAGYAAQAAAAANAAAAAAAAAAAEAAACAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1PNAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHV2aWV3TWF0cml4X1N0YWdlMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVmVydGljZXNHUAoJaGFsZjQgY29sb3IgPSBpbkNvbG9yOwoJY29sb3IgPSBjb2xvci5iZ3JhOwoJY29sb3IgPSBjb2xvcjsKCWNvbG9yID0gaGFsZjQoY29sb3IucmdiICogY29sb3IuYSwgY29sb3IuYSk7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX3Bvc2l0aW9uID0gdXZpZXdNYXRyaXhfU3RhZ2UwLnh6ICogcG9zaXRpb24gKyB1dmlld01hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfcG9zaXRpb24ueCAsIF90bXBfMF9wb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAAhBAAAdW5pZm9ybSBoYWxmNCB1Y29sb3JfU3RhZ2UxX2MwOwppbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDb25zdENvbG9yUHJvY2Vzc29yX1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHVjb2xvcl9TdGFnZTFfYzA7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBCbHVycmVkRWRnZUZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiBpbnB1dEFscGhhID0gX2lucHV0Lnc7CgloYWxmIGZhY3RvciA9IDEuMCAtIGlucHV0QWxwaGE7CglAc3dpdGNoICgwKSAKCXsKCQljYXNlIDA6ICAgICAgICBmYWN0b3IgPSBleHAoKC1mYWN0b3IgKiBmYWN0b3IpICogNC4wKSAtIDAuMDE3OTk5OTk5MjI1MTM5NjE4OwoJCWJyZWFrOwoJCWNhc2UgMTogICAgICAgIGZhY3RvciA9IHNtb290aHN0ZXAoMS4wLCAwLjAsIGZhY3Rvcik7CgkJYnJlYWs7Cgl9Cglfb3V0cHV0ID0gaGFsZjQoZmFjdG9yKTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVmVydGljZXNHUAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKFNrTW9kZSBiZWhhdmlvcikKCQlvdXRwdXRfU3RhZ2UxID0gYmxlbmRfbW9kdWxhdGUoQ29uc3RDb2xvclByb2Nlc3Nvcl9TdGFnZTFfYzAoaGFsZjQoMSkpLCBCbHVycmVkRWRnZUZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMShvdXRwdXRDb2xvcl9TdGFnZTApKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24HAAAAaW5Db2xvcgABAAAAAAAAAA==","CAZAAAACBAAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAArQIAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAECA4AAAAIAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAOACAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQloYWxmIGRpc3RhbmNlVG9Jbm5lckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqIChkIC0gY2lyY2xlRWRnZS53KSk7CgkJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgkJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAMCBEAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAAAAAAAAAQAAAAEAACYAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAOoCAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAMCBMAAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAABAAAAA6IAAAAEAAAXAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQBvBwAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMTsKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCV9vdXRwdXQgPSBfaW5wdXQgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7CgkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZAAAACBAAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAFgIAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAACAYAABEAJAAABGAABAAOAAEIA777777YZAAAAAFAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCWNvbG9yID0gY29sb3IgKiBpbkNvdmVyYWdlOwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAABVAQAAaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","CAZAAAACAYAAAAAIAAABGAABAD7777777777777777776FAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1PkAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAWgEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAEAAAAKAAAAaW5Qb3NpdGlvbgAAAQAAAAAAAAA=","CAZAAAECA4AAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABMAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAACA4AAAAYAAAAAAAAAAAAQAAAACMAACAA4AAIQADYACQAAAAAAAAMAALQAAAAAAAAAAAAAAAQAAAACYACYAA":"AgAAAExTS1PjAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNEaW1lbnNpb25zSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSAodGV4SWR4KTsKCXZJbnRUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAXAwAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBmbG9hdDIgdkludFRleHR1cmVDb29yZHNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGlzdGFuY2VGaWVsZFBhdGgKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfU3RhZ2UwOwoJCWhhbGY0IHRleENvbG9yOwoJCXsKCQkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB1dikucnJycjsKCQl9CgkJaGFsZiBkaXN0YW5jZSA9IDcuOTY4NzUqKHRleENvbG9yLnIgLSAwLjUwMTk2MDc4NDMxKTsKCQloYWxmIGFmd2lkdGg7CgkJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeSh2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTAueSkpKTsKCQloYWxmIHZhbCA9IHNtb290aHN0ZXAoLWFmd2lkdGgsIGFmd2lkdGgsIGRpc3RhbmNlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh2YWwpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZAAAACAYAABAAIAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1M3AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJdmluQ292ZXJhZ2VfU3RhZ2UwID0gaW5Db3ZlcmFnZTsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAArAEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCQloYWxmIGFscGhhID0gMS4wOwoJCWFscGhhID0gdmluQ292ZXJhZ2VfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZAAAMCBEAAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAAAAAAAAAQAAAAEAACYAA":"AgAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAEBAAAAAAAAAQEA5QIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAACBAAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAA4AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAICBIAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAQEAAAAAAAABAQDCAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAECA4AAAAIAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAADgAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJCWVkZ2VBbHBoYSAqPSBpbm5lckFscGhhOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","CAZAAAECA4AAAAAAAAABKAADAAKQAAYACUAAGAATAAAQAFIAAMABKAADAAOAAEIAEAADEAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MXBAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgcmFkaWk7CglyYWRpaS54ID0gZG90KHJhZGlpX3NlbGVjdG9yLCByYWRpaV94KTsKCXJhZGlpLnkgPSBkb3QocmFkaWlfc2VsZWN0b3IsIHJhZGlpX3kpOwoJYm9vbCBpc19hcmNfc2VjdGlvbiA9IChyYWRpaS54ID4gMCk7CglyYWRpaSA9IGFicyhyYWRpaSk7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpOwoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZTsKCWlmIChpc19hcmNfc2VjdGlvbikgCgl7CgkJdmFyY2Nvb3JkX1N0YWdlMC54eSA9IDEgLSBhYnMocmFkaXVzX291dHNldCk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqICh2YXJjY29vcmRfU3RhZ2UwLnh5L3JhZGlpICogY29ybmVyICogMik7Cgl9CgllbHNlIAoJewoJCXZhcmNjb29yZF9TdGFnZTAgPSBmbG9hdDQoMCk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAACgCAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0NCB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgkJaWYgKGZsb2F0MigwKSAhPSB2YXJjY29vcmRfU3RhZ2UwLnh5KSAKCQl7CgkJCWZsb2F0IGZuID0gZG90KHZhcmNjb29yZF9TdGFnZTAueHksIHZhcmNjb29yZF9TdGFnZTAueHkpIC0gMTsKCQkJaWYgKGZuID4gMCkgCgkJCXsKCQkJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDApOwoJCQl9CgkJfQoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAHAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECAUAAAEYAAEABYAARAANQAAQAAAAAAAAMABEQAAAAAAAAAAAAAABAAAAAEAAFQAA":"AgAAAExTS1OFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFJSZWN0U2hhZG93Cgl2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAB1AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBoYWxmMyB2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUlJlY3RTaGFkb3cKCQloYWxmMyBzaGFkb3dQYXJhbXM7CgkJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CgkJZmxvYXQyIHV2ID0gZmxvYXQyKHNoYWRvd1BhcmFtcy56ICogKDEuMCAtIGQpLCAwLjUpOwoJCWhhbGYgZmFjdG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdXYpLnJycnIuYTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChmYWN0b3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","CAZAAAECAUAAAAAAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAAAAAAAAABAAAAAEAAFQAA":"AgAAAExTS1NuAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TdGFnZTAgPSBpblF1YWRFZGdlOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAB+AwAAaW4gaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWhhbGYgZWRnZUFscGhhOwoJCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQlpZiAodlF1YWRFZGdlX1N0YWdlMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TdGFnZTAudyA+IDAuMCkgCgkJewoJCQllZGdlQWxwaGEgPSBtaW4obWluKHZRdWFkRWRnZV9TdGFnZTAueiwgdlF1YWRFZGdlX1N0YWdlMC53KSArIDAuNSwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWhhbGYyIGdGID0gaGFsZjIoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KTsKCQkJZWRnZUFscGhhID0gKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAAAAAIIDAAWAATYABEAAAAABAAAAAABBAMADYAB4AACQAAAABQAAAAABAAAAAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAfRQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TdGFnZTFfYzBfYzAueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC53KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAEAAAIIDAAWAATYABEAAAAABAAAQAABBAMADYAB4AACQAAAABQAAAAABAAAQAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAshQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gY2xhbXAoc3Vic2V0Q29vcmQueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC55LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgY2xhbXBlZENvb3JkKTsKCV9vdXRwdXQgPSB0ZXh0dXJlQ29sb3I7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgR2F1c3NpYW5Db252b2x1dGlvbgoJCWZsb2F0MiBfY29vcmRzID0gdkxvY2FsQ29vcmRfU3RhZ2UwLnh5OwoJCW91dHB1dF9TdGFnZTEgPSBoYWxmNCgwLCAwLCAwLCAwKTsKCQlmbG9hdDIgY29vcmQgPSBfY29vcmRzIC0gMTIuMCAqIHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWZsb2F0MiBjb29yZFNhbXBsZWQgPSBoYWxmMigwLCAwKTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAICBIAAAAIAAAABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1M6CQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqIChhcmNjb29yZC9yYWRpaSAqIDIpOwoJfQoJc2tfUG9zaXRpb24gPSBmbG9hdDQoZGV2Y29vcmQueCAsIGRldmNvb3JkLnksIDAsIDEpOwp9CgAAAAEBAAAAAAAAAQEAdQQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZ3g9dmFyY2Nvb3JkX1N0YWdlMC56LCBneT12YXJjY29vcmRfU3RhZ2UwLnc7CgkJCWZsb2F0IGZud2lkdGggPSBhYnMoZ3gpICsgYWJzKGd5KTsKCQkJaGFsZiBkID0gaGFsZihmbi9mbndpZHRoKTsKCQkJY292ZXJhZ2UgPSBjbGFtcCguNSAtIGQsIDAsIDEpOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZACAACBUAAAAYAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAABQACKAACQAAAAEAADQAAIAAAAAKAAHQAAIAAAAAAQAAAAGQACAAAEAAAAAAAAAAAAAPAAHQADYAB4AEAAAACMABAAABAAAAAAAAAAAABAAAAALQAFQAA":"AgAAAExTS1OYAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRFZGdlCgl2UXVhZEVkZ2VfU3RhZ2UwID0gaW5RdWFkRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBfdG1wXzFfaW5Qb3NpdGlvbi54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAH8JAAB1bmlmb3JtIGhhbGY0IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1c3RhcnRfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHVlbmRfU3RhZ2UxX2MwX2MxOwppbiBoYWxmNCB2UXVhZEVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7Cglfb3V0cHV0ID0gbWl4KHVzdGFydF9TdGFnZTFfYzBfYzEsIHVlbmRfU3RhZ2UxX2MwX2MxLCB0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWhhbGYgZWRnZUFscGhhOwoJCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQlpZiAodlF1YWRFZGdlX1N0YWdlMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TdGFnZTAudyA+IDAuMCkgCgkJewoJCQllZGdlQWxwaGEgPSBtaW4obWluKHZRdWFkRWRnZV9TdGFnZTAueiwgdlF1YWRFZGdlX1N0YWdlMC53KSArIDAuNSwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWhhbGYyIGdGID0gaGFsZjIoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KTsKCQkJZWRnZUFscGhhID0gKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yCgkJaGFsZjQgY29uc3RDb2xvcjsKCQlAaWYgKGZhbHNlKSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgwKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCk7CgkJfQoJCW91dHB1dF9TdGFnZTEgPSBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGNvbnN0Q29sb3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","CAZACAECBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAIIDAAWAATYABAAAAAAAAAAAAABBAMADYAB4AACAAAAAAAAAAAANAAAAAAAAAAAAAIIDABKAAAQAAQAAAAAAAAAAAAQAAAAGQACYAA":"AgAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAACIAwAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKENvbXBvc2UtT25lIGJlaGF2aW9yKQoJCW91dHB1dF9TdGFnZTEgPSBibGVuZF9tb2R1bGF0ZShNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpKSwgb3V0cHV0Q29sb3JfU3RhZ2UwKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAICBIAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAoAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAMCBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAGAAAAAYAAFYAAQAAAADZAAAAAAYAAAAEAAAGAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAHoGAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBhbHBoYTsKCUBzd2l0Y2ggKDMpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZiB4U3ViLCB5U3ViOwoJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueCksIDAuMCk7CgkJeFN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnogLSBza19GcmFnQ29vcmQueCksIDAuMCk7CgkJeVN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC55IC0gdXJlY3RVbmlmb3JtX1N0YWdlMV9jMC55KSwgMC4wKTsKCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQlhbHBoYSA9ICgxLjAgKyBtYXgoeFN1YiwgLTEuMCkpICogKDEuMCArIG1heCh5U3ViLCAtMS4wKSk7Cgl9CglAaWYgKDMgPT0gMiB8fCAzID09IDMpIAoJewoJCWFscGhhID0gMS4wIC0gYWxwaGE7Cgl9CgloYWxmNCBpbnB1dENvbG9yID0gX2lucHV0OwoJX291dHB1dCA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCkgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAEAAAIIDAAWAATYABEAAAAAAAAAQAABBAMADYAB4AACQAAAABQAAAAAAAAAQAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAfRQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gc3Vic2V0Q29vcmQueTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAACAUAABEAAAAABGAABAAOAAAYABQAEIAAAAAAAAAAAAAAAEAAAAAOAAWAA":"AgAAAExTS1MxAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkCgl2SGFpclF1YWRFZGdlX1N0YWdlMCA9IGluSGFpclF1YWRFZGdlOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABjAwAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwp1bmlmb3JtIGhhbGYgdUNvdmVyYWdlX1N0YWdlMDsKaW4gaGFsZjQgdkhhaXJRdWFkRWRnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh1Q292ZXJhZ2VfU3RhZ2UwICogZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADgAAAGluSGFpclF1YWRFZGdlAAABAAAAAAAAAA==","CAZAAAACBAAAADQBAEACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAA5gYAAHVuaWZvcm0gaGFsZiB1U3JjVEZfU3RhZ2UwWzddOwp1bmlmb3JtIGhhbGYzeDMgdUNvbG9yWGZvcm1fU3RhZ2UwOwp1bmlmb3JtIGhhbGYgdURzdFRGX1N0YWdlMFs3XTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmIHNyY190Zl9TdGFnZTAoaGFsZiB4KSAKewoJaGFsZiBHID0gdVNyY1RGX1N0YWdlMFswXTsKCWhhbGYgQSA9IHVTcmNURl9TdGFnZTBbMV07CgloYWxmIEIgPSB1U3JjVEZfU3RhZ2UwWzJdOwoJaGFsZiBDID0gdVNyY1RGX1N0YWdlMFszXTsKCWhhbGYgRCA9IHVTcmNURl9TdGFnZTBbNF07CgloYWxmIEUgPSB1U3JjVEZfU3RhZ2UwWzVdOwoJaGFsZiBGID0gdVNyY1RGX1N0YWdlMFs2XTsKCWhhbGYgcyA9IHNpZ24oeCk7Cgl4ID0gYWJzKHgpOwoJeCA9ICh4IDwgRCkgPyAoQyAqIHgpICsgRiA6IHBvdyhBICogeCArIEIsIEcpICsgRTsKCXJldHVybiBzICogeDsKfQpoYWxmIGRzdF90Zl9TdGFnZTAoaGFsZiB4KSAKewoJaGFsZiBHID0gdURzdFRGX1N0YWdlMFswXTsKCWhhbGYgQSA9IHVEc3RURl9TdGFnZTBbMV07CgloYWxmIEIgPSB1RHN0VEZfU3RhZ2UwWzJdOwoJaGFsZiBDID0gdURzdFRGX1N0YWdlMFszXTsKCWhhbGYgRCA9IHVEc3RURl9TdGFnZTBbNF07CgloYWxmIEUgPSB1RHN0VEZfU3RhZ2UwWzVdOwoJaGFsZiBGID0gdURzdFRGX1N0YWdlMFs2XTsKCWhhbGYgcyA9IHNpZ24oeCk7Cgl4ID0gYWJzKHgpOwoJeCA9ICh4IDwgRCkgPyAoQyAqIHgpICsgRiA6IHBvdyhBICogeCArIEIsIEcpICsgRTsKCXJldHVybiBzICogeDsKfQpoYWxmNCBnYW11dF94Zm9ybV9TdGFnZTAoaGFsZjQgY29sb3IpIAp7Cgljb2xvci5yZ2IgPSAodUNvbG9yWGZvcm1fU3RhZ2UwICogY29sb3IucmdiKTsKCXJldHVybiBjb2xvcjsKfQpoYWxmNCBjb2xvcl94Zm9ybV9TdGFnZTAoZmxvYXQ0IGNvbG9yKSAKewoJY29sb3IuciA9IHNyY190Zl9TdGFnZTAoaGFsZihjb2xvci5yKSk7Cgljb2xvci5nID0gc3JjX3RmX1N0YWdlMChoYWxmKGNvbG9yLmcpKTsKCWNvbG9yLmIgPSBzcmNfdGZfU3RhZ2UwKGhhbGYoY29sb3IuYikpOwoJY29sb3IgPSBnYW11dF94Zm9ybV9TdGFnZTAoaGFsZjQoY29sb3IpKTsKCWNvbG9yLnIgPSBkc3RfdGZfU3RhZ2UwKGhhbGYoY29sb3IucikpOwoJY29sb3IuZyA9IGRzdF90Zl9TdGFnZTAoaGFsZihjb2xvci5nKSk7Cgljb2xvci5iID0gZHN0X3RmX1N0YWdlMChoYWxmKGNvbG9yLmIpKTsKCXJldHVybiBoYWxmNChjb2xvcik7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKGNvbG9yX3hmb3JtX1N0YWdlMChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB0ZXhDb29yZCkpICogb3V0cHV0Q29sb3JfU3RhZ2UwKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAACAYAAAEAYAAABGAABAAOAAEIA7777777777776FAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1NnAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBjb2xvciA9IGluQ29sb3I7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAVQEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgABAAAAAAAAAA==","CAZACAECBMAAAAIAAAAAAAAAAAAAAAAACMAACAA4AAIQB777777RQADSAAAAAAAAEAACOAAEAAAAAAAAAAAAAAAAAAYAAGYAAQAAAAANAAAAAAAAAAAEAAACAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1N6AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFZlcnRpY2VzR1AKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCWNvbG9yID0gY29sb3IuYmdyYTsKCWNvbG9yID0gY29sb3I7Cgljb2xvciA9IGhhbGY0KGNvbG9yLnJnYiAqIGNvbG9yLmEsIGNvbG9yLmEpOwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMF9wb3NpdGlvbiA9IHBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX3Bvc2l0aW9uLnggLCBfdG1wXzBfcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAhBAAAdW5pZm9ybSBoYWxmNCB1Y29sb3JfU3RhZ2UxX2MwOwppbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDb25zdENvbG9yUHJvY2Vzc29yX1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHVjb2xvcl9TdGFnZTFfYzA7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBCbHVycmVkRWRnZUZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiBpbnB1dEFscGhhID0gX2lucHV0Lnc7CgloYWxmIGZhY3RvciA9IDEuMCAtIGlucHV0QWxwaGE7CglAc3dpdGNoICgwKSAKCXsKCQljYXNlIDA6ICAgICAgICBmYWN0b3IgPSBleHAoKC1mYWN0b3IgKiBmYWN0b3IpICogNC4wKSAtIDAuMDE3OTk5OTk5MjI1MTM5NjE4OwoJCWJyZWFrOwoJCWNhc2UgMTogICAgICAgIGZhY3RvciA9IHNtb290aHN0ZXAoMS4wLCAwLjAsIGZhY3Rvcik7CgkJYnJlYWs7Cgl9Cglfb3V0cHV0ID0gaGFsZjQoZmFjdG9yKTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVmVydGljZXNHUAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKFNrTW9kZSBiZWhhdmlvcikKCQlvdXRwdXRfU3RhZ2UxID0gYmxlbmRfbW9kdWxhdGUoQ29uc3RDb2xvclByb2Nlc3Nvcl9TdGFnZTFfYzAoaGFsZjQoMSkpLCBCbHVycmVkRWRnZUZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMShvdXRwdXRDb2xvcl9TdGFnZTApKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24HAAAAaW5Db2xvcgABAAAAAAAAAA==","CAZAAAICBIAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEANwQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjdWxhclJSZWN0CgkJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCQlmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxLlJCOwoJCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTEueCAtIGxlbmd0aChkeHkpKSk7CgkJb3V0cHV0X1N0YWdlMSA9IG91dHB1dENvdmVyYWdlX1N0YWdlMCAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAMCBAAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAA2AAAAAAAAAAAAAAABAAAAAKAAC4AAIAAAAAAAAAAAAIAAAABYABMAA":"AgAAAExTS1NGAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAQEAAAAAAAABAQCCBQAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKS5ycnJyOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","CAZAAAMCBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHWAAAAAYAABQAAQAAAADZAAAAA6YAAAAEAAAGAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBALoEAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7Cglfb3V0cHV0ID0gX2lucHV0ICogYWxwaGE7CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKSAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","CAZACAACB4AAAAAAAAAGOAIAAAJQAAIACIAAAAA4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAACAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAQAAAALQAEAAAEAAAAAAAAAAAAKAAAABWAAWAA":"AgAAAExTS1McAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0IHZjb3ZlcmFnZV9TdGFnZTA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBwb3NpdGlvbiA9IHBvc2l0aW9uLnh5OwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJdmNvdmVyYWdlX1N0YWdlMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBsb2NhbENvb3JkLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAvQcAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gKGhhbGY0KDEuMCkgLSBvdXRwdXRfU3RhZ2UxKSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAECBMAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAEAAAIIDAAWAATYABAAAAAABAAAQAABBAMADYAB4AACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1NdAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTE7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMSkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAACcEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1N0YWdlMV9jMC54LCB1Y2xhbXBfU3RhZ2UxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfU3RhZ2UxX2MwLnksIHVjbGFtcF9TdGFnZTFfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJX291dHB1dCA9IHRleHR1cmVDb2xvcjsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE1hdHJpeEVmZmVjdAoJCW91dHB1dF9TdGFnZTEgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAICBIAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAAAQAAAAGQAB6AAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAcQQAAHVuaWZvcm0gZmxvYXQ0IHVjaXJjbGVfU3RhZ2UxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJZmxvYXQyIHRleENvb3JkOwoJCXRleENvb3JkID0gdmxvY2FsQ29vcmRfU3RhZ2UwOwoJCW91dHB1dENvbG9yX1N0YWdlMCA9IChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB0ZXhDb29yZCkgKiBvdXRwdXRDb2xvcl9TdGFnZTApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjbGVFZmZlY3QKCQlmbG9hdDIgcHJldkNlbnRlcjsKCQlmbG9hdCBwcmV2UmFkaXVzID0gLTEuMDAwMDAwOwoJCWhhbGYgZDsKCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TdGFnZTEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TdGFnZTEudykgLSAxLjApICogdWNpcmNsZV9TdGFnZTEueik7CgkJfQoJCWVsc2UgCgkJewoJCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1N0YWdlMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1N0YWdlMS53KSkgKiB1Y2lyY2xlX1N0YWdlMS56KTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlAaWYgKDEgPT0gMSB8fCAxID09IDMpIAoJCXsKCQkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBjbGFtcChkLCAwLjAsIDEuMCk7CgkJfQoJCWVsc2UgCgkJewoJCQlvdXRwdXRfU3RhZ2UxID0gZCA+IDAuNSA/IGlucHV0Q29sb3IgOiBoYWxmNCgwLjApOwoJCX0KCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAECAYAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAA2AAAAAAAAAAAAAAACAAAAAKAALAAA":"AgAAAExTS1NGAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAZAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKS5ycnJyOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAIIDAAWAATYABEAAAAAAAAAAAABBAMADYAB4AACQAAAABQAAAAAAAAAAAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAOxMAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgX2Nvb3Jkcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgR2F1c3NpYW5Db252b2x1dGlvbgoJCWZsb2F0MiBfY29vcmRzID0gdkxvY2FsQ29vcmRfU3RhZ2UwLnh5OwoJCW91dHB1dF9TdGFnZTEgPSBoYWxmNCgwLCAwLCAwLCAwKTsKCQlmbG9hdDIgY29vcmQgPSBfY29vcmRzIC0gMTIuMCAqIHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWZsb2F0MiBjb29yZFNhbXBsZWQgPSBoYWxmMigwLCAwKTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAECA4AAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAWwEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZACAACB4AAAAAAAAAGOAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAACAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAQAAAALQAEAAAEAAAAAAAAAAAAEAAAABWAAWAA":"AgAAAExTS1OvAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAYQcAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yCgkJaGFsZjQgY29uc3RDb2xvcjsKCQlAaWYgKGZhbHNlKSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgwKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCk7CgkJfQoJCW91dHB1dF9TdGFnZTEgPSBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGNvbnN0Q29sb3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAECA4AAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAGABAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECA4AAAAAAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAEwCAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA=="}} \ No newline at end of file diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 5dac30365..61628d128 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,6 +1,6 @@ Thanks for using Aves! -v1.3.2: -- multi-page TIFF support -- cropped panorama support -- album grouping options +v1.3.3: +- multi-track HEIF support +- image export (including embedded and multi-page images) +- listen to Media Store changes Full changelog available on Github \ No newline at end of file