diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 02c18dd4d..40d2592d7 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -17,8 +17,8 @@ jobs: # Available versions may lag behind https://github.com/flutter/flutter.git - uses: subosito/flutter-action@v2 with: - flutter-version: '3.0.2' - channel: 'stable' + flutter-version: '3.3.0-0.0.pre' + channel: 'beta' - name: Clone the repository. uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 273a45cba..890051962 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,8 +19,8 @@ jobs: # Available versions may lag behind https://github.com/flutter/flutter.git - uses: subosito/flutter-action@v2 with: - flutter-version: '3.0.2' - channel: 'stable' + flutter-version: '3.3.0-0.0.pre' + channel: 'beta' # Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1): # https://issuetracker.google.com/issues/144111441 @@ -56,15 +56,15 @@ jobs: rm release.keystore.asc mkdir outputs (cd scripts/; ./apply_flavor_play.sh) - flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.0.2.sksl.json + flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.0-0.0.pre.sksl.json cp build/app/outputs/bundle/playRelease/*.aab outputs - flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.0.2.sksl.json + flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.0-0.0.pre.sksl.json cp build/app/outputs/apk/play/release/*.apk outputs (cd scripts/; ./apply_flavor_huawei.sh) - flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.0.2.sksl.json + flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.3.0-0.0.pre.sksl.json cp build/app/outputs/apk/huawei/release/*.apk outputs (cd scripts/; ./apply_flavor_izzy.sh) - flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.0.2.sksl.json + flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.3.0-0.0.pre.sksl.json cp build/app/outputs/apk/izzy/release/*.apk outputs rm $AVES_STORE_FILE env: diff --git a/CHANGELOG.md b/CHANGELOG.md index ac8faa429..7a1ce5e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.6.10] - 2022-07-24 + +### Added + +- Search: `on this day` and month filters in date filter section +- Stats: histogram and contextual date filters +- Screen saver +- Widget: photo frame + +### Changed + +- viewer: black background when overlay is disabled with light theme +- filter chip long press menu shows full label +- upgraded Flutter to beta v3.3.0-0.0.pre + +### Fixed + +- analysis service stuck when storage has ambiguous directories + ## [v1.6.9] - 2022-06-18 ### Added diff --git a/README.md b/README.md index 9af7d5a6a..4a6d48055 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ It scans your media collection to identify **motion photos**, **panoramas** (aka **Navigation and search** is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc. -Aves integrates with Android (from **API 19 to 32**, i.e. from KitKat to Android 12L) with features such as **app shortcuts** and **global search** handling. It also works as a **media viewer and picker**. +Aves integrates with Android (from **API 19 to 33**, i.e. from KitKat to Android 13) with features such as **widgets**, **app shortcuts**, **screen saver** and **global search** handling. It also works as a **media viewer and picker**. ## Screenshots @@ -101,7 +101,7 @@ Some users have expressed the wish to financially support the project. Thanks! [Donate with PayPal](https://paypal.me/ThibaultDeckers) + height="40">](https://paypal.me/ThibaultDeckersFr) [Donate using Liberapay](https://liberapay.com/deckerst/donate) diff --git a/android/app/agconnect-services.json b/android/app/agconnect-services.json index 876ecb775..d406d5ce2 100644 --- a/android/app/agconnect-services.json +++ b/android/app/agconnect-services.json @@ -5,18 +5,25 @@ "DE":"connect-dre.dbankcloud.cn", "DE_back":"connect-dre.hispace.hicloud.com", "RU":"connect-drru.hispace.dbankcloud.ru", - "RU_back":"connect-drru.hispace.dbankcloud.ru", + "RU_back":"connect-drru.hispace.dbankcloud.cn", "SG":"connect-dra.dbankcloud.cn", "SG_back":"connect-dra.hispace.hicloud.com" }, + "websocketgw_all":{ + "CN":"connect-ws-drcn.hispace.dbankcloud.cn", + "CN_back":"connect-ws-drcn.hispace.dbankcloud.com", + "DE":"connect-ws-dre.hispace.dbankcloud.cn", + "DE_back":"connect-ws-dre.hispace.dbankcloud.com", + "RU":"connect-ws-drru.hispace.dbankcloud.ru", + "RU_back":"connect-ws-drru.hispace.dbankcloud.cn", + "SG":"connect-ws-dra.hispace.dbankcloud.cn", + "SG_back":"connect-ws-dra.hispace.dbankcloud.com" + }, "client":{ "cp_id":"2640082000020010713", "product_id":"99536292102197525", - "client_id":"874325707927340288", - "client_secret":"DCAFAE5C0440ABDBD6DDB2B6EBD7D9B0870C10FCA64759CCD63020D168803AB5", "project_id":"99536292102197525", "app_id":"106014023", - "api_key":"DAEDAEzScQA5ri36P2NEiVPSFrOJeYZ0DbEJZMGJrBadW+QudBr5BGHD3vO0tsL1VeBy0RPZefPic3hAWUijcBxCv0zRv0iBjQEptQ==", "package_name":"deckers.thibault.aves" }, "oauth_client":{ @@ -30,17 +37,17 @@ "configuration_version":"3.0", "appInfos":[ { - "package_name":"deckers.thibault.aves.profile", + "package_name":"deckers.thibault.aves", "client":{ - "app_id":"106031461" + "app_id":"106014023" }, "app_info":{ - "package_name":"deckers.thibault.aves.profile", - "app_id":"106031461" + "package_name":"deckers.thibault.aves", + "app_id":"106014023" }, "oauth_client":{ "client_type":1, - "client_id":"106031461" + "client_id":"106014023" } }, { @@ -58,17 +65,17 @@ } }, { - "package_name":"deckers.thibault.aves", + "package_name":"deckers.thibault.aves.profile", "client":{ - "app_id":"106014023" + "app_id":"106031461" }, "app_info":{ - "package_name":"deckers.thibault.aves", - "app_id":"106014023" + "package_name":"deckers.thibault.aves.profile", + "app_id":"106031461" }, "oauth_client":{ "client_type":1, - "client_id":"106014023" + "client_id":"106031461" } } ] diff --git a/android/app/build.gradle b/android/app/build.gradle index 78f84d8c2..aebce2c0e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -38,6 +38,7 @@ if (keystorePropertiesFile.exists()) { keystoreProperties['keyAlias'] = System.getenv('AVES_KEY_ALIAS') keystoreProperties['keyPassword'] = System.getenv('AVES_KEY_PASSWORD') keystoreProperties['googleApiKey'] = System.getenv('AVES_GOOGLE_API_KEY') + keystoreProperties['huaweiApiKey'] = System.getenv('AVES_HUAWEI_API_KEY') } android { @@ -47,7 +48,6 @@ android { main.java.srcDirs += 'src/main/kotlin' } - defaultConfig { applicationId appId // minSdkVersion constraints: @@ -60,7 +60,8 @@ android { targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName - manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey']] + manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey'], + huaweiApiKey: keystoreProperties['huaweiApiKey']] multiDexEnabled true resValue 'string', 'search_provider', "${appId}.search_provider" } @@ -140,7 +141,6 @@ android { lint { disable 'InvalidPackage' } - namespace 'deckers.thibault.aves' } flutter { @@ -169,7 +169,7 @@ dependencies { // huawei flavor only huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.5.2.300' - kapt 'androidx.annotation:annotation:1.3.0' + kapt 'androidx.annotation:annotation:1.4.0' kapt 'com.github.bumptech.glide:compiler:4.13.0' compileOnly rootProject.findProject(':streams_channel') diff --git a/android/app/src/debug/res/xml/screen_saver.xml b/android/app/src/debug/res/xml/screen_saver.xml new file mode 100644 index 000000000..7b51a7bea --- /dev/null +++ b/android/app/src/debug/res/xml/screen_saver.xml @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 68c90244b..8e1be5b28 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,14 @@ + + + + diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt index aa84e0c63..3d98d201d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt @@ -11,10 +11,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import app.loup.streams_channel.StreamsChannel import deckers.thibault.aves.MainActivity.Companion.OPEN_FROM_ANALYSIS_SERVICE -import deckers.thibault.aves.channel.calls.DeviceHandler -import deckers.thibault.aves.channel.calls.GeocodingHandler -import deckers.thibault.aves.channel.calls.MediaStoreHandler -import deckers.thibault.aves.channel.calls.MetadataFetchHandler +import deckers.thibault.aves.channel.calls.* import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler import deckers.thibault.aves.utils.FlutterUtils @@ -25,7 +22,7 @@ import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.runBlocking class AnalysisService : MethodChannel.MethodCallHandler, Service() { - private var backgroundFlutterEngine: FlutterEngine? = null + private var flutterEngine: FlutterEngine? = null private var backgroundChannel: MethodChannel? = null private var serviceLooper: Looper? = null private var serviceHandler: ServiceHandler? = null @@ -37,18 +34,27 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() { runBlocking { FlutterUtils.initFlutterEngine(context, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) { - backgroundFlutterEngine = it + flutterEngine = it } } - val messenger = backgroundFlutterEngine!!.dartExecutor.binaryMessenger + val messenger = flutterEngine!!.dartExecutor + // channels for analysis + + // dart -> platform -> dart + // - need Context MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) + MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) + + // result streaming: dart -> platform ->->-> dart + // - need Context StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) } StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) } + // channel for service management backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply { setMethodCallHandler(context) @@ -67,7 +73,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() { override fun onBind(intent: Intent) = analysisServiceBinder - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val channel = NotificationChannelCompat.Builder(CHANNEL_ANALYSIS, NotificationManagerCompat.IMPORTANCE_LOW) .setName(getText(R.string.analysis_channel_name)) .setShowBadge(false) @@ -76,7 +82,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() { startForeground(NOTIFICATION_ID, buildNotification()) val msgData = Bundle() - intent.extras?.let { + intent?.extras?.let { msgData.putAll(it) } serviceHandler?.obtainMessage()?.let { msg -> diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetConfigureActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetConfigureActivity.kt new file mode 100644 index 000000000..70c55dbaf --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetConfigureActivity.kt @@ -0,0 +1,61 @@ +package deckers.thibault.aves + +import android.appwidget.AppWidgetManager +import android.content.Intent +import android.os.Bundle +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +class HomeWidgetSettingsActivity : MainActivity() { + private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // cancel if user does not complete widget setup + setResult(RESULT_CANCELED) + + intent.extras?.let { + appWidgetId = it.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + intentDataMap = extractIntentData(intent) + } + + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish() + return + } + + val messenger = flutterEngine!!.dartExecutor + MethodChannel(messenger, CHANNEL).setMethodCallHandler { call: MethodCall, result: MethodChannel.Result -> + when (call.method) { + "configure" -> { + result.success(null) + saveWidget() + } + else -> result.notImplemented() + } + } + } + + private fun saveWidget() { + val appWidgetManager = AppWidgetManager.getInstance(context) + val widgetInfo = appWidgetManager.getAppWidgetOptions(appWidgetId) + HomeWidgetProvider().onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, widgetInfo) + + val intent = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + setResult(RESULT_OK, intent) + finish() + } + + override fun extractIntentData(intent: Intent?): MutableMap { + return hashMapOf( + INTENT_DATA_KEY_ACTION to INTENT_ACTION_WIDGET_SETTINGS, + INTENT_DATA_KEY_WIDGET_ID to appWidgetId, + ) + } + + companion object { + private const val CHANNEL = "deckers.thibault/aves/widget_configure" + } +} + diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt new file mode 100644 index 000000000..eab4536ff --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt @@ -0,0 +1,202 @@ +package deckers.thibault.aves + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.widget.RemoteViews +import app.loup.streams_channel.StreamsChannel +import deckers.thibault.aves.channel.calls.DeviceHandler +import deckers.thibault.aves.channel.calls.MediaFetchHandler +import deckers.thibault.aves.channel.calls.MediaStoreHandler +import deckers.thibault.aves.channel.streams.ImageByteStreamHandler +import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler +import deckers.thibault.aves.utils.FlutterUtils +import deckers.thibault.aves.utils.LogUtils +import io.flutter.FlutterInjector +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.dart.DartExecutor +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.* +import java.nio.ByteBuffer +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import kotlin.math.roundToInt + +class HomeWidgetProvider : AppWidgetProvider() { + private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + Log.d(LOG_TAG, "Widget onUpdate widgetIds=${appWidgetIds.contentToString()}") + for (widgetId in appWidgetIds) { + val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId) + + defaultScope.launch { + val backgroundBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = false) + updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, backgroundBytes) + + val imageBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false) + updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageBytes) + } + } + } + + override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager?, widgetId: Int, widgetInfo: Bundle?) { + Log.d(LOG_TAG, "Widget onAppWidgetOptionsChanged widgetId=$widgetId") + appWidgetManager ?: return + widgetInfo ?: return + + if (imageByteFetchJob != null) { + imageByteFetchJob?.cancel() + } + imageByteFetchJob = defaultScope.launch { + delay(500) + val imageBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true) + updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageBytes) + } + } + + private suspend fun getBytes( + context: Context, + widgetId: Int, + widgetInfo: Bundle, + drawEntryImage: Boolean, + reuseEntry: Boolean = false, + ): ByteArray? { + val devicePixelRatio = context.resources.displayMetrics.density + val widthPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) * devicePixelRatio).roundToInt() + val heightPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) * devicePixelRatio).roundToInt() + if (widthPx == 0 || heightPx == 0) return null + + initFlutterEngine(context) + val messenger = flutterEngine!!.dartExecutor + val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL) + try { + val bytes = suspendCoroutine { cont -> + defaultScope.launch { + FlutterUtils.runOnUiThread { + channel.invokeMethod("drawWidget", hashMapOf( + "widgetId" to widgetId, + "widthPx" to widthPx, + "heightPx" to heightPx, + "devicePixelRatio" to devicePixelRatio, + "drawEntryImage" to drawEntryImage, + "reuseEntry" to reuseEntry, + ), object : MethodChannel.Result { + override fun success(result: Any?) { + cont.resume(result) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + cont.resumeWithException(Exception("$errorCode: $errorMessage\n$errorDetails")) + } + + override fun notImplemented() { + cont.resumeWithException(Exception("not implemented")) + } + }) + } + } + } + if (bytes is ByteArray) return bytes + } catch (e: Exception) { + Log.e(LOG_TAG, "failed to draw widget for widgetId=$widgetId widthPx=$widthPx heightPx=$heightPx", e) + } + return null + } + + private fun updateWidgetImage( + context: Context, + appWidgetManager: AppWidgetManager, + widgetId: Int, + widgetInfo: Bundle, + bytes: ByteArray?, + ) { + bytes ?: return + + val devicePixelRatio = context.resources.displayMetrics.density + val widthPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) * devicePixelRatio).roundToInt() + val heightPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) * devicePixelRatio).roundToInt() + + try { + val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888) + bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes)) + + // set a unique URI to prevent the intent (and its extras) from being shared by different widgets + val intent = Intent(MainActivity.INTENT_ACTION_WIDGET_OPEN, Uri.parse("widget://$widgetId"), context, MainActivity::class.java) + .putExtra(MainActivity.EXTRA_KEY_WIDGET_ID, widgetId) + + val activity = PendingIntent.getActivity( + context, + 0, + intent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + ) + + val views = RemoteViews(context.packageName, R.layout.app_widget).apply { + setImageViewBitmap(R.id.widget_img, bitmap) + setOnClickPendingIntent(R.id.widget_img, activity) + } + + appWidgetManager.updateAppWidget(widgetId, views) + bitmap.recycle() + } catch (e: Exception) { + Log.e(LOG_TAG, "failed to draw widget", e) + } + } + + companion object { + private val LOG_TAG = LogUtils.createTag() + private const val WIDGET_DART_ENTRYPOINT = "widgetMain" + private const val WIDGET_DRAW_CHANNEL = "deckers.thibault/aves/widget_draw" + + private var flutterEngine: FlutterEngine? = null + private var imageByteFetchJob: Job? = null + + private suspend fun initFlutterEngine(context: Context) { + if (flutterEngine != null) return + + FlutterUtils.runOnUiThread { + flutterEngine = FlutterEngine(context.applicationContext) + } + initChannels(context) + + flutterEngine!!.apply { + if (!dartExecutor.isExecutingDart) { + val appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath() + val entrypoint = DartExecutor.DartEntrypoint(appBundlePathOverride, WIDGET_DART_ENTRYPOINT) + FlutterUtils.runOnUiThread { + dartExecutor.executeDartEntrypoint(entrypoint) + } + } + } + } + + private fun initChannels(context: Context) { + val messenger = flutterEngine!!.dartExecutor + + // dart -> platform -> dart + // - need Context + MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context)) + MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context)) + MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(context)) + + // result streaming: dart -> platform ->->-> dart + // - need Context + StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(context, args) } + StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(context, args) } + } + + } +} 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 f7f1a5b4b..81d468d32 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -2,10 +2,14 @@ package deckers.thibault.aves import android.annotation.SuppressLint import android.app.SearchManager +import android.appwidget.AppWidgetManager import android.content.ClipData import android.content.Intent import android.net.Uri -import android.os.* +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.util.Log import androidx.annotation.RequiresApi import androidx.core.content.pm.ShortcutInfoCompat @@ -13,6 +17,8 @@ import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat import app.loup.streams_channel.StreamsChannel import deckers.thibault.aves.channel.calls.* +import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler +import deckers.thibault.aves.channel.calls.window.WindowHandler import deckers.thibault.aves.channel.streams.* import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.getParcelableExtraCompat @@ -23,12 +29,12 @@ import io.flutter.plugin.common.MethodChannel import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap -class MainActivity : FlutterActivity() { +open class MainActivity : FlutterActivity() { private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler private lateinit var intentStreamHandler: IntentStreamHandler private lateinit var analysisStreamHandler: AnalysisStreamHandler - private lateinit var intentDataMap: MutableMap + internal lateinit var intentDataMap: MutableMap private lateinit var analysisHandler: AnalysisHandler override fun onCreate(savedInstanceState: Bundle?) { @@ -51,7 +57,7 @@ class MainActivity : FlutterActivity() { // ) super.onCreate(savedInstanceState) - val messenger = flutterEngine!!.dartExecutor.binaryMessenger + val messenger = flutterEngine!!.dartExecutor // dart -> platform -> dart // - need Context @@ -63,14 +69,17 @@ class MainActivity : FlutterActivity() { MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this)) MethodChannel(messenger, GlobalSearchHandler.CHANNEL).setMethodCallHandler(GlobalSearchHandler(this)) + MethodChannel(messenger, HomeWidgetHandler.CHANNEL).setMethodCallHandler(HomeWidgetHandler(this)) + MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(this)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) - // - need Activity + // - need ContextWrapper MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) - MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this)) + MethodChannel(messenger, MediaEditHandler.CHANNEL).setMethodCallHandler(MediaEditHandler(this)) MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this)) - MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this)) + // - need Activity + MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this)) // result streaming: dart -> platform ->->-> dart // - need Context @@ -78,7 +87,7 @@ class MainActivity : FlutterActivity() { StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) } // - need Activity StreamsChannel(messenger, ImageOpStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageOpStreamHandler(this, args) } - StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory { args -> StorageAccessStreamHandler(this, args) } + StreamsChannel(messenger, ActivityResultStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ActivityResultStreamHandler(this, args) } // change monitoring: platform -> dart mediaStoreChangeStreamHandler = MediaStoreChangeStreamHandler(this).apply { @@ -93,15 +102,16 @@ class MainActivity : FlutterActivity() { intentStreamHandler = IntentStreamHandler().apply { EventChannel(messenger, IntentStreamHandler.CHANNEL).setStreamHandler(this) } - // detail fetch: dart -> platform + // intent detail & result: dart -> platform intentDataMap = extractIntentData(intent) - MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result -> + MethodChannel(messenger, INTENT_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "getIntentData" -> { result.success(intentDataMap) intentDataMap.clear() } - "pick" -> pick(call) + "submitPickedItems" -> submitPickedItems(call) + "submitPickedCollectionFilters" -> submitPickedCollectionFilters(call) } } @@ -156,27 +166,33 @@ class MainActivity : FlutterActivity() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { - DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(data, resultCode, requestCode) + DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(requestCode, resultCode, data) DELETE_SINGLE_PERMISSION_REQUEST, MEDIA_WRITE_BULK_PERMISSION_REQUEST -> onScopedStoragePermissionResult(resultCode) CREATE_FILE_REQUEST, OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data) + PICK_COLLECTION_FILTERS_REQUEST -> onCollectionFiltersPickResult(resultCode, data) } } - @SuppressLint("WrongConstant", "ObsoleteSdkInt") - private fun onDocumentTreeAccessResult(data: Intent?, resultCode: Int, requestCode: Int) { - val treeUri = data?.data + private fun onCollectionFiltersPickResult(resultCode: Int, intent: Intent?) { + val filters = if (resultCode == RESULT_OK) extractFiltersFromIntent(intent) else null + pendingCollectionFilterPickHandler?.let { it(filters) } + } + + private fun onDocumentTreeAccessResult(requestCode: Int, resultCode: Int, intent: Intent?) { + val treeUri = intent?.data if (resultCode != RESULT_OK || treeUri == null) { onStorageAccessResult(requestCode, null) return } + @SuppressLint("WrongConstant", "ObsoleteSdkInt") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - val canPersist = (data.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0 + val canPersist = (intent.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0 if (canPersist) { // save access permissions across reboots - val takeFlags = (data.flags + val takeFlags = (intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) try { @@ -197,18 +213,11 @@ class MainActivity : FlutterActivity() { } } - private fun extractIntentData(intent: Intent?): MutableMap { - when (intent?.action) { + open fun extractIntentData(intent: Intent?): MutableMap { + when (val action = intent?.action) { Intent.ACTION_MAIN -> { - intent.getStringExtra(SHORTCUT_KEY_PAGE)?.let { page -> - var filters = intent.getStringArrayExtra(SHORTCUT_KEY_FILTERS_ARRAY)?.toList() - if (filters == null) { - // fallback for shortcuts created on API < 26 - val filterString = intent.getStringExtra(SHORTCUT_KEY_FILTERS_STRING) - if (filterString != null) { - filters = filterString.split(EXTRA_STRING_ARRAY_SEPARATOR) - } - } + intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page -> + val filters = extractFiltersFromIntent(intent) return hashMapOf( INTENT_DATA_KEY_PAGE to page, INTENT_DATA_KEY_FILTERS to filters, @@ -228,7 +237,7 @@ class MainActivity : FlutterActivity() { } Intent.ACTION_GET_CONTENT, Intent.ACTION_PICK -> { return hashMapOf( - INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK, + INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK_ITEMS, INTENT_DATA_KEY_MIME_TYPE to intent.type, INTENT_DATA_KEY_ALLOW_MULTIPLE to (intent.extras?.getBoolean(Intent.EXTRA_ALLOW_MULTIPLE) ?: false), ) @@ -244,6 +253,22 @@ class MainActivity : FlutterActivity() { INTENT_DATA_KEY_QUERY to intent.getStringExtra(SearchManager.QUERY), ) } + INTENT_ACTION_PICK_COLLECTION_FILTERS -> { + val initialFilters = extractFiltersFromIntent(intent) + return hashMapOf( + INTENT_DATA_KEY_ACTION to action, + INTENT_DATA_KEY_FILTERS to initialFilters, + ) + } + INTENT_ACTION_WIDGET_OPEN -> { + val widgetId = intent.getIntExtra(EXTRA_KEY_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + if (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + return hashMapOf( + INTENT_DATA_KEY_ACTION to action, + INTENT_DATA_KEY_WIDGET_ID to widgetId, + ) + } + } Intent.ACTION_RUN -> { // flutter run } @@ -254,7 +279,22 @@ class MainActivity : FlutterActivity() { return HashMap() } - private fun pick(call: MethodCall) { + private fun extractFiltersFromIntent(intent: Intent?): List? { + intent ?: return null + + val filters = intent.getStringArrayExtra(EXTRA_KEY_FILTERS_ARRAY)?.toList() + if (filters != null) return filters + + // fallback for shortcuts created on API < 26 + val filterString = intent.getStringExtra(EXTRA_KEY_FILTERS_STRING) + if (filterString != null) { + return filterString.split(EXTRA_STRING_ARRAY_SEPARATOR) + } + + return null + } + + private fun submitPickedItems(call: MethodCall) { val pickedUris = call.argument>("uris") if (pickedUris != null && pickedUris.isNotEmpty()) { val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(context, Uri.parse(uriString)) } @@ -278,6 +318,19 @@ class MainActivity : FlutterActivity() { finish() } + private fun submitPickedCollectionFilters(call: MethodCall) { + val filters = call.argument>("filters") + if (filters != null) { + val intent = Intent() + .putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray()) + .putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR)) + 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 @@ -291,7 +344,7 @@ class MainActivity : FlutterActivity() { .setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_search else R.drawable.ic_shortcut_search)) .setIntent( Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) - .putExtra(SHORTCUT_KEY_PAGE, "/search") + .putExtra(EXTRA_KEY_PAGE, "/search") ) .build() @@ -300,7 +353,7 @@ class MainActivity : FlutterActivity() { .setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_movie else R.drawable.ic_shortcut_movie)) .setIntent( Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) - .putExtra(SHORTCUT_KEY_PAGE, "/collection") + .putExtra(EXTRA_KEY_PAGE, "/collection") .putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}")) ) .build() @@ -314,7 +367,7 @@ class MainActivity : FlutterActivity() { companion object { private val LOG_TAG = LogUtils.createTag() - const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer" + const val INTENT_CHANNEL = "deckers.thibault/aves/intent" const val EXTRA_STRING_ARRAY_SEPARATOR = "###" const val DOCUMENT_TREE_ACCESS_REQUEST = 1 const val OPEN_FROM_ANALYSIS_SERVICE = 2 @@ -322,29 +375,39 @@ class MainActivity : FlutterActivity() { const val OPEN_FILE_REQUEST = 4 const val DELETE_SINGLE_PERMISSION_REQUEST = 5 const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6 + const val PICK_COLLECTION_FILTERS_REQUEST = 7 - const val INTENT_DATA_KEY_ACTION = "action" - const val INTENT_DATA_KEY_FILTERS = "filters" - const val INTENT_DATA_KEY_MIME_TYPE = "mimeType" - const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple" - const val INTENT_DATA_KEY_PAGE = "page" - const val INTENT_DATA_KEY_URI = "uri" - const val INTENT_DATA_KEY_QUERY = "query" - - const val INTENT_ACTION_PICK = "pick" + const val INTENT_ACTION_PICK_ITEMS = "pick_items" + const val INTENT_ACTION_PICK_COLLECTION_FILTERS = "pick_collection_filters" + const val INTENT_ACTION_SCREEN_SAVER = "screen_saver" + const val INTENT_ACTION_SCREEN_SAVER_SETTINGS = "screen_saver_settings" const val INTENT_ACTION_SEARCH = "search" const val INTENT_ACTION_SET_WALLPAPER = "set_wallpaper" const val INTENT_ACTION_VIEW = "view" + const val INTENT_ACTION_WIDGET_OPEN = "widget_open" + const val INTENT_ACTION_WIDGET_SETTINGS = "widget_settings" - const val SHORTCUT_KEY_PAGE = "page" - const val SHORTCUT_KEY_FILTERS_ARRAY = "filters" - const val SHORTCUT_KEY_FILTERS_STRING = "filtersString" + const val INTENT_DATA_KEY_ACTION = "action" + const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple" + const val INTENT_DATA_KEY_FILTERS = "filters" + const val INTENT_DATA_KEY_MIME_TYPE = "mimeType" + const val INTENT_DATA_KEY_PAGE = "page" + const val INTENT_DATA_KEY_QUERY = "query" + const val INTENT_DATA_KEY_URI = "uri" + const val INTENT_DATA_KEY_WIDGET_ID = "widgetId" + + const val EXTRA_KEY_PAGE = "page" + const val EXTRA_KEY_FILTERS_ARRAY = "filters" + const val EXTRA_KEY_FILTERS_STRING = "filtersString" + const val EXTRA_KEY_WIDGET_ID = "widgetId" // request code to pending runnable val pendingStorageAccessResultHandlers = ConcurrentHashMap() var pendingScopedStoragePermissionCompleter: CompletableFuture? = null + var pendingCollectionFilterPickHandler: ((filters: List?) -> Unit)? = null + private fun onStorageAccessResult(requestCode: Int, uri: Uri?) { Log.i(LOG_TAG, "onStorageAccessResult with requestCode=$requestCode, uri=$uri") val handler = pendingStorageAccessResultHandlers.remove(requestCode) ?: return diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverService.kt b/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverService.kt new file mode 100644 index 000000000..b6373d3ae --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverService.kt @@ -0,0 +1,136 @@ +package deckers.thibault.aves + +import android.service.dreams.DreamService +import android.util.Log +import android.view.View +import app.loup.streams_channel.StreamsChannel +import deckers.thibault.aves.channel.calls.* +import deckers.thibault.aves.channel.calls.window.ServiceWindowHandler +import deckers.thibault.aves.channel.calls.window.WindowHandler +import deckers.thibault.aves.channel.streams.ImageByteStreamHandler +import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler +import deckers.thibault.aves.utils.LogUtils +import io.flutter.FlutterInjector +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.android.FlutterSurfaceView +import io.flutter.embedding.android.FlutterView +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint +import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister +import io.flutter.plugin.common.MethodChannel + +// for FlutterView-level integration, cf https://docs.flutter.dev/development/add-to-app/android/add-flutter-view +class ScreenSaverService : DreamService() { + private var flutterEngine: FlutterEngine? = null + private var flutterView: FlutterView? = null + + override fun onAttachedToWindow() { + Log.i(LOG_TAG, "onAttachedToWindow") + super.onAttachedToWindow() + initDream() + createEngine() + setContentView(createView()) + } + + override fun onDreamingStarted() { + Log.i(LOG_TAG, "onDreamingStarted") + super.onDreamingStarted() + onStart() + } + + override fun onDreamingStopped() { + Log.i(LOG_TAG, "onDreamingStopped") + release() + super.onDreamingStopped() + } + + override fun onDetachedFromWindow() { + Log.i(LOG_TAG, "onDetachedFromWindow") + destroyView() + super.onDetachedFromWindow() + } + + private fun initDream() { + isInteractive = false + isFullscreen = true + } + + private fun createEngine() { + flutterEngine = flutterEngine ?: FlutterEngine(this, null, false) + GeneratedPluginRegister.registerGeneratedPlugins(flutterEngine!!) + initChannels() + } + + private fun createView(): View { + flutterView = FlutterView(this, FlutterSurfaceView(this)).apply { + id = FlutterActivity.FLUTTER_VIEW_ID + attachToFlutterEngine(flutterEngine!!) + } + return flutterView!! + } + + private fun destroyView() { + flutterEngine?.lifecycleChannel?.appIsDetached() + flutterView?.detachFromFlutterEngine() + } + + private fun release() { + destroyView() + flutterEngine = null + flutterView = null + } + + private fun onStart() { + flutterEngine!!.apply { + if (!dartExecutor.isExecutingDart) { + navigationChannel.setInitialRoute(DEFAULT_INITIAL_ROUTE) + val appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath() + val entrypoint = DartEntrypoint(appBundlePathOverride, DEFAULT_DART_ENTRYPOINT) + dartExecutor.executeDartEntrypoint(entrypoint) + } + lifecycleChannel.appIsResumed() + } + } + + private fun initChannels() { + val messenger = flutterEngine!!.dartExecutor + + // dart -> platform -> dart + // - need Context + MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) + MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) + MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(this)) + MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) + MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) + // - need ContextWrapper + MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) + // - need Service + MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ServiceWindowHandler(this)) + + // result streaming: dart -> platform ->->-> dart + // - need Context + StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) } + StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) } + + // intent handling + // detail fetch: dart -> platform + MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + "getIntentData" -> { + result.success(intentDataMap) + } + } + } + } + + companion object { + private val LOG_TAG = LogUtils.createTag() + private val intentDataMap: Map = hashMapOf( + MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SCREEN_SAVER, + ) + + // from `FlutterActivityLaunchConfigs` + const val DEFAULT_DART_ENTRYPOINT = "main" + const val DEFAULT_INITIAL_ROUTE = "/" + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverSettingsActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverSettingsActivity.kt new file mode 100644 index 000000000..dbc2e74f3 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/ScreenSaverSettingsActivity.kt @@ -0,0 +1,11 @@ +package deckers.thibault.aves + +import android.content.Intent + +class ScreenSaverSettingsActivity : MainActivity() { + override fun extractIntentData(intent: Intent?): MutableMap { + return hashMapOf( + INTENT_DATA_KEY_ACTION to INTENT_ACTION_SCREEN_SAVER_SETTINGS, + ) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt index 8d4472dbe..59920a934 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt @@ -23,7 +23,7 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvider() { +class SearchSuggestionsProvider : ContentProvider() { private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? { @@ -67,15 +67,23 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid } private suspend fun getSuggestions(context: Context, query: String): List { - if (backgroundFlutterEngine == null) { + if (flutterEngine == null) { FlutterUtils.initFlutterEngine(context, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) { - backgroundFlutterEngine = it + flutterEngine = it } } - val messenger = backgroundFlutterEngine!!.dartExecutor.binaryMessenger + val messenger = flutterEngine!!.dartExecutor val backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL) - backgroundChannel.setMethodCallHandler(this) + backgroundChannel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result -> + when (call.method) { + "initialized" -> { + Log.d(LOG_TAG, "background channel is ready") + result.success(null) + } + else -> result.notImplemented() + } + } try { return suspendCoroutine { cont -> @@ -96,7 +104,7 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid } override fun notImplemented() { - cont.resumeWithException(NotImplementedError("getSuggestions")) + cont.resumeWithException(Exception("not implemented")) } }) } @@ -108,16 +116,6 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid } } - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - "initialized" -> { - Log.d(LOG_TAG, "background channel is ready") - result.success(null) - } - else -> result.notImplemented() - } - } - override fun onCreate(): Boolean = true override fun getType(uri: Uri): String? = null @@ -137,6 +135,6 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid const val SHARED_PREFERENCES_KEY = "platform_search" const val CALLBACK_HANDLE_KEY = "callback_handle" - private var backgroundFlutterEngine: FlutterEngine? = null + private var flutterEngine: FlutterEngine? = null } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt index d7f28867e..4fe1f9a6a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt @@ -2,10 +2,15 @@ package deckers.thibault.aves import android.content.Intent import android.net.Uri -import android.os.* +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.util.Log import app.loup.streams_channel.StreamsChannel import deckers.thibault.aves.channel.calls.* +import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler +import deckers.thibault.aves.channel.calls.window.WindowHandler import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.getParcelableExtraCompat @@ -23,18 +28,19 @@ class WallpaperActivity : FlutterActivity() { super.onCreate(savedInstanceState) - val messenger = flutterEngine!!.dartExecutor.binaryMessenger + val messenger = flutterEngine!!.dartExecutor // dart -> platform -> dart // - need Context MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) + MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) - // - need Activity + // - need ContextWrapper MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) - MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this)) MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this)) - MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this)) + // - need Activity + MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this)) // result streaming: dart -> platform ->->-> dart // - need Context @@ -43,7 +49,7 @@ class WallpaperActivity : FlutterActivity() { // intent handling // detail fetch: dart -> platform intentDataMap = extractIntentData(intent) - MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result -> + MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "getIntentData" -> { result.success(intentDataMap) @@ -67,16 +73,6 @@ class WallpaperActivity : FlutterActivity() { } } - override fun onStop() { - Log.i(LOG_TAG, "onStop") - super.onStop() - } - - override fun onDestroy() { - Log.i(LOG_TAG, "onDestroy") - super.onDestroy() - } - private fun extractIntentData(intent: Intent?): MutableMap { when (intent?.action) { Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> { @@ -102,6 +98,5 @@ class WallpaperActivity : FlutterActivity() { companion object { private val LOG_TAG = LogUtils.createTag() - const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer" } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt index 6deead748..eaf3e0bfa 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt @@ -1,8 +1,8 @@ package deckers.thibault.aves.channel.calls import android.annotation.SuppressLint -import android.app.Activity import android.content.Context +import android.content.ContextWrapper import android.os.Build import android.provider.Settings import android.util.Log @@ -13,7 +13,7 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -class AccessibilityHandler(private val activity: Activity) : MethodCallHandler { +class AccessibilityHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved) @@ -28,7 +28,7 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler { @SuppressLint("ObsoleteSdkInt") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { try { - removed = Settings.Global.getFloat(activity.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f + removed = Settings.Global.getFloat(contextWrapper.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f } catch (e: Exception) { Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null) } @@ -49,7 +49,7 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler { val originalTimeoutMillis = call.argument("originalTimeoutMillis") val content = call.argument>("content") if (originalTimeoutMillis == null || content == null) { - result.error("getRecommendedTimeoutMillis-args", "failed because of missing arguments", null) + result.error("getRecommendedTimeoutMillis-args", "missing arguments", null) return } @@ -66,7 +66,7 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler { } } - val am = activity.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager + val am = contextWrapper.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager if (am == null) { result.error("getRecommendedTimeoutMillis-service", "failed to get accessibility manager", null) return diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt index 7f083ae55..75fa2a306 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt @@ -16,7 +16,10 @@ import deckers.thibault.aves.utils.ContextUtils.isMyServiceRunning import deckers.thibault.aves.utils.LogUtils import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch class AnalysisHandler(private val activity: Activity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler, AnalysisServiceListener { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) @@ -33,7 +36,7 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp private fun registerCallback(call: MethodCall, result: MethodChannel.Result) { val callbackHandle = call.argument("callbackHandle")?.toLong() if (callbackHandle == null) { - result.error("registerCallback-args", "failed because of missing arguments", null) + result.error("registerCallback-args", "missing arguments", null) return } @@ -47,7 +50,7 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp private fun startAnalysis(call: MethodCall, result: MethodChannel.Result) { val force = call.argument("force") if (force == null) { - result.error("startAnalysis-args", "failed because of missing arguments", null) + result.error("startAnalysis-args", "missing arguments", null) return } @@ -56,9 +59,9 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp if (!activity.isMyServiceRunning(AnalysisService::class.java)) { val intent = Intent(activity, AnalysisService::class.java) - intent.putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START) - intent.putExtra(AnalysisService.KEY_ENTRY_IDS, entryIds?.toIntArray()) - intent.putExtra(AnalysisService.KEY_FORCE, force) + .putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START) + .putExtra(AnalysisService.KEY_ENTRY_IDS, entryIds?.toIntArray()) + .putExtra(AnalysisService.KEY_FORCE, force) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { activity.startForegroundService(intent) } else { 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 c5b2d2223..5a9ea5ace 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 @@ -20,9 +20,9 @@ import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.request.RequestOptions import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity.Companion.EXTRA_STRING_ARRAY_SEPARATOR -import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_FILTERS_ARRAY -import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_FILTERS_STRING -import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_PAGE +import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY +import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING +import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE import deckers.thibault.aves.R import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend @@ -142,7 +142,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val packageName = call.argument("packageName") val sizeDip = call.argument("sizeDip")?.toDouble() if (packageName == null || sizeDip == null) { - result.error("getAppIcon-args", "failed because of missing arguments", null) + result.error("getAppIcon-args", "missing arguments", null) return } @@ -208,7 +208,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val label = call.argument("label") if (uri == null) { - result.error("copyToClipboard-args", "failed because of missing arguments", null) + result.error("copyToClipboard-args", "missing arguments", null) return } @@ -235,7 +235,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val mimeType = call.argument("mimeType") if (uri == null) { - result.error("edit-args", "failed because of missing arguments", null) + result.error("edit-args", "missing arguments", null) return } @@ -252,7 +252,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val mimeType = call.argument("mimeType") if (uri == null) { - result.error("open-args", "failed because of missing arguments", null) + result.error("open-args", "missing arguments", null) return } @@ -267,7 +267,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { private fun openMap(call: MethodCall, result: MethodChannel.Result) { val geoUri = call.argument("geoUri")?.let { Uri.parse(it) } if (geoUri == null) { - result.error("openMap-args", "failed because of missing arguments", null) + result.error("openMap-args", "missing arguments", null) return } @@ -282,7 +282,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val mimeType = call.argument("mimeType") if (uri == null) { - result.error("setAs-args", "failed because of missing arguments", null) + result.error("setAs-args", "missing arguments", null) return } @@ -298,7 +298,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val title = call.argument("title") val urisByMimeType = call.argument>>("urisByMimeType") if (urisByMimeType == null) { - result.error("setAs-args", "failed because of missing arguments", null) + result.error("setAs-args", "missing arguments", null) return } @@ -378,7 +378,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val filters = call.argument>("filters") val uri = call.argument("uri")?.let { Uri.parse(it) } if (label == null || (filters == null && uri == null)) { - result.error("pin-args", "failed because of missing arguments", null) + result.error("pin-args", "missing arguments", null) return } @@ -407,11 +407,11 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val intent = when { uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java) filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java) - .putExtra(SHORTCUT_KEY_PAGE, "/collection") - .putExtra(SHORTCUT_KEY_FILTERS_ARRAY, filters.toTypedArray()) + .putExtra(EXTRA_KEY_PAGE, "/collection") + .putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray()) // on API 25, `String[]` or `ArrayList` extras are null when using the shortcut // so we use a joined `String` as fallback - .putExtra(SHORTCUT_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR)) + .putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR)) else -> { result.error("pin-intent", "failed to build intent", null) return 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 a9d23fe13..f4019a6b5 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 @@ -138,7 +138,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler { private fun getBitmapFactoryInfo(call: MethodCall, result: MethodChannel.Result) { val uri = call.argument("uri")?.let { Uri.parse(it) } if (uri == null) { - result.error("getBitmapDecoderInfo-args", "failed because of missing arguments", null) + result.error("getBitmapDecoderInfo-args", "missing arguments", null) return } @@ -167,7 +167,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } if (mimeType == null || uri == null) { - result.error("getContentResolverMetadata-args", "failed because of missing arguments", null) + result.error("getContentResolverMetadata-args", "missing arguments", null) return } @@ -224,7 +224,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { - result.error("getExifInterfaceMetadata-args", "failed because of missing arguments", null) + result.error("getExifInterfaceMetadata-args", "missing arguments", null) return } @@ -250,7 +250,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler { private fun getMediaMetadataRetrieverMetadata(call: MethodCall, result: MethodChannel.Result) { val uri = call.argument("uri")?.let { Uri.parse(it) } if (uri == null) { - result.error("getMediaMetadataRetrieverMetadata-args", "failed because of missing arguments", null) + result.error("getMediaMetadataRetrieverMetadata-args", "missing arguments", null) return } @@ -276,7 +276,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { - result.error("getMetadataExtractorSummary-args", "failed because of missing arguments", null) + result.error("getMetadataExtractorSummary-args", "missing arguments", null) return } @@ -319,7 +319,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } if (mimeType == null || uri == null) { - result.error("getPixyMetadata-args", "failed because of missing arguments", null) + result.error("getPixyMetadata-args", "missing arguments", null) return } @@ -340,7 +340,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler { private fun getTiffStructure(call: MethodCall, result: MethodChannel.Result) { val uri = call.argument("uri")?.let { Uri.parse(it) } if (uri == null) { - result.error("getTiffStructure-args", "failed because of missing arguments", null) + result.error("getTiffStructure-args", "missing arguments", null) return } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index ef9210cf1..15511c3bf 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -57,7 +57,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { - result.error("getExifThumbnails-args", "failed because of missing arguments", null) + result.error("getExifThumbnails-args", "missing arguments", null) return } @@ -88,7 +88,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { val sizeBytes = call.argument("sizeBytes")?.toLong() val displayName = call.argument("displayName") if (mimeType == null || uri == null || sizeBytes == null) { - result.error("extractMotionPhotoVideo-args", "failed because of missing arguments", null) + result.error("extractMotionPhotoVideo-args", "missing arguments", null) return } @@ -108,7 +108,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val displayName = call.argument("displayName") if (uri == null) { - result.error("extractVideoEmbeddedPicture-args", "failed because of missing arguments", null) + result.error("extractVideoEmbeddedPicture-args", "missing arguments", null) return } @@ -143,7 +143,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { val dataPropPath = call.argument("propPath") val embedMimeType = call.argument("propMimeType") if (mimeType == null || uri == null || dataPropPath == null || embedMimeType == null) { - result.error("extractXmpDataProp-args", "failed because of missing arguments", null) + result.error("extractXmpDataProp-args", "missing arguments", null) return } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt index 1c16e2b02..7fac5c664 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt @@ -35,7 +35,7 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler { val localeString = call.argument("locale") val maxResults = call.argument("maxResults") ?: 1 if (latitude == null || longitude == null) { - result.error("getAddress-args", "failed because of missing arguments", null) + result.error("getAddress-args", "missing arguments", null) return } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt index c4b701327..d0d7767e9 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt @@ -23,7 +23,7 @@ class GlobalSearchHandler(private val context: Context) : MethodCallHandler { private fun registerCallback(call: MethodCall, result: MethodChannel.Result) { val callbackHandle = call.argument("callbackHandle")?.toLong() if (callbackHandle == null) { - result.error("registerCallback-args", "failed because of missing arguments", null) + result.error("registerCallback-args", "missing arguments", null) return } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/HomeWidgetHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/HomeWidgetHandler.kt new file mode 100644 index 000000000..eb6e8e81d --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/HomeWidgetHandler.kt @@ -0,0 +1,32 @@ +package deckers.thibault.aves.channel.calls + +import android.appwidget.AppWidgetManager +import android.content.Context +import deckers.thibault.aves.HomeWidgetProvider +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +class HomeWidgetHandler(private val context: Context) : MethodChannel.MethodCallHandler { + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "update" -> Coresult.safe(call, result, ::update) + else -> result.notImplemented() + } + } + + private fun update(call: MethodCall, result: MethodChannel.Result) { + val widgetId = call.argument("widgetId") + if (widgetId == null) { + result.error("update-args", "missing arguments", null) + return + } + + val appWidgetManager = AppWidgetManager.getInstance(context) + HomeWidgetProvider().onUpdate(context, appWidgetManager, intArrayOf(widgetId)) + result.success(null) + } + + companion object { + const val CHANNEL = "deckers.thibault/aves/widget_update" + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaEditHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaEditHandler.kt new file mode 100644 index 000000000..533a078b4 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaEditHandler.kt @@ -0,0 +1,77 @@ +package deckers.thibault.aves.channel.calls + +import android.content.ContextWrapper +import android.net.Uri +import android.util.Log +import deckers.thibault.aves.channel.calls.Coresult.Companion.safe +import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend +import deckers.thibault.aves.model.FieldMap +import deckers.thibault.aves.model.NameConflictStrategy +import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback +import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider +import deckers.thibault.aves.utils.LogUtils +import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch + +class MediaEditHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "cancelFileOp" -> safe(call, result, ::cancelFileOp) + "captureFrame" -> ioScope.launch { safeSuspend(call, result, ::captureFrame) } + else -> result.notImplemented() + } + } + + private fun cancelFileOp(call: MethodCall, result: MethodChannel.Result) { + val opId = call.argument("opId") + if (opId == null) { + result.error("cancelFileOp-args", "missing arguments", null) + return + } + + Log.i(LOG_TAG, "cancelling file op $opId") + cancelledOps.add(opId) + + result.success(null) + } + + private suspend fun captureFrame(call: MethodCall, result: MethodChannel.Result) { + val uri = call.argument("uri")?.let { Uri.parse(it) } + val desiredName = call.argument("desiredName") + val exifFields = call.argument("exif") ?: HashMap() + val bytes = call.argument("bytes") + var destinationDir = call.argument("destinationPath") + val nameConflictStrategy = NameConflictStrategy.get(call.argument("nameConflictStrategy")) + if (uri == null || desiredName == null || bytes == null || destinationDir == null || nameConflictStrategy == null) { + result.error("captureFrame-args", "missing arguments", null) + return + } + + val provider = getProvider(uri) + if (provider == null) { + result.error("captureFrame-provider", "failed to find provider for uri=$uri", null) + return + } + + destinationDir = ensureTrailingSeparator(destinationDir) + provider.captureFrame(contextWrapper, desiredName, exifFields, bytes, destinationDir, nameConflictStrategy, object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = result.success(fields) + override fun onFailure(throwable: Throwable) = result.error("captureFrame-failure", "failed to capture frame for uri=$uri", throwable.message) + }) + } + + companion object { + private val LOG_TAG = LogUtils.createTag() + const val CHANNEL = "deckers.thibault/aves/media_edit" + + val cancelledOps = HashSet() + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchHandler.kt similarity index 63% rename from android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchHandler.kt index 44c0cc405..ba032ec2b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchHandler.kt @@ -1,9 +1,8 @@ package deckers.thibault.aves.channel.calls -import android.app.Activity +import android.content.Context import android.graphics.Rect import android.net.Uri -import android.util.Log import com.bumptech.glide.Glide import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend @@ -12,31 +11,29 @@ import deckers.thibault.aves.channel.calls.fetchers.SvgRegionFetcher import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher import deckers.thibault.aves.model.FieldMap -import deckers.thibault.aves.model.NameConflictStrategy import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider -import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes -import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import kotlin.math.roundToInt -class MediaFileHandler(private val activity: Activity) : MethodCallHandler { +class MediaFetchHandler(private val context: Context) : MethodCallHandler { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private val density = activity.resources.displayMetrics.density + private val density = context.resources.displayMetrics.density - private val regionFetcher = RegionFetcher(activity) + private val regionFetcher = RegionFetcher(context) override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "getEntry" -> ioScope.launch { safe(call, result, ::getEntry) } "getThumbnail" -> ioScope.launch { safeSuspend(call, result, ::getThumbnail) } "getRegion" -> ioScope.launch { safeSuspend(call, result, ::getRegion) } - "cancelFileOp" -> safe(call, result, ::cancelFileOp) - "captureFrame" -> ioScope.launch { safeSuspend(call, result, ::captureFrame) } "clearSizedThumbnailDiskCache" -> ioScope.launch { safe(call, result, ::clearSizedThumbnailDiskCache) } else -> result.notImplemented() } @@ -46,7 +43,7 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler { val mimeType = call.argument("mimeType") // MIME type is optional val uri = call.argument("uri")?.let { Uri.parse(it) } if (uri == null) { - result.error("getEntry-args", "failed because of missing arguments", null) + result.error("getEntry-args", "missing arguments", null) return } @@ -56,7 +53,7 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler { return } - provider.fetchSingle(activity, uri, mimeType, object : ImageOpCallback { + provider.fetchSingle(context, uri, mimeType, object : ImageOpCallback { override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri", throwable.message) }) @@ -74,13 +71,13 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler { val defaultSizeDip = call.argument("defaultSizeDip")?.toDouble() if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) { - result.error("getThumbnail-args", "failed because of missing arguments", null) + result.error("getThumbnail-args", "missing arguments", null) return } // convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter ThumbnailFetcher( - activity, + context, uri, mimeType, dateModifiedSecs, @@ -107,20 +104,20 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler { val imageHeight = call.argument("imageHeight") if (uri == null || mimeType == null || sampleSize == null || x == null || y == null || width == null || height == null || imageWidth == null || imageHeight == null) { - result.error("getRegion-args", "failed because of missing arguments", null) + result.error("getRegion-args", "missing arguments", null) return } val regionRect = Rect(x, y, x + width, y + height) when (mimeType) { - MimeTypes.SVG -> SvgRegionFetcher(activity).fetch( + MimeTypes.SVG -> SvgRegionFetcher(context).fetch( uri = uri, regionRect = regionRect, imageWidth = imageWidth, imageHeight = imageHeight, result = result, ) - MimeTypes.TIFF -> TiffRegionFetcher(activity).fetch( + MimeTypes.TIFF -> TiffRegionFetcher(context).fetch( uri = uri, page = pageId ?: 0, sampleSize = sampleSize, @@ -140,53 +137,12 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler { } } - private fun cancelFileOp(call: MethodCall, result: MethodChannel.Result) { - val opId = call.argument("opId") - if (opId == null) { - result.error("cancelFileOp-args", "failed because of missing arguments", null) - return - } - - Log.i(LOG_TAG, "cancelling file op $opId") - cancelledOps.add(opId) - - result.success(null) - } - - private suspend fun captureFrame(call: MethodCall, result: MethodChannel.Result) { - val uri = call.argument("uri")?.let { Uri.parse(it) } - val desiredName = call.argument("desiredName") - val exifFields = call.argument("exif") ?: HashMap() - val bytes = call.argument("bytes") - var destinationDir = call.argument("destinationPath") - val nameConflictStrategy = NameConflictStrategy.get(call.argument("nameConflictStrategy")) - if (uri == null || desiredName == null || bytes == null || destinationDir == null || nameConflictStrategy == null) { - result.error("captureFrame-args", "failed because of missing arguments", null) - return - } - - val provider = getProvider(uri) - if (provider == null) { - result.error("captureFrame-provider", "failed to find provider for uri=$uri", null) - return - } - - destinationDir = ensureTrailingSeparator(destinationDir) - provider.captureFrame(activity, desiredName, exifFields, bytes, destinationDir, nameConflictStrategy, object : ImageOpCallback { - override fun onSuccess(fields: FieldMap) = result.success(fields) - override fun onFailure(throwable: Throwable) = result.error("captureFrame-failure", "failed to capture frame for uri=$uri", throwable.message) - }) - } - private fun clearSizedThumbnailDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { - Glide.get(activity).clearDiskCache() + Glide.get(context).clearDiskCache() result.success(null) } companion object { - private val LOG_TAG = LogUtils.createTag() - const val CHANNEL = "deckers.thibault/aves/media_file" - - val cancelledOps = HashSet() + const val CHANNEL = "deckers.thibault/aves/media_fetch" } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaStoreHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaStoreHandler.kt index a8761be8f..34db16413 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaStoreHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaStoreHandler.kt @@ -28,7 +28,7 @@ class MediaStoreHandler(private val context: Context) : MethodCallHandler { private fun checkObsoleteContentIds(call: MethodCall, result: MethodChannel.Result) { val knownContentIds = call.argument>("knownContentIds") if (knownContentIds == null) { - result.error("checkObsoleteContentIds-args", "failed because of missing arguments", null) + result.error("checkObsoleteContentIds-args", "missing arguments", null) return } result.success(MediaStoreImageProvider().checkObsoleteContentIds(context, knownContentIds)) @@ -37,7 +37,7 @@ class MediaStoreHandler(private val context: Context) : MethodCallHandler { private fun checkObsoletePaths(call: MethodCall, result: MethodChannel.Result) { val knownPathById = call.argument>("knownPathById") if (knownPathById == null) { - result.error("checkObsoletePaths-args", "failed because of missing arguments", null) + result.error("checkObsoletePaths-args", "missing arguments", null) return } result.success(MediaStoreImageProvider().checkObsoletePaths(context, knownPathById)) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt index 7b6d7ecfb..bda1f0c97 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt @@ -1,6 +1,6 @@ package deckers.thibault.aves.channel.calls -import android.app.Activity +import android.content.ContextWrapper import android.net.Uri import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.model.ExifOrientationOp @@ -15,7 +15,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { +class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { @@ -33,7 +33,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { private fun rotate(call: MethodCall, result: MethodChannel.Result) { val clockwise = call.argument("clockwise") if (clockwise == null) { - result.error("rotate-args", "failed because of missing arguments", null) + result.error("rotate-args", "missing arguments", null) return } @@ -48,7 +48,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { private fun editOrientation(call: MethodCall, result: MethodChannel.Result, op: ExifOrientationOp) { val entryMap = call.argument("entry") if (entryMap == null) { - result.error("editOrientation-args", "failed because of missing arguments", null) + result.error("editOrientation-args", "missing arguments", null) return } @@ -66,7 +66,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { return } - provider.editOrientation(activity, path, uri, mimeType, op, object : ImageOpCallback { + provider.editOrientation(contextWrapper, path, uri, mimeType, op, object : ImageOpCallback { override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onFailure(throwable: Throwable) = result.error("editOrientation-failure", "failed to change orientation for mimeType=$mimeType uri=$uri", throwable.message) }) @@ -78,7 +78,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { val fields = call.argument>("fields") val entryMap = call.argument("entry") if (entryMap == null || fields == null) { - result.error("editDate-args", "failed because of missing arguments", null) + result.error("editDate-args", "missing arguments", null) return } @@ -96,7 +96,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { return } - provider.editDate(activity, path, uri, mimeType, dateMillis, shiftMinutes, fields, object : ImageOpCallback { + provider.editDate(contextWrapper, path, uri, mimeType, dateMillis, shiftMinutes, fields, object : ImageOpCallback { override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onFailure(throwable: Throwable) = result.error("editDate-failure", "failed to edit date for mimeType=$mimeType uri=$uri", throwable.message) }) @@ -107,7 +107,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { val entryMap = call.argument("entry") val autoCorrectTrailerOffset = call.argument("autoCorrectTrailerOffset") if (entryMap == null || metadata == null || autoCorrectTrailerOffset == null) { - result.error("editMetadata-args", "failed because of missing arguments", null) + result.error("editMetadata-args", "missing arguments", null) return } @@ -125,7 +125,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { return } - provider.editMetadata(activity, path, uri, mimeType, metadata, autoCorrectTrailerOffset, callback = object : ImageOpCallback { + provider.editMetadata(contextWrapper, path, uri, mimeType, metadata, autoCorrectTrailerOffset, callback = object : ImageOpCallback { override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onFailure(throwable: Throwable) = result.error("editMetadata-failure", "failed to edit metadata for mimeType=$mimeType uri=$uri", throwable.message) }) @@ -134,7 +134,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { private fun removeTrailerVideo(call: MethodCall, result: MethodChannel.Result) { val entryMap = call.argument("entry") if (entryMap == null) { - result.error("removeTrailerVideo-args", "failed because of missing arguments", null) + result.error("removeTrailerVideo-args", "missing arguments", null) return } @@ -152,7 +152,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { return } - provider.removeTrailerVideo(activity, path, uri, mimeType, object : ImageOpCallback { + provider.removeTrailerVideo(contextWrapper, path, uri, mimeType, object : ImageOpCallback { override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onFailure(throwable: Throwable) = result.error("removeTrailerVideo-failure", "failed to remove trailer video for mimeType=$mimeType uri=$uri", throwable.message) }) @@ -162,7 +162,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { val types = call.argument>("types") val entryMap = call.argument("entry") if (entryMap == null || types == null) { - result.error("removeTypes-args", "failed because of missing arguments", null) + result.error("removeTypes-args", "missing arguments", null) return } @@ -180,7 +180,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { return } - provider.removeMetadataTypes(activity, path, uri, mimeType, types.toSet(), object : ImageOpCallback { + provider.removeMetadataTypes(contextWrapper, path, uri, mimeType, types.toSet(), object : ImageOpCallback { override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onFailure(throwable: Throwable) = result.error("removeTypes-failure", "failed to remove metadata for mimeType=$mimeType uri=$uri", throwable.message) }) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index b76620d2c..45234542f 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -115,7 +115,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { - result.error("getAllMetadata-args", "failed because of missing arguments", null) + result.error("getAllMetadata-args", "missing arguments", null) return } @@ -424,7 +424,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { val path = call.argument("path") val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { - result.error("getCatalogMetadata-args", "failed because of missing arguments", null) + result.error("getCatalogMetadata-args", "missing arguments", null) return } @@ -691,7 +691,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { - result.error("getOverlayMetadata-args", "failed because of missing arguments", null) + result.error("getOverlayMetadata-args", "missing arguments", null) return } @@ -761,7 +761,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { - result.error("getGeoTiffInfo-args", "failed because of missing arguments", null) + result.error("getGeoTiffInfo-args", "missing arguments", null) return } @@ -802,7 +802,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null || sizeBytes == null) { - result.error("getMultiPageInfo-args", "failed because of missing arguments", null) + result.error("getMultiPageInfo-args", "missing arguments", null) return } @@ -824,7 +824,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { - result.error("getPanoramaInfo-args", "failed because of missing arguments", null) + result.error("getPanoramaInfo-args", "missing arguments", null) return } @@ -863,7 +863,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } if (mimeType == null || uri == null) { - result.error("getIptc-args", "failed because of missing arguments", null) + result.error("getIptc-args", "missing arguments", null) return } @@ -888,7 +888,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { val uri = call.argument("uri")?.let { Uri.parse(it) } val sizeBytes = call.argument("sizeBytes")?.toLong() if (mimeType == null || uri == null) { - result.error("getXmp-args", "failed because of missing arguments", null) + result.error("getXmp-args", "missing arguments", null) return } @@ -918,7 +918,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { private fun hasContentResolverProp(call: MethodCall, result: MethodChannel.Result) { val prop = call.argument("prop") if (prop == null) { - result.error("hasContentResolverProp-args", "failed because of missing arguments", null) + result.error("hasContentResolverProp-args", "missing arguments", null) return } @@ -938,7 +938,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { 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) + result.error("getContentResolverProp-args", "missing arguments", null) return } @@ -992,7 +992,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { val sizeBytes = call.argument("sizeBytes")?.toLong() val field = call.argument("field") if (mimeType == null || uri == null || field == null) { - result.error("getDate-args", "failed because of missing arguments", null) + result.error("getDate-args", "missing arguments", null) return } 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 9fc6adbc6..435705285 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 @@ -55,7 +55,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler { ) ) } - } catch (e: IllegalArgumentException) { + } catch (e: Exception) { // ignore } } @@ -80,7 +80,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler { "state" to EnvironmentCompat.getStorageState(volumeFile) ) ) - } catch (e: IllegalArgumentException) { + } catch (e: Exception) { // ignore } } @@ -91,7 +91,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler { private fun getFreeSpace(call: MethodCall, result: MethodChannel.Result) { val path = call.argument("path") if (path == null) { - result.error("getFreeSpace-args", "failed because of missing arguments", null) + result.error("getFreeSpace-args", "missing arguments", null) return } @@ -112,7 +112,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler { private fun getInaccessibleDirectories(call: MethodCall, result: MethodChannel.Result) { val dirPaths = call.argument>("dirPaths") if (dirPaths == null) { - result.error("getInaccessibleDirectories-args", "failed because of missing arguments", null) + result.error("getInaccessibleDirectories-args", "missing arguments", null) return } @@ -126,7 +126,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler { private fun revokeDirectoryAccess(call: MethodCall, result: MethodChannel.Result) { val path = call.argument("path") if (path == null) { - result.error("revokeDirectoryAccess-args", "failed because of missing arguments", null) + result.error("revokeDirectoryAccess-args", "missing arguments", null) return } @@ -142,7 +142,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler { private fun deleteEmptyDirectories(call: MethodCall, result: MethodChannel.Result) { val dirPaths = call.argument>("dirPaths") if (dirPaths == null) { - result.error("deleteEmptyDirectories-args", "failed because of missing arguments", null) + result.error("deleteEmptyDirectories-args", "missing arguments", null) return } @@ -167,7 +167,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler { private fun canInsertMedia(call: MethodCall, result: MethodChannel.Result) { val directories = call.argument>("directories") if (directories == null) { - result.error("canInsertMedia-args", "failed because of missing arguments", null) + result.error("canInsertMedia-args", "missing arguments", null) return } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WallpaperHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WallpaperHandler.kt index c4f1eab9d..5b91a6334 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WallpaperHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WallpaperHandler.kt @@ -1,9 +1,9 @@ package deckers.thibault.aves.channel.calls -import android.app.Activity import android.app.WallpaperManager import android.app.WallpaperManager.FLAG_LOCK import android.app.WallpaperManager.FLAG_SYSTEM +import android.content.ContextWrapper import android.os.Build import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import io.flutter.plugin.common.MethodCall @@ -14,7 +14,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -class WallpaperHandler(private val activity: Activity) : MethodCallHandler { +class WallpaperHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { @@ -29,11 +29,11 @@ class WallpaperHandler(private val activity: Activity) : MethodCallHandler { val home = call.argument("home") val lock = call.argument("lock") if (bytes == null || home == null || lock == null) { - result.error("setWallpaper-args", "failed because of missing arguments", null) + result.error("setWallpaper-args", "missing arguments", null) return } - val manager = WallpaperManager.getInstance(activity) + val manager = WallpaperManager.getInstance(contextWrapper) val supported = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || manager.isWallpaperSupported val allowed = Build.VERSION.SDK_INT < Build.VERSION_CODES.N || manager.isSetWallpaperAllowed if (!supported || !allowed) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt deleted file mode 100644 index 528e70941..000000000 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt +++ /dev/null @@ -1,89 +0,0 @@ -package deckers.thibault.aves.channel.calls - -import android.app.Activity -import android.os.Build -import android.provider.Settings -import android.util.Log -import android.view.WindowManager -import deckers.thibault.aves.channel.calls.Coresult.Companion.safe -import deckers.thibault.aves.utils.LogUtils -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler - -class WindowHandler(private val activity: Activity) : MethodCallHandler { - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - "keepScreenOn" -> safe(call, result, ::keepScreenOn) - "isRotationLocked" -> safe(call, result, ::isRotationLocked) - "requestOrientation" -> safe(call, result, ::requestOrientation) - "canSetCutoutMode" -> safe(call, result, ::canSetCutoutMode) - "setCutoutMode" -> safe(call, result, ::setCutoutMode) - else -> result.notImplemented() - } - } - - private fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) { - val on = call.argument("on") - if (on == null) { - result.error("keepOn-args", "failed because of missing arguments", null) - return - } - - val window = activity.window - val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - if (on) { - window.addFlags(flag) - } else { - window.clearFlags(flag) - } - result.success(null) - } - - private fun isRotationLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { - var locked = false - try { - locked = Settings.System.getInt(activity.contentResolver, Settings.System.ACCELEROMETER_ROTATION) == 0 - } catch (e: Exception) { - Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null) - } - result.success(locked) - } - - private fun requestOrientation(call: MethodCall, result: MethodChannel.Result) { - val orientation = call.argument("orientation") - if (orientation == null) { - result.error("requestOrientation-args", "failed because of missing arguments", null) - return - } - activity.requestedOrientation = orientation - result.success(true) - } - - private fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { - result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - } - - private fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) { - val use = call.argument("use") - if (use == null) { - result.error("setCutoutMode-args", "failed because of missing arguments", null) - return - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val mode = if (use) { - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - } else { - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - } - activity.window.attributes.layoutInDisplayCutoutMode = mode - } - result.success(true) - } - - companion object { - private val LOG_TAG = LogUtils.createTag() - const val CHANNEL = "deckers.thibault/aves/window" - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt new file mode 100644 index 000000000..88bda61d4 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt @@ -0,0 +1,67 @@ +package deckers.thibault.aves.channel.calls.window + +import android.app.Activity +import android.os.Build +import android.view.WindowManager +import deckers.thibault.aves.utils.LogUtils +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +class ActivityWindowHandler(private val activity: Activity) : WindowHandler(activity) { + override fun isActivity(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + result.success(true) + } + + override fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) { + val on = call.argument("on") + if (on == null) { + result.error("keepOn-args", "missing arguments", null) + return + } + + val window = activity.window + val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + if (on) { + window.addFlags(flag) + } else { + window.clearFlags(flag) + } + result.success(null) + } + + override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) { + val orientation = call.argument("orientation") + if (orientation == null) { + result.error("requestOrientation-args", "missing arguments", null) + return + } + activity.requestedOrientation = orientation + result.success(true) + } + + override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + } + + override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) { + val use = call.argument("use") + if (use == null) { + result.error("setCutoutMode-args", "missing arguments", null) + return + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val mode = if (use) { + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } else { + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER + } + activity.window.attributes.layoutInDisplayCutoutMode = mode + } + result.success(true) + } + + companion object { + private val LOG_TAG = LogUtils.createTag() + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt new file mode 100644 index 000000000..55794ade4 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt @@ -0,0 +1,27 @@ +package deckers.thibault.aves.channel.calls.window + +import android.app.Service +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +class ServiceWindowHandler(service: Service) : WindowHandler(service) { + override fun isActivity(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + result.success(false) + } + + override fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) { + result.success(null) + } + + override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) { + result.success(false) + } + + override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + result.success(false) + } + + override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) { + result.success(false) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt new file mode 100644 index 000000000..184d2398d --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt @@ -0,0 +1,48 @@ +package deckers.thibault.aves.channel.calls.window + +import android.content.ContextWrapper +import android.provider.Settings +import android.util.Log +import deckers.thibault.aves.channel.calls.Coresult +import deckers.thibault.aves.utils.LogUtils +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +abstract class WindowHandler(private val contextWrapper: ContextWrapper) : MethodChannel.MethodCallHandler { + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "isActivity" -> Coresult.safe(call, result, ::isActivity) + "keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn) + "isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked) + "requestOrientation" -> Coresult.safe(call, result, ::requestOrientation) + "canSetCutoutMode" -> Coresult.safe(call, result, ::canSetCutoutMode) + "setCutoutMode" -> Coresult.safe(call, result, ::setCutoutMode) + else -> result.notImplemented() + } + } + + abstract fun isActivity(call: MethodCall, result: MethodChannel.Result) + + abstract fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) + + private fun isRotationLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + var locked = false + try { + locked = Settings.System.getInt(contextWrapper.contentResolver, Settings.System.ACCELEROMETER_ROTATION) == 0 + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null) + } + result.success(locked) + } + + abstract fun requestOrientation(call: MethodCall, result: MethodChannel.Result) + + abstract fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) + + abstract fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) + + companion object { + private val LOG_TAG = LogUtils.createTag() + const val CHANNEL = "deckers.thibault/aves/window" + } +} diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt similarity index 85% rename from android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt index 33397a1b2..e01adca3a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt @@ -22,9 +22,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -// starting activity to give access with the native dialog +// starting activity to get a result (e.g. storage access via native dialog) // breaks the regular `MethodChannel` so we use a stream channel instead -class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?) : EventChannel.StreamHandler { +class ActivityResultStreamHandler(private val activity: Activity, arguments: Any?) : EventChannel.StreamHandler { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private lateinit var eventSink: EventSink private lateinit var handler: Handler @@ -48,6 +48,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? "requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() } "createFile" -> ioScope.launch { createFile() } "openFile" -> ioScope.launch { openFile() } + "pickCollectionFilters" -> pickCollectionFilters() else -> endOfStream() } } @@ -55,7 +56,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? private suspend fun requestDirectoryAccess() { val path = args["path"] as String? if (path == null) { - error("requestDirectoryAccess-args", "failed because of missing arguments", null) + error("requestDirectoryAccess-args", "missing arguments", null) return } @@ -77,7 +78,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? val uris = (args["uris"] as List<*>?)?.mapNotNull { if (it is String) Uri.parse(it) else null } val mimeTypes = (args["mimeTypes"] as List<*>?)?.mapNotNull { if (it is String) it else null } if (uris == null || uris.isEmpty() || mimeTypes == null || mimeTypes.size != uris.size) { - error("requestMediaFileAccess-args", "failed because of missing arguments", null) + error("requestMediaFileAccess-args", "missing arguments", null) return } @@ -111,7 +112,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? val mimeType = args["mimeType"] as String? val bytes = args["bytes"] as ByteArray? if (name == null || mimeType == null || bytes == null) { - error("createFile-args", "failed because of missing arguments", null) + error("createFile-args", "missing arguments", null) return } @@ -186,6 +187,18 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? } } + private fun pickCollectionFilters() { + val initialFilters = (args["initialFilters"] as List<*>?)?.mapNotNull { if (it is String) it else null } ?: listOf() + val intent = Intent(MainActivity.INTENT_ACTION_PICK_COLLECTION_FILTERS, null, activity, MainActivity::class.java) + .putExtra(MainActivity.EXTRA_KEY_FILTERS_ARRAY, initialFilters.toTypedArray()) + .putExtra(MainActivity.EXTRA_KEY_FILTERS_STRING, initialFilters.joinToString(MainActivity.EXTRA_STRING_ARRAY_SEPARATOR)) + MainActivity.pendingCollectionFilterPickHandler = { filters -> + success(filters) + endOfStream() + } + activity.startActivityForResult(intent, MainActivity.PICK_COLLECTION_FILTERS_REQUEST) + } + override fun onCancel(arguments: Any?) {} private fun success(result: Any?) { @@ -221,8 +234,8 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? } companion object { - private val LOG_TAG = LogUtils.createTag() - const val CHANNEL = "deckers.thibault/aves/storage_access_stream" + private val LOG_TAG = LogUtils.createTag() + const val CHANNEL = "deckers.thibault/aves/activity_result_stream" private const val BUFFER_SIZE = 2 shl 17 // 256kB } } \ 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 7e58f4ed0..504573e6b 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 @@ -87,7 +87,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments val pageId = arguments["pageId"] as Int? if (mimeType == null || uri == null) { - error("streamImage-args", "failed because of missing arguments", null) + error("streamImage-args", "missing arguments", null) endOfStream() return } 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 dadbda2f1..0e149942b 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,7 +5,7 @@ import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log -import deckers.thibault.aves.channel.calls.MediaFileHandler.Companion.cancelledOps +import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.NameConflictStrategy @@ -143,7 +143,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments val height = arguments["height"] as Int? val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?) if (destinationDir == null || mimeType == null || width == null || height == null || nameConflictStrategy == null) { - error("export-args", "failed because of missing arguments", null) + error("export-args", "missing arguments", null) return } @@ -174,7 +174,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?) val rawEntryMap = arguments["entriesByDestination"] as Map<*, *>? if (copy == null || nameConflictStrategy == null || rawEntryMap == null || rawEntryMap.isEmpty()) { - error("move-args", "failed because of missing arguments", null) + error("move-args", "missing arguments", null) return } @@ -207,7 +207,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments val rawEntryMap = arguments["entriesToNewName"] as Map<*, *>? if (rawEntryMap == null || rawEntryMap.isEmpty()) { - error("rename-args", "failed because of missing arguments", null) + error("rename-args", "missing arguments", null) return } 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 c5861f208..e1734338c 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 @@ -20,6 +20,6 @@ class IntentStreamHandler : EventChannel.StreamHandler { } companion object { - const val CHANNEL = "deckers.thibault/aves/intent" + const val CHANNEL = "deckers.thibault/aves/new_intent_stream" } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt index 7a6cfae94..66d6f1035 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 @@ -1,7 +1,7 @@ package deckers.thibault.aves.model.provider -import android.app.Activity import android.content.Context +import android.content.ContextWrapper import android.net.Uri import android.util.Log import deckers.thibault.aves.model.SourceEntry @@ -37,7 +37,7 @@ internal class FileImageProvider : ImageProvider() { } } - override suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) { + override suspend fun delete(contextWrapper: ContextWrapper, uri: Uri, path: String?, mimeType: String) { val file = File(File(uri.path!!).path) if (!file.exists()) return 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 12ce8e4a5..679e96b9a 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,6 +2,7 @@ package deckers.thibault.aves.model.provider import android.app.Activity import android.content.Context +import android.content.ContextWrapper import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap @@ -45,7 +46,7 @@ abstract class ImageProvider { callback.onFailure(UnsupportedOperationException("`fetchSingle` is not supported by this image provider")) } - open suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) { + open suspend fun delete(contextWrapper: ContextWrapper, uri: Uri, path: String?, mimeType: String) { throw UnsupportedOperationException("`delete` is not supported by this image provider") } @@ -151,7 +152,7 @@ abstract class ImageProvider { desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" } val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( - activity = activity, + contextWrapper = activity, dir = targetDir, desiredNameWithoutExtension = desiredNameWithoutExtension, mimeType = exportMimeType, @@ -242,7 +243,7 @@ abstract class ImageProvider { @Suppress("BlockingMethodInNonBlockingContext") suspend fun captureFrame( - activity: Activity, + contextWrapper: ContextWrapper, desiredNameWithoutExtension: String, exifFields: FieldMap, bytes: ByteArray, @@ -250,7 +251,7 @@ abstract class ImageProvider { nameConflictStrategy: NameConflictStrategy, callback: ImageOpCallback, ) { - val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) + val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(contextWrapper, targetDir) if (!File(targetDir).exists()) { callback.onFailure(Exception("failed to create directory at path=$targetDir")) return @@ -265,7 +266,7 @@ abstract class ImageProvider { val captureMimeType = MimeTypes.JPEG val targetNameWithoutExtension = try { resolveTargetFileNameWithoutExtension( - activity = activity, + contextWrapper = contextWrapper, dir = targetDir, desiredNameWithoutExtension = desiredNameWithoutExtension, mimeType = captureMimeType, @@ -287,7 +288,7 @@ abstract class ImageProvider { // through a document URI, not a tree URI // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first val targetTreeFile = targetDirDocFile.createFile(captureMimeType, targetNameWithoutExtension) - val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) + val targetDocFile = DocumentFileCompat.fromSingleUri(contextWrapper, targetTreeFile.uri) try { if (exifFields.isEmpty()) { @@ -355,7 +356,7 @@ abstract class ImageProvider { val fileName = targetDocFile.name val targetFullPath = targetDir + fileName - val newFields = MediaStoreImageProvider().scanNewPath(activity, targetFullPath, captureMimeType) + val newFields = MediaStoreImageProvider().scanNewPath(contextWrapper, targetFullPath, captureMimeType) callback.onSuccess(newFields) } catch (e: Exception) { callback.onFailure(e) @@ -364,7 +365,7 @@ abstract class ImageProvider { // returns available name to use, or `null` to skip it suspend fun resolveTargetFileNameWithoutExtension( - activity: Activity, + contextWrapper: ContextWrapper, dir: String, desiredNameWithoutExtension: String, mimeType: String, @@ -386,9 +387,9 @@ abstract class ImageProvider { if (targetFile.exists()) { val path = targetFile.path MediaStoreImageProvider().apply { - val uri = getContentUriForPath(activity, path) + val uri = getContentUriForPath(contextWrapper, path) uri ?: throw Exception("failed to find content URI for path=$path") - delete(activity, uri, path, mimeType) + delete(contextWrapper, uri, path, mimeType) } } desiredNameWithoutExtension 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 c8f38a340..e3e9ff736 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 @@ -3,10 +3,7 @@ package deckers.thibault.aves.model.provider import android.annotation.SuppressLint import android.app.Activity import android.app.RecoverableSecurityException -import android.content.ContentResolver -import android.content.ContentUris -import android.content.ContentValues -import android.content.Context +import android.content.* import android.media.MediaScannerConnection import android.net.Uri import android.os.Build @@ -280,7 +277,7 @@ class MediaStoreImageProvider : ImageProvider() { private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType // `uri` is a media URI, not a document URI - override suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) { + override suspend fun delete(contextWrapper: ContextWrapper, uri: Uri, path: String?, mimeType: String) { path ?: throw Exception("failed to delete file because path is null") // the following situations are possible: @@ -291,10 +288,10 @@ class MediaStoreImageProvider : ImageProvider() { val fileExists = file.exists() if (fileExists) { - if (StorageUtils.canEditByFile(activity, path)) { - if (hasEntry(activity, uri)) { + if (StorageUtils.canEditByFile(contextWrapper, path)) { + if (hasEntry(contextWrapper, uri)) { Log.d(LOG_TAG, "delete [permission:file, file exists, content exists] content at uri=$uri path=$path") - activity.contentResolver.delete(uri, null, null) + contextWrapper.contentResolver.delete(uri, null, null) } // in theory, deleting via content resolver should remove the file on storage // in practice, the file may still be there afterwards @@ -303,31 +300,31 @@ class MediaStoreImageProvider : ImageProvider() { if (file.delete()) { // in theory, scanning an obsolete path should remove the entry from the Media Store // in practice, the entry may still be there afterwards - scanObsoletePath(activity, uri, path, mimeType) + scanObsoletePath(contextWrapper, uri, path, mimeType) return } } else { return } - } else if (!isMediaUriPermissionGranted(activity, uri, mimeType) - && StorageUtils.requireAccessPermission(activity, path) + } else if (!isMediaUriPermissionGranted(contextWrapper, uri, mimeType) + && StorageUtils.requireAccessPermission(contextWrapper, path) ) { // the delete request may yield a `RecoverableSecurityException` when using scoped storage, // even if we have permissions on the tree document via SAF - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && hasEntry(activity, uri)) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && hasEntry(contextWrapper, uri)) { Log.d(LOG_TAG, "delete [permission:doc, file exists, content exists] content at uri=$uri path=$path") - activity.contentResolver.delete(uri, null, null) + contextWrapper.contentResolver.delete(uri, null, null) } // in theory, deleting via content resolver should remove the file on storage // in practice, the file may still be there afterwards if (file.exists()) { Log.d(LOG_TAG, "delete [permission:doc, file exists after content delete] document at uri=$uri path=$path") - val df = StorageUtils.getDocumentFile(activity, path, uri) + val df = StorageUtils.getDocumentFile(contextWrapper, path, uri) @Suppress("BlockingMethodInNonBlockingContext") if (df != null && df.delete()) { - scanObsoletePath(activity, uri, path, mimeType) + scanObsoletePath(contextWrapper, uri, path, mimeType) return } throw Exception("failed to delete document with df=$df") @@ -343,28 +340,28 @@ class MediaStoreImageProvider : ImageProvider() { try { Log.d(LOG_TAG, "delete [file exists=$fileExists] content at uri=$uri path=$path") - if (activity.contentResolver.delete(uri, null, null) > 0) return + if (contextWrapper.contentResolver.delete(uri, null, null) > 0) return - if (hasEntry(activity, uri) || file.exists()) { + if (hasEntry(contextWrapper, uri) || file.exists()) { throw Exception("failed to delete row from content provider") } } catch (securityException: SecurityException) { // even if the app has access permission granted on the containing directory, // the delete request may yield a `RecoverableSecurityException` on Android 10+ // when the underlying file no longer exists and this is an orphaned entry in the Media Store - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && contextWrapper is Activity) { Log.w(LOG_TAG, "caught a security exception when attempting to delete uri=$uri", securityException) val rse = securityException as? RecoverableSecurityException ?: throw securityException val intentSender = rse.userAction.actionIntent.intentSender // request user permission for this item MainActivity.pendingScopedStoragePermissionCompleter = CompletableFuture() - activity.startIntentSenderForResult(intentSender, DELETE_SINGLE_PERMISSION_REQUEST, null, 0, 0, 0, null) + contextWrapper.startIntentSenderForResult(intentSender, DELETE_SINGLE_PERMISSION_REQUEST, null, 0, 0, 0, null) val granted = MainActivity.pendingScopedStoragePermissionCompleter!!.join() MainActivity.pendingScopedStoragePermissionCompleter = null if (granted) { - delete(activity, uri, path, mimeType) + delete(contextWrapper, uri, path, mimeType) } else { throw Exception("failed to get delete permission") } @@ -494,7 +491,7 @@ class MediaStoreImageProvider : ImageProvider() { val desiredNameWithoutExtension = desiredName.substringBeforeLast(".") val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( - activity = activity, + contextWrapper = activity, dir = targetDir, desiredNameWithoutExtension = desiredNameWithoutExtension, mimeType = mimeType, @@ -641,7 +638,7 @@ class MediaStoreImageProvider : ImageProvider() { val dir = oldFile.parent ?: return skippedFieldMap val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( - activity = activity, + contextWrapper = activity, dir = dir, desiredNameWithoutExtension = desiredNameWithoutExtension, mimeType = mimeType, diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt index cd7dffcfc..9be2fbe67 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt @@ -24,7 +24,6 @@ fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): Ap @Suppress("deprecation") getApplicationInfo(packageName, flags) } - } fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flags: Int): List { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt index 8a0e3d2a0..ba7d1537e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt @@ -290,7 +290,7 @@ object StorageUtils { if (volume != null && uuid.equals(volume.uuid, ignoreCase = true)) { return volumePath } - } catch (e: IllegalArgumentException) { + } catch (e: Exception) { // ignore } } diff --git a/android/app/src/main/res/layout/app_widget.xml b/android/app/src/main/res/layout/app_widget.xml new file mode 100644 index 000000000..b4a2c168f --- /dev/null +++ b/android/app/src/main/res/layout/app_widget.xml @@ -0,0 +1,5 @@ + diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml index c74efc923..d4c317a8c 100644 --- a/android/app/src/main/res/values-de/strings.xml +++ b/android/app/src/main/res/values-de/strings.xml @@ -1,6 +1,7 @@  Aves + Bilderrahmen Hintergrundbild Suche Videos diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml index 54e4d386f..9310bcb90 100644 --- a/android/app/src/main/res/values-fr/strings.xml +++ b/android/app/src/main/res/values-fr/strings.xml @@ -1,6 +1,7 @@  Aves + Cadre photo Fond d’écran Recherche Vidéos diff --git a/android/app/src/main/res/values-it/strings.xml b/android/app/src/main/res/values-it/strings.xml index 0574f5b49..d01d270fc 100644 --- a/android/app/src/main/res/values-it/strings.xml +++ b/android/app/src/main/res/values-it/strings.xml @@ -1,6 +1,7 @@ Aves + Cornice foto Sfondo Ricerca Video diff --git a/android/app/src/main/res/values-ja/strings.xml b/android/app/src/main/res/values-ja/strings.xml index c7b8be013..fd266ed73 100644 --- a/android/app/src/main/res/values-ja/strings.xml +++ b/android/app/src/main/res/values-ja/strings.xml @@ -1,6 +1,7 @@  Aves + フォトフレーム 壁紙 検索 動画 diff --git a/android/app/src/main/res/values-ko/strings.xml b/android/app/src/main/res/values-ko/strings.xml index 2680d4a88..726821585 100644 --- a/android/app/src/main/res/values-ko/strings.xml +++ b/android/app/src/main/res/values-ko/strings.xml @@ -1,6 +1,7 @@  아베스 + 사진 액자 배경화면 검색 동영상 diff --git a/android/app/src/main/res/values-pt/strings.xml b/android/app/src/main/res/values-pt/strings.xml index dd30d7117..0f9e6f5d1 100644 --- a/android/app/src/main/res/values-pt/strings.xml +++ b/android/app/src/main/res/values-pt/strings.xml @@ -1,6 +1,7 @@ Aves + Porta-retratos Papel de parede Procurar Vídeos diff --git a/android/app/src/main/res/values-zh/strings.xml b/android/app/src/main/res/values-zh/strings.xml index 7db4775d2..c3453296f 100644 --- a/android/app/src/main/res/values-zh/strings.xml +++ b/android/app/src/main/res/values-zh/strings.xml @@ -1,6 +1,7 @@ Aves + 相框 壁纸 搜索 视频 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 5c24af947..cb2b215c0 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,6 +1,7 @@ Aves + Photo Frame Wallpaper Search Videos diff --git a/android/app/src/main/res/xml/app_widget_info.xml b/android/app/src/main/res/xml/app_widget_info.xml new file mode 100644 index 000000000..af3ae7c12 --- /dev/null +++ b/android/app/src/main/res/xml/app_widget_info.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/android/app/src/main/res/xml/screen_saver.xml b/android/app/src/main/res/xml/screen_saver.xml new file mode 100644 index 000000000..78dbc6777 --- /dev/null +++ b/android/app/src/main/res/xml/screen_saver.xml @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/android/app/src/profile/res/xml/screen_saver.xml b/android/app/src/profile/res/xml/screen_saver.xml new file mode 100644 index 000000000..012d5b878 --- /dev/null +++ b/android/app/src/profile/res/xml/screen_saver.xml @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 4d7683bf2..e8e5eb4cc 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.7.0' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() @@ -10,8 +10,8 @@ buildscript { classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // GMS & Firebase Crashlytics (used by some flavors only) - classpath 'com.google.gms:google-services:4.3.10' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.0' + classpath 'com.google.gms:google-services:4.3.13' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1' // HMS (used by some flavors only) classpath 'com.huawei.agconnect:agcp:1.5.2.300' } diff --git a/android/key_template.properties b/android/key_template.properties index e335f59b6..838a53514 100644 --- a/android/key_template.properties +++ b/android/key_template.properties @@ -3,3 +3,4 @@ storePassword= keyAlias= keyPassword= googleApiKey= +huaweiApiKey= diff --git a/fastlane/metadata/android/en-US/changelogs/1076.txt b/fastlane/metadata/android/en-US/changelogs/1076.txt new file mode 100644 index 000000000..cce1f4f7d --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/1076.txt @@ -0,0 +1,5 @@ +In v1.6.10: +- add the photo frame widget to your home +- use your photos as screen saver +- search photos taken "on this day" +Full changelog available on GitHub diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 4b84e8ccd..8806906a1 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -2,4 +2,4 @@ Navigation and search is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc. -Aves integrates with Android (from API 19 to 33, i.e. from KitKat to Android 13) with features such as app shortcuts and global search handling. It also works as a media viewer and picker. \ No newline at end of file +Aves integrates with Android (from API 19 to 33, i.e. from KitKat to Android 13) with features such as widgets, app shortcuts, screen saver and global search handling. It also works as a media viewer and picker. \ No newline at end of file diff --git a/lib/app_mode.dart b/lib/app_mode.dart index 0c4840ba3..de3cd6455 100644 --- a/lib/app_mode.dart +++ b/lib/app_mode.dart @@ -1,22 +1,34 @@ enum AppMode { main, + pickCollectionFiltersExternal, pickSingleMediaExternal, pickMultipleMediaExternal, pickMediaInternal, pickFilterInternal, + screenSaver, setWallpaper, slideshow, view, } extension ExtraAppMode on AppMode { - bool get canSearch => this == AppMode.main || this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal; + bool get canNavigate => { + AppMode.main, + AppMode.pickCollectionFiltersExternal, + AppMode.pickSingleMediaExternal, + AppMode.pickMultipleMediaExternal, + }.contains(this); - bool get canSelectMedia => this == AppMode.main || this == AppMode.pickMultipleMediaExternal; + bool get canSelectMedia => { + AppMode.main, + AppMode.pickMultipleMediaExternal, + }.contains(this); bool get canSelectFilter => this == AppMode.main; - bool get hasDrawer => this == AppMode.main || this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal; - - bool get isPickingMedia => this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal || this == AppMode.pickMediaInternal; + bool get isPickingMedia => { + AppMode.pickSingleMediaExternal, + AppMode.pickMultipleMediaExternal, + AppMode.pickMediaInternal, + }.contains(this); } diff --git a/lib/image_providers/app_icon_image_provider.dart b/lib/image_providers/app_icon_image_provider.dart index c9f70632e..0729c7ce1 100644 --- a/lib/image_providers/app_icon_image_provider.dart +++ b/lib/image_providers/app_icon_image_provider.dart @@ -1,4 +1,4 @@ -import 'dart:ui' as ui show Codec; +import 'dart:ui' as ui; import 'package:aves/services/common/services.dart'; import 'package:equatable/equatable.dart'; @@ -27,7 +27,7 @@ class AppIconImage extends ImageProvider { } @override - ImageStreamCompleter load(AppIconImageKey key, DecoderCallback decode) { + ImageStreamCompleter loadBuffer(AppIconImageKey key, DecoderBufferCallback decode) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), scale: key.scale, @@ -37,10 +37,11 @@ class AppIconImage extends ImageProvider { ); } - Future _loadAsync(AppIconImageKey key, DecoderCallback decode) async { + Future _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async { try { final bytes = await androidAppService.getAppIcon(key.packageName, key.size); - return await decode(bytes.isEmpty ? kTransparentImage : bytes); + final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes); + return await decode(buffer); } catch (error) { debugPrint('$runtimeType _loadAsync failed with packageName=$packageName, error=$error'); throw StateError('$packageName app icon decoding failed'); diff --git a/lib/image_providers/region_provider.dart b/lib/image_providers/region_provider.dart index 97faccc96..fbb78fbda 100644 --- a/lib/image_providers/region_provider.dart +++ b/lib/image_providers/region_provider.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'dart:math'; -import 'dart:ui' as ui show Codec; +import 'dart:ui' as ui; import 'package:aves/services/common/services.dart'; import 'package:equatable/equatable.dart'; @@ -18,7 +18,7 @@ class RegionProvider extends ImageProvider { } @override - ImageStreamCompleter load(RegionProviderKey key, DecoderCallback decode) { + ImageStreamCompleter loadBuffer(RegionProviderKey key, DecoderBufferCallback decode) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), scale: 1.0, @@ -28,12 +28,12 @@ class RegionProvider extends ImageProvider { ); } - Future _loadAsync(RegionProviderKey key, DecoderCallback decode) async { + Future _loadAsync(RegionProviderKey key, DecoderBufferCallback decode) async { final uri = key.uri; final mimeType = key.mimeType; final pageId = key.pageId; try { - final bytes = await mediaFileService.getRegion( + final bytes = await mediaFetchService.getRegion( uri, mimeType, key.rotationDegrees, @@ -47,7 +47,8 @@ class RegionProvider extends ImageProvider { if (bytes.isEmpty) { throw StateError('$uri ($mimeType) region loading failed'); } - return await decode(bytes); + final buffer = await ui.ImmutableBuffer.fromUint8List(bytes); + return await decode(buffer); } catch (error) { // loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF) debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error'); @@ -57,11 +58,11 @@ class RegionProvider extends ImageProvider { @override void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, RegionProviderKey key, ImageErrorListener handleError) { - mediaFileService.resumeLoading(key); + mediaFetchService.resumeLoading(key); super.resolveStreamForKey(configuration, stream, key, handleError); } - void pause() => mediaFileService.cancelRegion(key); + void pause() => mediaFetchService.cancelRegion(key); } @immutable diff --git a/lib/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart index 3c5b5699c..bf13c32bc 100644 --- a/lib/image_providers/thumbnail_provider.dart +++ b/lib/image_providers/thumbnail_provider.dart @@ -1,4 +1,4 @@ -import 'dart:ui' as ui show Codec; +import 'dart:ui' as ui; import 'package:aves/services/common/services.dart'; import 'package:equatable/equatable.dart'; @@ -19,7 +19,7 @@ class ThumbnailProvider extends ImageProvider { } @override - ImageStreamCompleter load(ThumbnailProviderKey key, DecoderCallback decode) { + ImageStreamCompleter loadBuffer(ThumbnailProviderKey key, DecoderBufferCallback decode) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), scale: 1.0, @@ -30,12 +30,12 @@ class ThumbnailProvider extends ImageProvider { ); } - Future _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async { + Future _loadAsync(ThumbnailProviderKey key, DecoderBufferCallback decode) async { final uri = key.uri; final mimeType = key.mimeType; final pageId = key.pageId; try { - final bytes = await mediaFileService.getThumbnail( + final bytes = await mediaFetchService.getThumbnail( uri: uri, mimeType: mimeType, pageId: pageId, @@ -48,7 +48,8 @@ class ThumbnailProvider extends ImageProvider { if (bytes.isEmpty) { throw StateError('$uri ($mimeType) loading failed'); } - return await decode(bytes); + final buffer = await ui.ImmutableBuffer.fromUint8List(bytes); + return await decode(buffer); } catch (error) { // loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF) debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error'); @@ -58,11 +59,11 @@ class ThumbnailProvider extends ImageProvider { @override void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) { - mediaFileService.resumeLoading(key); + mediaFetchService.resumeLoading(key); super.resolveStreamForKey(configuration, stream, key, handleError); } - void pause() => mediaFileService.cancelThumbnail(key); + void pause() => mediaFetchService.cancelThumbnail(key); } @immutable diff --git a/lib/image_providers/uri_image_provider.dart b/lib/image_providers/uri_image_provider.dart index 1aeca959e..e2850b0e2 100644 --- a/lib/image_providers/uri_image_provider.dart +++ b/lib/image_providers/uri_image_provider.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'dart:ui' as ui show Codec; +import 'dart:ui' as ui; import 'package:aves/services/common/services.dart'; import 'package:equatable/equatable.dart'; @@ -32,7 +32,7 @@ class UriImage extends ImageProvider with EquatableMixin { } @override - ImageStreamCompleter load(UriImage key, DecoderCallback decode) { + ImageStreamCompleter loadBuffer(UriImage key, DecoderBufferCallback decode) { final chunkEvents = StreamController(); return MultiFrameImageStreamCompleter( @@ -45,11 +45,11 @@ class UriImage extends ImageProvider with EquatableMixin { ); } - Future _loadAsync(UriImage key, DecoderCallback decode, StreamController chunkEvents) async { + Future _loadAsync(UriImage key, DecoderBufferCallback decode, StreamController chunkEvents) async { assert(key == this); try { - final bytes = await mediaFileService.getImage( + final bytes = await mediaFetchService.getImage( uri, mimeType, rotationDegrees, @@ -66,7 +66,8 @@ class UriImage extends ImageProvider with EquatableMixin { if (bytes.isEmpty) { throw StateError('$uri ($mimeType) loading failed'); } - return await decode(bytes); + final buffer = await ui.ImmutableBuffer.fromUint8List(bytes); + return await decode(buffer); } catch (error) { // loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF) debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error'); diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 893f94f9f..0b6314a68 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -25,7 +25,8 @@ "showTooltip": "Anzeigen", "hideTooltip": "Ausblenden", "actionRemove": "Entfernen", - "resetButtonTooltip": "Zurücksetzen", + "resetTooltip": "Zurücksetzen", + "saveTooltip": "Speichern", "doubleBackExitMessage": "Zum Verlassen erneut auf „Zurück“ tippen.", "doNotAskAgain": "Nicht noch einmal fragen", @@ -94,6 +95,7 @@ "filterFavouriteLabel": "Favorit", "filterLocationEmptyLabel": "Ungeortet", "filterTagEmptyLabel": "Unmarkiert", + "filterOnThisDayLabel": "Am heutigen Tag", "filterRatingUnratedLabel": "Nicht bewertet", "filterRatingRejectedLabel": "Verworfen", "filterTypeAnimatedLabel": "Animationen", @@ -303,7 +305,6 @@ "aboutBug": "Fehlerbericht", "aboutBugSaveLogInstruction": "Anwendungsprotokolle in einer Datei speichern", - "aboutBugSaveLogButton": "Speichern", "aboutBugCopyInfoInstruction": "Systeminformationen kopieren", "aboutBugCopyInfoButton": "Kopieren", "aboutBugReportInstruction": "Bericht auf GitHub mit den Protokollen und Systeminformationen", @@ -417,6 +418,7 @@ "searchCollectionFieldHint": "Sammlung durchsuchen", "searchSectionRecent": "Neueste", + "searchSectionDate": "Datum", "searchSectionAlbums": "Alben", "searchSectionCountries": "Länder", "searchSectionPlaces": "Orte", @@ -502,6 +504,7 @@ "settingsViewerSlideshowTitle": "Diashow", "settingsSlideshowRepeat": "Wiederholung", "settingsSlideshowShuffle": "Mischen", + "settingsSlideshowFillScreen": "Bildschirm ausfüllen", "settingsSlideshowTransitionTile": "Übergang", "settingsSlideshowTransitionTitle": "Übergang", "settingsSlideshowIntervalTile": "Intervall", @@ -584,6 +587,11 @@ "settingsUnitSystemTile": "Einheiten", "settingsUnitSystemTitle": "Einheiten", + "settingsScreenSaverPageTitle": "Bildschirmschoner", + + "settingsWidgetPageTitle": "Bilderrahmen", + "settingsWidgetShowOutline": "Gliederung", + "statsPageTitle": "Statistiken", "statsWithGps": "{count, plural, =1{1 Element mit Standort} other{{count} Elemente mit Standort}}", "statsTopCountries": "Top-Länder", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5657ade65..049cc3542 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -53,7 +53,8 @@ "showTooltip": "Show", "hideTooltip": "Hide", "actionRemove": "Remove", - "resetButtonTooltip": "Reset", + "resetTooltip": "Reset", + "saveTooltip": "Save", "doubleBackExitMessage": "Tap “back” again to exit.", "doNotAskAgain": "Do not ask again", @@ -122,6 +123,7 @@ "filterFavouriteLabel": "Favorite", "filterLocationEmptyLabel": "Unlocated", "filterTagEmptyLabel": "Untagged", + "filterOnThisDayLabel": "On this day", "filterRatingUnratedLabel": "Unrated", "filterRatingRejectedLabel": "Rejected", "filterTypeAnimatedLabel": "Animated", @@ -433,7 +435,6 @@ "aboutBug": "Bug Report", "aboutBugSaveLogInstruction": "Save app logs to a file", - "aboutBugSaveLogButton": "Save", "aboutBugCopyInfoInstruction": "Copy system information", "aboutBugCopyInfoButton": "Copy", "aboutBugReportInstruction": "Report on GitHub with the logs and system information", @@ -597,6 +598,7 @@ "searchCollectionFieldHint": "Search collection", "searchSectionRecent": "Recent", + "searchSectionDate": "Date", "searchSectionAlbums": "Albums", "searchSectionCountries": "Countries", "searchSectionPlaces": "Places", @@ -682,6 +684,7 @@ "settingsViewerSlideshowTitle": "Slideshow", "settingsSlideshowRepeat": "Repeat", "settingsSlideshowShuffle": "Shuffle", + "settingsSlideshowFillScreen": "Fill screen", "settingsSlideshowTransitionTile": "Transition", "settingsSlideshowTransitionTitle": "Transition", "settingsSlideshowIntervalTile": "Interval", @@ -764,6 +767,11 @@ "settingsUnitSystemTile": "Units", "settingsUnitSystemTitle": "Units", + "settingsScreenSaverPageTitle": "Screen Saver", + + "settingsWidgetPageTitle": "Photo Frame", + "settingsWidgetShowOutline": "Outline", + "statsPageTitle": "Stats", "statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}", "@statsWithGps": { diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index f99ee17d3..f9bfde672 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -25,7 +25,8 @@ "showTooltip": "Mostrar", "hideTooltip": "Ocultar", "actionRemove": "Remover", - "resetButtonTooltip": "Restablecer", + "resetTooltip": "Restablecer", + "saveTooltip": "Guardar", "doubleBackExitMessage": "Presione «atrás» nuevamente para salir.", "doNotAskAgain": "No preguntar nuevamente", @@ -303,7 +304,6 @@ "aboutBug": "Reporte de errores", "aboutBugSaveLogInstruction": "Guardar registros de la aplicación a un archivo", - "aboutBugSaveLogButton": "Guardar", "aboutBugCopyInfoInstruction": "Copiar información del sistema", "aboutBugCopyInfoButton": "Copiar", "aboutBugReportInstruction": "Reportar en GitHub con los registros y la información del sistema", @@ -417,6 +417,7 @@ "searchCollectionFieldHint": "Buscar en colección", "searchSectionRecent": "Reciente", + "searchSectionDate": "Fecha", "searchSectionAlbums": "Álbumes", "searchSectionCountries": "Países", "searchSectionPlaces": "Lugares", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7c7437f10..95c0e5529 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -25,7 +25,8 @@ "showTooltip": "Afficher", "hideTooltip": "Masquer", "actionRemove": "Supprimer", - "resetButtonTooltip": "Réinitialiser", + "resetTooltip": "Réinitialiser", + "saveTooltip": "Sauvegarder", "doubleBackExitMessage": "Pressez «\u00A0retour\u00A0» à nouveau pour quitter.", "doNotAskAgain": "Ne pas demander de nouveau", @@ -94,6 +95,7 @@ "filterFavouriteLabel": "Favori", "filterLocationEmptyLabel": "Sans lieu", "filterTagEmptyLabel": "Sans libellé", + "filterOnThisDayLabel": "Ce jour-là", "filterRatingUnratedLabel": "Sans notation", "filterRatingRejectedLabel": "Rejeté", "filterTypeAnimatedLabel": "Animation", @@ -303,7 +305,6 @@ "aboutBug": "Rapports d’erreur", "aboutBugSaveLogInstruction": "Sauvegarder les logs de l’app vers un fichier", - "aboutBugSaveLogButton": "Sauvegarder", "aboutBugCopyInfoInstruction": "Copier les informations d’environnement", "aboutBugCopyInfoButton": "Copier", "aboutBugReportInstruction": "Créer une «\u00A0issue\u00A0» sur GitHub en attachant les logs et informations d’environnement", @@ -417,6 +418,7 @@ "searchCollectionFieldHint": "Recherche", "searchSectionRecent": "Historique", + "searchSectionDate": "Date", "searchSectionAlbums": "Albums", "searchSectionCountries": "Pays", "searchSectionPlaces": "Lieux", @@ -502,6 +504,7 @@ "settingsViewerSlideshowTitle": "Diaporama", "settingsSlideshowRepeat": "Répéter", "settingsSlideshowShuffle": "Aléatoire", + "settingsSlideshowFillScreen": "Remplir l’écran", "settingsSlideshowTransitionTile": "Transition", "settingsSlideshowTransitionTitle": "Transition", "settingsSlideshowIntervalTile": "Intervalle", @@ -574,7 +577,7 @@ "settingsThemeBrightness": "Thème", "settingsThemeColorHighlights": "Surlignages colorés", "settingsThemeEnableDynamicColor": "Couleur dynamique", - "settingsDisplayRefreshRateModeTile": "Fréquence d’actualisation de l'écran", + "settingsDisplayRefreshRateModeTile": "Fréquence d’actualisation de l’écran", "settingsDisplayRefreshRateModeTitle": "Fréquence d’actualisation", "settingsSectionLanguage": "Langue & Formats", @@ -584,6 +587,11 @@ "settingsUnitSystemTile": "Unités", "settingsUnitSystemTitle": "Unités", + "settingsScreenSaverPageTitle": "Économiseur d’écran", + + "settingsWidgetPageTitle": "Cadre photo", + "settingsWidgetShowOutline": "Contours", + "statsPageTitle": "Statistiques", "statsWithGps": "{count, plural, =1{1 élément localisé} other{{count} éléments localisés}}", "statsTopCountries": "Top pays", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 295d70ad8..c067ecd89 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -25,7 +25,8 @@ "showTooltip": "Tampilkan", "hideTooltip": "Sembunyikan", "actionRemove": "Hapus", - "resetButtonTooltip": "Ulang", + "resetTooltip": "Ulang", + "saveTooltip": "Simpan", "doubleBackExitMessage": "Ketuk “kembali” lagi untuk keluar.", "doNotAskAgain": "Jangan tanya lagi", @@ -81,6 +82,9 @@ "videoActionSetSpeed": "Kecepatan pemutaran", "videoActionSettings": "Pengaturan", + "slideshowActionResume": "Lanjutkan", + "slideshowActionShowInCollection": "Tampilkan di Koleksi", + "entryInfoActionEditDate": "Ubah tanggal & waktu", "entryInfoActionEditLocation": "Ubah lokasi", "entryInfoActionEditRating": "Ubah nilai", @@ -145,10 +149,23 @@ "displayRefreshRatePreferHighest": "Penyegaran tertinggi", "displayRefreshRatePreferLowest": "Penyegaran terendah", + "slideshowVideoPlaybackSkip": "Lewati", + "slideshowVideoPlaybackMuted": "Mainkan bisu", + "slideshowVideoPlaybackWithSound": "Mainkan dengan suara", + "themeBrightnessLight": "Terang", "themeBrightnessDark": "Gelap", "themeBrightnessBlack": "Hitam", + "viewerTransitionSlide": "Menggeser", + "viewerTransitionParallax": "Paralaks", + "viewerTransitionFade": "Memudar", + "viewerTransitionZoomIn": "Membesar", + + "wallpaperTargetHome": "Tampilan depan", + "wallpaperTargetLock": "Tampilan kunci", + "wallpaperTargetHomeLock": "Tampilan depan dan kunci", + "albumTierNew": "Baru", "albumTierPinned": "Disemat", "albumTierSpecial": "Biasa", @@ -263,6 +280,7 @@ "menuActionSelectAll": "Pilih semua", "menuActionSelectNone": "Pilih tidak ada", "menuActionMap": "Peta", + "menuActionSlideshow": "Tampilan slide", "menuActionStats": "Statistik", "viewDialogTabSort": "Sortir", @@ -286,7 +304,6 @@ "aboutBug": "Lapor Bug", "aboutBugSaveLogInstruction": "Simpan log aplikasi ke file", - "aboutBugSaveLogButton": "Simpan", "aboutBugCopyInfoInstruction": "Salin informasi sistem", "aboutBugCopyInfoButton": "Salin", "aboutBugReportInstruction": "Laporkan ke GitHub dengan log dan informasi sistem", @@ -350,6 +367,7 @@ "collectionEmptyFavourites": "Tidak ada favorit", "collectionEmptyVideos": "Tidak ada video", "collectionEmptyImages": "Tidak ada gambar", + "collectionEmptyGrantAccessButtonLabel": "Berikan akses", "collectionSelectSectionTooltip": "Pilih bagian", "collectionDeselectSectionTooltip": "Batalkan pilihan bagian", @@ -399,6 +417,7 @@ "searchCollectionFieldHint": "Cari koleksi", "searchSectionRecent": "Terkini", + "searchSectionDate": "Tanggal", "searchSectionAlbums": "Album", "searchSectionCountries": "Negara", "searchSectionPlaces": "Tempat", @@ -480,6 +499,17 @@ "settingsViewerShowOverlayThumbnails": "Tampilkan thumbnail", "settingsViewerEnableOverlayBlurEffect": "Efek Kabur", + "settingsViewerSlideshowTile": "Tampilan slide", + "settingsViewerSlideshowTitle": "Tampilan Slide", + "settingsSlideshowRepeat": "Ulangi", + "settingsSlideshowShuffle": "Acak", + "settingsSlideshowTransitionTile": "Transisi", + "settingsSlideshowTransitionTitle": "Transisi", + "settingsSlideshowIntervalTile": "Interval", + "settingsSlideshowIntervalTitle": "Interval", + "settingsSlideshowVideoPlaybackTile": "Putaran ulang video", + "settingsSlideshowVideoPlaybackTitle": "Putaran Ulang Video", + "settingsVideoPageTitle": "Pengaturan Video", "settingsSectionVideo": "Video", "settingsVideoShowVideos": "Tampilkan video", @@ -544,6 +574,7 @@ "settingsSectionDisplay": "Tampilan", "settingsThemeBrightness": "Tema", "settingsThemeColorHighlights": "Highlight warna", + "settingsThemeEnableDynamicColor": "Warna dinamis", "settingsDisplayRefreshRateModeTile": "Tingkat penyegaran tampilan", "settingsDisplayRefreshRateModeTitle": "Tingkat Penyegaran", @@ -561,6 +592,7 @@ "statsTopTags": "Label Teratas", "viewerOpenPanoramaButtonLabel": "BUKA PANORAMA", + "viewerSetWallpaperButtonLabel": "TETAPKAN SEBAGAI WALLPAPER", "viewerErrorUnknown": "Ups!", "viewerErrorDoesNotExist": "File tidak ada lagi.", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 33b957412..6527e85bf 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -25,7 +25,8 @@ "showTooltip": "Mostra", "hideTooltip": "Nascondi", "actionRemove": "Rimuovi", - "resetButtonTooltip": "Reimposta", + "resetTooltip": "Reimposta", + "saveTooltip": "Salva", "doubleBackExitMessage": "Tocca di nuovo «indietro» per uscire", "doNotAskAgain": "Non chiedere di nuovo", @@ -94,6 +95,7 @@ "filterFavouriteLabel": "Preferiti", "filterLocationEmptyLabel": "Senza posizione", "filterTagEmptyLabel": "Senza etichetta", + "filterOnThisDayLabel": "In questo giorno", "filterRatingUnratedLabel": "Non valutato", "filterRatingRejectedLabel": "Rifiutato", "filterTypeAnimatedLabel": "Animato", @@ -303,7 +305,6 @@ "aboutBug": "Segnalazione bug", "aboutBugSaveLogInstruction": "Salva i log dell’app in un file", - "aboutBugSaveLogButton": "Salva", "aboutBugCopyInfoInstruction": "Copia le informazioni di sistema", "aboutBugCopyInfoButton": "Copia", "aboutBugReportInstruction": "Segnala su GitHub con i log e le informazioni di sistema", @@ -417,6 +418,7 @@ "searchCollectionFieldHint": "Cerca raccolta", "searchSectionRecent": "Recenti", + "searchSectionDate": "Data", "searchSectionAlbums": "Album", "searchSectionCountries": "Paesi", "searchSectionPlaces": "Luoghi", @@ -502,6 +504,7 @@ "settingsViewerSlideshowTitle": "Presentazione", "settingsSlideshowRepeat": "Ripeti", "settingsSlideshowShuffle": "Ordine casuale", + "settingsSlideshowFillScreen": "Riempi schermo", "settingsSlideshowTransitionTile": "Transizione", "settingsSlideshowTransitionTitle": "Transizione", "settingsSlideshowIntervalTile": "Intervallo", @@ -584,6 +587,11 @@ "settingsUnitSystemTile": "Unità", "settingsUnitSystemTitle": "Unità", + "settingsScreenSaverPageTitle": "Salvaschermo", + + "settingsWidgetPageTitle": "Cornice foto", + "settingsWidgetShowOutline": "Contorno", + "statsPageTitle": "Statistiche", "statsWithGps": "{count, plural, =1{1 elemento con posizione} other{{count} elementi con posizione}}", "statsTopCountries": "Paesi più frequenti", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 1e5881ae2..70da86577 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -25,7 +25,8 @@ "showTooltip": "表示する", "hideTooltip": "非表示にする", "actionRemove": "削除", - "resetButtonTooltip": "リセット", + "resetTooltip": "リセット", + "saveTooltip": "保存", "doubleBackExitMessage": "終了するには「戻る」をもう一度タップしてください。", "doNotAskAgain": "今後このメッセージを表示しない", @@ -81,6 +82,9 @@ "videoActionSetSpeed": "再生速度", "videoActionSettings": "設定", + "slideshowActionResume": "再開", + "slideshowActionShowInCollection": "コレクションで表示", + "entryInfoActionEditDate": "日時を編集", "entryInfoActionEditLocation": "位置情報を編集", "entryInfoActionEditRating": "評価を編集", @@ -91,6 +95,7 @@ "filterFavouriteLabel": "お気に入り", "filterLocationEmptyLabel": "位置情報なし", "filterTagEmptyLabel": "タグ情報なし", + "filterOnThisDayLabel": "過去のこの日", "filterRatingUnratedLabel": "評価情報なし", "filterRatingRejectedLabel": "拒否", "filterTypeAnimatedLabel": "アニメーション", @@ -145,10 +150,23 @@ "displayRefreshRatePreferHighest": "高レート", "displayRefreshRatePreferLowest": "低レート", + "slideshowVideoPlaybackSkip": "スキップ", + "slideshowVideoPlaybackMuted": "ミュート再生", + "slideshowVideoPlaybackWithSound": "音声あり再生", + "themeBrightnessLight": "ライト", "themeBrightnessDark": "ダーク", "themeBrightnessBlack": "ブラック", + "viewerTransitionSlide": "スライド", + "viewerTransitionParallax": "パララックス", + "viewerTransitionFade": "フェード", + "viewerTransitionZoomIn": "ズームイン", + + "wallpaperTargetHome": "ホーム画面", + "wallpaperTargetLock": "ロック画面", + "wallpaperTargetHomeLock": "ホームおよびロック画面", + "albumTierNew": "新規", "albumTierPinned": "固定", "albumTierSpecial": "全体", @@ -263,6 +281,7 @@ "menuActionSelectAll": "すべて選択", "menuActionSelectNone": "選択を解除", "menuActionMap": "地図", + "menuActionSlideshow": "スライドショー", "menuActionStats": "統計", "viewDialogTabSort": "並べ替え", @@ -286,7 +305,6 @@ "aboutBug": "バグの報告", "aboutBugSaveLogInstruction": "アプリのログをファイルに保存", - "aboutBugSaveLogButton": "保存", "aboutBugCopyInfoInstruction": "システム情報をコピー", "aboutBugCopyInfoButton": "コピー", "aboutBugReportInstruction": "ログとシステム情報とともに GitHub で報告", @@ -350,6 +368,7 @@ "collectionEmptyFavourites": "お気に入りはありません", "collectionEmptyVideos": "動画はありません", "collectionEmptyImages": "画像はありません", + "collectionEmptyGrantAccessButtonLabel": "アクセスを許可", "collectionSelectSectionTooltip": "セクションを選択", "collectionDeselectSectionTooltip": "セクションの選択を解除", @@ -399,6 +418,7 @@ "searchCollectionFieldHint": "コレクションを検索", "searchSectionRecent": "最近", + "searchSectionDate": "日付", "searchSectionAlbums": "アルバム", "searchSectionCountries": "国", "searchSectionPlaces": "場所", @@ -480,6 +500,18 @@ "settingsViewerShowOverlayThumbnails": "サムネイルを表示", "settingsViewerEnableOverlayBlurEffect": "ぼかし効果", + "settingsViewerSlideshowTile": "スライドショー", + "settingsViewerSlideshowTitle": "スライドショー", + "settingsSlideshowRepeat": "繰り返し", + "settingsSlideshowShuffle": "シャッフル", + "settingsSlideshowFillScreen": "画面いっぱいに表示", + "settingsSlideshowTransitionTile": "トランジション", + "settingsSlideshowTransitionTitle": "トランジション", + "settingsSlideshowIntervalTile": "間隔", + "settingsSlideshowIntervalTitle": "間隔", + "settingsSlideshowVideoPlaybackTile": "動画を再生", + "settingsSlideshowVideoPlaybackTitle": "動画再生", + "settingsVideoPageTitle": "動画設定", "settingsSectionVideo": "動画", "settingsVideoShowVideos": "動画を表示", @@ -544,6 +576,7 @@ "settingsSectionDisplay": "ディスプレイ", "settingsThemeBrightness": "テーマ", "settingsThemeColorHighlights": "カラー強調表示", + "settingsThemeEnableDynamicColor": "ダイナミックカラー", "settingsDisplayRefreshRateModeTile": "ディスプレイ リフレッシュ レート", "settingsDisplayRefreshRateModeTitle": "リフレッシュ レート", @@ -554,6 +587,11 @@ "settingsUnitSystemTile": "単位", "settingsUnitSystemTitle": "単位", + "settingsScreenSaverPageTitle": "スクリーンセーバー", + + "settingsWidgetPageTitle": "フォトフレーム", + "settingsWidgetShowOutline": "枠", + "statsPageTitle": "統計", "statsWithGps": "{count, plural, other{位置情報のあるアイテム {count} 件}}", "statsTopCountries": "上位の国", @@ -561,6 +599,7 @@ "statsTopTags": "上位のタグ", "viewerOpenPanoramaButtonLabel": "パノラマを開く", + "viewerSetWallpaperButtonLabel": "壁紙設定", "viewerErrorUnknown": "エラー", "viewerErrorDoesNotExist": "ファイルが存在しません。", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 91bc57c73..81d606add 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -25,7 +25,8 @@ "showTooltip": "보기", "hideTooltip": "숨기기", "actionRemove": "제거", - "resetButtonTooltip": "복원", + "resetTooltip": "복원", + "saveTooltip": "저장", "doubleBackExitMessage": "종료하려면 한번 더 누르세요.", "doNotAskAgain": "다시 묻지 않기", @@ -94,6 +95,7 @@ "filterFavouriteLabel": "즐겨찾기", "filterLocationEmptyLabel": "장소 없음", "filterTagEmptyLabel": "태그 없음", + "filterOnThisDayLabel": "이 날", "filterRatingUnratedLabel": "별점 없음", "filterRatingRejectedLabel": "거부됨", "filterTypeAnimatedLabel": "애니메이션", @@ -303,7 +305,6 @@ "aboutBug": "버그 보고", "aboutBugSaveLogInstruction": "앱 로그를 파일에 저장하기", - "aboutBugSaveLogButton": "저장", "aboutBugCopyInfoInstruction": "시스템 정보를 복사하기", "aboutBugCopyInfoButton": "복사", "aboutBugReportInstruction": "로그와 시스템 정보를 첨부하여 깃허브에서 이슈를 제출하기", @@ -417,6 +418,7 @@ "searchCollectionFieldHint": "미디어 검색", "searchSectionRecent": "최근 검색기록", + "searchSectionDate": "날짜", "searchSectionAlbums": "앨범", "searchSectionCountries": "국가", "searchSectionPlaces": "장소", @@ -502,6 +504,7 @@ "settingsViewerSlideshowTitle": "슬라이드쇼", "settingsSlideshowRepeat": "반복", "settingsSlideshowShuffle": "순서섞기", + "settingsSlideshowFillScreen": "화면 채우기", "settingsSlideshowTransitionTile": "전환 효과", "settingsSlideshowTransitionTitle": "전환 효과", "settingsSlideshowIntervalTile": "교체 주기", @@ -584,6 +587,11 @@ "settingsUnitSystemTile": "단위법", "settingsUnitSystemTitle": "단위법", + "settingsScreenSaverPageTitle": "화면보호기", + + "settingsWidgetPageTitle": "사진 액자", + "settingsWidgetShowOutline": "윤곽", + "statsPageTitle": "통계", "statsWithGps": "{count, plural, other{{count}개 위치가 있음}}", "statsTopCountries": "국가 랭킹", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index de05648f1..fc09762ba 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -25,7 +25,8 @@ "showTooltip": "Mostrar", "hideTooltip": "Ocultar", "actionRemove": "Remover", - "resetButtonTooltip": "Resetar", + "resetTooltip": "Resetar", + "saveTooltip": "Salve", "doubleBackExitMessage": "Toque em “voltar” novamente para sair.", "doNotAskAgain": "Não pergunte novamente", @@ -94,6 +95,7 @@ "filterFavouriteLabel": "Favorito", "filterLocationEmptyLabel": "Não localizado", "filterTagEmptyLabel": "Sem etiqueta", + "filterOnThisDayLabel": "Neste dia", "filterRatingUnratedLabel": "Sem classificação", "filterRatingRejectedLabel": "Rejeitado", "filterTypeAnimatedLabel": "Animado", @@ -303,7 +305,6 @@ "aboutBug": "Relatório de erro", "aboutBugSaveLogInstruction": "Salvar registros de aplicativos em um arquivo", - "aboutBugSaveLogButton": "Salve", "aboutBugCopyInfoInstruction": "Copiar informações do sistema", "aboutBugCopyInfoButton": "Copiar", "aboutBugReportInstruction": "Relatório no GitHub com os logs e informações do sistema", @@ -417,6 +418,7 @@ "searchCollectionFieldHint": "Pesquisar coleção", "searchSectionRecent": "Recente", + "searchSectionDate": "Data", "searchSectionAlbums": "Álbuns", "searchSectionCountries": "Países", "searchSectionPlaces": "Locais", @@ -502,6 +504,7 @@ "settingsViewerSlideshowTitle": "Apresentação de slides", "settingsSlideshowRepeat": "Repetir", "settingsSlideshowShuffle": "Embaralhar", + "settingsSlideshowFillScreen": "Preencher tela", "settingsSlideshowTransitionTile": "Transição", "settingsSlideshowTransitionTitle": "Transição", "settingsSlideshowIntervalTile": "Intervalo", @@ -584,6 +587,11 @@ "settingsUnitSystemTile": "Unidades", "settingsUnitSystemTitle": "Unidades", + "settingsScreenSaverPageTitle": "Protetor de tela", + + "settingsWidgetPageTitle": "Porta-retratos", + "settingsWidgetShowOutline": "Contorno", + "statsPageTitle": "Estatísticas", "statsWithGps": "{count, plural, =1{1 item com localização} other{{count} itens com localização}}", "statsTopCountries": "Principais Países", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b8f7efa3a..3a3a14338 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -25,7 +25,8 @@ "showTooltip": "Показать", "hideTooltip": "Скрыть", "actionRemove": "Удалить", - "resetButtonTooltip": "Сбросить", + "resetTooltip": "Сбросить", + "saveTooltip": "Сохранить", "doubleBackExitMessage": "Нажмите «Назад» еще раз, чтобы выйти.", "doNotAskAgain": "Больше не спрашивать", @@ -81,6 +82,9 @@ "videoActionSetSpeed": "Скорость вопспроизведения", "videoActionSettings": "Настройки", + "slideshowActionResume": "Продолжить", + "slideshowActionShowInCollection": "Показать в Коллекции", + "entryInfoActionEditDate": "Изменить дату и время", "entryInfoActionEditLocation": "Изменить местоположение", "entryInfoActionEditRating": "Изменить рейтинг", @@ -145,10 +149,23 @@ "displayRefreshRatePreferHighest": "Наивысшая частота", "displayRefreshRatePreferLowest": "Наименьшая частота", + "slideshowVideoPlaybackSkip": "Пропустить", + "slideshowVideoPlaybackMuted": "Играть без звука", + "slideshowVideoPlaybackWithSound": "Играть со звуком", + "themeBrightnessLight": "Светлая", "themeBrightnessDark": "Тёмная", "themeBrightnessBlack": "Чёрная", + "viewerTransitionSlide": "Скольжение", + "viewerTransitionParallax": "Параллакс", + "viewerTransitionFade": "Затухание", + "viewerTransitionZoomIn": "Приближение", + + "wallpaperTargetHome": "Домашний экран", + "wallpaperTargetLock": "Экран блокировки", + "wallpaperTargetHomeLock": "Домашний экран и экран блокировки", + "albumTierNew": "Новые", "albumTierPinned": "Закрепленные", "albumTierSpecial": "Стандартные", @@ -263,6 +280,7 @@ "menuActionSelectAll": "Выбрать все", "menuActionSelectNone": "Снять выделение", "menuActionMap": "Карта", + "menuActionSlideshow": "Слайд-шоу", "menuActionStats": "Статистика", "viewDialogTabSort": "Сортировка", @@ -286,7 +304,6 @@ "aboutBug": "Отчет об ошибке", "aboutBugSaveLogInstruction": "Сохраните логи приложения в файл", - "aboutBugSaveLogButton": "Сохранить", "aboutBugCopyInfoInstruction": "Скопируйте системную информацию", "aboutBugCopyInfoButton": "Скопировать", "aboutBugReportInstruction": "Отправьте отчёт об ошибке на GitHub вместе с логами и системной информацией", @@ -350,6 +367,7 @@ "collectionEmptyFavourites": "Нет избранных", "collectionEmptyVideos": "Нет видео", "collectionEmptyImages": "Нет изображений", + "collectionEmptyGrantAccessButtonLabel": "Предоставить доступ", "collectionSelectSectionTooltip": "Выбрать раздел", "collectionDeselectSectionTooltip": "Снять выбор с раздела", @@ -399,6 +417,7 @@ "searchCollectionFieldHint": "Поиск по коллекции", "searchSectionRecent": "Недавние", + "searchSectionDate": "Дата", "searchSectionAlbums": "Альбомы", "searchSectionCountries": "Страны", "searchSectionPlaces": "Локации", @@ -480,6 +499,17 @@ "settingsViewerShowOverlayThumbnails": "Показать эскизы", "settingsViewerEnableOverlayBlurEffect": "Наложение эффекта размытия", + "settingsViewerSlideshowTile": "Слайд-шоу", + "settingsViewerSlideshowTitle": "Слайд-шоу", + "settingsSlideshowRepeat": "Повтор", + "settingsSlideshowShuffle": "Вперемешку", + "settingsSlideshowTransitionTile": "Эффект перехода", + "settingsSlideshowTransitionTitle": "Эффект Перехода", + "settingsSlideshowIntervalTile": "Интервал", + "settingsSlideshowIntervalTitle": "Интервал", + "settingsSlideshowVideoPlaybackTile": "Проигрывание видео", + "settingsSlideshowVideoPlaybackTitle": "Проигрывание Видео", + "settingsVideoPageTitle": "Настройки видео", "settingsSectionVideo": "Видео", "settingsVideoShowVideos": "Показать видео", @@ -544,6 +574,7 @@ "settingsSectionDisplay": "Отображение", "settingsThemeBrightness": "Тема", "settingsThemeColorHighlights": "Цветовые акценты", + "settingsThemeEnableDynamicColor": "Динамический цвет", "settingsDisplayRefreshRateModeTile": "Частота обновления экрана", "settingsDisplayRefreshRateModeTitle": "Частота обновления", @@ -561,6 +592,7 @@ "statsTopTags": "Топ тегов", "viewerOpenPanoramaButtonLabel": "ОТКРЫТЬ ПАНОРАМУ", + "viewerSetWallpaperButtonLabel": "УСТАНОВИТЬ КАК ОБОИ", "viewerErrorUnknown": "Упс!", "viewerErrorDoesNotExist": "Файл больше не существует.", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 84c152411..fdd7fb364 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -25,7 +25,8 @@ "showTooltip": "Göster", "hideTooltip": "Gizle", "actionRemove": "Kaldır", - "resetButtonTooltip": "Sıfırla", + "resetTooltip": "Sıfırla", + "saveTooltip": "Kaydet", "doubleBackExitMessage": "Çıkmak için tekrar “geri”, düğmesine dokunun.", "doNotAskAgain": "Bir daha sorma", @@ -295,7 +296,6 @@ "aboutBug": "Hata Bildirimi", "aboutBugSaveLogInstruction": "Uygulama günlüklerini bir dosyaya kaydet", - "aboutBugSaveLogButton": "Kaydet", "aboutBugCopyInfoInstruction": "Sistem bilgilerini kopyala", "aboutBugCopyInfoButton": "Kopyala", "aboutBugReportInstruction": "GitHub'da günlükleri ve sistem bilgilerini içeren bir rapor oluştur", @@ -418,6 +418,7 @@ "searchCollectionFieldHint": "Koleksiyonu ara", "searchSectionRecent": "Yakın zamanda", + "searchSectionDate": "Tarih", "searchSectionAlbums": "Albümler", "searchSectionCountries": "Ülkeler", "searchSectionPlaces": "Yerler", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1cadd97fd..9a6f0ea2a 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -25,7 +25,8 @@ "showTooltip": "显示", "hideTooltip": "隐藏", "actionRemove": "移除", - "resetButtonTooltip": "重置", + "resetTooltip": "重置", + "saveTooltip": "保存", "doubleBackExitMessage": "再按一次退出", "doNotAskAgain": "不再询问", @@ -94,6 +95,7 @@ "filterFavouriteLabel": "收藏夹", "filterLocationEmptyLabel": "未定位", "filterTagEmptyLabel": "无标签", + "filterOnThisDayLabel": "选择日期", "filterRatingUnratedLabel": "未评分", "filterRatingRejectedLabel": "拒绝", "filterTypeAnimatedLabel": "动画", @@ -303,7 +305,6 @@ "aboutBug": "报告错误", "aboutBugSaveLogInstruction": "将应用日志保存到文件", - "aboutBugSaveLogButton": "保存", "aboutBugCopyInfoInstruction": "复制系统信息", "aboutBugCopyInfoButton": "复制", "aboutBugReportInstruction": "在 GitHub 上报告日志和系统信息", @@ -417,6 +418,7 @@ "searchCollectionFieldHint": "搜索媒体集", "searchSectionRecent": "最近", + "searchSectionDate": "日期", "searchSectionAlbums": "相册", "searchSectionCountries": "国家", "searchSectionPlaces": "地点", @@ -502,6 +504,7 @@ "settingsViewerSlideshowTitle": "幻灯片", "settingsSlideshowRepeat": "重复", "settingsSlideshowShuffle": "随机播放", + "settingsSlideshowFillScreen": "填充屏幕", "settingsSlideshowTransitionTile": "过渡动画", "settingsSlideshowTransitionTitle": "过渡动画", "settingsSlideshowIntervalTile": "时间间隔", @@ -584,6 +587,11 @@ "settingsUnitSystemTile": "单位", "settingsUnitSystemTitle": "单位", + "settingsScreenSaverPageTitle": "屏保", + + "settingsWidgetPageTitle": "相框", + "settingsWidgetShowOutline": "轮廓", + "statsPageTitle": "统计", "statsWithGps": "{count, plural, other{{count} 项带位置信息}}", "statsTopCountries": "热门国家", diff --git a/lib/main_huawei.dart b/lib/main_huawei.dart index 6475eb310..962c7317a 100644 --- a/lib/main_huawei.dart +++ b/lib/main_huawei.dart @@ -1,6 +1,10 @@ import 'package:aves/app_flavor.dart'; import 'package:aves/main_common.dart'; +import 'package:aves/widget_common.dart'; -void main() { - mainCommon(AppFlavor.huawei); -} +const _flavor = AppFlavor.huawei; + +void main() => mainCommon(_flavor); + +@pragma('vm:entry-point') +void widgetMain() => widgetMainCommon(_flavor); diff --git a/lib/main_izzy.dart b/lib/main_izzy.dart index 4d3e27549..bfb24afaf 100644 --- a/lib/main_izzy.dart +++ b/lib/main_izzy.dart @@ -1,6 +1,10 @@ import 'package:aves/app_flavor.dart'; import 'package:aves/main_common.dart'; +import 'package:aves/widget_common.dart'; -void main() { - mainCommon(AppFlavor.izzy); -} +const _flavor = AppFlavor.izzy; + +void main() => mainCommon(_flavor); + +@pragma('vm:entry-point') +void widgetMain() => widgetMainCommon(_flavor); diff --git a/lib/main_play.dart b/lib/main_play.dart index 503d95000..d4e6779e0 100644 --- a/lib/main_play.dart +++ b/lib/main_play.dart @@ -1,6 +1,10 @@ import 'package:aves/app_flavor.dart'; import 'package:aves/main_common.dart'; +import 'package:aves/widget_common.dart'; -void main() { - mainCommon(AppFlavor.play); -} +const _flavor = AppFlavor.play; + +void main() => mainCommon(_flavor); + +@pragma('vm:entry-point') +void widgetMain() => widgetMainCommon(_flavor); diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 8071699f1..38781677d 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -249,7 +249,9 @@ class AvesEntry { bool get is360 => _catalogMetadata?.is360 ?? false; - bool get canEdit => path != null && !trashed; + bool get isMediaStoreContent => uri.startsWith('content://media/'); + + bool get canEdit => path != null && !trashed && isMediaStoreContent; bool get canEditDate => canEdit && (canEditExif || canEditXmp); @@ -689,7 +691,7 @@ class AvesEntry { await metadataDb.removeIds({id}, dataTypes: dataTypes); } - final updatedEntry = await mediaFileService.getEntry(uri, mimeType); + final updatedEntry = await mediaFetchService.getEntry(uri, mimeType); if (updatedEntry != null) { await applyNewFields(updatedEntry.toMap(), persist: persist); } @@ -699,7 +701,7 @@ class AvesEntry { Future delete() { final completer = Completer(); - mediaFileService.delete(entries: {this}).listen( + mediaEditService.delete(entries: {this}).listen( (event) => completer.complete(event.success && !event.skipped), onError: completer.completeError, onDone: () { diff --git a/lib/model/entry_dirs.dart b/lib/model/entry_dirs.dart index d18ff2dd8..73c206a6b 100644 --- a/lib/model/entry_dirs.dart +++ b/lib/model/entry_dirs.dart @@ -58,9 +58,13 @@ class EntryDir { var resolved = vrl.volumePath; final parts = pContext.split(vrl.relativeDir); for (final part in parts) { - final partLower = part.toLowerCase(); - final childrenDirs = Directory(resolved).listSync().where((v) => v.absolute is Directory).toSet(); - final found = childrenDirs.firstWhereOrNull((v) => pContext.basename(v.path).toLowerCase() == partLower); + FileSystemEntity? found; + final dir = Directory(resolved); + if (dir.existsSync()) { + final partLower = part.toLowerCase(); + final childrenDirs = dir.listSync().where((v) => v.absolute is Directory).toSet(); + found = childrenDirs.firstWhereOrNull((v) => pContext.basename(v.path).toLowerCase() == partLower); + } resolved = found?.path ?? '$resolved${pContext.separator}$part'; } return resolved; diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index e9a3cc57e..431f52e18 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -20,11 +20,12 @@ class AlbumFilter extends CoveredCollectionFilter { const AlbumFilter(this.album, this.displayName); - AlbumFilter.fromMap(Map json) - : this( - json['album'], - json['uniqueName'], - ); + factory AlbumFilter.fromMap(Map json) { + return AlbumFilter( + json['album'], + json['uniqueName'], + ); + } @override Map toMap() => { diff --git a/lib/model/filters/coordinate.dart b/lib/model/filters/coordinate.dart index 5b42c7e41..f08edc62e 100644 --- a/lib/model/filters/coordinate.dart +++ b/lib/model/filters/coordinate.dart @@ -23,11 +23,12 @@ class CoordinateFilter extends CollectionFilter { const CoordinateFilter(this.sw, this.ne, {this.minuteSecondPadding = false}); - CoordinateFilter.fromMap(Map json) - : this( - LatLng.fromJson(json['sw']), - LatLng.fromJson(json['ne']), - ); + factory CoordinateFilter.fromMap(Map json) { + return CoordinateFilter( + LatLng.fromJson(json['sw']), + LatLng.fromJson(json['ne']), + ); + } @override Map toMap() => { diff --git a/lib/model/filters/date.dart b/lib/model/filters/date.dart new file mode 100644 index 000000000..6262bd8c5 --- /dev/null +++ b/lib/model/filters/date.dart @@ -0,0 +1,137 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/theme/format.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/time_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; + +class DateFilter extends CollectionFilter { + static const type = 'date'; + + final DateLevel level; + late final DateTime? date; + late final DateTime _effectiveDate; + late final EntryFilter _test; + + static final onThisDay = DateFilter(DateLevel.md, null); + + @override + List get props => [level, date]; + + DateFilter(this.level, this.date) { + _effectiveDate = date ?? DateTime.now(); + switch (level) { + case DateLevel.y: + _test = (entry) => entry.bestDate?.isAtSameYearAs(_effectiveDate) ?? false; + break; + case DateLevel.ym: + _test = (entry) => entry.bestDate?.isAtSameMonthAs(_effectiveDate) ?? false; + break; + case DateLevel.ymd: + _test = (entry) => entry.bestDate?.isAtSameDayAs(_effectiveDate) ?? false; + break; + case DateLevel.md: + final month = _effectiveDate.month; + final day = _effectiveDate.day; + _test = (entry) { + final bestDate = entry.bestDate; + return bestDate != null && bestDate.month == month && bestDate.day == day; + }; + break; + case DateLevel.m: + final month = _effectiveDate.month; + _test = (entry) => entry.bestDate?.month == month; + break; + case DateLevel.d: + final day = _effectiveDate.day; + _test = (entry) => entry.bestDate?.day == day; + break; + } + } + + factory DateFilter.fromMap(Map json) { + final dateString = json['date'] as String?; + return DateFilter( + DateLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? DateLevel.ymd, + dateString != null ? DateTime.tryParse(dateString) : null, + ); + } + + @override + Map toMap() => { + 'type': type, + 'level': level.toString(), + 'date': date?.toIso8601String(), + }; + + @override + EntryFilter get test => _test; + + @override + bool isCompatible(CollectionFilter other) { + if (other is DateFilter) { + return isCompatibleLevel(level, other.level); + } else { + return true; + } + } + + static bool isCompatibleLevel(DateLevel a, DateLevel b) { + switch (a) { + case DateLevel.y: + return {DateLevel.md, DateLevel.m, DateLevel.d}.contains(b); + case DateLevel.ym: + return DateLevel.d == b; + case DateLevel.ymd: + return false; + case DateLevel.md: + return DateLevel.y == b; + case DateLevel.m: + return {DateLevel.y, DateLevel.d}.contains(b); + case DateLevel.d: + return {DateLevel.y, DateLevel.ym, DateLevel.m}.contains(b); + } + } + + @override + String get universalLabel => _effectiveDate.toIso8601String(); + + @override + String getLabel(BuildContext context) { + final l10n = context.l10n; + final locale = l10n.localeName; + switch (level) { + case DateLevel.y: + return DateFormat.y(locale).format(_effectiveDate); + case DateLevel.ym: + return DateFormat.yMMM(locale).format(_effectiveDate); + case DateLevel.ymd: + return formatDay(_effectiveDate, locale); + case DateLevel.md: + if (date != null) { + return DateFormat.MMMd(locale).format(_effectiveDate); + } else { + return l10n.filterOnThisDayLabel; + } + case DateLevel.m: + return DateFormat.MMMM(locale).format(_effectiveDate); + case DateLevel.d: + return DateFormat.d(locale).format(_effectiveDate); + } + } + + @override + Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { + return Icon(AIcons.date, size: size); + } + + @override + String get category => type; + + @override + String get key => '$type-$level-$date'; +} + +enum DateLevel { y, ym, ymd, md, m, d } diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index eccbfb3f7..f0787538c 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -4,6 +4,7 @@ import 'package:aves/model/covers.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/coordinate.dart'; +import 'package:aves/model/filters/date.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; @@ -28,6 +29,7 @@ abstract class CollectionFilter extends Equatable implements Comparable true; + bool isCompatible(CollectionFilter other) => category != other.category; String get universalLabel; diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index 17a94a947..8b3bf1098 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -31,11 +31,12 @@ class LocationFilter extends CoveredCollectionFilter { } } - LocationFilter.fromMap(Map json) - : this( - LocationLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? LocationLevel.place, - json['location'], - ); + factory LocationFilter.fromMap(Map json) { + return LocationFilter( + LocationLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? LocationLevel.place, + json['location'], + ); + } @override Map toMap() => { diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 51338cb6f..98fb3f9de 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -43,10 +43,11 @@ class MimeFilter extends CollectionFilter { _icon = icon ?? AIcons.vector; } - MimeFilter.fromMap(Map json) - : this( - json['mime'], - ); + factory MimeFilter.fromMap(Map json) { + return MimeFilter( + json['mime'], + ); + } @override Map toMap() => { diff --git a/lib/model/filters/path.dart b/lib/model/filters/path.dart index 406b7de89..ce1c1ead8 100644 --- a/lib/model/filters/path.dart +++ b/lib/model/filters/path.dart @@ -15,10 +15,11 @@ class PathFilter extends CollectionFilter { PathFilter(this.path) : _rootAlbum = path.substring(0, path.length - 1); - PathFilter.fromMap(Map json) - : this( - json['path'], - ); + factory PathFilter.fromMap(Map json) { + return PathFilter( + json['path'], + ); + } @override Map toMap() => { diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index 8d73336fd..7785b0345 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -59,10 +59,11 @@ class QueryFilter extends CollectionFilter { _test = not ? (entry) => !testTitle(entry) : testTitle; } - QueryFilter.fromMap(Map json) - : this( - json['query'], - ); + factory QueryFilter.fromMap(Map json) { + return QueryFilter( + json['query'], + ); + } @override Map toMap() => { @@ -74,7 +75,7 @@ class QueryFilter extends CollectionFilter { EntryFilter get test => _test; @override - bool get isUnique => false; + bool isCompatible(CollectionFilter other) => true; @override String get universalLabel => query; diff --git a/lib/model/filters/rating.dart b/lib/model/filters/rating.dart index 495faebe4..7eadd4e8c 100644 --- a/lib/model/filters/rating.dart +++ b/lib/model/filters/rating.dart @@ -13,10 +13,11 @@ class RatingFilter extends CollectionFilter { const RatingFilter(this.rating); - RatingFilter.fromMap(Map json) - : this( - json['rating'] ?? 0, - ); + factory RatingFilter.fromMap(Map json) { + return RatingFilter( + json['rating'] ?? 0, + ); + } @override Map toMap() => { diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index 750e420cb..8c57586ab 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -20,11 +20,12 @@ class TagFilter extends CoveredCollectionFilter { } } - TagFilter.fromMap(Map json) - : this( - json['tag'], - not: json['not'] ?? false, - ); + factory TagFilter.fromMap(Map json) { + return TagFilter( + json['tag'], + not: json['not'] ?? false, + ); + } @override Map toMap() => { @@ -37,7 +38,7 @@ class TagFilter extends CoveredCollectionFilter { EntryFilter get test => _test; @override - bool get isUnique => false; + bool isCompatible(CollectionFilter other) => true; @override String get universalLabel => tag; diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart index 300f6bc19..548163004 100644 --- a/lib/model/filters/type.dart +++ b/lib/model/filters/type.dart @@ -59,10 +59,11 @@ class TypeFilter extends CollectionFilter { } } - TypeFilter.fromMap(Map json) - : this._private( - json['itemType'], - ); + factory TypeFilter.fromMap(Map json) { + return TypeFilter._private( + json['itemType'], + ); + } @override Map toMap() => { diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 473e1fbb7..4585b4eaa 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -31,7 +31,7 @@ class SettingsDefaults { static const mustBackTwiceToExit = true; static const keepScreenOn = KeepScreenOn.viewerOnly; static const homePage = HomePageSetting.collection; - static const showBottomNavigationBar = true; + static const enableBottomNavigationBar = true; static const confirmDeleteForever = true; static const confirmMoveToBin = true; static const confirmMoveUndatedItems = true; @@ -128,10 +128,15 @@ class SettingsDefaults { // slideshow static const slideshowRepeat = false; static const slideshowShuffle = false; + static const slideshowFillScreen = false; static const slideshowTransition = ViewerTransition.fade; static const slideshowVideoPlayback = SlideshowVideoPlayback.playMuted; static const slideshowInterval = SlideshowInterval.s5; + // widget + static const widgetOutline = false; + static const widgetShape = WidgetShape.rrect; + // platform settings static const isRotationLocked = false; static const areAnimationsRemoved = false; diff --git a/lib/model/settings/enums/display_refresh_rate_mode.dart b/lib/model/settings/enums/display_refresh_rate_mode.dart index d650d5337..050c37ffd 100644 --- a/lib/model/settings/enums/display_refresh_rate_mode.dart +++ b/lib/model/settings/enums/display_refresh_rate_mode.dart @@ -1,4 +1,6 @@ +import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; @@ -16,17 +18,22 @@ extension ExtraDisplayRefreshRateMode on DisplayRefreshRateMode { } } - void apply() { + Future apply() async { + if (!await windowService.isActivity()) return; + + final androidInfo = await DeviceInfoPlugin().androidInfo; + if ((androidInfo.version.sdkInt ?? 0) < 23) return; + debugPrint('Apply display refresh rate: $name'); switch (this) { case DisplayRefreshRateMode.auto: - FlutterDisplayMode.setPreferredMode(DisplayMode.auto); + await FlutterDisplayMode.setPreferredMode(DisplayMode.auto); break; case DisplayRefreshRateMode.highest: - FlutterDisplayMode.setHighRefreshRate(); + await FlutterDisplayMode.setHighRefreshRate(); break; case DisplayRefreshRateMode.lowest: - FlutterDisplayMode.setLowRefreshRate(); + await FlutterDisplayMode.setLowRefreshRate(); break; } } diff --git a/lib/model/settings/enums/enums.dart b/lib/model/settings/enums/enums.dart index b16349ef2..4b129de39 100644 --- a/lib/model/settings/enums/enums.dart +++ b/lib/model/settings/enums/enums.dart @@ -29,3 +29,5 @@ enum VideoControls { play, playSeek, playOutside, none } enum VideoLoopMode { never, shortOnly, always } enum ViewerTransition { slide, parallax, fade, zoomIn } + +enum WidgetShape { rrect, circle, heart } \ No newline at end of file diff --git a/lib/model/settings/enums/widget_shape.dart b/lib/model/settings/enums/widget_shape.dart new file mode 100644 index 000000000..a0975a797 --- /dev/null +++ b/lib/model/settings/enums/widget_shape.dart @@ -0,0 +1,44 @@ +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:flutter/material.dart'; + +extension ExtraWidgetShape on WidgetShape { + Path path(Size widgetSize, double devicePixelRatio) { + final rect = Rect.fromLTWH(0, 0, widgetSize.width, widgetSize.height); + switch (this) { + case WidgetShape.rrect: + return Path()..addRRect(BorderRadius.circular(24 * devicePixelRatio).toRRect(rect)); + case WidgetShape.circle: + return Path() + ..addOval(Rect.fromCircle( + center: rect.center, + radius: rect.shortestSide / 2, + )); + case WidgetShape.heart: + final center = rect.center; + final dim = rect.shortestSide; + const p0dy = -.4; + const p1dx = .5; + const p1dy = -.4; + const p2dx = .8; + const p2dy = .5; + const p3dy = .5 - p0dy; + return Path() + ..moveTo(center.dx, center.dy) + ..relativeMoveTo(0, dim * p0dy) + ..relativeCubicTo(dim * -p1dx, dim * p1dy, dim * -p2dx, dim * p2dy, 0, dim * p3dy) + ..moveTo(center.dx, center.dy) + ..relativeMoveTo(0, dim * p0dy) + ..relativeCubicTo(dim * p1dx, dim * p1dy, dim * p2dx, dim * p2dy, 0, dim * p3dy); + } + } + + Size size(Size widgetSize) { + switch (this) { + case WidgetShape.rrect: + return widgetSize; + case WidgetShape.circle: + case WidgetShape.heart: + return Size.square(widgetSize.shortestSide); + } + } +} diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 624c359b4..3cf292801 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -27,7 +27,7 @@ class Settings extends ChangeNotifier { Settings._private(); - static const Set internalKeys = { + static const Set _internalKeys = { hasAcceptedTermsKey, catalogTimeZoneKey, videoShowRawTimedTextKey, @@ -36,6 +36,7 @@ class Settings extends ChangeNotifier { platformTransitionAnimationScaleKey, topEntryIdsKey, }; + static const _widgetKeyPrefix = 'widget_'; // app static const hasAcceptedTermsKey = 'has_accepted_terms'; @@ -60,7 +61,7 @@ class Settings extends ChangeNotifier { static const mustBackTwiceToExitKey = 'must_back_twice_to_exit'; static const keepScreenOnKey = 'keep_screen_on'; static const homePageKey = 'home_page'; - static const showBottomNavigationBarKey = 'show_bottom_navigation_bar'; + static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar'; static const confirmDeleteForeverKey = 'confirm_delete_forever'; static const confirmMoveToBinKey = 'confirm_move_to_bin'; static const confirmMoveUndatedItemsKey = 'confirm_move_undated_items'; @@ -138,13 +139,27 @@ class Settings extends ChangeNotifier { // file picker static const filePickerShowHiddenFilesKey = 'file_picker_show_hidden_files'; + // screen saver + static const screenSaverFillScreenKey = 'screen_saver_fill_screen'; + static const screenSaverTransitionKey = 'screen_saver_transition'; + static const screenSaverVideoPlaybackKey = 'screen_saver_video_playback'; + static const screenSaverIntervalKey = 'screen_saver_interval'; + static const screenSaverCollectionFiltersKey = 'screen_saver_collection_filters'; + // slideshow static const slideshowRepeatKey = 'slideshow_loop'; static const slideshowShuffleKey = 'slideshow_shuffle'; + static const slideshowFillScreenKey = 'slideshow_fill_screen'; static const slideshowTransitionKey = 'slideshow_transition'; static const slideshowVideoPlaybackKey = 'slideshow_video_playback'; static const slideshowIntervalKey = 'slideshow_interval'; + // widget + static const widgetOutlinePrefixKey = '${_widgetKeyPrefix}outline_'; + static const widgetShapePrefixKey = '${_widgetKeyPrefix}shape_'; + static const widgetCollectionFiltersPrefixKey = '${_widgetKeyPrefix}collection_filters_'; + static const widgetUriPrefixKey = '${_widgetKeyPrefix}uri_'; + // platform settings // cf Android `Settings.System.ACCELEROMETER_ROTATION` static const platformAccelerometerRotationKey = 'accelerometer_rotation'; @@ -161,14 +176,18 @@ class Settings extends ChangeNotifier { } } + Future reload() => settingsStore.reload(); + Future reset({required bool includeInternalKeys}) async { if (includeInternalKeys) { await settingsStore.clear(); } else { - await Future.forEach(settingsStore.getKeys().whereNot(Settings.internalKeys.contains), settingsStore.remove); + await Future.forEach(settingsStore.getKeys().whereNot(isInternalKey), settingsStore.remove); } } + bool isInternalKey(String key) => _internalKeys.contains(key) || key.startsWith(_widgetKeyPrefix); + Future setContextualDefaults() async { // performance final performanceClass = await deviceService.getPerformanceClass(); @@ -315,9 +334,9 @@ class Settings extends ChangeNotifier { set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString()); - bool get showBottomNavigationBar => getBoolOrDefault(showBottomNavigationBarKey, SettingsDefaults.showBottomNavigationBar); + bool get enableBottomNavigationBar => getBoolOrDefault(enableBottomNavigationBarKey, SettingsDefaults.enableBottomNavigationBar); - set showBottomNavigationBar(bool newValue) => setAndNotify(showBottomNavigationBarKey, newValue); + set enableBottomNavigationBar(bool newValue) => setAndNotify(enableBottomNavigationBarKey, newValue); bool get confirmDeleteForever => getBoolOrDefault(confirmDeleteForeverKey, SettingsDefaults.confirmDeleteForever); @@ -583,6 +602,28 @@ class Settings extends ChangeNotifier { set filePickerShowHiddenFiles(bool newValue) => setAndNotify(filePickerShowHiddenFilesKey, newValue); + // screen saver + + bool get screenSaverFillScreen => getBoolOrDefault(screenSaverFillScreenKey, SettingsDefaults.slideshowFillScreen); + + set screenSaverFillScreen(bool newValue) => setAndNotify(screenSaverFillScreenKey, newValue); + + ViewerTransition get screenSaverTransition => getEnumOrDefault(screenSaverTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values); + + set screenSaverTransition(ViewerTransition newValue) => setAndNotify(screenSaverTransitionKey, newValue.toString()); + + SlideshowVideoPlayback get screenSaverVideoPlayback => getEnumOrDefault(screenSaverVideoPlaybackKey, SettingsDefaults.slideshowVideoPlayback, SlideshowVideoPlayback.values); + + set screenSaverVideoPlayback(SlideshowVideoPlayback newValue) => setAndNotify(screenSaverVideoPlaybackKey, newValue.toString()); + + SlideshowInterval get screenSaverInterval => getEnumOrDefault(screenSaverIntervalKey, SettingsDefaults.slideshowInterval, SlideshowInterval.values); + + set screenSaverInterval(SlideshowInterval newValue) => setAndNotify(screenSaverIntervalKey, newValue.toString()); + + Set get screenSaverCollectionFilters => (getStringList(screenSaverCollectionFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); + + set screenSaverCollectionFilters(Set newValue) => setAndNotify(screenSaverCollectionFiltersKey, newValue.map((filter) => filter.toJson()).toList()); + // slideshow bool get slideshowRepeat => getBoolOrDefault(slideshowRepeatKey, SettingsDefaults.slideshowRepeat); @@ -593,6 +634,10 @@ class Settings extends ChangeNotifier { set slideshowShuffle(bool newValue) => setAndNotify(slideshowShuffleKey, newValue); + bool get slideshowFillScreen => getBoolOrDefault(slideshowFillScreenKey, SettingsDefaults.slideshowFillScreen); + + set slideshowFillScreen(bool newValue) => setAndNotify(slideshowFillScreenKey, newValue); + ViewerTransition get slideshowTransition => getEnumOrDefault(slideshowTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values); set slideshowTransition(ViewerTransition newValue) => setAndNotify(slideshowTransitionKey, newValue.toString()); @@ -605,6 +650,27 @@ class Settings extends ChangeNotifier { set slideshowInterval(SlideshowInterval newValue) => setAndNotify(slideshowIntervalKey, newValue.toString()); + // widget + + Color? getWidgetOutline(int widgetId) { + final value = getInt('$widgetOutlinePrefixKey$widgetId'); + return value != null ? Color(value) : null; + } + + void setWidgetOutline(int widgetId, Color? newValue) => setAndNotify('$widgetOutlinePrefixKey$widgetId', newValue?.value); + + WidgetShape getWidgetShape(int widgetId) => getEnumOrDefault('$widgetShapePrefixKey$widgetId', SettingsDefaults.widgetShape, WidgetShape.values); + + void setWidgetShape(int widgetId, WidgetShape newValue) => setAndNotify('$widgetShapePrefixKey$widgetId', newValue.toString()); + + Set getWidgetCollectionFilters(int widgetId) => (getStringList('$widgetCollectionFiltersPrefixKey$widgetId') ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); + + void setWidgetCollectionFilters(int widgetId, Set newValue) => setAndNotify('$widgetCollectionFiltersPrefixKey$widgetId', newValue.map((filter) => filter.toJson()).toList()); + + String? getWidgetUri(int widgetId) => getString('$widgetUriPrefixKey$widgetId'); + + void setWidgetUri(int widgetId, String? newValue) => setAndNotify('$widgetUriPrefixKey$widgetId', newValue); + // convenience methods int? getInt(String key) => settingsStore.getInt(key); @@ -687,7 +753,7 @@ class Settings extends ChangeNotifier { // import/export Map export() => Map.fromEntries( - settingsStore.getKeys().whereNot(internalKeys.contains).map((k) => MapEntry(k, settingsStore.get(k))), + settingsStore.getKeys().whereNot(isInternalKey).map((k) => MapEntry(k, settingsStore.get(k))), ); Future import(dynamic jsonMap) async { @@ -735,7 +801,7 @@ class Settings extends ChangeNotifier { case isErrorReportingAllowedKey: case enableDynamicColorKey: case enableBlurEffectKey: - case showBottomNavigationBarKey: + case enableBottomNavigationBarKey: case mustBackTwiceToExitKey: case confirmDeleteForeverKey: case confirmMoveToBinKey: @@ -763,8 +829,10 @@ class Settings extends ChangeNotifier { case subtitleShowOutlineKey: case saveSearchHistoryKey: case filePickerShowHiddenFilesKey: + case screenSaverFillScreenKey: case slideshowRepeatKey: case slideshowShuffleKey: + case slideshowFillScreenKey: if (newValue is bool) { settingsStore.setBool(key, newValue); } else { @@ -792,6 +860,9 @@ class Settings extends ChangeNotifier { case unitSystemKey: case accessibilityAnimationsKey: case timeToTakeActionKey: + case screenSaverTransitionKey: + case screenSaverVideoPlaybackKey: + case screenSaverIntervalKey: case slideshowTransitionKey: case slideshowVideoPlaybackKey: case slideshowIntervalKey: @@ -809,6 +880,7 @@ class Settings extends ChangeNotifier { case collectionBrowsingQuickActionsKey: case collectionSelectionQuickActionsKey: case viewerQuickActionsKey: + case screenSaverCollectionFiltersKey: if (newValue is List) { settingsStore.setStringList(key, newValue.cast()); } else { diff --git a/lib/model/settings/store/store.dart b/lib/model/settings/store/store.dart index b12fa639f..84a7febc1 100644 --- a/lib/model/settings/store/store.dart +++ b/lib/model/settings/store/store.dart @@ -3,6 +3,8 @@ abstract class SettingsStore { Future init(); + Future reload(); + Future clear(); Future remove(String key); diff --git a/lib/model/settings/store/store_shared_pref.dart b/lib/model/settings/store/store_shared_pref.dart index d7d6fd6fd..10e2477c0 100644 --- a/lib/model/settings/store/store_shared_pref.dart +++ b/lib/model/settings/store/store_shared_pref.dart @@ -17,6 +17,9 @@ class SharedPrefSettingsStore implements SettingsStore { } } + @override + Future reload() => _prefs!.reload(); + @override Future clear() => _prefs!.clear(); diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index d0598213e..71c94c239 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -150,9 +150,7 @@ class CollectionLens with ChangeNotifier { void addFilter(CollectionFilter filter) { if (filters.contains(filter)) return; - if (filter.isUnique) { - filters.removeWhere((old) => old.category == filter.category); - } + filters.removeWhere((other) => !filter.isCompatible(other)); filters.add(filter); _onFilterChanged(); } diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 02b6c0b9f..3ba0bca9e 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -370,6 +370,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM AnalysisController? analysisController, String? directory, bool loadTopEntriesFirst = false, + bool canAnalyze = true, }); Future> refreshUris(Set changedUris, {AnalysisController? analysisController}); diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 28fbc4b76..47eb06685 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -24,6 +24,7 @@ class MediaStoreSource extends CollectionSource { AnalysisController? analysisController, String? directory, bool loadTopEntriesFirst = false, + bool canAnalyze = true, }) async { if (_initState == SourceInitializationState.none) { await _loadEssentials(); @@ -35,6 +36,7 @@ class MediaStoreSource extends CollectionSource { analysisController: analysisController, directory: directory, loadTopEntriesFirst: loadTopEntriesFirst, + canAnalyze: canAnalyze, )); } @@ -63,6 +65,7 @@ class MediaStoreSource extends CollectionSource { AnalysisController? analysisController, String? directory, required bool loadTopEntriesFirst, + required bool canAnalyze, }) async { debugPrint('$runtimeType refresh start'); final stopwatch = Stopwatch()..start(); @@ -182,7 +185,11 @@ class MediaStoreSource extends CollectionSource { if (analysisIds != null) { analysisEntries = visibleEntries.where((entry) => analysisIds.contains(entry.id)).toSet(); } - await analyze(analysisController, entries: analysisEntries); + if (canAnalyze) { + await analyze(analysisController, entries: analysisEntries); + } else { + stateNotifier.value = SourceState.ready; + } // the home page may not reflect the current derived filters // as the initial addition of entries is silent, @@ -228,7 +235,7 @@ class MediaStoreSource extends CollectionSource { for (final kv in uriByContentId.entries) { final contentId = kv.key; final uri = kv.value; - final sourceEntry = await mediaFileService.getEntry(uri, null); + final sourceEntry = await mediaFetchService.getEntry(uri, null); if (sourceEntry != null) { final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId); // compare paths because some apps move files without updating their `last modified date` diff --git a/lib/model/source/trash.dart b/lib/model/source/trash.dart index cbd2fd3ad..51c98dd2a 100644 --- a/lib/model/source/trash.dart +++ b/lib/model/source/trash.dart @@ -19,7 +19,7 @@ mixin TrashMixin on SourceBase { final processed = {}; final completer = Completer>(); - mediaFileService.delete(entries: expiredEntries).listen( + mediaEditService.delete(entries: expiredEntries).listen( processed.add, onError: completer.completeError, onDone: () async { diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index e5e7f1a00..ef90fb267 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -23,7 +23,7 @@ import 'package:flutter/foundation.dart'; class VideoMetadataFormatter { static final _dateY4M2D2H2m2s2Pattern = RegExp(r'(\d{4})[-/](\d{2})[-/](\d{2}) (\d{2}):(\d{2}):(\d{2})'); - static final _dateY4M2D2H2m2s2APmPattern = RegExp(r'(\d{4})[-/](\d{2})[-/](\d{2})T(\d+):(\d+):(\d+) ([ap]m)Z'); + static final _dateY4M2D2H2m2s2APmPattern = RegExp(r'(\d{4})[-/](\d{1,2})[-/](\d{1,2})T(\d+):(\d+):(\d+)( ([ap]m))?Z'); static final _ambiguousDatePatterns = { RegExp(r'^\d{2}[-/]\d{2}[-/]\d{4}$'), }; @@ -124,6 +124,7 @@ class VideoMetadataFormatter { // - `UTC 2021-05-30 19:14:21` // - `2021/10/31 21:23:17` // - `2021-09-10T7:14:49 pmZ` + // - `2012-1-1T12:00:00Z` // - `2021` (not enough to build a date) var match = _dateY4M2D2H2m2s2Pattern.firstMatch(dateString); @@ -149,7 +150,7 @@ class VideoMetadataFormatter { final hour = int.tryParse(match.group(4)!); final minute = int.tryParse(match.group(5)!); final second = int.tryParse(match.group(6)!); - final pm = match.group(7) == 'pm'; + final pm = match.group(8) == 'pm'; if (year != null && month != null && day != null && hour != null && minute != null && second != null) { final date = DateTime(year, month, day, hour + (pm ? 12 : 0), minute, second, 0); diff --git a/lib/ref/mime_types.dart b/lib/ref/mime_types.dart index 63ac92ee7..b1156ffc4 100644 --- a/lib/ref/mime_types.dart +++ b/lib/ref/mime_types.dart @@ -17,7 +17,10 @@ class MimeTypes { static const art = 'image/x-jg'; static const cdr = 'image/x-coreldraw'; static const djvu = 'image/vnd.djvu'; + static const jpeg2000 = 'image/jp2'; static const jxl = 'image/jxl'; + static const pat = 'image/x-coreldrawpattern'; + static const pnm = 'image/x-portable-anymap'; static const psdVnd = 'image/vnd.adobe.photoshop'; static const psdX = 'image/x-photoshop'; @@ -25,7 +28,8 @@ class MimeTypes { static const cr2 = 'image/x-canon-cr2'; static const crw = 'image/x-canon-crw'; static const dcr = 'image/x-kodak-dcr'; - static const dng = 'image/x-adobe-dng'; + static const dng = 'image/dng'; + static const dngX = 'image/x-adobe-dng'; static const erf = 'image/x-epson-erf'; static const k25 = 'image/x-kodak-k25'; static const kdc = 'image/x-kodak-kdc'; @@ -50,13 +54,16 @@ class MimeTypes { static const aviVnd = 'video/vnd.avi'; static const flv = 'video/flv'; static const flvX = 'video/x-flv'; - static const mkv = 'video/x-matroska'; + static const mkv = 'video/mkv'; + static const mkvX = 'video/x-matroska'; static const mov = 'video/quicktime'; + static const mp2p = 'video/mp2p'; static const mp2t = 'video/mp2t'; // .m2ts, .ts static const mp2ts = 'video/mp2ts'; // .ts (prefer `mp2t` when possible) static const mp4 = 'video/mp4'; static const mpeg = 'video/mpeg'; static const ogv = 'video/ogg'; + static const realVideo = 'video/x-pn-realvideo'; static const webm = 'video/webm'; static const wmv = 'video/x-ms-wmv'; @@ -72,14 +79,14 @@ class MimeTypes { // formats that support transparency static const Set alphaImages = {avif, bmp, bmpX, gif, heic, heif, ico, png, svg, tiff, webp}; - static const Set rawImages = {arw, cr2, crw, dcr, dng, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f}; + static const Set rawImages = {arw, cr2, crw, dcr, dng, dngX, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f}; // TODO TLAD [codec] make it dynamic if it depends on OS/lib versions - static const Set undecodableImages = {art, cdr, crw, djvu, jxl, psdVnd, psdX, octetStream, zip}; + static const Set undecodableImages = {art, cdr, crw, djvu, jpeg2000, jxl, pat, pnm, psdVnd, psdX, octetStream, zip}; static const Set _knownOpaqueImages = {jpeg}; - static const Set _knownVideos = {v3gpp, asf, avi, aviVnd, flv, flvX, mkv, mov, mp2t, mp2ts, mp4, mpeg, ogv, webm, wmv}; + static const Set _knownVideos = {v3gpp, asf, avi, aviVnd, flv, flvX, mkv, mkvX, mov, mp2p, mp2t, mp2ts, mp4, mpeg, ogv, realVideo, webm, wmv}; static final Set knownMediaTypes = { anyImage, diff --git a/lib/services/accessibility_service.dart b/lib/services/accessibility_service.dart index 433a33077..843e72053 100644 --- a/lib/services/accessibility_service.dart +++ b/lib/services/accessibility_service.dart @@ -2,11 +2,11 @@ import 'package:aves/services/common/services.dart'; import 'package:flutter/services.dart'; class AccessibilityService { - static const platform = MethodChannel('deckers.thibault/aves/accessibility'); + static const _platform = MethodChannel('deckers.thibault/aves/accessibility'); static Future areAnimationsRemoved() async { try { - final result = await platform.invokeMethod('areAnimationsRemoved'); + final result = await _platform.invokeMethod('areAnimationsRemoved'); if (result != null) return result as bool; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -16,7 +16,7 @@ class AccessibilityService { static Future hasRecommendedTimeouts() async { try { - final result = await platform.invokeMethod('hasRecommendedTimeouts'); + final result = await _platform.invokeMethod('hasRecommendedTimeouts'); if (result != null) return result as bool; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -26,7 +26,7 @@ class AccessibilityService { static Future getRecommendedTimeToRead(int originalTimeoutMillis) async { try { - final result = await platform.invokeMethod('getRecommendedTimeoutMillis', { + final result = await _platform.invokeMethod('getRecommendedTimeoutMillis', { 'originalTimeoutMillis': originalTimeoutMillis, 'content': ['icons', 'text'] }); @@ -39,7 +39,7 @@ class AccessibilityService { static Future getRecommendedTimeToTakeAction(int originalTimeoutMillis) async { try { - final result = await platform.invokeMethod('getRecommendedTimeoutMillis', { + final result = await _platform.invokeMethod('getRecommendedTimeoutMillis', { 'originalTimeoutMillis': originalTimeoutMillis, 'content': ['controls', 'icons', 'text'] }); diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart index 7c75aa6e4..706e24090 100644 --- a/lib/services/analysis_service.dart +++ b/lib/services/analysis_service.dart @@ -8,16 +8,17 @@ import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/model/source/source_state.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; class AnalysisService { - static const platform = MethodChannel('deckers.thibault/aves/analysis'); + static const _platform = MethodChannel('deckers.thibault/aves/analysis'); static Future registerCallback() async { try { - await platform.invokeMethod('registerCallback', { + await _platform.invokeMethod('registerCallback', { 'callbackHandle': PluginUtilities.getCallbackHandle(_init)?.toRawHandle(), }); } on PlatformException catch (e, stack) { @@ -27,7 +28,7 @@ class AnalysisService { static Future startService({required bool force, List? entryIds}) async { try { - await platform.invokeMethod('startService', { + await _platform.invokeMethod('startService', { 'entryIds': entryIds, 'force': force, }); @@ -42,6 +43,7 @@ const _channel = MethodChannel('deckers.thibault/aves/analysis_service_backgroun Future _init() async { WidgetsFlutterBinding.ensureInitialized(); initPlatformServices(); + await androidFileUtils.init(); await metadataDb.init(); await mobileServices.init(); await settings.init(monitorPlatformSettings: false); diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index d61979773..e9fbbade8 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/services/common/services.dart'; @@ -34,9 +32,9 @@ abstract class AndroidAppService { } class PlatformAndroidAppService implements AndroidAppService { - static const platform = MethodChannel('deckers.thibault/aves/app'); + static const _platform = MethodChannel('deckers.thibault/aves/app'); - static final knownAppDirs = { + static final _knownAppDirs = { 'com.kakao.talk': {'KakaoTalkDownload'}, 'com.sony.playmemories.mobile': {'Imaging Edge Mobile'}, 'nekox.messenger': {'NekoX'}, @@ -45,10 +43,10 @@ class PlatformAndroidAppService implements AndroidAppService { @override Future> getPackages() async { try { - final result = await platform.invokeMethod('getPackages'); + final result = await _platform.invokeMethod('getPackages'); final packages = (result as List).cast().map(Package.fromMap).toSet(); // additional info for known directories - knownAppDirs.forEach((packageName, dirs) { + _knownAppDirs.forEach((packageName, dirs) { final package = packages.firstWhereOrNull((package) => package.packageName == packageName); if (package != null) { package.ownedDirs.addAll(dirs); @@ -64,7 +62,7 @@ class PlatformAndroidAppService implements AndroidAppService { @override Future getAppIcon(String packageName, double size) async { try { - final result = await platform.invokeMethod('getAppIcon', { + final result = await _platform.invokeMethod('getAppIcon', { 'packageName': packageName, 'sizeDip': size, }); @@ -78,7 +76,7 @@ class PlatformAndroidAppService implements AndroidAppService { @override Future getAppInstaller() async { try { - return await platform.invokeMethod('getAppInstaller'); + return await _platform.invokeMethod('getAppInstaller'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } @@ -88,7 +86,7 @@ class PlatformAndroidAppService implements AndroidAppService { @override Future copyToClipboard(String uri, String? label) async { try { - final result = await platform.invokeMethod('copyToClipboard', { + final result = await _platform.invokeMethod('copyToClipboard', { 'uri': uri, 'label': label, }); @@ -102,7 +100,7 @@ class PlatformAndroidAppService implements AndroidAppService { @override Future edit(String uri, String mimeType) async { try { - final result = await platform.invokeMethod('edit', { + final result = await _platform.invokeMethod('edit', { 'uri': uri, 'mimeType': mimeType, }); @@ -116,7 +114,7 @@ class PlatformAndroidAppService implements AndroidAppService { @override Future open(String uri, String mimeType) async { try { - final result = await platform.invokeMethod('open', { + final result = await _platform.invokeMethod('open', { 'uri': uri, 'mimeType': mimeType, }); @@ -134,7 +132,7 @@ class PlatformAndroidAppService implements AndroidAppService { final geoUri = 'geo:$latitude,$longitude?q=$latitude,$longitude'; try { - final result = await platform.invokeMethod('openMap', { + final result = await _platform.invokeMethod('openMap', { 'geoUri': geoUri, }); if (result != null) return result as bool; @@ -147,7 +145,7 @@ class PlatformAndroidAppService implements AndroidAppService { @override Future setAs(String uri, String mimeType) async { try { - final result = await platform.invokeMethod('setAs', { + final result = await _platform.invokeMethod('setAs', { 'uri': uri, 'mimeType': mimeType, }); @@ -164,7 +162,7 @@ class PlatformAndroidAppService implements AndroidAppService { // e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats final urisByMimeType = groupBy(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); try { - final result = await platform.invokeMethod('share', { + final result = await _platform.invokeMethod('share', { 'urisByMimeType': urisByMimeType, }); if (result != null) return result as bool; @@ -177,7 +175,7 @@ class PlatformAndroidAppService implements AndroidAppService { @override Future shareSingle(String uri, String mimeType) async { try { - final result = await platform.invokeMethod('share', { + final result = await _platform.invokeMethod('share', { 'urisByMimeType': { mimeType: [uri] }, @@ -196,7 +194,7 @@ class PlatformAndroidAppService implements AndroidAppService { Uint8List? iconBytes; if (coverEntry != null) { final size = coverEntry.isVideo ? 0.0 : 256.0; - iconBytes = await mediaFileService.getThumbnail( + iconBytes = await mediaFetchService.getThumbnail( uri: coverEntry.uri, mimeType: coverEntry.mimeType, pageId: coverEntry.pageId, @@ -207,7 +205,7 @@ class PlatformAndroidAppService implements AndroidAppService { ); } try { - await platform.invokeMethod('pinShortcut', { + await _platform.invokeMethod('pinShortcut', { 'label': label, 'iconBytes': iconBytes, 'filters': filters?.map((filter) => filter.toJson()).toList(), diff --git a/lib/services/android_debug_service.dart b/lib/services/android_debug_service.dart index 6c7e445c6..f657470fc 100644 --- a/lib/services/android_debug_service.dart +++ b/lib/services/android_debug_service.dart @@ -4,11 +4,11 @@ import 'package:aves/services/common/services.dart'; import 'package:flutter/services.dart'; class AndroidDebugService { - static const platform = MethodChannel('deckers.thibault/aves/debug'); + static const _platform = MethodChannel('deckers.thibault/aves/debug'); static Future crash() async { try { - await platform.invokeMethod('crash'); + await _platform.invokeMethod('crash'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } @@ -16,7 +16,7 @@ class AndroidDebugService { static Future exception() async { try { - await platform.invokeMethod('exception'); + await _platform.invokeMethod('exception'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } @@ -24,7 +24,7 @@ class AndroidDebugService { static Future safeException() async { try { - await platform.invokeMethod('safeException'); + await _platform.invokeMethod('safeException'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } @@ -32,7 +32,7 @@ class AndroidDebugService { static Future exceptionInCoroutine() async { try { - await platform.invokeMethod('exceptionInCoroutine'); + await _platform.invokeMethod('exceptionInCoroutine'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } @@ -40,7 +40,7 @@ class AndroidDebugService { static Future safeExceptionInCoroutine() async { try { - await platform.invokeMethod('safeExceptionInCoroutine'); + await _platform.invokeMethod('safeExceptionInCoroutine'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } @@ -48,7 +48,7 @@ class AndroidDebugService { static Future getContextDirs() async { try { - final result = await platform.invokeMethod('getContextDirs'); + final result = await _platform.invokeMethod('getContextDirs'); if (result != null) return result as Map; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -58,7 +58,7 @@ class AndroidDebugService { static Future> getCodecs() async { try { - final result = await platform.invokeMethod('getCodecs'); + final result = await _platform.invokeMethod('getCodecs'); if (result != null) return (result as List).cast(); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -68,7 +68,7 @@ class AndroidDebugService { static Future getEnv() async { try { - final result = await platform.invokeMethod('getEnv'); + final result = await _platform.invokeMethod('getEnv'); if (result != null) return result as Map; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -79,7 +79,7 @@ class AndroidDebugService { static Future getBitmapFactoryInfo(AvesEntry entry) async { try { // returns map with all data available when decoding image bounds with `BitmapFactory` - final result = await platform.invokeMethod('getBitmapFactoryInfo', { + final result = await _platform.invokeMethod('getBitmapFactoryInfo', { 'uri': entry.uri, }); if (result != null) return result as Map; @@ -92,7 +92,7 @@ class AndroidDebugService { static Future getContentResolverMetadata(AvesEntry entry) async { try { // returns map with all data available from the content resolver - final result = await platform.invokeMethod('getContentResolverMetadata', { + final result = await _platform.invokeMethod('getContentResolverMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, }); @@ -106,7 +106,7 @@ class AndroidDebugService { static Future getExifInterfaceMetadata(AvesEntry entry) async { try { // returns map with all data available from the `ExifInterface` library - final result = await platform.invokeMethod('getExifInterfaceMetadata', { + final result = await _platform.invokeMethod('getExifInterfaceMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, @@ -121,7 +121,7 @@ class AndroidDebugService { static Future getMediaMetadataRetrieverMetadata(AvesEntry entry) async { try { // returns map with all data available from `MediaMetadataRetriever` - final result = await platform.invokeMethod('getMediaMetadataRetrieverMetadata', { + final result = await _platform.invokeMethod('getMediaMetadataRetrieverMetadata', { 'uri': entry.uri, }); if (result != null) return result as Map; @@ -134,7 +134,7 @@ class AndroidDebugService { static Future getMetadataExtractorSummary(AvesEntry entry) async { try { // returns map with the MIME type and tag count for each directory found by `metadata-extractor` - final result = await platform.invokeMethod('getMetadataExtractorSummary', { + final result = await _platform.invokeMethod('getMetadataExtractorSummary', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, @@ -149,7 +149,7 @@ class AndroidDebugService { static Future getPixyMetadata(AvesEntry entry) async { try { // returns map with all data available from the `PixyMeta` library - final result = await platform.invokeMethod('getPixyMetadata', { + final result = await _platform.invokeMethod('getPixyMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, }); @@ -164,7 +164,7 @@ class AndroidDebugService { if (entry.mimeType != MimeTypes.tiff) return {}; try { - final result = await platform.invokeMethod('getTiffStructure', { + final result = await _platform.invokeMethod('getTiffStructure', { 'uri': entry.uri, }); if (result != null) return result as Map; diff --git a/lib/services/common/services.dart b/lib/services/common/services.dart index 9a2a1e062..378132105 100644 --- a/lib/services/common/services.dart +++ b/lib/services/common/services.dart @@ -6,7 +6,8 @@ import 'package:aves/model/settings/store/store_shared_pref.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/device_service.dart'; import 'package:aves/services/media/embedded_data_service.dart'; -import 'package:aves/services/media/media_file_service.dart'; +import 'package:aves/services/media/media_edit_service.dart'; +import 'package:aves/services/media/media_fetch_service.dart'; import 'package:aves/services/media/media_store_service.dart'; import 'package:aves/services/metadata/metadata_edit_service.dart'; import 'package:aves/services/metadata/metadata_fetch_service.dart'; @@ -31,7 +32,8 @@ final MetadataDb metadataDb = getIt(); final AndroidAppService androidAppService = getIt(); final DeviceService deviceService = getIt(); final EmbeddedDataService embeddedDataService = getIt(); -final MediaFileService mediaFileService = getIt(); +final MediaEditService mediaEditService = getIt(); +final MediaFetchService mediaFetchService = getIt(); final MediaStoreService mediaStoreService = getIt(); final MetadataEditService metadataEditService = getIt(); final MetadataFetchService metadataFetchService = getIt(); @@ -48,7 +50,8 @@ void initPlatformServices() { getIt.registerLazySingleton(PlatformAndroidAppService.new); getIt.registerLazySingleton(PlatformDeviceService.new); getIt.registerLazySingleton(PlatformEmbeddedDataService.new); - getIt.registerLazySingleton(PlatformMediaFileService.new); + getIt.registerLazySingleton(PlatformMediaEditService.new); + getIt.registerLazySingleton(PlatformMediaFetchService.new); getIt.registerLazySingleton(PlatformMediaStoreService.new); getIt.registerLazySingleton(PlatformMetadataEditService.new); getIt.registerLazySingleton(PlatformMetadataFetchService.new); diff --git a/lib/services/device_service.dart b/lib/services/device_service.dart index 3360204f2..5bafe40f7 100644 --- a/lib/services/device_service.dart +++ b/lib/services/device_service.dart @@ -16,12 +16,12 @@ abstract class DeviceService { } class PlatformDeviceService implements DeviceService { - static const platform = MethodChannel('deckers.thibault/aves/device'); + static const _platform = MethodChannel('deckers.thibault/aves/device'); @override Future> getCapabilities() async { try { - final result = await platform.invokeMethod('getCapabilities'); + final result = await _platform.invokeMethod('getCapabilities'); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -32,7 +32,7 @@ class PlatformDeviceService implements DeviceService { @override Future getDefaultTimeZone() async { try { - return await platform.invokeMethod('getDefaultTimeZone'); + return await _platform.invokeMethod('getDefaultTimeZone'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } @@ -42,7 +42,7 @@ class PlatformDeviceService implements DeviceService { @override Future> getLocales() async { try { - final result = await platform.invokeMethod('getLocales'); + final result = await _platform.invokeMethod('getLocales'); if (result != null) { return (result as List).cast().map((tags) { final language = tags['language'] as String?; @@ -62,7 +62,7 @@ class PlatformDeviceService implements DeviceService { @override Future getPerformanceClass() async { try { - final result = await platform.invokeMethod('getPerformanceClass'); + final result = await _platform.invokeMethod('getPerformanceClass'); if (result != null) return result as int; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -73,7 +73,7 @@ class PlatformDeviceService implements DeviceService { @override Future isSystemFilePickerEnabled() async { try { - final result = await platform.invokeMethod('isSystemFilePickerEnabled'); + final result = await _platform.invokeMethod('isSystemFilePickerEnabled'); if (result != null) return result as bool; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); diff --git a/lib/services/geocoding_service.dart b/lib/services/geocoding_service.dart index 41512449e..353cea173 100644 --- a/lib/services/geocoding_service.dart +++ b/lib/services/geocoding_service.dart @@ -7,12 +7,12 @@ import 'package:flutter/services.dart'; import 'package:latlong2/latlong.dart'; class GeocodingService { - static const platform = MethodChannel('deckers.thibault/aves/geocoding'); + static const _platform = MethodChannel('deckers.thibault/aves/geocoding'); // geocoding requires Google Play Services static Future> getAddress(LatLng coordinates, Locale locale) async { try { - final result = await platform.invokeMethod('getAddress', { + final result = await _platform.invokeMethod('getAddress', { 'latitude': coordinates.latitude, 'longitude': coordinates.longitude, 'locale': locale.toString(), diff --git a/lib/services/global_search.dart b/lib/services/global_search.dart index 19415e4e2..ee71345f5 100644 --- a/lib/services/global_search.dart +++ b/lib/services/global_search.dart @@ -7,11 +7,11 @@ import 'package:flutter/widgets.dart'; import 'package:intl/date_symbol_data_local.dart'; class GlobalSearch { - static const platform = MethodChannel('deckers.thibault/aves/global_search'); + static const _platform = MethodChannel('deckers.thibault/aves/global_search'); static Future registerCallback() async { try { - await platform.invokeMethod('registerCallback', { + await _platform.invokeMethod('registerCallback', { 'callbackHandle': PluginUtilities.getCallbackHandle(_init)?.toRawHandle(), }); } on PlatformException catch (e, stack) { diff --git a/lib/services/intent_service.dart b/lib/services/intent_service.dart new file mode 100644 index 000000000..47d46e691 --- /dev/null +++ b/lib/services/intent_service.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/services.dart'; +import 'package:streams_channel/streams_channel.dart'; + +class IntentService { + static const _platform = MethodChannel('deckers.thibault/aves/intent'); + static final _stream = StreamsChannel('deckers.thibault/aves/activity_result_stream'); + + static Future> getIntentData() async { + try { + // returns nullable map with 'action' and possibly 'uri' 'mimeType' + final result = await _platform.invokeMethod('getIntentData'); + if (result != null) return (result as Map).cast(); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return {}; + } + + static Future submitPickedItems(List uris) async { + try { + await _platform.invokeMethod('submitPickedItems', { + 'uris': uris, + }); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + } + + static Future submitPickedCollectionFilters(Set? filters) async { + try { + await _platform.invokeMethod('submitPickedCollectionFilters', { + 'filters': filters?.map((filter) => filter.toJson()).toList(), + }); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + } + + static Future?> pickCollectionFilters(Set? initialFilters) async { + try { + final completer = Completer?>(); + _stream.receiveBroadcastStream({ + 'op': 'pickCollectionFilters', + 'initialFilters': initialFilters?.map((filter) => filter.toJson()).toList(), + }).listen( + (data) { + final result = (data as List?)?.cast().map(CollectionFilter.fromJson).whereNotNull().toSet(); + completer.complete(result); + }, + onError: completer.completeError, + onDone: () { + if (!completer.isCompleted) completer.complete(null); + }, + cancelOnError: true, + ); + // `await` here, so that `completeError` will be caught below + return await completer.future; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return null; + } +} diff --git a/lib/services/media/embedded_data_service.dart b/lib/services/media/embedded_data_service.dart index 3e117548d..477bc1703 100644 --- a/lib/services/media/embedded_data_service.dart +++ b/lib/services/media/embedded_data_service.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:aves/model/entry.dart'; import 'package:aves/services/common/services.dart'; import 'package:flutter/services.dart'; @@ -15,12 +13,12 @@ abstract class EmbeddedDataService { } class PlatformEmbeddedDataService implements EmbeddedDataService { - static const platform = MethodChannel('deckers.thibault/aves/embedded'); + static const _platform = MethodChannel('deckers.thibault/aves/embedded'); @override Future> getExifThumbnails(AvesEntry entry) async { try { - final result = await platform.invokeMethod('getExifThumbnails', { + final result = await _platform.invokeMethod('getExifThumbnails', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, @@ -35,7 +33,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { @override Future extractMotionPhotoVideo(AvesEntry entry) async { try { - final result = await platform.invokeMethod('extractMotionPhotoVideo', { + final result = await _platform.invokeMethod('extractMotionPhotoVideo', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, @@ -51,7 +49,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { @override Future extractVideoEmbeddedPicture(AvesEntry entry) async { try { - final result = await platform.invokeMethod('extractVideoEmbeddedPicture', { + final result = await _platform.invokeMethod('extractVideoEmbeddedPicture', { 'uri': entry.uri, 'displayName': '${entry.bestTitle} • Cover', }); @@ -65,7 +63,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { @override Future extractXmpDataProp(AvesEntry entry, String? propPath, String? propMimeType) async { try { - final result = await platform.invokeMethod('extractXmpDataProp', { + final result = await _platform.invokeMethod('extractXmpDataProp', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, diff --git a/lib/services/media/media_edit_service.dart b/lib/services/media/media_edit_service.dart new file mode 100644 index 000000000..00b7788f2 --- /dev/null +++ b/lib/services/media/media_edit_service.dart @@ -0,0 +1,215 @@ +import 'dart:async'; + +import 'package:aves/model/entry.dart'; +import 'package:aves/services/common/image_op_events.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/services/media/enums.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:streams_channel/streams_channel.dart'; + +abstract class MediaEditService { + String get newOpId; + + Future cancelFileOp(String opId); + + Stream delete({ + String? opId, + required Iterable entries, + }); + + Stream move({ + String? opId, + required Map> entriesByDestination, + required bool copy, + required NameConflictStrategy nameConflictStrategy, + }); + + Stream export( + Iterable entries, { + required EntryExportOptions options, + required String destinationAlbum, + required NameConflictStrategy nameConflictStrategy, + }); + + Stream rename({ + String? opId, + required Map entriesToNewName, + }); + + Future> captureFrame( + AvesEntry entry, { + required String desiredName, + required Map exif, + required Uint8List bytes, + required String destinationAlbum, + required NameConflictStrategy nameConflictStrategy, + }); +} + +class PlatformMediaEditService implements MediaEditService { + static const _platform = MethodChannel('deckers.thibault/aves/media_edit'); + static final _opStream = StreamsChannel('deckers.thibault/aves/media_op_stream'); + + static Map _toPlatformEntryMap(AvesEntry entry) { + return { + 'uri': entry.uri, + 'path': entry.path, + 'pageId': entry.pageId, + 'mimeType': entry.mimeType, + 'width': entry.width, + 'height': entry.height, + 'rotationDegrees': entry.rotationDegrees, + 'isFlipped': entry.isFlipped, + 'dateModifiedSecs': entry.dateModifiedSecs, + 'sizeBytes': entry.sizeBytes, + 'trashed': entry.trashed, + 'trashPath': entry.trashDetails?.path, + }; + } + + @override + String get newOpId => DateTime.now().millisecondsSinceEpoch.toString(); + + @override + Future cancelFileOp(String opId) async { + try { + await _platform.invokeMethod('cancelFileOp', { + 'opId': opId, + }); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + } + + @override + Stream delete({ + String? opId, + required Iterable entries, + }) { + try { + return _opStream + .receiveBroadcastStream({ + 'op': 'delete', + 'id': opId, + 'entries': entries.map(_toPlatformEntryMap).toList(), + }) + .where((event) => event is Map) + .map((event) => ImageOpEvent.fromMap(event as Map)); + } on PlatformException catch (e, stack) { + reportService.recordError(e, stack); + return Stream.error(e); + } + } + + @override + Stream move({ + String? opId, + required Map> entriesByDestination, + required bool copy, + required NameConflictStrategy nameConflictStrategy, + }) { + try { + return _opStream + .receiveBroadcastStream({ + 'op': 'move', + 'id': opId, + 'entriesByDestination': entriesByDestination.map((destination, entries) => MapEntry(destination, entries.map(_toPlatformEntryMap).toList())), + 'copy': copy, + 'nameConflictStrategy': nameConflictStrategy.toPlatform(), + }) + .where((event) => event is Map) + .map((event) => MoveOpEvent.fromMap(event as Map)); + } on PlatformException catch (e, stack) { + reportService.recordError(e, stack); + return Stream.error(e); + } + } + + @override + Stream export( + Iterable entries, { + required EntryExportOptions options, + required String destinationAlbum, + required NameConflictStrategy nameConflictStrategy, + }) { + try { + return _opStream + .receiveBroadcastStream({ + 'op': 'export', + 'entries': entries.map(_toPlatformEntryMap).toList(), + 'mimeType': options.mimeType, + 'width': options.width, + 'height': options.height, + 'destinationPath': destinationAlbum, + 'nameConflictStrategy': nameConflictStrategy.toPlatform(), + }) + .where((event) => event is Map) + .map((event) => ExportOpEvent.fromMap(event as Map)); + } on PlatformException catch (e, stack) { + reportService.recordError(e, stack); + return Stream.error(e); + } + } + + @override + Stream rename({ + String? opId, + required Map entriesToNewName, + }) { + try { + return _opStream + .receiveBroadcastStream({ + 'op': 'rename', + 'id': opId, + 'entriesToNewName': entriesToNewName.map((key, value) => MapEntry(_toPlatformEntryMap(key), value)), + }) + .where((event) => event is Map) + .map((event) => MoveOpEvent.fromMap(event as Map)); + } on PlatformException catch (e, stack) { + reportService.recordError(e, stack); + return Stream.error(e); + } + } + + @override + Future> captureFrame( + AvesEntry entry, { + required String desiredName, + required Map exif, + required Uint8List bytes, + required String destinationAlbum, + required NameConflictStrategy nameConflictStrategy, + }) async { + try { + final result = await _platform.invokeMethod('captureFrame', { + 'uri': entry.uri, + 'desiredName': desiredName, + 'exif': exif, + 'bytes': bytes, + 'destinationPath': destinationAlbum, + 'nameConflictStrategy': nameConflictStrategy.toPlatform(), + }); + if (result != null) return (result as Map).cast(); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return {}; + } +} + +@immutable +class EntryExportOptions extends Equatable { + final String mimeType; + final int width, height; + + @override + List get props => [mimeType, width, height]; + + const EntryExportOptions({ + required this.mimeType, + required this.width, + required this.height, + }); +} diff --git a/lib/services/media/media_fetch_service.dart b/lib/services/media/media_fetch_service.dart new file mode 100644 index 000000000..b5a3f6862 --- /dev/null +++ b/lib/services/media/media_fetch_service.dart @@ -0,0 +1,255 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:aves/model/entry.dart'; +import 'package:aves/ref/mime_types.dart'; +import 'package:aves/services/common/output_buffer.dart'; +import 'package:aves/services/common/service_policy.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:streams_channel/streams_channel.dart'; + +abstract class MediaFetchService { + Future getEntry(String uri, String? mimeType); + + Future getSvg( + String uri, + String mimeType, { + int? expectedContentLength, + BytesReceivedCallback? onBytesReceived, + }); + + Future getImage( + String uri, + String mimeType, + int? rotationDegrees, + bool isFlipped, { + int? pageId, + int? expectedContentLength, + BytesReceivedCallback? onBytesReceived, + }); + + // `rect`: region to decode, with coordinates in reference to `imageSize` + Future getRegion( + String uri, + String mimeType, + int rotationDegrees, + bool isFlipped, + int sampleSize, + Rectangle regionRect, + Size imageSize, { + int? pageId, + Object? taskKey, + int? priority, + }); + + 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, + }); + + Future clearSizedThumbnailDiskCache(); + + bool cancelRegion(Object taskKey); + + bool cancelThumbnail(Object taskKey); + + Future? resumeLoading(Object taskKey); +} + +class PlatformMediaFetchService implements MediaFetchService { + static const _platform = MethodChannel('deckers.thibault/aves/media_fetch'); + static final _byteStream = StreamsChannel('deckers.thibault/aves/media_byte_stream'); + static const double _thumbnailDefaultSize = 64.0; + + @override + Future getEntry(String uri, String? mimeType) async { + try { + final result = await _platform.invokeMethod('getEntry', { + 'uri': uri, + 'mimeType': mimeType, + }) as Map; + return AvesEntry.fromMap(result); + } on PlatformException catch (e, stack) { + // do not report issues with simple parameter-less media content + // as it is likely an obsolete Media Store entry + if (!uri.startsWith('content://media/') || uri.contains('?')) { + await reportService.recordError(e, stack); + } + } + return null; + } + + @override + Future getSvg( + String uri, + String mimeType, { + int? expectedContentLength, + BytesReceivedCallback? onBytesReceived, + }) => + getImage( + uri, + mimeType, + 0, + false, + expectedContentLength: expectedContentLength, + onBytesReceived: onBytesReceived, + ); + + @override + Future getImage( + String uri, + String mimeType, + int? rotationDegrees, + bool isFlipped, { + int? pageId, + int? expectedContentLength, + BytesReceivedCallback? onBytesReceived, + }) async { + try { + final completer = Completer.sync(); + final sink = OutputBuffer(); + var bytesReceived = 0; + _byteStream.receiveBroadcastStream({ + 'uri': uri, + 'mimeType': mimeType, + 'rotationDegrees': rotationDegrees ?? 0, + 'isFlipped': isFlipped, + 'pageId': pageId, + }).listen( + (data) { + final chunk = data as Uint8List; + sink.add(chunk); + if (onBytesReceived != null) { + bytesReceived += chunk.length; + try { + onBytesReceived(bytesReceived, expectedContentLength); + } catch (error, stack) { + completer.completeError(error, stack); + return; + } + } + }, + onError: completer.completeError, + onDone: () { + sink.close(); + completer.complete(sink.bytes); + }, + cancelOnError: true, + ); + // `await` here, so that `completeError` will be caught below + return await completer.future; + } on PlatformException catch (e, stack) { + if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { + await reportService.recordError(e, stack); + } + } + return Uint8List(0); + } + + @override + Future getRegion( + String uri, + String mimeType, + int rotationDegrees, + bool isFlipped, + int sampleSize, + Rectangle regionRect, + Size imageSize, { + int? pageId, + Object? taskKey, + int? priority, + }) { + return servicePolicy.call( + () async { + try { + final result = await _platform.invokeMethod('getRegion', { + 'uri': uri, + 'mimeType': mimeType, + 'pageId': pageId, + 'sampleSize': sampleSize, + 'regionX': regionRect.left, + 'regionY': regionRect.top, + 'regionWidth': regionRect.width, + 'regionHeight': regionRect.height, + 'imageWidth': imageSize.width.toInt(), + 'imageHeight': imageSize.height.toInt(), + }); + if (result != null) return result as Uint8List; + } on PlatformException catch (e, stack) { + if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { + await reportService.recordError(e, stack); + } + } + return Uint8List(0); + }, + priority: priority ?? ServiceCallPriority.getRegion, + key: taskKey, + ); + } + + @override + 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, + }) { + return servicePolicy.call( + () async { + try { + final result = await _platform.invokeMethod('getThumbnail', { + 'uri': uri, + 'mimeType': mimeType, + 'dateModifiedSecs': dateModifiedSecs, + 'rotationDegrees': rotationDegrees, + 'isFlipped': isFlipped, + 'widthDip': extent, + 'heightDip': extent, + 'pageId': pageId, + 'defaultSizeDip': _thumbnailDefaultSize, + }); + if (result != null) return result as Uint8List; + } on PlatformException catch (e, stack) { + if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { + await reportService.recordError(e, stack); + } + } + return Uint8List(0); + }, + priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), + key: taskKey, + ); + } + + @override + Future clearSizedThumbnailDiskCache() async { + try { + return _platform.invokeMethod('clearSizedThumbnailDiskCache'); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + } + + @override + bool cancelRegion(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getRegion]); + + @override + bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getFastThumbnail, ServiceCallPriority.getSizedThumbnail]); + + @override + Future? resumeLoading(Object taskKey) => servicePolicy.resume(taskKey); +} diff --git a/lib/services/media/media_file_service.dart b/lib/services/media/media_file_service.dart deleted file mode 100644 index ae4542659..000000000 --- a/lib/services/media/media_file_service.dart +++ /dev/null @@ -1,459 +0,0 @@ -import 'dart:async'; -import 'dart:math'; -import 'dart:typed_data'; -import 'dart:ui'; - -import 'package:aves/model/entry.dart'; -import 'package:aves/ref/mime_types.dart'; -import 'package:aves/services/common/image_op_events.dart'; -import 'package:aves/services/common/output_buffer.dart'; -import 'package:aves/services/common/service_policy.dart'; -import 'package:aves/services/common/services.dart'; -import 'package:aves/services/media/enums.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:streams_channel/streams_channel.dart'; - -abstract class MediaFileService { - String get newOpId; - - Future getEntry(String uri, String? mimeType); - - Future getSvg( - String uri, - String mimeType, { - int? expectedContentLength, - BytesReceivedCallback? onBytesReceived, - }); - - Future getImage( - String uri, - String mimeType, - int? rotationDegrees, - bool isFlipped, { - int? pageId, - int? expectedContentLength, - BytesReceivedCallback? onBytesReceived, - }); - - // `rect`: region to decode, with coordinates in reference to `imageSize` - Future getRegion( - String uri, - String mimeType, - int rotationDegrees, - bool isFlipped, - int sampleSize, - Rectangle regionRect, - Size imageSize, { - int? pageId, - Object? taskKey, - int? priority, - }); - - 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, - }); - - Future clearSizedThumbnailDiskCache(); - - bool cancelRegion(Object taskKey); - - bool cancelThumbnail(Object taskKey); - - Future? resumeLoading(Object taskKey); - - Future cancelFileOp(String opId); - - Stream delete({ - String? opId, - required Iterable entries, - }); - - Stream move({ - String? opId, - required Map> entriesByDestination, - required bool copy, - required NameConflictStrategy nameConflictStrategy, - }); - - Stream export( - Iterable entries, { - required EntryExportOptions options, - required String destinationAlbum, - required NameConflictStrategy nameConflictStrategy, - }); - - Stream rename({ - String? opId, - required Map entriesToNewName, - }); - - Future> captureFrame( - AvesEntry entry, { - required String desiredName, - required Map exif, - required Uint8List bytes, - required String destinationAlbum, - required NameConflictStrategy nameConflictStrategy, - }); -} - -class PlatformMediaFileService implements MediaFileService { - static const platform = MethodChannel('deckers.thibault/aves/media_file'); - static final StreamsChannel _byteStreamChannel = StreamsChannel('deckers.thibault/aves/media_byte_stream'); - static final StreamsChannel _opStreamChannel = StreamsChannel('deckers.thibault/aves/media_op_stream'); - static const double thumbnailDefaultSize = 64.0; - - static Map _toPlatformEntryMap(AvesEntry entry) { - return { - 'uri': entry.uri, - 'path': entry.path, - 'pageId': entry.pageId, - 'mimeType': entry.mimeType, - 'width': entry.width, - 'height': entry.height, - 'rotationDegrees': entry.rotationDegrees, - 'isFlipped': entry.isFlipped, - 'dateModifiedSecs': entry.dateModifiedSecs, - 'sizeBytes': entry.sizeBytes, - 'trashed': entry.trashed, - 'trashPath': entry.trashDetails?.path, - }; - } - - @override - String get newOpId => DateTime.now().millisecondsSinceEpoch.toString(); - - @override - Future getEntry(String uri, String? mimeType) async { - try { - final result = await platform.invokeMethod('getEntry', { - 'uri': uri, - 'mimeType': mimeType, - }) as Map; - return AvesEntry.fromMap(result); - } on PlatformException catch (e, stack) { - // do not report issues with simple parameter-less media content - // as it is likely an obsolete Media Store entry - if (!uri.startsWith('content://media/external/') || uri.contains('?')) { - await reportService.recordError(e, stack); - } - } - return null; - } - - @override - Future getSvg( - String uri, - String mimeType, { - int? expectedContentLength, - BytesReceivedCallback? onBytesReceived, - }) => - getImage( - uri, - mimeType, - 0, - false, - expectedContentLength: expectedContentLength, - onBytesReceived: onBytesReceived, - ); - - @override - Future getImage( - String uri, - String mimeType, - int? rotationDegrees, - bool isFlipped, { - int? pageId, - int? expectedContentLength, - BytesReceivedCallback? onBytesReceived, - }) async { - try { - final completer = Completer.sync(); - final sink = OutputBuffer(); - var bytesReceived = 0; - _byteStreamChannel.receiveBroadcastStream({ - 'uri': uri, - 'mimeType': mimeType, - 'rotationDegrees': rotationDegrees ?? 0, - 'isFlipped': isFlipped, - 'pageId': pageId, - }).listen( - (data) { - final chunk = data as Uint8List; - sink.add(chunk); - if (onBytesReceived != null) { - bytesReceived += chunk.length; - try { - onBytesReceived(bytesReceived, expectedContentLength); - } catch (error, stack) { - completer.completeError(error, stack); - return; - } - } - }, - onError: completer.completeError, - onDone: () { - sink.close(); - completer.complete(sink.bytes); - }, - cancelOnError: true, - ); - // `await` here, so that `completeError` will be caught below - return await completer.future; - } on PlatformException catch (e, stack) { - if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { - await reportService.recordError(e, stack); - } - } - return Uint8List(0); - } - - @override - Future getRegion( - String uri, - String mimeType, - int rotationDegrees, - bool isFlipped, - int sampleSize, - Rectangle regionRect, - Size imageSize, { - int? pageId, - Object? taskKey, - int? priority, - }) { - return servicePolicy.call( - () async { - try { - final result = await platform.invokeMethod('getRegion', { - 'uri': uri, - 'mimeType': mimeType, - 'pageId': pageId, - 'sampleSize': sampleSize, - 'regionX': regionRect.left, - 'regionY': regionRect.top, - 'regionWidth': regionRect.width, - 'regionHeight': regionRect.height, - 'imageWidth': imageSize.width.toInt(), - 'imageHeight': imageSize.height.toInt(), - }); - if (result != null) return result as Uint8List; - } on PlatformException catch (e, stack) { - if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { - await reportService.recordError(e, stack); - } - } - return Uint8List(0); - }, - priority: priority ?? ServiceCallPriority.getRegion, - key: taskKey, - ); - } - - @override - 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, - }) { - return servicePolicy.call( - () async { - try { - final result = await platform.invokeMethod('getThumbnail', { - 'uri': uri, - 'mimeType': mimeType, - 'dateModifiedSecs': dateModifiedSecs, - 'rotationDegrees': rotationDegrees, - 'isFlipped': isFlipped, - 'widthDip': extent, - 'heightDip': extent, - 'pageId': pageId, - 'defaultSizeDip': thumbnailDefaultSize, - }); - if (result != null) return result as Uint8List; - } on PlatformException catch (e, stack) { - if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { - await reportService.recordError(e, stack); - } - } - return Uint8List(0); - }, - priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), - key: taskKey, - ); - } - - @override - Future clearSizedThumbnailDiskCache() async { - try { - return platform.invokeMethod('clearSizedThumbnailDiskCache'); - } on PlatformException catch (e, stack) { - await reportService.recordError(e, stack); - } - } - - @override - bool cancelRegion(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getRegion]); - - @override - bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getFastThumbnail, ServiceCallPriority.getSizedThumbnail]); - - @override - Future? resumeLoading(Object taskKey) => servicePolicy.resume(taskKey); - - @override - Future cancelFileOp(String opId) async { - try { - await platform.invokeMethod('cancelFileOp', { - 'opId': opId, - }); - } on PlatformException catch (e, stack) { - await reportService.recordError(e, stack); - } - } - - @override - Stream delete({ - String? opId, - required Iterable entries, - }) { - try { - return _opStreamChannel - .receiveBroadcastStream({ - 'op': 'delete', - 'id': opId, - 'entries': entries.map(_toPlatformEntryMap).toList(), - }) - .where((event) => event is Map) - .map((event) => ImageOpEvent.fromMap(event as Map)); - } on PlatformException catch (e, stack) { - reportService.recordError(e, stack); - return Stream.error(e); - } - } - - @override - Stream move({ - String? opId, - required Map> entriesByDestination, - required bool copy, - required NameConflictStrategy nameConflictStrategy, - }) { - try { - return _opStreamChannel - .receiveBroadcastStream({ - 'op': 'move', - 'id': opId, - 'entriesByDestination': entriesByDestination.map((destination, entries) => MapEntry(destination, entries.map(_toPlatformEntryMap).toList())), - 'copy': copy, - 'nameConflictStrategy': nameConflictStrategy.toPlatform(), - }) - .where((event) => event is Map) - .map((event) => MoveOpEvent.fromMap(event as Map)); - } on PlatformException catch (e, stack) { - reportService.recordError(e, stack); - return Stream.error(e); - } - } - - @override - Stream export( - Iterable entries, { - required EntryExportOptions options, - required String destinationAlbum, - required NameConflictStrategy nameConflictStrategy, - }) { - try { - return _opStreamChannel - .receiveBroadcastStream({ - 'op': 'export', - 'entries': entries.map(_toPlatformEntryMap).toList(), - 'mimeType': options.mimeType, - 'width': options.width, - 'height': options.height, - 'destinationPath': destinationAlbum, - 'nameConflictStrategy': nameConflictStrategy.toPlatform(), - }) - .where((event) => event is Map) - .map((event) => ExportOpEvent.fromMap(event as Map)); - } on PlatformException catch (e, stack) { - reportService.recordError(e, stack); - return Stream.error(e); - } - } - - @override - Stream rename({ - String? opId, - required Map entriesToNewName, - }) { - try { - return _opStreamChannel - .receiveBroadcastStream({ - 'op': 'rename', - 'id': opId, - 'entriesToNewName': entriesToNewName.map((key, value) => MapEntry(_toPlatformEntryMap(key), value)), - }) - .where((event) => event is Map) - .map((event) => MoveOpEvent.fromMap(event as Map)); - } on PlatformException catch (e, stack) { - reportService.recordError(e, stack); - return Stream.error(e); - } - } - - @override - Future> captureFrame( - AvesEntry entry, { - required String desiredName, - required Map exif, - required Uint8List bytes, - required String destinationAlbum, - required NameConflictStrategy nameConflictStrategy, - }) async { - try { - final result = await platform.invokeMethod('captureFrame', { - 'uri': entry.uri, - 'desiredName': desiredName, - 'exif': exif, - 'bytes': bytes, - 'destinationPath': destinationAlbum, - 'nameConflictStrategy': nameConflictStrategy.toPlatform(), - }); - if (result != null) return (result as Map).cast(); - } on PlatformException catch (e, stack) { - await reportService.recordError(e, stack); - } - return {}; - } -} - -@immutable -class EntryExportOptions extends Equatable { - final String mimeType; - final int width, height; - - @override - List get props => [mimeType, width, height]; - - const EntryExportOptions({ - required this.mimeType, - required this.width, - required this.height, - }); -} diff --git a/lib/services/media/media_store_service.dart b/lib/services/media/media_store_service.dart index 8a6647485..203fd2944 100644 --- a/lib/services/media/media_store_service.dart +++ b/lib/services/media/media_store_service.dart @@ -18,13 +18,13 @@ abstract class MediaStoreService { } class PlatformMediaStoreService implements MediaStoreService { - static const platform = MethodChannel('deckers.thibault/aves/media_store'); - static final StreamsChannel _streamChannel = StreamsChannel('deckers.thibault/aves/media_store_stream'); + static const _platform = MethodChannel('deckers.thibault/aves/media_store'); + static final _stream = StreamsChannel('deckers.thibault/aves/media_store_stream'); @override Future> checkObsoleteContentIds(List knownContentIds) async { try { - final result = await platform.invokeMethod('checkObsoleteContentIds', { + final result = await _platform.invokeMethod('checkObsoleteContentIds', { 'knownContentIds': knownContentIds, }); return (result as List).cast(); @@ -37,7 +37,7 @@ class PlatformMediaStoreService implements MediaStoreService { @override Future> checkObsoletePaths(Map knownPathById) async { try { - final result = await platform.invokeMethod('checkObsoletePaths', { + final result = await _platform.invokeMethod('checkObsoletePaths', { 'knownPathById': knownPathById, }); return (result as List).cast(); @@ -50,7 +50,7 @@ class PlatformMediaStoreService implements MediaStoreService { @override Stream getEntries(Map knownEntries, {String? directory}) { try { - return _streamChannel + return _stream .receiveBroadcastStream({ 'knownEntries': knownEntries, 'directory': directory, @@ -67,7 +67,7 @@ class PlatformMediaStoreService implements MediaStoreService { @override Future scanFile(String path, String mimeType) async { try { - final result = await platform.invokeMethod('scanFile', { + final result = await _platform.invokeMethod('scanFile', { 'path': path, 'mimeType': mimeType, }); diff --git a/lib/services/metadata/metadata_edit_service.dart b/lib/services/metadata/metadata_edit_service.dart index bdc7cd300..6cd48fae4 100644 --- a/lib/services/metadata/metadata_edit_service.dart +++ b/lib/services/metadata/metadata_edit_service.dart @@ -23,7 +23,7 @@ abstract class MetadataEditService { } class PlatformMetadataEditService implements MetadataEditService { - static const platform = MethodChannel('deckers.thibault/aves/metadata_edit'); + static const _platform = MethodChannel('deckers.thibault/aves/metadata_edit'); static Map _toPlatformEntryMap(AvesEntry entry) { return { @@ -44,7 +44,7 @@ class PlatformMetadataEditService implements MetadataEditService { Future> rotate(AvesEntry entry, {required bool clockwise}) async { try { // returns map with: 'rotationDegrees' 'isFlipped' - final result = await platform.invokeMethod('rotate', { + final result = await _platform.invokeMethod('rotate', { 'entry': _toPlatformEntryMap(entry), 'clockwise': clockwise, }); @@ -61,7 +61,7 @@ class PlatformMetadataEditService implements MetadataEditService { Future> flip(AvesEntry entry) async { try { // returns map with: 'rotationDegrees' 'isFlipped' - final result = await platform.invokeMethod('flip', { + final result = await _platform.invokeMethod('flip', { 'entry': _toPlatformEntryMap(entry), }); if (result != null) return (result as Map).cast(); @@ -76,7 +76,7 @@ class PlatformMetadataEditService implements MetadataEditService { @override Future> editExifDate(AvesEntry entry, DateModifier modifier) async { try { - final result = await platform.invokeMethod('editDate', { + final result = await _platform.invokeMethod('editDate', { 'entry': _toPlatformEntryMap(entry), 'dateMillis': modifier.setDateTime?.millisecondsSinceEpoch, 'shiftMinutes': modifier.shiftMinutes, @@ -98,7 +98,7 @@ class PlatformMetadataEditService implements MetadataEditService { bool autoCorrectTrailerOffset = true, }) async { try { - final result = await platform.invokeMethod('editMetadata', { + final result = await _platform.invokeMethod('editMetadata', { 'entry': _toPlatformEntryMap(entry), 'metadata': metadata.map((type, value) => MapEntry(_toPlatformMetadataType(type), value)), 'autoCorrectTrailerOffset': autoCorrectTrailerOffset, @@ -115,7 +115,7 @@ class PlatformMetadataEditService implements MetadataEditService { @override Future> removeTrailerVideo(AvesEntry entry) async { try { - final result = await platform.invokeMethod('removeTrailerVideo', { + final result = await _platform.invokeMethod('removeTrailerVideo', { 'entry': _toPlatformEntryMap(entry), }); if (result != null) return (result as Map).cast(); @@ -130,7 +130,7 @@ class PlatformMetadataEditService implements MetadataEditService { @override Future> removeTypes(AvesEntry entry, Set types) async { try { - final result = await platform.invokeMethod('removeTypes', { + final result = await _platform.invokeMethod('removeTypes', { 'entry': _toPlatformEntryMap(entry), 'types': types.map(_toPlatformMetadataType).toList(), }); diff --git a/lib/services/metadata/metadata_fetch_service.dart b/lib/services/metadata/metadata_fetch_service.dart index ad756bcf5..59e5a8641 100644 --- a/lib/services/metadata/metadata_fetch_service.dart +++ b/lib/services/metadata/metadata_fetch_service.dart @@ -38,14 +38,14 @@ abstract class MetadataFetchService { } class PlatformMetadataFetchService implements MetadataFetchService { - static const platform = MethodChannel('deckers.thibault/aves/metadata_fetch'); + static const _platform = MethodChannel('deckers.thibault/aves/metadata_fetch'); @override Future getAllMetadata(AvesEntry entry) async { if (entry.isSvg) return {}; try { - final result = await platform.invokeMethod('getAllMetadata', { + final result = await _platform.invokeMethod('getAllMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, @@ -76,7 +76,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { // 'longitude': longitude (double) // 'xmpSubjects': ';' separated XMP subjects (string) // 'xmpTitleDescription': XMP title or XMP description (string) - final result = await platform.invokeMethod('getCatalogMetadata', { + final result = await _platform.invokeMethod('getCatalogMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'path': entry.path, @@ -106,7 +106,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { try { // returns map with values for: 'aperture' (double), 'exposureTime' (description), 'focalLength' (double), 'iso' (int) - final result = await platform.invokeMethod('getOverlayMetadata', { + final result = await _platform.invokeMethod('getOverlayMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, @@ -123,7 +123,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { @override Future getGeoTiffInfo(AvesEntry entry) async { try { - final result = await platform.invokeMethod('getGeoTiffInfo', { + final result = await _platform.invokeMethod('getGeoTiffInfo', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, @@ -140,7 +140,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { @override Future getMultiPageInfo(AvesEntry entry) async { try { - final result = await platform.invokeMethod('getMultiPageInfo', { + final result = await _platform.invokeMethod('getMultiPageInfo', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, @@ -167,7 +167,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { // returns map with values for: // 'croppedAreaLeft' (int), 'croppedAreaTop' (int), 'croppedAreaWidth' (int), 'croppedAreaHeight' (int), // 'fullPanoWidth' (int), 'fullPanoHeight' (int) - final result = await platform.invokeMethod('getPanoramaInfo', { + final result = await _platform.invokeMethod('getPanoramaInfo', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, @@ -184,7 +184,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { @override Future>?> getIptc(AvesEntry entry) async { try { - final result = await platform.invokeMethod('getIptc', { + final result = await _platform.invokeMethod('getIptc', { 'mimeType': entry.mimeType, 'uri': entry.uri, }); @@ -200,7 +200,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { @override Future getXmp(AvesEntry entry) async { try { - final result = await platform.invokeMethod('getXmp', { + final result = await _platform.invokeMethod('getXmp', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, @@ -222,7 +222,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { if (exists != null) return SynchronousFuture(exists); try { - exists = await platform.invokeMethod('hasContentResolverProp', { + exists = await _platform.invokeMethod('hasContentResolverProp', { 'prop': prop, }); } on PlatformException catch (e, stack) { @@ -236,7 +236,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { @override Future getContentResolverProp(AvesEntry entry, String prop) async { try { - return await platform.invokeMethod('getContentResolverProp', { + return await _platform.invokeMethod('getContentResolverProp', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'prop': prop, @@ -252,7 +252,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { @override Future getDate(AvesEntry entry, MetadataField field) async { try { - final result = await platform.invokeMethod('getDate', { + final result = await _platform.invokeMethod('getDate', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, diff --git a/lib/services/metadata/svg_metadata_service.dart b/lib/services/metadata/svg_metadata_service.dart index 424839825..50c6b8cb9 100644 --- a/lib/services/metadata/svg_metadata_service.dart +++ b/lib/services/metadata/svg_metadata_service.dart @@ -17,7 +17,7 @@ class SvgMetadataService { static Future getSize(AvesEntry entry) async { try { - final data = await mediaFileService.getSvg(entry.uri, entry.mimeType); + final data = await mediaFetchService.getSvg(entry.uri, entry.mimeType); final document = XmlDocument.parse(utf8.decode(data)); final root = document.rootElement; @@ -63,7 +63,7 @@ class SvgMetadataService { } try { - final data = await mediaFileService.getSvg(entry.uri, entry.mimeType); + final data = await mediaFetchService.getSvg(entry.uri, entry.mimeType); final document = XmlDocument.parse(utf8.decode(data)); final root = document.rootElement; diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 6c7b8e45a..a42f36e0a 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:aves/services/common/output_buffer.dart'; import 'package:aves/services/common/services.dart'; @@ -40,13 +39,13 @@ abstract class StorageService { } class PlatformStorageService implements StorageService { - static const platform = MethodChannel('deckers.thibault/aves/storage'); - static final StreamsChannel storageAccessChannel = StreamsChannel('deckers.thibault/aves/storage_access_stream'); + static const _platform = MethodChannel('deckers.thibault/aves/storage'); + static final _stream = StreamsChannel('deckers.thibault/aves/activity_result_stream'); @override Future> getStorageVolumes() async { try { - final result = await platform.invokeMethod('getStorageVolumes'); + final result = await _platform.invokeMethod('getStorageVolumes'); return (result as List).cast().map(StorageVolume.fromMap).toSet(); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -57,7 +56,7 @@ class PlatformStorageService implements StorageService { @override Future getFreeSpace(StorageVolume volume) async { try { - final result = await platform.invokeMethod('getFreeSpace', { + final result = await _platform.invokeMethod('getFreeSpace', { 'path': volume.path, }); return result as int?; @@ -70,7 +69,7 @@ class PlatformStorageService implements StorageService { @override Future> getGrantedDirectories() async { try { - final result = await platform.invokeMethod('getGrantedDirectories'); + final result = await _platform.invokeMethod('getGrantedDirectories'); return (result as List).cast(); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -81,7 +80,7 @@ class PlatformStorageService implements StorageService { @override Future> getInaccessibleDirectories(Iterable dirPaths) async { try { - final result = await platform.invokeMethod('getInaccessibleDirectories', { + final result = await _platform.invokeMethod('getInaccessibleDirectories', { 'dirPaths': dirPaths.toList(), }); if (result != null) { @@ -96,7 +95,7 @@ class PlatformStorageService implements StorageService { @override Future> getRestrictedDirectories() async { try { - final result = await platform.invokeMethod('getRestrictedDirectories'); + final result = await _platform.invokeMethod('getRestrictedDirectories'); if (result != null) { return (result as List).cast().map(VolumeRelativeDirectory.fromMap).toSet(); } @@ -109,7 +108,7 @@ class PlatformStorageService implements StorageService { @override Future revokeDirectoryAccess(String path) async { try { - await platform.invokeMethod('revokeDirectoryAccess', { + await _platform.invokeMethod('revokeDirectoryAccess', { 'path': path, }); } on PlatformException catch (e, stack) { @@ -122,7 +121,7 @@ class PlatformStorageService implements StorageService { @override Future deleteEmptyDirectories(Iterable dirPaths) async { try { - final result = await platform.invokeMethod('deleteEmptyDirectories', { + final result = await _platform.invokeMethod('deleteEmptyDirectories', { 'dirPaths': dirPaths.toList(), }); if (result != null) return result as int; @@ -135,7 +134,7 @@ class PlatformStorageService implements StorageService { @override Future canRequestMediaFileAccess() async { try { - final result = await platform.invokeMethod('canRequestMediaFileBulkAccess'); + final result = await _platform.invokeMethod('canRequestMediaFileBulkAccess'); if (result != null) return result as bool; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -146,7 +145,7 @@ class PlatformStorageService implements StorageService { @override Future canInsertMedia(Set directories) async { try { - final result = await platform.invokeMethod('canInsertMedia', { + final result = await _platform.invokeMethod('canInsertMedia', { 'directories': directories.map((v) => v.toMap()).toList(), }); if (result != null) return result as bool; @@ -161,7 +160,7 @@ class PlatformStorageService implements StorageService { Future requestDirectoryAccess(String path) async { try { final completer = Completer(); - storageAccessChannel.receiveBroadcastStream({ + _stream.receiveBroadcastStream({ 'op': 'requestDirectoryAccess', 'path': path, }).listen( @@ -185,7 +184,7 @@ class PlatformStorageService implements StorageService { Future requestMediaFileAccess(List uris, List mimeTypes) async { try { final completer = Completer(); - storageAccessChannel.receiveBroadcastStream({ + _stream.receiveBroadcastStream({ 'op': 'requestMediaFileAccess', 'uris': uris, 'mimeTypes': mimeTypes, @@ -216,7 +215,7 @@ class PlatformStorageService implements StorageService { Future createFile(String name, String mimeType, Uint8List bytes) async { try { final completer = Completer(); - storageAccessChannel.receiveBroadcastStream({ + _stream.receiveBroadcastStream({ 'op': 'createFile', 'name': name, 'mimeType': mimeType, @@ -242,7 +241,7 @@ class PlatformStorageService implements StorageService { try { final completer = Completer.sync(); final sink = OutputBuffer(); - storageAccessChannel.receiveBroadcastStream({ + _stream.receiveBroadcastStream({ 'op': 'openFile', 'mimeType': mimeType, }).listen( diff --git a/lib/services/viewer_service.dart b/lib/services/viewer_service.dart deleted file mode 100644 index 335bd7b71..000000000 --- a/lib/services/viewer_service.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:aves/services/common/services.dart'; -import 'package:flutter/services.dart'; - -class ViewerService { - static const platform = MethodChannel('deckers.thibault/aves/viewer'); - - static Future> getIntentData() async { - try { - // returns nullable map with 'action' and possibly 'uri' 'mimeType' - final result = await platform.invokeMethod('getIntentData'); - if (result != null) return (result as Map).cast(); - } on PlatformException catch (e, stack) { - await reportService.recordError(e, stack); - } - return {}; - } - - static Future pick(List uris) async { - try { - await platform.invokeMethod('pick', { - 'uris': uris, - }); - } on PlatformException catch (e, stack) { - await reportService.recordError(e, stack); - } - } -} diff --git a/lib/services/wallpaper_service.dart b/lib/services/wallpaper_service.dart index 78d3e1d00..bff7e2152 100644 --- a/lib/services/wallpaper_service.dart +++ b/lib/services/wallpaper_service.dart @@ -1,15 +1,13 @@ -import 'dart:typed_data'; - import 'package:aves/model/wallpaper_target.dart'; import 'package:aves/services/common/services.dart'; import 'package:flutter/services.dart'; class WallpaperService { - static const platform = MethodChannel('deckers.thibault/aves/wallpaper'); + static const _platform = MethodChannel('deckers.thibault/aves/wallpaper'); static Future set(Uint8List bytes, WallpaperTarget target) async { try { - await platform.invokeMethod('setWallpaper', { + await _platform.invokeMethod('setWallpaper', { 'bytes': bytes, 'home': {WallpaperTarget.home, WallpaperTarget.homeLock}.contains(target), 'lock': {WallpaperTarget.lock, WallpaperTarget.homeLock}.contains(target), diff --git a/lib/services/widget_service.dart b/lib/services/widget_service.dart new file mode 100644 index 000000000..f938e8cd1 --- /dev/null +++ b/lib/services/widget_service.dart @@ -0,0 +1,29 @@ +import 'package:aves/services/common/services.dart'; +import 'package:flutter/services.dart'; + +class WidgetService { + static const _configureChannel = MethodChannel('deckers.thibault/aves/widget_configure'); + static const _updateChannel = MethodChannel('deckers.thibault/aves/widget_update'); + + static Future configure() async { + try { + await _configureChannel.invokeMethod('configure'); + return true; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return false; + } + + static Future update(int widgetId) async { + try { + await _updateChannel.invokeMethod('update', { + 'widgetId': widgetId, + }); + return true; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return false; + } +} diff --git a/lib/services/window_service.dart b/lib/services/window_service.dart index 2a0b53b1b..1c99b1161 100644 --- a/lib/services/window_service.dart +++ b/lib/services/window_service.dart @@ -3,6 +3,8 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; abstract class WindowService { + Future isActivity(); + Future keepScreenOn(bool on); Future isRotationLocked(); @@ -15,12 +17,23 @@ abstract class WindowService { } class PlatformWindowService implements WindowService { - static const platform = MethodChannel('deckers.thibault/aves/window'); + static const _platform = MethodChannel('deckers.thibault/aves/window'); + + @override + Future isActivity() async { + try { + final result = await _platform.invokeMethod('isActivity'); + if (result != null) return result as bool; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return false; + } @override Future keepScreenOn(bool on) async { try { - await platform.invokeMethod('keepScreenOn', { + await _platform.invokeMethod('keepScreenOn', { 'on': on, }); } on PlatformException catch (e, stack) { @@ -31,7 +44,7 @@ class PlatformWindowService implements WindowService { @override Future isRotationLocked() async { try { - final result = await platform.invokeMethod('isRotationLocked'); + final result = await _platform.invokeMethod('isRotationLocked'); if (result != null) return result as bool; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -58,7 +71,7 @@ class PlatformWindowService implements WindowService { break; } try { - await platform.invokeMethod('requestOrientation', { + await _platform.invokeMethod('requestOrientation', { 'orientation': orientationCode, }); } on PlatformException catch (e, stack) { @@ -69,7 +82,7 @@ class PlatformWindowService implements WindowService { @override Future canSetCutoutMode() async { try { - final result = await platform.invokeMethod('canSetCutoutMode'); + final result = await _platform.invokeMethod('canSetCutoutMode'); if (result != null) return result as bool; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -80,7 +93,7 @@ class PlatformWindowService implements WindowService { @override Future setCutoutMode(bool use) async { try { - await platform.invokeMethod('setCutoutMode', { + await _platform.invokeMethod('setCutoutMode', { 'use': use, }); } on PlatformException catch (e, stack) { diff --git a/lib/theme/themes.dart b/lib/theme/themes.dart index 0cb4e1fd0..b701deb0d 100644 --- a/lib/theme/themes.dart +++ b/lib/theme/themes.dart @@ -73,7 +73,7 @@ class Themes { ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( - primary: _lightLabelColor, + foregroundColor: _lightLabelColor, ), ), tooltipTheme: _tooltipTheme, @@ -128,7 +128,7 @@ class Themes { ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( - primary: _darkLabelColor, + foregroundColor: _darkLabelColor, ), ), tooltipTheme: _tooltipTheme, diff --git a/lib/utils/collection_utils.dart b/lib/utils/collection_utils.dart index 2f5db0ce1..74b87fac5 100644 --- a/lib/utils/collection_utils.dart +++ b/lib/utils/collection_utils.dart @@ -1,15 +1,15 @@ import 'package:collection/collection.dart'; extension ExtraMapNullableKey on Map { - Map whereNotNullKey() => {for (var v in keys.whereNotNull()) v: this[v]!}; + Map whereNotNullKey() => {for (var v in keys.whereNotNull()) v: this[v] as V}; } extension ExtraMapNullableValue on Map { - Map whereNotNullValue() => {for (var kv in entries.where((kv) => kv.value != null)) kv.key: kv.value!}; + Map whereNotNullValue() => {for (var kv in entries.where((kv) => kv.value != null)) kv.key: kv.value as V}; } extension ExtraMapNullableKeyValue on Map { Map whereNotNullKey() => {for (var v in keys.whereNotNull()) v: this[v]}; - Map whereNotNullValue() => {for (var kv in entries.where((kv) => kv.value != null)) kv.key: kv.value!}; + Map whereNotNullValue() => {for (var kv in entries.where((kv) => kv.value != null)) kv.key: kv.value as V}; } diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index ec9e2a8c2..64a0a629b 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -9,6 +9,8 @@ class Constants { // so we give it a `strutStyle` with a slightly larger height static const overflowStrutStyle = StrutStyle(height: 1.3); + static const double colorPickerRadius = 16; + static const titleTextStyle = TextStyle( fontSize: 20, fontWeight: FontWeight.w300, @@ -22,6 +24,11 @@ class Constants { ) ]; + static const boraBoraGradientColors = [ + Color(0xff2bc0e4), + Color(0xffeaecc6), + ]; + // Bidi fun, cf https://www.unicode.org/reports/tr9/ // First Strong Isolate static const fsi = '\u2068'; @@ -279,7 +286,7 @@ class Constants { sourceUrl: 'https://github.com/flutter/packages/tree/master/packages/palette_generator', ), Dependency( - name: 'Panorama', + name: 'Panorama (Aves fork)', license: 'Apache 2.0', sourceUrl: 'https://github.com/zesage/panorama', ), diff --git a/lib/utils/time_utils.dart b/lib/utils/time_utils.dart index f448aef68..c6d4dd563 100644 --- a/lib/utils/time_utils.dart +++ b/lib/utils/time_utils.dart @@ -14,6 +14,12 @@ extension ExtraDateTime on DateTime { bool get isThisMonth => isAtSameMonthAs(DateTime.now()); bool get isThisYear => isAtSameYearAs(DateTime.now()); + + DateTime get date => DateTime(year, month, day); + + DateTime addMonths(int months) => DateTime(year, month + months, day, hour, minute, second, millisecond, microsecond); + + DateTime addDays(int days) => DateTime(year, month, day + days, hour, minute, second, millisecond, microsecond); } final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); @@ -41,7 +47,8 @@ DateTime? dateTimeFromMillis(int? millis, {bool isUtc = false}) { final _unixStampMillisPattern = RegExp(r'\d{13}'); final _unixStampSecPattern = RegExp(r'\d{10}'); -final _plainPattern = RegExp(r'(\d{8})([_-\s](\d{6})([_-\s](\d{3}))?)?'); +final _dateYMD8Hms6Sub3Pattern = RegExp(r'(\d{8})([_-\s](\d{6})([_-\s](\d{3}))?)?'); +final _dateY4M2D2H2m2s2Sub3Pattern = RegExp(r'(\d{4})-(\d{1,2})-(\d{1,2})-(\d{1,2})-(\d{1,2})-(\d{1,2})-(\d{1,3})'); DateTime? parseUnknownDateFormat(String? s) { if (s == null) return null; @@ -68,7 +75,7 @@ DateTime? parseUnknownDateFormat(String? s) { } } - match = _plainPattern.firstMatch(s); + match = _dateYMD8Hms6Sub3Pattern.firstMatch(s); if (match != null) { final dateString = match.group(1); final timeString = match.group(3); @@ -95,5 +102,20 @@ DateTime? parseUnknownDateFormat(String? s) { } } + match = _dateY4M2D2H2m2s2Sub3Pattern.firstMatch(s); + if (match != null) { + final year = int.tryParse(match.group(1)!); + final month = int.tryParse(match.group(2)!); + final day = int.tryParse(match.group(3)!); + final hour = int.tryParse(match.group(4)!); + final minute = int.tryParse(match.group(5)!); + final second = int.tryParse(match.group(6)!); + final millis = int.tryParse(match.group(7)!); + + if (year != null && month != null && day != null && hour != null && minute != null && second != null && millis != null) { + return DateTime(year, month, day, hour, minute, second, millis); + } + } + return null; } diff --git a/lib/widget_common.dart b/lib/widget_common.dart new file mode 100644 index 000000000..79ad994ef --- /dev/null +++ b/lib/widget_common.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:aves/app_flavor.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'; +import 'package:aves/model/source/media_store_source.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/widgets/home_widget.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +const _widgetDrawChannel = MethodChannel('deckers.thibault/aves/widget_draw'); + +void widgetMainCommon(AppFlavor flavor) async { + WidgetsFlutterBinding.ensureInitialized(); + initPlatformServices(); + await settings.init(monitorPlatformSettings: false); + + _widgetDrawChannel.setMethodCallHandler((call) async { + // widget settings may be modified in a different process after channel setup + await settings.reload(); + + switch (call.method) { + case 'drawWidget': + return _drawWidget(call.arguments); + default: + throw PlatformException(code: 'not-implemented', message: 'failed to handle method=${call.method}'); + } + }); +} + +Future _drawWidget(dynamic args) async { + final widgetId = args['widgetId'] as int; + final widthPx = args['widthPx'] as int; + final heightPx = args['heightPx'] as int; + final devicePixelRatio = args['devicePixelRatio'] as double; + final drawEntryImage = args['drawEntryImage'] as bool; + final reuseEntry = args['reuseEntry'] as bool; + + final entry = drawEntryImage ? await _getWidgetEntry(widgetId, reuseEntry) : null; + final painter = HomeWidgetPainter( + entry: entry, + devicePixelRatio: devicePixelRatio, + ); + return painter.drawWidget( + widthPx: widthPx, + heightPx: heightPx, + outline: settings.getWidgetOutline(widgetId), + shape: settings.getWidgetShape(widgetId), + ); +} + +Future _getWidgetEntry(int widgetId, bool reuseEntry) async { + final uri = reuseEntry ? settings.getWidgetUri(widgetId) : null; + if (uri != null) { + final entry = await mediaFetchService.getEntry(uri, null); + if (entry != null) return entry; + } + + final filters = settings.getWidgetCollectionFilters(widgetId); + final source = MediaStoreSource(); + final readyCompleter = Completer(); + source.stateNotifier.addListener(() { + if (source.stateNotifier.value == SourceState.ready) { + readyCompleter.complete(); + } + }); + await source.init(canAnalyze: false); + await readyCompleter.future; + + final entries = CollectionLens(source: source, filters: filters).sortedEntries; + entries.shuffle(); + final entry = entries.firstOrNull; + if (entry != null) { + settings.setWidgetUri(widgetId, entry.uri); + } + return entry; +} diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart index 2cdb16d6e..ed44b7809 100644 --- a/lib/widgets/about/bug_report.dart +++ b/lib/widgets/about/bug_report.dart @@ -1,6 +1,5 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; import 'package:aves/app_flavor.dart'; import 'package:aves/flutter_version.dart'; @@ -68,7 +67,7 @@ class _BugReportState extends State with FeedbackMixin { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildStep(1, l10n.aboutBugSaveLogInstruction, l10n.aboutBugSaveLogButton, _saveLogs), + _buildStep(1, l10n.aboutBugSaveLogInstruction, l10n.saveTooltip, _saveLogs), _buildStep(2, l10n.aboutBugCopyInfoInstruction, l10n.aboutBugCopyInfoButton, _copySystemInfo), FutureBuilder( future: _infoLoader, diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 5b7c64661..87e43b8cb 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -83,7 +83,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { // the list itself needs to be reassigned List _navigatorObservers = [AvesApp.pageRouteObserver]; final EventChannel _mediaStoreChangeChannel = const OptionalEventChannel('deckers.thibault/aves/media_store_change'); - final EventChannel _newIntentChannel = const OptionalEventChannel('deckers.thibault/aves/intent'); + final EventChannel _newIntentChannel = const OptionalEventChannel('deckers.thibault/aves/new_intent_stream'); final EventChannel _analysisCompletionChannel = const OptionalEventChannel('deckers.thibault/aves/analysis_events'); final EventChannel _errorChannel = const OptionalEventChannel('deckers.thibault/aves/error'); @@ -228,8 +228,10 @@ class _AvesAppState extends State with WidgetsBindingObserver { case AppMode.pickMultipleMediaExternal: _saveTopEntries(); break; + case AppMode.pickCollectionFiltersExternal: case AppMode.pickMediaInternal: case AppMode.pickFilterInternal: + case AppMode.screenSaver: case AppMode.setWallpaper: case AppMode.slideshow: case AppMode.view: diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 5d1624d68..bd14c2eda 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -138,7 +138,7 @@ class _CollectionAppBarState extends State with SingleTickerPr return AvesAppBar( contentHeight: appBarContentHeight, leading: _buildAppBarLeading( - hasDrawer: appMode.hasDrawer, + hasDrawer: appMode.canNavigate, isSelecting: isSelecting, ), title: _buildAppBarTitle(isSelecting), @@ -228,7 +228,7 @@ class _CollectionAppBarState extends State with SingleTickerPr ); } return InteractiveAppBarTitle( - onTap: appMode.canSearch ? _goToSearch : null, + onTap: appMode.canNavigate ? _goToSearch : null, child: title, ); } diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 30005f025..6c9d116e9 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -380,8 +380,10 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge selector: (context, mq) => mq.effectiveBottomPadding, builder: (context, mqPaddingBottom, child) { return Selector( - selector: (context, s) => s.showBottomNavigationBar, - builder: (context, showBottomNavigationBar, child) { + selector: (context, s) => s.enableBottomNavigationBar, + builder: (context, enableBottomNavigationBar, child) { + final canNavigate = context.select, bool>((v) => v.value.canNavigate); + final showBottomNavigationBar = canNavigate && enableBottomNavigationBar; final navBarHeight = showBottomNavigationBar ? AppBottomNavBar.height : 0; return Selector, List>( selector: (context, layout) => layout.sectionLayouts, diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index d36b14166..fda86253b 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -10,14 +10,14 @@ import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/services/viewer_service.dart'; +import 'package:aves/services/intent_service.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/collection_grid.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/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_fab.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/query_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart'; @@ -79,7 +79,6 @@ class _CollectionPageState extends State { @override Widget build(BuildContext context) { - final appMode = context.watch>().value; final liveFilter = _collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?; return MediaQueryDataProvider( child: SelectionProvider( @@ -87,8 +86,10 @@ class _CollectionPageState extends State { selector: (context, selection) => selection.selectedItems.isNotEmpty, builder: (context, hasSelection, child) { return Selector( - selector: (context, s) => s.showBottomNavigationBar, - builder: (context, showBottomNavigationBar, child) { + selector: (context, s) => s.enableBottomNavigationBar, + builder: (context, enableBottomNavigationBar, child) { + final canNavigate = context.select, bool>((v) => v.value.canNavigate); + final showBottomNavigationBar = canNavigate && enableBottomNavigationBar; return NotificationListener( onNotification: (notification) { _draggableScrollBarEventStreamController.add(notification.event); @@ -126,25 +127,7 @@ class _CollectionPageState extends State { ), ), ), - floatingActionButton: appMode == AppMode.pickMultipleMediaExternal && hasSelection - ? TooltipTheme( - data: TooltipTheme.of(context).copyWith( - preferBelow: false, - ), - child: FloatingActionButton( - tooltip: context.l10n.collectionPickPageTitle, - onPressed: () { - final items = context.read>().selectedItems; - final uris = items.map((entry) => entry.uri).toList(); - ViewerService.pick(uris); - }, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16)), - ), - child: const Icon(AIcons.apply), - ), - ) - : null, + floatingActionButton: _buildFab(context, hasSelection), drawer: AppDrawer(currentCollection: _collection), bottomNavigationBar: showBottomNavigationBar ? AppBottomNavBar( @@ -164,6 +147,40 @@ class _CollectionPageState extends State { ); } + Widget? _buildFab(BuildContext context, bool hasSelection) { + final appMode = context.watch>().value; + switch (appMode) { + case AppMode.pickMultipleMediaExternal: + return hasSelection + ? AvesFab( + tooltip: context.l10n.collectionPickPageTitle, + onPressed: () { + final items = context.read>().selectedItems; + final uris = items.map((entry) => entry.uri).toList(); + IntentService.submitPickedItems(uris); + }, + ) + : null; + case AppMode.pickCollectionFiltersExternal: + return AvesFab( + tooltip: context.l10n.collectionPickPageTitle, + onPressed: () { + final filters = _collection.filters; + IntentService.submitPickedCollectionFilters(filters); + }, + ); + case AppMode.main: + case AppMode.pickSingleMediaExternal: + case AppMode.pickMediaInternal: + case AppMode.pickFilterInternal: + case AppMode.screenSaver: + case AppMode.setWallpaper: + case AppMode.slideshow: + case AppMode.view: + return null; + } + } + Future _checkInitHighlight() async { final highlightTest = widget.highlightTest; if (highlightTest == null) return; diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 09a719122..ebb532e35 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -65,7 +65,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware return isSelecting && selectedItemCount == itemCount; // browsing case EntrySetAction.searchCollection: - return appMode.canSearch && !isSelecting; + return appMode.canNavigate && !isSelecting; case EntrySetAction.toggleTitleSearch: return !isSelecting; case EntrySetAction.addShortcut: @@ -283,12 +283,12 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware if (!pureTrash && !await checkStoragePermissionForAlbums(context, selectionDirs, entries: entries)) return; source.pauseMonitoring(); - final opId = mediaFileService.newOpId; + final opId = mediaEditService.newOpId; await showOpReport( context: context, - opStream: mediaFileService.delete(opId: opId, entries: entries), + opStream: mediaEditService.delete(opId: opId, entries: entries), itemCount: todoCount, - onCancel: () => mediaFileService.cancelFileOp(opId), + onCancel: () => mediaEditService.cancelFileOp(opId), onDone: (processed) async { final successOps = processed.where((e) => e.success).toSet(); final deletedOps = successOps.where((e) => !e.skipped).toSet(); diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index ee4129c49..5170fff8e 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -1,9 +1,12 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/identity/aves_app_bar.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/material.dart'; class FilterBar extends StatefulWidget { + static const EdgeInsets chipPadding = EdgeInsets.symmetric(horizontal: 4); + static const EdgeInsets rowPadding = EdgeInsets.symmetric(horizontal: 4); static const double verticalPadding = 16; static const double preferredHeight = AvesFilterChip.minChipHeight + verticalPadding; @@ -14,7 +17,7 @@ class FilterBar extends StatefulWidget { FilterBar({ super.key, required Set filters, - required this.removable, + this.removable = false, this.onTap, }) : filters = List.from(filters)..sort(); @@ -26,6 +29,8 @@ class _FilterBarState extends State { final GlobalKey _animatedListKey = GlobalKey(debugLabel: 'filter-bar-animated-list'); CollectionFilter? _userTappedFilter; + List get filters => widget.filters; + FilterCallback? get onTap => widget.onTap; @override @@ -86,33 +91,64 @@ class _FilterBarState extends State { onNotification: (notification) => true, child: AnimatedList( key: _animatedListKey, - initialItemCount: widget.filters.length, + initialItemCount: filters.length, scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 4), + padding: FilterBar.rowPadding, itemBuilder: (context, index, animation) { - if (index >= widget.filters.length) return const SizedBox(); - return _buildChip(widget.filters.toList()[index]); + if (index >= filters.length) return const SizedBox(); + return _buildChip(filters.toList()[index]); }, ), ), ); } - Padding _buildChip(CollectionFilter filter) { + Widget _buildChip(CollectionFilter filter) { + return _Chip( + filter: filter, + removable: widget.removable, + single: filters.length == 1, + onTap: onTap != null + ? (filter) { + _userTappedFilter = filter; + onTap!(filter); + } + : null, + ); + } +} + +class _Chip extends StatelessWidget { + final CollectionFilter filter; + final bool removable, single; + final FilterCallback? onTap; + + const _Chip({ + required this.filter, + required this.removable, + required this.single, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), + padding: FilterBar.chipPadding, child: Center( child: AvesFilterChip( key: ValueKey(filter), filter: filter, - removable: widget.removable, - heroType: HeroType.always, - onTap: onTap != null - ? (filter) { - _userTappedFilter = filter; - onTap!(filter); - } + removable: removable, + maxWidth: single + ? AvesFilterChip.computeMaxWidth( + context, + minChipPerRow: 1, + chipPadding: FilterBar.chipPadding.horizontal, + rowPadding: FilterBar.rowPadding.horizontal + AvesFloatingBar.margin.horizontal, + ) : null, + heroType: HeroType.always, + onTap: onTap, ), ), ); diff --git a/lib/widgets/collection/grid/tile.dart b/lib/widgets/collection/grid/tile.dart index 3eef2ef9c..5f3f3c20a 100644 --- a/lib/widgets/collection/grid/tile.dart +++ b/lib/widgets/collection/grid/tile.dart @@ -3,7 +3,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/enums.dart'; -import 'package:aves/services/viewer_service.dart'; +import 'package:aves/services/intent_service.dart'; import 'package:aves/widgets/collection/grid/list_details.dart'; import 'package:aves/widgets/collection/grid/list_details_theme.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; @@ -44,7 +44,7 @@ class InteractiveTile extends StatelessWidget { } break; case AppMode.pickSingleMediaExternal: - ViewerService.pick([entry.uri]); + IntentService.submitPickedItems([entry.uri]); break; case AppMode.pickMultipleMediaExternal: final selection = context.read>(); @@ -53,7 +53,9 @@ class InteractiveTile extends StatelessWidget { case AppMode.pickMediaInternal: Navigator.pop(context, entry); break; + case AppMode.pickCollectionFiltersExternal: case AppMode.pickFilterInternal: + case AppMode.screenSaver: case AppMode.setWallpaper: case AppMode.slideshow: case AppMode.view: diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index 07f08265f..8b14cc46e 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -119,17 +119,17 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final source = context.read(); source.pauseMonitoring(); - final opId = mediaFileService.newOpId; + final opId = mediaEditService.newOpId; await showOpReport( context: context, - opStream: mediaFileService.move( + opStream: mediaEditService.move( opId: opId, entriesByDestination: entriesByDestination, copy: copy, nameConflictStrategy: nameConflictStrategy, ), itemCount: todoCount, - onCancel: () => mediaFileService.cancelFileOp(opId), + onCancel: () => mediaEditService.cancelFileOp(opId), onDone: (processed) async { final successOps = processed.where((v) => v.success).toSet(); @@ -226,15 +226,15 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final source = context.read(); source.pauseMonitoring(); - final opId = mediaFileService.newOpId; + final opId = mediaEditService.newOpId; await showOpReport( context: context, - opStream: mediaFileService.rename( + opStream: mediaEditService.rename( opId: opId, entriesToNewName: entriesToNewName, ), itemCount: todoCount, - onCancel: () => mediaFileService.cancelFileOp(opId), + onCancel: () => mediaEditService.cancelFileOp(opId), onDone: (processed) async { final successOps = processed.where((e) => e.success).toSet(); final movedOps = successOps.where((e) => !e.skipped).toSet(); diff --git a/lib/widgets/common/action_mixins/overlay_snack_bar.dart b/lib/widgets/common/action_mixins/overlay_snack_bar.dart index 5d8f883fd..5140e9d02 100644 --- a/lib/widgets/common/action_mixins/overlay_snack_bar.dart +++ b/lib/widgets/common/action_mixins/overlay_snack_bar.dart @@ -73,7 +73,7 @@ class OverlaySnackBar extends StatelessWidget { child: TextButtonTheme( data: TextButtonThemeData( style: TextButton.styleFrom( - primary: buttonColor, + foregroundColor: buttonColor, padding: const EdgeInsets.symmetric(horizontal: horizontalPadding), ), ), diff --git a/lib/widgets/common/basic/color_list_tile.dart b/lib/widgets/common/basic/color_list_tile.dart index 588f06b42..cbaec158c 100644 --- a/lib/widgets/common/basic/color_list_tile.dart +++ b/lib/widgets/common/basic/color_list_tile.dart @@ -1,3 +1,4 @@ +import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; @@ -9,7 +10,7 @@ class ColorListTile extends StatelessWidget { final Color value; final ValueSetter onChanged; - static const double radius = 16.0; + static const radius = Constants.colorPickerRadius; const ColorListTile({ super.key, diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart index ce6b182d8..671c27738 100644 --- a/lib/widgets/common/basic/insets.dart +++ b/lib/widgets/common/basic/insets.dart @@ -25,6 +25,26 @@ class BottomGestureAreaProtector extends StatelessWidget { } } +// It will prevent the body from scrolling when a user swipe from top to show the status bar when system UI is hidden. +class TopGestureAreaProtector extends StatelessWidget { + const TopGestureAreaProtector({super.key}); + + @override + Widget build(BuildContext context) { + return Positioned( + left: 0, + top: 0, + right: 0, + height: context.select((mq) => mq.systemGestureInsets.top), + child: GestureDetector( + // absorb vertical gestures only + onVerticalDragDown: (details) {}, + behavior: HitTestBehavior.translucent, + ), + ); + } +} + // It will prevent the body from scrolling when a user swipe from edges to use Android Q style navigation gestures. class SideGestureAreaProtector extends StatelessWidget { const SideGestureAreaProtector({super.key}); diff --git a/lib/widgets/common/basic/outlined_text.dart b/lib/widgets/common/basic/outlined_text.dart index 4b1e787d9..7def450af 100644 --- a/lib/widgets/common/basic/outlined_text.dart +++ b/lib/widgets/common/basic/outlined_text.dart @@ -67,8 +67,8 @@ class OutlinedText extends StatelessWidget { style: (span.style ?? const TextStyle()).copyWith( foreground: Paint() ..style = PaintingStyle.stroke - ..strokeWidth = outlineWidth - ..color = outlineColor, + ..color = outlineColor + ..strokeWidth = outlineWidth, ), ); diff --git a/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart b/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart index 9e8ffb093..d9b6f0df3 100644 --- a/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart +++ b/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart @@ -2,7 +2,6 @@ import 'dart:math' as math; import 'package:flutter/gestures.dart'; -import 'package:vector_math/vector_math_64.dart'; // adapted from Flutter `ScaleGestureRecognizer` in `/gestures/scale.dart` // ignore_for_file: curly_braces_in_flow_control_structures, deprecated_member_use, unnecessary_null_comparison diff --git a/lib/widgets/common/fx/blurred.dart b/lib/widgets/common/fx/blurred.dart index c3374401a..8e33a34b4 100644 --- a/lib/widgets/common/fx/blurred.dart +++ b/lib/widgets/common/fx/blurred.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; final _filter = ImageFilter.blur(sigmaX: 4, sigmaY: 4); +const _identity = ColorFilter.matrix([1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0]); class BlurredRect extends StatelessWidget { final bool enabled; @@ -16,14 +17,13 @@ class BlurredRect extends StatelessWidget { @override Widget build(BuildContext context) { - return enabled - ? ClipRect( - child: BackdropFilter( - filter: _filter, - child: child, - ), - ) - : child; + return ClipRect( + child: BackdropFilter( + // do not modify tree when disabling filter + filter: enabled ? _filter : _identity, + child: child, + ), + ); } } @@ -57,12 +57,11 @@ class BlurredRRect extends StatelessWidget { Widget build(BuildContext context) { return ClipRRect( borderRadius: borderRadius, - child: enabled - ? BackdropFilter( - filter: _filter, - child: child, - ) - : child, + child: BackdropFilter( + // do not modify tree when disabling filter + filter: enabled ? _filter : _identity, + child: child, + ), ); } } @@ -80,12 +79,11 @@ class BlurredOval extends StatelessWidget { @override Widget build(BuildContext context) { return ClipOval( - child: enabled - ? BackdropFilter( - filter: _filter, - child: child, - ) - : child, + child: BackdropFilter( + // do not modify tree when disabling filter + filter: enabled ? _filter : _identity, + child: child, + ), ); } } diff --git a/lib/widgets/common/grid/item_tracker.dart b/lib/widgets/common/grid/item_tracker.dart index 8f5755999..f9b07c896 100644 --- a/lib/widgets/common/grid/item_tracker.dart +++ b/lib/widgets/common/grid/item_tracker.dart @@ -169,6 +169,7 @@ class _GridItemTrackerState extends State> with WidgetsBin if (pivotItem != null) { WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; context.read().trackItem(pivotItem, animate: false); }); } diff --git a/lib/widgets/common/identity/aves_fab.dart b/lib/widgets/common/identity/aves_fab.dart new file mode 100644 index 000000000..df50d8bbd --- /dev/null +++ b/lib/widgets/common/identity/aves_fab.dart @@ -0,0 +1,30 @@ +import 'package:aves/theme/icons.dart'; +import 'package:flutter/material.dart'; + +class AvesFab extends StatelessWidget { + final String tooltip; + final VoidCallback onPressed; + + const AvesFab({ + super.key, + required this.tooltip, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), + child: FloatingActionButton( + tooltip: tooltip, + onPressed: onPressed, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + child: const Icon(AIcons.apply), + ), + ); + } +} diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 637cca171..6d58cb513 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/chip_actions.dart'; @@ -12,6 +13,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/collection/filter_bar.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; @@ -45,7 +47,8 @@ class AvesFilterChip extends StatefulWidget { final AvesFilterDecoration? decoration; final String? banner; final Widget? leadingOverride, details; - final double padding, maxWidth; + final double padding; + final double? maxWidth; final HeroType heroType; final FilterCallback? onTap; final OffsetFilterCallback? onLongPress; @@ -54,7 +57,6 @@ class AvesFilterChip extends StatefulWidget { static const double outlineWidth = 2; static const double minChipHeight = kMinInteractiveDimension; static const double minChipWidth = 80; - static const double defaultMaxChipWidth = 160; static const double iconSize = 18; static const double fontSize = 14; static const double decoratedContentVerticalPadding = 5; @@ -71,14 +73,25 @@ class AvesFilterChip extends StatefulWidget { this.leadingOverride, this.details, this.padding = 6.0, - this.maxWidth = defaultMaxChipWidth, + this.maxWidth, this.heroType = HeroType.onTap, this.onTap, this.onLongPress = showDefaultLongPressMenu, }); + static double computeMaxWidth( + BuildContext context, { + required int minChipPerRow, + required double chipPadding, + required double rowPadding, + }) { + return context.select((mq) { + return (mq.size.width - mq.padding.horizontal - chipPadding * minChipPerRow - rowPadding) / minChipPerRow; + }); + } + static Future showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { - if (context.read>().value == AppMode.main) { + if (context.read>().value.canNavigate) { final actions = [ if (filter is AlbumFilter) ChipAction.goToAlbumPage, if ((filter is LocationFilter && filter.level == LocationLevel.country)) ChipAction.goToCountryPage, @@ -95,14 +108,18 @@ class AvesFilterChip extends StatefulWidget { final selectedAction = await showMenu( context: context, position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size), - items: actions - .map((action) => PopupMenuItem( - value: action, - child: MenuIconTheme( - child: MenuRow(text: action.getText(context), icon: action.getIcon()), - ), - )) - .toList(), + items: [ + PopupMenuItem( + child: Text(filter.getLabel(context)), + ), + const PopupMenuDivider(), + ...actions.map((action) => PopupMenuItem( + value: action, + child: MenuIconTheme( + child: MenuRow(text: action.getText(context), icon: action.getIcon()), + ), + )), + ], ); if (selectedAction != null) { // wait for the popup menu to hide before proceeding with the action @@ -261,7 +278,15 @@ class _AvesFilterChipState extends State { Widget chip = Container( constraints: BoxConstraints( minWidth: AvesFilterChip.minChipWidth, - maxWidth: widget.maxWidth, + maxWidth: max( + AvesFilterChip.minChipWidth, + widget.maxWidth ?? + AvesFilterChip.computeMaxWidth( + context, + minChipPerRow: 2, + chipPadding: FilterBar.chipPadding.horizontal, + rowPadding: FilterBar.rowPadding.horizontal, + )), minHeight: AvesFilterChip.minChipHeight, ), child: Stack( diff --git a/lib/widgets/common/magnifier/core/core.dart b/lib/widgets/common/magnifier/core/core.dart index 9364dcbc2..aa259d35a 100644 --- a/lib/widgets/common/magnifier/core/core.dart +++ b/lib/widgets/common/magnifier/core/core.dart @@ -169,12 +169,14 @@ class _MagnifierCoreState extends State with TickerProviderStateM final pps = details.velocity.pixelsPerSecond; if (pps != Offset.zero) { final newPosition = clampPosition(position: _position + pps * widget.panInertia); - final tween = Tween(begin: _position, end: newPosition); - const curve = Curves.easeOutCubic; - _positionAnimation = tween.animate(CurvedAnimation(parent: _positionAnimationController, curve: curve)); - _positionAnimationController - ..duration = _getAnimationDurationForVelocity(curve: curve, tween: tween, targetPixelPerSecond: pps) - ..forward(from: 0.0); + if (_position != newPosition) { + final tween = Tween(begin: _position, end: newPosition); + const curve = Curves.easeOutCubic; + _positionAnimation = tween.animate(CurvedAnimation(parent: _positionAnimationController, curve: curve)); + _positionAnimationController + ..duration = _getAnimationDurationForVelocity(curve: curve, tween: tween, targetPixelPerSecond: pps) + ..forward(from: 0.0); + } } } diff --git a/lib/widgets/common/map/compass.dart b/lib/widgets/common/map/compass.dart index 0e8cc4f70..1cbdd2f91 100644 --- a/lib/widgets/common/map/compass.dart +++ b/lib/widgets/common/map/compass.dart @@ -29,9 +29,9 @@ class CompassPainter extends CustomPainter { ..color = color.withOpacity(.6); final strokePaint = Paint() ..style = PaintingStyle.stroke + ..color = color ..strokeWidth = 1.7 - ..strokeJoin = StrokeJoin.round - ..color = color; + ..strokeJoin = StrokeJoin.round; canvas.drawPath(northTriangle, fillPaint); canvas.drawPath(northTriangle, strokePaint); diff --git a/lib/widgets/common/map/leaflet/tile_layers.dart b/lib/widgets/common/map/leaflet/tile_layers.dart index 5602a58c3..5b6f55191 100644 --- a/lib/widgets/common/map/leaflet/tile_layers.dart +++ b/lib/widgets/common/map/leaflet/tile_layers.dart @@ -15,8 +15,8 @@ class OSMHotLayer extends StatelessWidget { urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], backgroundColor: _tileLayerBackgroundColor, - tileProvider: _NetworkTileProvider(), retinaMode: context.select((mq) => mq.devicePixelRatio) > 1, + userAgentPackageName: device.userAgent, ), ); } @@ -32,8 +32,8 @@ class StamenTonerLayer extends StatelessWidget { urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png', subdomains: ['a', 'b', 'c', 'd'], backgroundColor: _tileLayerBackgroundColor, - tileProvider: _NetworkTileProvider(), retinaMode: context.select((mq) => mq.devicePixelRatio) > 1, + userAgentPackageName: device.userAgent, ), ); } @@ -49,22 +49,9 @@ class StamenWatercolorLayer extends StatelessWidget { urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg', subdomains: ['a', 'b', 'c', 'd'], backgroundColor: _tileLayerBackgroundColor, - tileProvider: _NetworkTileProvider(), retinaMode: context.select((mq) => mq.devicePixelRatio) > 1, + userAgentPackageName: device.userAgent, ), ); } } - -class _NetworkTileProvider extends NetworkTileProvider { - final Map headers = { - 'User-Agent': device.userAgent, - }; - - _NetworkTileProvider(); - - @override - ImageProvider getImage(Coords coords, TileLayerOptions options) { - return NetworkImage(getTileUrl(coords, options), headers: headers); - } -} diff --git a/lib/widgets/common/search/page.dart b/lib/widgets/common/search/page.dart index 0ebe1c5e2..50cb90bdb 100644 --- a/lib/widgets/common/search/page.dart +++ b/lib/widgets/common/search/page.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/common/identity/aves_app_bar.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/search/delegate.dart'; import 'package:aves/widgets/common/search/route.dart'; import 'package:flutter/material.dart'; @@ -115,37 +116,39 @@ class _SearchPageState extends State { case null: break; } - return Scaffold( - appBar: AppBar( - leading: Hero( - tag: AvesAppBar.leadingHeroTag, - transitionOnUserGestures: true, - child: Center(child: widget.delegate.buildLeading(context)), - ), - title: Hero( - tag: AvesAppBar.titleHeroTag, - transitionOnUserGestures: true, - child: DefaultTextStyle.merge( - style: const TextStyle(fontFeatures: [FontFeature.disable('smcp')]), - child: TextField( - controller: widget.delegate.queryTextController, - focusNode: _focusNode, - decoration: InputDecoration( - border: InputBorder.none, - hintText: widget.delegate.searchFieldLabel, - hintStyle: theme.inputDecorationTheme.hintStyle, + return MediaQueryDataProvider( + child: Scaffold( + appBar: AppBar( + leading: Hero( + tag: AvesAppBar.leadingHeroTag, + transitionOnUserGestures: true, + child: Center(child: widget.delegate.buildLeading(context)), + ), + title: Hero( + tag: AvesAppBar.titleHeroTag, + transitionOnUserGestures: true, + child: DefaultTextStyle.merge( + style: const TextStyle(fontFeatures: [FontFeature.disable('smcp')]), + child: TextField( + controller: widget.delegate.queryTextController, + focusNode: _focusNode, + decoration: InputDecoration( + border: InputBorder.none, + hintText: widget.delegate.searchFieldLabel, + hintStyle: theme.inputDecorationTheme.hintStyle, + ), + textInputAction: TextInputAction.search, + style: theme.textTheme.headline6, + onSubmitted: (_) => widget.delegate.showResults(context), ), - textInputAction: TextInputAction.search, - style: theme.textTheme.headline6, - onSubmitted: (_) => widget.delegate.showResults(context), ), ), + actions: widget.delegate.buildActions(context), + ), + body: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: body, ), - actions: widget.delegate.buildActions(context), - ), - body: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: body, ), ); } diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index ae4542f72..09d29853f 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -172,7 +172,7 @@ class _ThumbnailImageState extends State { if (widget.cancellableNotifier?.value ?? false) { final key = await _currentProviderStream?.provider.provider.obtainKey(ImageConfiguration.empty); if (key is ThumbnailProviderKey) { - mediaFileService.cancelThumbnail(key); + mediaFetchService.cancelThumbnail(key); } } } diff --git a/lib/widgets/common/tile_extent_controller.dart b/lib/widgets/common/tile_extent_controller.dart index 80c587ba1..24cfd947b 100644 --- a/lib/widgets/common/tile_extent_controller.dart +++ b/lib/widgets/common/tile_extent_controller.dart @@ -84,7 +84,9 @@ class TileExtentController { int _effectiveColumnCountForExtent(double extent) { if (extent > 0) { final columnCount = _columnCountForExtent(extent); - return columnCount.clamp(_effectiveColumnCountMin(), _effectiveColumnCountMax()).round(); + final countMin = _effectiveColumnCountMin(); + final countMax = _effectiveColumnCountMax(); + return columnCount.clamp(countMin, max(countMin, countMax)).round(); } return columnCountDefault; } diff --git a/lib/widgets/debug/cache.dart b/lib/widgets/debug/cache.dart index 1ba8e936c..fddbb33a9 100644 --- a/lib/widgets/debug/cache.dart +++ b/lib/widgets/debug/cache.dart @@ -45,7 +45,7 @@ class _DebugCacheSectionState extends State with AutomaticKee ), const SizedBox(width: 8), ElevatedButton( - onPressed: mediaFileService.clearSizedThumbnailDiskCache, + onPressed: mediaFetchService.clearSizedThumbnailDiskCache, child: const Text('Clear'), ), ], diff --git a/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart index d243e54b1..7c39adc58 100644 --- a/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart @@ -64,7 +64,7 @@ class _TagEditorPageState extends State { IconButton( icon: const Icon(AIcons.reset), onPressed: _reset, - tooltip: l10n.resetButtonTooltip, + tooltip: l10n.resetTooltip, ), ], ), diff --git a/lib/widgets/dialogs/export_entry_dialog.dart b/lib/widgets/dialogs/export_entry_dialog.dart index 6ef6d5787..7482b887b 100644 --- a/lib/widgets/dialogs/export_entry_dialog.dart +++ b/lib/widgets/dialogs/export_entry_dialog.dart @@ -1,6 +1,6 @@ import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; -import 'package:aves/services/media/media_file_service.dart'; +import 'package:aves/services/media/media_edit_service.dart'; import 'package:aves/utils/mime_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart index c46f34f3d..6babbb57e 100644 --- a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart @@ -9,6 +9,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/basic/color_list_tile.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/borders.dart'; @@ -53,7 +54,7 @@ class _CoverSelectionDialogState extends State { static const double itemPickerExtent = 46; static const double appPickerExtent = 32; - static const double colorPickerRadius = 16; + static const double colorPickerRadius = Constants.colorPickerRadius; double tabBarHeight(BuildContext context) => 64 * max(1, MediaQuery.textScaleFactorOf(context)); diff --git a/lib/widgets/filter_grids/common/action_delegates/album_set.dart b/lib/widgets/filter_grids/common/action_delegates/album_set.dart index 51b6ab0a9..90f8b0a5a 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.dart @@ -250,12 +250,12 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with if (!await checkStoragePermissionForAlbums(context, filledAlbums)) return; source.pauseMonitoring(); - final opId = mediaFileService.newOpId; + final opId = mediaEditService.newOpId; await showOpReport( context: context, - opStream: mediaFileService.delete(opId: opId, entries: todoEntries), + opStream: mediaEditService.delete(opId: opId, entries: todoEntries), itemCount: todoCount, - onCancel: () => mediaFileService.cancelFileOp(opId), + onCancel: () => mediaEditService.cancelFileOp(opId), onDone: (processed) async { final successOps = processed.where((event) => event.success); final deletedOps = successOps.where((e) => !e.skipped).toSet(); @@ -314,10 +314,10 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with } source.pauseMonitoring(); - final opId = mediaFileService.newOpId; + final opId = mediaEditService.newOpId; await showOpReport( context: context, - opStream: mediaFileService.move( + opStream: mediaEditService.move( opId: opId, entriesByDestination: {destinationAlbum: todoEntries}, copy: false, @@ -325,7 +325,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with nameConflictStrategy: NameConflictStrategy.rename, ), itemCount: todoCount, - onCancel: () => mediaFileService.cancelFileOp(opId), + onCancel: () => mediaEditService.cancelFileOp(opId), onDone: (processed) async { final successOps = processed.where((e) => e.success).toSet(); final movedOps = successOps.where((e) => !e.skipped).toSet(); diff --git a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart index 45bbc0831..a9ef0e0c0 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -61,7 +61,7 @@ abstract class ChipSetActionDelegate with FeedbackMi return isSelecting && selectedItemCount == itemCount; // browsing case ChipSetAction.search: - return appMode.canSearch && !isSelecting; + return appMode.canNavigate && !isSelecting; case ChipSetAction.createAlbum: return false; // browsing or selecting diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart index 0edf0b257..25bb1e244 100644 --- a/lib/widgets/filter_grids/common/app_bar.dart +++ b/lib/widgets/filter_grids/common/app_bar.dart @@ -77,7 +77,7 @@ class _FilterGridAppBarState extends State extends State>().value; return InteractiveAppBarTitle( - onTap: appMode.canSearch ? _goToSearch : null, + onTap: appMode.canNavigate ? _goToSearch : null, child: SourceStateAwareAppBarTitle( title: Text(widget.title), source: source, diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 08938aec0..448cd0167 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -74,8 +74,10 @@ class FilterGridPage extends StatelessWidget { Widget build(BuildContext context) { return MediaQueryDataProvider( child: Selector( - selector: (context, s) => s.showBottomNavigationBar, - builder: (context, showBottomNavigationBar, child) { + selector: (context, s) => s.enableBottomNavigationBar, + builder: (context, enableBottomNavigationBar, child) { + final canNavigate = context.select, bool>((v) => v.value.canNavigate); + final showBottomNavigationBar = canNavigate && enableBottomNavigationBar; return NotificationListener( onNotification: (notification) { _draggableScrollBarEventStreamController.add(notification.event); @@ -524,8 +526,10 @@ class _FilterScrollView extends StatelessWidget { selector: (context, mq) => mq.effectiveBottomPadding, builder: (context, mqPaddingBottom, child) { return Selector( - selector: (context, s) => s.showBottomNavigationBar, - builder: (context, showBottomNavigationBar, child) { + selector: (context, s) => s.enableBottomNavigationBar, + builder: (context, enableBottomNavigationBar, child) { + final canNavigate = context.select, bool>((v) => v.value.canNavigate); + final showBottomNavigationBar = canNavigate && enableBottomNavigationBar; final navBarHeight = showBottomNavigationBar ? AppBottomNavBar.height : 0; return DraggableScrollbar( backgroundColor: Colors.white, diff --git a/lib/widgets/filter_grids/common/filter_tile.dart b/lib/widgets/filter_grids/common/filter_tile.dart index 60a165d51..031c61cdc 100644 --- a/lib/widgets/filter_grids/common/filter_tile.dart +++ b/lib/widgets/filter_grids/common/filter_tile.dart @@ -47,13 +47,14 @@ class _InteractiveFilterTileState extends State>().value; + final appMode = context.read?>()?.value; switch (appMode) { case AppMode.main: + case AppMode.pickCollectionFiltersExternal: case AppMode.pickSingleMediaExternal: case AppMode.pickMultipleMediaExternal: - final selection = context.read>>(); - if (selection.isSelecting) { + final selection = context.read>?>(); + if (selection != null && selection.isSelecting) { selection.toggleSelection(gridItem); } else { _goToCollection(context, filter); @@ -63,9 +64,11 @@ class _InteractiveFilterTileState extends State(context, filter); break; case AppMode.pickMediaInternal: + case AppMode.screenSaver: case AppMode.setWallpaper: case AppMode.slideshow: case AppMode.view: + case null: break; } } diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 039cea723..665a16a39 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -13,7 +13,8 @@ import 'package:aves/model/source/enums.dart'; import 'package:aves/services/analysis_service.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/global_search.dart'; -import 'package:aves/services/viewer_service.dart'; +import 'package:aves/services/intent_service.dart'; +import 'package:aves/services/widget_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'; @@ -21,7 +22,10 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/search/search_delegate.dart'; +import 'package:aves/widgets/settings/screen_saver_settings_page.dart'; +import 'package:aves/widgets/settings/home_widget_settings_page.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; +import 'package:aves/widgets/viewer/screen_saver_page.dart'; import 'package:aves/widgets/wallpaper_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -45,13 +49,28 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { AvesEntry? _viewerEntry; - String? _shortcutRouteName, _shortcutSearchQuery; - Set? _shortcutFilters; + int? _widgetId; + String? _initialRouteName, _initialSearchQuery; + Set? _initialFilters; - static const actionPick = 'pick'; + static const actionPickItems = 'pick_items'; + static const actionPickCollectionFilters = 'pick_collection_filters'; + static const actionScreenSaver = 'screen_saver'; + static const actionScreenSaverSettings = 'screen_saver_settings'; static const actionSearch = 'search'; static const actionSetWallpaper = 'set_wallpaper'; static const actionView = 'view'; + static const actionWidgetOpen = 'widget_open'; + static const actionWidgetSettings = 'widget_settings'; + + static const intentDataKeyAction = 'action'; + static const intentDataKeyAllowMultiple = 'allowMultiple'; + static const intentDataKeyFilters = 'filters'; + static const intentDataKeyMimeType = 'mimeType'; + static const intentDataKeyPage = 'page'; + static const intentDataKeyQuery = 'query'; + static const intentDataKeyUri = 'uri'; + static const intentDataKeyWidgetId = 'widgetId'; static const allowedShortcutRoutes = [ CollectionPage.routeName, @@ -71,20 +90,21 @@ class _HomePageState extends State { Future _setup() async { final stopwatch = Stopwatch()..start(); - // do not check whether permission was granted, - // as some app stores hide in some countries - // apps that force quit on permission denial - await [ - Permission.storage, - // to access media with unredacted metadata with scoped storage (Android 10+) - Permission.accessMediaLocation, - ].request(); + if (await windowService.isActivity()) { + // do not check whether permission was granted, because some app stores + // hide in some countries apps that force quit on permission denial + await [ + Permission.storage, + // to access media with unredacted metadata with scoped storage (Android 10+) + Permission.accessMediaLocation, + ].request(); + } var appMode = AppMode.main; - final intentData = widget.intentData ?? await ViewerService.getIntentData(); - final intentAction = intentData['action']; + final intentData = widget.intentData ?? await IntentService.getIntentData(); + final intentAction = intentData[intentDataKeyAction]; - if (intentAction != actionSetWallpaper) { + if (!{actionScreenSaver, actionSetWallpaper}.contains(intentAction)) { await androidFileUtils.init(); if (settings.isInstalledAppAccessAllowed) { unawaited(androidFileUtils.initAppNames()); @@ -95,42 +115,68 @@ class _HomePageState extends State { await reportService.log('Intent data=$intentData'); switch (intentAction) { case actionView: - _viewerEntry = await _initViewerEntry( - uri: intentData['uri'], - mimeType: intentData['mimeType'], - ); - if (_viewerEntry != null) { - appMode = AppMode.view; + case actionWidgetOpen: + String? uri, mimeType; + final widgetId = intentData[intentDataKeyWidgetId]; + if (widgetId != null) { + uri = settings.getWidgetUri(widgetId); + unawaited(WidgetService.update(widgetId)); + } else { + uri = intentData[intentDataKeyUri]; + mimeType = intentData[intentDataKeyMimeType]; + } + if (uri != null) { + _viewerEntry = await _initViewerEntry( + uri: uri, + mimeType: mimeType, + ); + if (_viewerEntry != null) { + appMode = AppMode.view; + } } break; - case actionPick: + case actionPickItems: // TODO TLAD apply pick mimetype(s) // some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?) - String? pickMimeTypes = intentData['mimeType']; - final multiple = intentData['allowMultiple'] ?? false; + String? pickMimeTypes = intentData[intentDataKeyMimeType]; + final multiple = intentData[intentDataKeyAllowMultiple] ?? false; debugPrint('pick mimeType=$pickMimeTypes multiple=$multiple'); appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal; break; + case actionPickCollectionFilters: + appMode = AppMode.pickCollectionFiltersExternal; + break; + case actionScreenSaver: + appMode = AppMode.screenSaver; + _initialRouteName = ScreenSaverPage.routeName; + break; + case actionScreenSaverSettings: + _initialRouteName = ScreenSaverSettingsPage.routeName; + break; case actionSearch: - _shortcutRouteName = CollectionSearchDelegate.pageRouteName; - _shortcutSearchQuery = intentData['query']; + _initialRouteName = CollectionSearchDelegate.pageRouteName; + _initialSearchQuery = intentData[intentDataKeyQuery]; break; case actionSetWallpaper: appMode = AppMode.setWallpaper; _viewerEntry = await _initViewerEntry( - uri: intentData['uri'], - mimeType: intentData['mimeType'], + uri: intentData[intentDataKeyUri], + mimeType: intentData[intentDataKeyMimeType], ); break; + case actionWidgetSettings: + _initialRouteName = HomeWidgetSettingsPage.routeName; + _widgetId = intentData[intentDataKeyWidgetId] ?? 0; + break; default: // do not use 'route' as extra key, as the Flutter framework acts on it - final extraRoute = intentData['page']; + final extraRoute = intentData[intentDataKeyPage]; if (allowedShortcutRoutes.contains(extraRoute)) { - _shortcutRouteName = extraRoute; + _initialRouteName = extraRoute; } - final extraFilters = intentData['filters']; - _shortcutFilters = extraFilters != null ? (extraFilters as List).cast().toSet() : null; } + final extraFilters = intentData[intentDataKeyFilters]; + _initialFilters = extraFilters != null ? (extraFilters as List).cast().map(CollectionFilter.fromJson).whereNotNull().toSet() : null; } context.read>().value = appMode; unawaited(reportService.setCustomKey('app_mode', appMode.toString())); @@ -138,6 +184,7 @@ class _HomePageState extends State { switch (appMode) { case AppMode.main: + case AppMode.pickCollectionFiltersExternal: case AppMode.pickSingleMediaExternal: case AppMode.pickMultipleMediaExternal: unawaited(GlobalSearch.registerCallback()); @@ -147,6 +194,12 @@ class _HomePageState extends State { loadTopEntriesFirst: settings.homePage == HomePageSetting.collection, ); break; + case AppMode.screenSaver: + final source = context.read(); + await source.init( + canAnalyze: false, + ); + break; case AppMode.view: if (_isViewerSourceable(_viewerEntry)) { final directory = _viewerEntry?.directory; @@ -184,7 +237,7 @@ class _HomePageState extends State { // convert this file path to a proper URI uri = Uri.file(uri).toString(); } - final entry = await mediaFileService.getEntry(uri, mimeType); + final entry = await mediaFetchService.getEntry(uri, mimeType); if (entry != null) { // cataloguing is essential for coordinates and video rotation await entry.catalog(background: false, force: false, persist: false); @@ -263,16 +316,35 @@ class _HomePageState extends State { routeName = CollectionPage.routeName; break; default: - routeName = _shortcutRouteName ?? settings.homePage.routeName; - filters = (_shortcutFilters ?? {}).map(CollectionFilter.fromJson).toSet(); + routeName = _initialRouteName ?? settings.homePage.routeName; + filters = _initialFilters ?? {}; break; } final source = context.read(); switch (routeName) { case AlbumListPage.routeName: return DirectMaterialPageRoute( - settings: const RouteSettings(name: AlbumListPage.routeName), - builder: (_) => const AlbumListPage(), + settings: RouteSettings(name: routeName), + builder: (context) => const AlbumListPage(), + ); + case ScreenSaverPage.routeName: + return DirectMaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: (context) => ScreenSaverPage( + source: source, + ), + ); + case ScreenSaverSettingsPage.routeName: + return DirectMaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: (context) => const ScreenSaverSettingsPage(), + ); + case HomeWidgetSettingsPage.routeName: + return DirectMaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: (context) => HomeWidgetSettingsPage( + widgetId: _widgetId!, + ), ); case CollectionSearchDelegate.pageRouteName: return SearchPageRoute( @@ -280,13 +352,13 @@ class _HomePageState extends State { searchFieldLabel: context.l10n.searchCollectionFieldHint, source: source, canPop: false, - initialQuery: _shortcutSearchQuery, + initialQuery: _initialSearchQuery, ), ); case CollectionPage.routeName: default: return DirectMaterialPageRoute( - settings: const RouteSettings(name: CollectionPage.routeName), + settings: RouteSettings(name: routeName), builder: (context) => CollectionPage( source: source, filters: filters, diff --git a/lib/widgets/home_widget.dart b/lib/widgets/home_widget.dart new file mode 100644 index 000000000..2d8625112 --- /dev/null +++ b/lib/widgets/home_widget.dart @@ -0,0 +1,86 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/widget_shape.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:flutter/material.dart'; + +class HomeWidgetPainter { + final AvesEntry? entry; + final double devicePixelRatio; + + static const backgroundGradient = LinearGradient( + begin: Alignment.bottomLeft, + end: Alignment.topRight, + colors: Constants.boraBoraGradientColors, + ); + + HomeWidgetPainter({ + required this.entry, + required this.devicePixelRatio, + }); + + Future drawWidget({ + required int widthPx, + required int heightPx, + required Color? outline, + required WidgetShape shape, + ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba, + }) async { + final widgetSizePx = Size(widthPx.toDouble(), heightPx.toDouble()); + final entryImage = await _getEntryImage(entry, shape.size(widgetSizePx)); + + final recorder = ui.PictureRecorder(); + final rect = Rect.fromLTWH(0, 0, widgetSizePx.width, widgetSizePx.height); + final canvas = Canvas(recorder, rect); + final path = shape.path(widgetSizePx, devicePixelRatio); + canvas.clipPath(path); + if (entryImage != null) { + canvas.drawImage(entryImage, Offset(widgetSizePx.width - entryImage.width, widgetSizePx.height - entryImage.height) / 2, Paint()); + } else { + canvas.drawPaint(Paint()..shader = backgroundGradient.createShader(rect)); + } + if (outline != null) { + drawOutline(canvas, path, devicePixelRatio, outline); + } + final widgetImage = await recorder.endRecording().toImage(widthPx, heightPx); + final byteData = await widgetImage.toByteData(format: format); + return byteData?.buffer.asUint8List() ?? Uint8List(0); + } + + static void drawOutline(ui.Canvas canvas, ui.Path outlinePath, double devicePixelRatio, Color color) { + canvas.drawPath( + outlinePath, + Paint() + ..style = PaintingStyle.stroke + ..color = color + ..strokeWidth = AvesFilterChip.outlineWidth * devicePixelRatio * 2 + ..strokeCap = StrokeCap.round); + } + + FutureOr _getEntryImage(AvesEntry? entry, Size sizePx) async { + if (entry == null) return null; + + final provider = entry.getThumbnail(extent: sizePx.longestSide / devicePixelRatio); + + final imageInfoCompleter = Completer(); + final imageStream = provider.resolve(ImageConfiguration.empty); + final imageStreamListener = ImageStreamListener((image, synchronousCall) async { + imageInfoCompleter.complete(image); + }, onError: imageInfoCompleter.completeError); + imageStream.addListener(imageStreamListener); + ImageInfo? regionImageInfo; + try { + regionImageInfo = await imageInfoCompleter.future; + } catch (error) { + debugPrint('failed to get widget image for entry=$entry with error=$error'); + } + imageStream.removeListener(imageStreamListener); + return regionImageInfo?.image; + } +} diff --git a/lib/widgets/navigation/nav_bar/nav_bar.dart b/lib/widgets/navigation/nav_bar/nav_bar.dart index b947607c0..8061c13d1 100644 --- a/lib/widgets/navigation/nav_bar/nav_bar.dart +++ b/lib/widgets/navigation/nav_bar/nav_bar.dart @@ -1,3 +1,4 @@ +import 'package:aves/app_mode.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/settings/settings.dart'; @@ -163,8 +164,10 @@ class NavBarPaddingSliver extends StatelessWidget { Widget build(BuildContext context) { return SliverToBoxAdapter( child: Selector( - selector: (context, s) => s.showBottomNavigationBar, - builder: (context, showBottomNavigationBar, child) { + selector: (context, s) => s.enableBottomNavigationBar, + builder: (context, enableBottomNavigationBar, child) { + final canNavigate = context.select, bool>((v) => v.value.canNavigate); + final showBottomNavigationBar = canNavigate && enableBottomNavigationBar; return SizedBox(height: showBottomNavigationBar ? AppBottomNavBar.height : 0); }, ), diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index 66ada398b..6b0793035 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -1,4 +1,5 @@ import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/filters/date.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; @@ -43,6 +44,8 @@ class CollectionSearchDelegate extends AvesSearchDelegate { MimeFilter(MimeTypes.svg), ]; + static final _monthFilters = List.generate(12, (i) => DateFilter(DateLevel.m, DateTime(1, i + 1))); + CollectionSearchDelegate({ required super.searchFieldLabel, required this.source, @@ -95,66 +98,12 @@ class CollectionSearchDelegate extends AvesSearchDelegate { title: context.l10n.searchSectionRecent, filters: history, ), - StreamBuilder( - stream: source.eventBus.on(), - builder: (context, snapshot) { - final filters = source.rawAlbums - .map((album) => AlbumFilter( - album, - source.getAlbumDisplayName(context, album), - )) - .where((filter) => containQuery(filter.displayName ?? filter.album)) - .toList() - ..sort(); - return _buildFilterRow( - context: context, - title: context.l10n.searchSectionAlbums, - filters: filters, - ); - }), - StreamBuilder( - stream: source.eventBus.on(), - builder: (context, snapshot) { - final filters = source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)).toList(); - return _buildFilterRow( - context: context, - title: context.l10n.searchSectionCountries, - filters: filters, - ); - }), - StreamBuilder( - stream: source.eventBus.on(), - builder: (context, snapshot) { - final filters = source.sortedPlaces.where(containQuery).map((s) => LocationFilter(LocationLevel.place, s)); - final noFilter = LocationFilter(LocationLevel.place, ''); - return _buildFilterRow( - context: context, - title: context.l10n.searchSectionPlaces, - filters: [ - if (containQuery(noFilter.getLabel(context))) noFilter, - ...filters, - ], - ); - }), - StreamBuilder( - stream: source.eventBus.on(), - builder: (context, snapshot) { - final filters = source.sortedTags.where(containQuery).map(TagFilter.new); - final noFilter = TagFilter(''); - return _buildFilterRow( - context: context, - title: context.l10n.searchSectionTags, - filters: [ - if (containQuery(noFilter.getLabel(context))) noFilter, - ...filters, - ], - ); - }), - _buildFilterRow( - context: context, - title: context.l10n.searchSectionRating, - filters: [0, 5, 4, 3, 2, 1, -1].map(RatingFilter.new).where((f) => containQuery(f.getLabel(context))).toList(), - ), + _buildDateFilters(context, containQuery), + _buildAlbumFilters(containQuery), + _buildCountryFilters(containQuery), + _buildPlaceFilters(containQuery), + _buildTagFilters(containQuery), + _buildRatingFilters(context, containQuery), ], ); }); @@ -178,6 +127,97 @@ class CollectionSearchDelegate extends AvesSearchDelegate { ); } + Widget _buildDateFilters(BuildContext context, _ContainQuery containQuery) { + final filters = [ + DateFilter.onThisDay, + ..._monthFilters, + ].where((f) => containQuery(f.getLabel(context))).toList(); + return _buildFilterRow( + context: context, + title: context.l10n.searchSectionDate, + filters: filters, + ); + } + + Widget _buildAlbumFilters(_ContainQuery containQuery) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) { + final filters = source.rawAlbums + .map((album) => AlbumFilter( + album, + source.getAlbumDisplayName(context, album), + )) + .where((filter) => containQuery(filter.displayName ?? filter.album)) + .toList() + ..sort(); + return _buildFilterRow( + context: context, + title: context.l10n.searchSectionAlbums, + filters: filters, + ); + }, + ); + } + + Widget _buildCountryFilters(_ContainQuery containQuery) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) { + final filters = source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)).toList(); + return _buildFilterRow( + context: context, + title: context.l10n.searchSectionCountries, + filters: filters, + ); + }, + ); + } + + Widget _buildPlaceFilters(_ContainQuery containQuery) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) { + final filters = source.sortedPlaces.where(containQuery).map((s) => LocationFilter(LocationLevel.place, s)); + final noFilter = LocationFilter(LocationLevel.place, ''); + return _buildFilterRow( + context: context, + title: context.l10n.searchSectionPlaces, + filters: [ + if (containQuery(noFilter.getLabel(context))) noFilter, + ...filters, + ], + ); + }, + ); + } + + Widget _buildTagFilters(_ContainQuery containQuery) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) { + final filters = source.sortedTags.where(containQuery).map(TagFilter.new); + final noFilter = TagFilter(''); + return _buildFilterRow( + context: context, + title: context.l10n.searchSectionTags, + filters: [ + if (containQuery(noFilter.getLabel(context))) noFilter, + ...filters, + ], + ); + }, + ); + } + + Widget _buildRatingFilters(BuildContext context, _ContainQuery containQuery) { + return _buildFilterRow( + context: context, + title: context.l10n.searchSectionRating, + filters: [0, 5, 4, 3, 2, 1, -1].map(RatingFilter.new).where((f) => containQuery(f.getLabel(context))).toList(), + ); + } + @override Widget buildResults(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -238,3 +278,5 @@ class CollectionSearchDelegate extends AvesSearchDelegate { ); } } + +typedef _ContainQuery = bool Function(String s); diff --git a/lib/widgets/settings/common/collection_tile.dart b/lib/widgets/settings/common/collection_tile.dart new file mode 100644 index 000000000..7f9a4c0fb --- /dev/null +++ b/lib/widgets/settings/common/collection_tile.dart @@ -0,0 +1,70 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/services/intent_service.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/collection/filter_bar.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/material.dart'; + +class SettingsCollectionTile extends StatelessWidget { + final Set filters; + final void Function(Set) onSelection; + + const SettingsCollectionTile({ + super.key, + required this.filters, + required this.onSelection, + }); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final hasSubtitle = filters.isEmpty; + + // size and padding to match `ListTile` + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: (hasSubtitle ? 72.0 : 56.0) + theme.visualDensity.baseSizeAdjustment.dy, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.collectionPageTitle, + style: textTheme.subtitle1!, + ), + if (hasSubtitle) + Text( + l10n.drawerCollectionAll, + style: textTheme.bodyText2!.copyWith(color: textTheme.caption!.color), + ), + ], + ), + const Spacer(), + IconButton( + onPressed: () async { + final selection = await IntentService.pickCollectionFilters(filters); + if (selection != null) { + onSelection(selection); + } + }, + icon: const Icon(AIcons.edit), + ), + ], + ), + ), + if (filters.isNotEmpty) FilterBar(filters: filters), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/settings/home_widget_settings_page.dart b/lib/widgets/settings/home_widget_settings_page.dart new file mode 100644 index 000000000..3d18b18ea --- /dev/null +++ b/lib/widgets/settings/home_widget_settings_page.dart @@ -0,0 +1,235 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/widget_shape.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/services/widget_service.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; +import 'package:aves/widgets/common/identity/buttons.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/home_widget.dart'; +import 'package:aves/widgets/settings/common/collection_tile.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class HomeWidgetSettingsPage extends StatefulWidget { + static const routeName = '/settings/home_widget'; + + final int widgetId; + + const HomeWidgetSettingsPage({ + super.key, + required this.widgetId, + }); + + @override + State createState() => _HomeWidgetSettingsPageState(); +} + +class _HomeWidgetSettingsPageState extends State { + late Color? _outline; + late WidgetShape _shape; + late Set _collectionFilters; + + int get widgetId => widget.widgetId; + + static const gradient = HomeWidgetPainter.backgroundGradient; + static final deselectedGradient = LinearGradient( + begin: gradient.begin, + end: gradient.end, + colors: gradient.colors.map((v) { + final l = (v.computeLuminance() * 0xFF).toInt(); + return Color.fromARGB(0xFF, l, l, l); + }).toList(), + stops: gradient.stops, + tileMode: gradient.tileMode, + transform: gradient.transform, + ); + + @override + void initState() { + super.initState(); + _outline = settings.getWidgetOutline(widgetId); + _shape = settings.getWidgetShape(widgetId); + _collectionFilters = settings.getWidgetCollectionFilters(widgetId); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return MediaQueryDataProvider( + child: Scaffold( + appBar: AppBar( + title: Text(l10n.settingsWidgetPageTitle), + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: ListView( + children: [ + _buildShapeSelector(), + ListTile( + title: Text(l10n.settingsWidgetShowOutline), + trailing: HomeWidgetOutlineSelector( + getter: () => _outline, + setter: (v) => setState(() => _outline = v), + ), + ), + SettingsCollectionTile( + filters: _collectionFilters, + onSelection: (v) => setState(() => _collectionFilters = v), + ), + ], + ), + ), + const Divider(height: 0), + Padding( + padding: const EdgeInsets.all(8), + child: AvesOutlinedButton( + label: l10n.saveTooltip, + onPressed: () { + _saveSettings(); + WidgetService.configure(); + }, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildShapeSelector() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), + child: Wrap( + spacing: 16, + runSpacing: 16, + children: WidgetShape.values.map((shape) { + final selected = shape == _shape; + final duration = context.read().formTransition; + return GestureDetector( + onTap: () => setState(() => _shape = shape), + child: AnimatedOpacity( + duration: duration, + opacity: selected ? 1.0 : .4, + child: AnimatedContainer( + duration: duration, + width: 96, + height: 124, + decoration: ShapeDecoration( + gradient: selected ? gradient : deselectedGradient, + shape: _WidgetShapeBorder(_outline, shape), + ), + ), + ), + ); + }).toList(), + ), + ); + } + + void _saveSettings() { + settings.setWidgetOutline(widgetId, _outline); + settings.setWidgetShape(widgetId, _shape); + if (!const SetEquality().equals(_collectionFilters, settings.getWidgetCollectionFilters(widgetId))) { + settings.setWidgetCollectionFilters(widgetId, _collectionFilters); + settings.setWidgetUri(widgetId, null); + } + } +} + +class _WidgetShapeBorder extends ShapeBorder { + final Color? outline; + final WidgetShape shape; + + static const _devicePixelRatio = 1.0; + + const _WidgetShapeBorder(this.outline, this.shape); + + @override + EdgeInsetsGeometry get dimensions => EdgeInsets.zero; + + @override + Path getInnerPath(Rect rect, {TextDirection? textDirection}) { + return getOuterPath(rect, textDirection: textDirection); + } + + @override + Path getOuterPath(Rect rect, {TextDirection? textDirection}) { + return shape.path(rect.size, _devicePixelRatio); + } + + @override + void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { + if (outline != null) { + final path = shape.path(rect.size, _devicePixelRatio); + canvas.clipPath(path); + HomeWidgetPainter.drawOutline(canvas, path, _devicePixelRatio, outline!); + } + } + + @override + ShapeBorder scale(double t) => this; +} + +class HomeWidgetOutlineSelector extends StatefulWidget { + final ValueGetter getter; + final ValueSetter setter; + + const HomeWidgetOutlineSelector({ + super.key, + required this.getter, + required this.setter, + }); + + @override + State createState() => _HomeWidgetOutlineSelectorState(); +} + +class _HomeWidgetOutlineSelectorState extends State { + static const radius = Constants.colorPickerRadius; + static const List options = [ + null, + Colors.black, + Colors.white, + ]; + + @override + Widget build(BuildContext context) { + return DropdownButtonHideUnderline( + child: DropdownButton( + items: _buildItems(context), + value: widget.getter(), + onChanged: (selected) { + widget.setter(selected); + setState(() {}); + }, + ), + ); + } + + List> _buildItems(BuildContext context) { + return options.map((selected) { + return DropdownMenuItem( + value: selected, + child: Container( + height: radius * 2, + width: radius * 2, + decoration: BoxDecoration( + color: selected, + border: AvesBorder.border(context), + shape: BoxShape.circle, + ), + child: selected == null ? const Icon(AIcons.clear) : null, + ), + ); + }).toList(); + } +} diff --git a/lib/widgets/settings/navigation/drawer_tab_albums.dart b/lib/widgets/settings/navigation/drawer_tab_albums.dart index 2708ef157..04e05a838 100644 --- a/lib/widgets/settings/navigation/drawer_tab_albums.dart +++ b/lib/widgets/settings/navigation/drawer_tab_albums.dart @@ -22,6 +22,8 @@ class DrawerAlbumTab extends StatefulWidget { } class _DrawerAlbumTabState extends State { + List get items => widget.items; + @override Widget build(BuildContext context) { final source = context.read(); @@ -32,7 +34,7 @@ class _DrawerAlbumTabState extends State { Flexible( child: ReorderableListView.builder( itemBuilder: (context, index) { - final album = widget.items[index]; + final album = items[index]; final filter = AlbumFilter(album, source.getAlbumDisplayName(context, album)); return ListTile( key: ValueKey(album), @@ -41,17 +43,17 @@ class _DrawerAlbumTabState extends State { trailing: IconButton( icon: const Icon(AIcons.clear), onPressed: () { - setState(() => widget.items.remove(album)); + setState(() => items.remove(album)); }, tooltip: context.l10n.actionRemove, ), ); }, - itemCount: widget.items.length, + itemCount: items.length, onReorder: (oldIndex, newIndex) { setState(() { if (oldIndex < newIndex) newIndex -= 1; - widget.items.insert(newIndex, widget.items.removeAt(oldIndex)); + items.insert(newIndex, items.removeAt(oldIndex)); }); }, shrinkWrap: true, @@ -64,8 +66,8 @@ class _DrawerAlbumTabState extends State { label: context.l10n.settingsNavigationDrawerAddAlbum, onPressed: () async { final album = await pickAlbum(context: context, moveType: null); - if (album == null) return; - setState(() => widget.items.add(album)); + if (album == null || items.contains(album)) return; + setState(() => items.add(album)); }, ), ], diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart index ecbd33b57..530feaeb3 100644 --- a/lib/widgets/settings/navigation/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -60,8 +60,8 @@ class SettingsTileShowBottomNavigationBar extends SettingsTile { @override Widget build(BuildContext context) => SettingsSwitchListTile( - selector: (context, s) => s.showBottomNavigationBar, - onChanged: (v) => settings.showBottomNavigationBar = v, + selector: (context, s) => s.enableBottomNavigationBar, + onChanged: (v) => settings.enableBottomNavigationBar = v, title: title(context), ); } diff --git a/lib/widgets/settings/screen_saver_settings_page.dart b/lib/widgets/settings/screen_saver_settings_page.dart new file mode 100644 index 000000000..bc7c17fe6 --- /dev/null +++ b/lib/widgets/settings/screen_saver_settings_page.dart @@ -0,0 +1,74 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/slideshow_interval.dart'; +import 'package:aves/model/settings/enums/slideshow_video_playback.dart'; +import 'package:aves/model/settings/enums/viewer_transition.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/settings/common/collection_tile.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ScreenSaverSettingsPage extends StatelessWidget { + static const routeName = '/settings/screen_saver'; + + const ScreenSaverSettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return MediaQueryDataProvider( + child: Scaffold( + appBar: AppBar( + title: Text(l10n.settingsScreenSaverPageTitle), + ), + body: SafeArea( + child: ListView( + children: [ + SettingsSwitchListTile( + selector: (context, s) => s.screenSaverFillScreen, + onChanged: (v) => settings.screenSaverFillScreen = v, + title: context.l10n.settingsSlideshowFillScreen, + ), + SettingsSelectionListTile( + values: ViewerTransition.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.screenSaverTransition, + onSelection: (v) => settings.screenSaverTransition = v, + tileTitle: l10n.settingsSlideshowTransitionTile, + dialogTitle: l10n.settingsSlideshowTransitionTitle, + ), + SettingsSelectionListTile( + values: SlideshowInterval.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.screenSaverInterval, + onSelection: (v) => settings.screenSaverInterval = v, + tileTitle: l10n.settingsSlideshowIntervalTile, + dialogTitle: l10n.settingsSlideshowIntervalTitle, + ), + SettingsSelectionListTile( + values: SlideshowVideoPlayback.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.screenSaverVideoPlayback, + onSelection: (v) => settings.screenSaverVideoPlayback = v, + tileTitle: l10n.settingsSlideshowVideoPlaybackTile, + dialogTitle: l10n.settingsSlideshowVideoPlaybackTitle, + ), + Selector>( + selector: (context, s) => s.screenSaverCollectionFilters, + builder: (context, filters, child) { + return SettingsCollectionTile( + filters: filters, + onSelection: (v) => settings.screenSaverCollectionFilters = v, + ); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/settings/video/subtitle_sample.dart b/lib/widgets/settings/video/subtitle_sample.dart index 3ac584b26..9b6d2d888 100644 --- a/lib/widgets/settings/video/subtitle_sample.dart +++ b/lib/widgets/settings/video/subtitle_sample.dart @@ -1,4 +1,5 @@ import 'package:aves/model/settings/settings.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/basic/outlined_text.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/borders.dart'; @@ -26,11 +27,7 @@ class SubtitleSample extends StatelessWidget { gradient: const LinearGradient( begin: Alignment.bottomLeft, end: Alignment.topRight, - colors: [ - // Bora Bora - Color(0xff2bc0e4), - Color(0xffeaecc6), - ], + colors: Constants.boraBoraGradientColors, ), border: AvesBorder.border(context), borderRadius: const BorderRadius.all(Radius.circular(24)), diff --git a/lib/widgets/settings/viewer/entry_background.dart b/lib/widgets/settings/viewer/entry_background.dart index ab5efdf60..9dd6f826c 100644 --- a/lib/widgets/settings/viewer/entry_background.dart +++ b/lib/widgets/settings/viewer/entry_background.dart @@ -1,5 +1,6 @@ import 'package:aves/model/settings/enums/entry_background.dart'; import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:flutter/material.dart'; @@ -19,6 +20,8 @@ class EntryBackgroundSelector extends StatefulWidget { } class _EntryBackgroundSelectorState extends State { + static const radius = Constants.colorPickerRadius; + @override Widget build(BuildContext context) { return DropdownButtonHideUnderline( @@ -36,7 +39,6 @@ class _EntryBackgroundSelectorState extends State { } List> _buildItems(BuildContext context) { - const radius = 12.0; return [ EntryBackground.white, EntryBackground.black, diff --git a/lib/widgets/settings/viewer/slideshow.dart b/lib/widgets/settings/viewer/slideshow.dart index 71bb3d811..e29504e28 100644 --- a/lib/widgets/settings/viewer/slideshow.dart +++ b/lib/widgets/settings/viewer/slideshow.dart @@ -31,6 +31,11 @@ class ViewerSlideshowPage extends StatelessWidget { onChanged: (v) => settings.slideshowShuffle = v, title: context.l10n.settingsSlideshowShuffle, ), + SettingsSwitchListTile( + selector: (context, s) => s.slideshowFillScreen, + onChanged: (v) => settings.slideshowFillScreen = v, + title: context.l10n.settingsSlideshowFillScreen, + ), SettingsSelectionListTile( values: ViewerTransition.values, getName: (context, v) => v.getName(context), diff --git a/lib/widgets/stats/date/axis.dart b/lib/widgets/stats/date/axis.dart new file mode 100644 index 000000000..af043e92b --- /dev/null +++ b/lib/widgets/stats/date/axis.dart @@ -0,0 +1,97 @@ +import 'dart:math'; + +import 'package:aves/model/filters/date.dart'; +import 'package:aves/utils/time_utils.dart'; +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:intl/intl.dart'; + +// cf charts.DateTimeTickFormatter factory internals for default formats +class TimeAxisSpec { + final List> tickSpecs; + + TimeAxisSpec(this.tickSpecs); + + factory TimeAxisSpec.forLevel({ + required String locale, + required DateLevel level, + required DateTime first, + required DateTime last, + }) { + switch (level) { + case DateLevel.ymd: + return TimeAxisSpec.days(locale, first, last); + case DateLevel.ym: + return TimeAxisSpec.months(locale, first, last); + case DateLevel.y: + default: + return TimeAxisSpec.years(locale, first, last); + } + } + + factory TimeAxisSpec.days(String locale, DateTime first, DateTime last) { + final daysTickLongFormat = DateFormat.MMMd(locale); + final daysTickShortFormat = DateFormat.d(locale); + + first = first.date; + last = last.date; + final rangeDays = last.difference(first).inDays; + final delta = max(1, (rangeDays / 5).ceil()); + + List> ticks = []; + int lastContext = -1; + DateFormat dateFormat; + for (int i = 0; i < rangeDays; i += delta) { + final tickDate = first.addDays(i); + if (lastContext != tickDate.month) { + lastContext = tickDate.month; + dateFormat = daysTickLongFormat; + } else { + dateFormat = daysTickShortFormat; + } + ticks.add(charts.TickSpec(tickDate, label: dateFormat.format(tickDate))); + } + return TimeAxisSpec(ticks); + } + + factory TimeAxisSpec.months(String locale, DateTime first, DateTime last) { + final monthsTickLongFormat = DateFormat.yMMM(locale); + final monthsTickShortFormat = DateFormat.MMM(locale); + + first = DateTime(first.year, first.month); + last = DateTime(last.year, last.month); + final rangeMonths = last.month - first.month + (last.month < first.month ? 12 : 0); + if (rangeMonths < 12) { + first = first.addMonths(-((12 - rangeMonths) / 2).floor()); + } + + List> ticks = []; + int lastContext = -1; + DateFormat dateFormat; + for (int i = 0; i < DateTime.monthsPerYear; i += 3) { + final tickDate = first.addMonths(2 + i); + if (lastContext != tickDate.year) { + lastContext = tickDate.year; + dateFormat = monthsTickLongFormat; + } else { + dateFormat = monthsTickShortFormat; + } + ticks.add(charts.TickSpec(tickDate, label: dateFormat.format(tickDate))); + } + return TimeAxisSpec(ticks); + } + + factory TimeAxisSpec.years(String locale, DateTime first, DateTime last) { + final dateFormat = DateFormat.y(locale); + + final firstYear = first.year; + final lastYear = last.year; + final delta = max(1, ((lastYear - firstYear) / 5).ceil()); + + List> ticks = []; + for (int year = firstYear; year <= lastYear; year += delta) { + final tickDate = DateTime(year); + ticks.add(charts.TickSpec(tickDate, label: dateFormat.format(tickDate))); + } + return TimeAxisSpec(ticks); + } +} diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index 7a572a4eb..814d55af7 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -26,7 +26,7 @@ class FilterTable extends StatelessWidget { required this.onFilterSelection, }); - static const chipWidth = AvesFilterChip.defaultMaxChipWidth; + static const chipWidth = 160.0; static const countWidth = 32.0; static const percentIndicatorMinWidth = 80.0; diff --git a/lib/widgets/stats/histogram.dart b/lib/widgets/stats/histogram.dart new file mode 100644 index 000000000..f4e3d2eb0 --- /dev/null +++ b/lib/widgets/stats/histogram.dart @@ -0,0 +1,205 @@ +import 'package:aves/model/entry.dart'; +import 'package:aves/model/filters/date.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/widgets/stats/date/axis.dart'; +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class Histogram extends StatefulWidget { + final Set entries; + final FilterCallback onFilterSelection; + + const Histogram({ + super.key, + required this.entries, + required this.onFilterSelection, + }); + + @override + State createState() => _HistogramState(); +} + +class _HistogramState extends State { + DateLevel _level = DateLevel.y; + DateTime? _firstDate, _lastDate; + final Map _entryCountPerDate = {}; + final ValueNotifier _selection = ValueNotifier(null); + + static const histogramHeight = 200.0; + + @override + void initState() { + super.initState(); + + final entriesByDateDescending = List.of(widget.entries)..sort(AvesEntry.compareByDate); + _lastDate = entriesByDateDescending.firstWhereOrNull((entry) => entry.bestDate != null)?.bestDate; + _firstDate = entriesByDateDescending.lastWhereOrNull((entry) => entry.bestDate != null)?.bestDate; + + if (_lastDate != null && _firstDate != null) { + final rangeDays = _lastDate!.difference(_firstDate!).inDays; + if (rangeDays > 1) { + if (rangeDays <= 31) { + _level = DateLevel.ymd; + } else if (rangeDays <= 365) { + _level = DateLevel.ym; + } + + final dates = entriesByDateDescending.map((entry) => entry.bestDate).whereNotNull(); + late DateTime Function(DateTime) groupByKey; + switch (_level) { + case DateLevel.ymd: + groupByKey = (v) => DateTime(v.year, v.month, v.day); + break; + case DateLevel.ym: + groupByKey = (v) => DateTime(v.year, v.month); + break; + default: + groupByKey = (v) => DateTime(v.year); + break; + } + _entryCountPerDate.addAll(groupBy(dates, groupByKey).map((k, v) => MapEntry(k, v.length))); + } + } + } + + @override + Widget build(BuildContext context) { + if (_entryCountPerDate.isEmpty) return const SizedBox(); + + final theme = Theme.of(context); + + final seriesData = _entryCountPerDate.entries.map((kv) { + return EntryByDate(date: kv.key, entryCount: kv.value); + }).toList(); + + final series = [ + charts.Series( + id: 'histogram', + colorFn: (d, i) => charts.ColorUtil.fromDartColor(theme.colorScheme.secondary), + domainFn: (d, i) => d.date, + measureFn: (d, i) => d.entryCount, + data: seriesData, + ), + ]; + + final locale = context.l10n.localeName; + final timeAxisSpec = _firstDate != null && _lastDate != null + ? TimeAxisSpec.forLevel( + locale: locale, + level: _level, + first: _firstDate!, + last: _lastDate!, + ) + : null; + final axisColor = charts.ColorUtil.fromDartColor(theme.colorScheme.onPrimary.withOpacity(.9)); + final measureLineColor = charts.ColorUtil.fromDartColor(theme.colorScheme.onPrimary.withOpacity(.1)); + final measureFormat = NumberFormat.decimalPattern(locale); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + height: histogramHeight, + child: charts.TimeSeriesChart( + series, + domainAxis: charts.DateTimeAxisSpec( + renderSpec: charts.SmallTickRendererSpec( + labelStyle: charts.TextStyleSpec(color: axisColor), + lineStyle: charts.LineStyleSpec(color: axisColor), + ), + tickProviderSpec: timeAxisSpec != null && timeAxisSpec.tickSpecs.isNotEmpty ? charts.StaticDateTimeTickProviderSpec(timeAxisSpec.tickSpecs) : null, + ), + primaryMeasureAxis: charts.NumericAxisSpec( + renderSpec: charts.GridlineRendererSpec( + labelStyle: charts.TextStyleSpec(color: axisColor), + lineStyle: charts.LineStyleSpec(color: measureLineColor), + ), + tickFormatterSpec: charts.BasicNumericTickFormatterSpec((v) { + // localize and hide 0 + return (v == null || v == 0) ? '' : measureFormat.format(v); + }), + ), + defaultRenderer: charts.BarRendererConfig(), + defaultInteractions: false, + behaviors: [ + charts.SelectNearest(), + charts.DomainHighlighter(), + ], + selectionModels: [ + charts.SelectionModelConfig( + changedListener: (model) => _selection.value = model.selectedDatum.firstOrNull?.datum as EntryByDate?, + ) + ], + ), + ), + ValueListenableBuilder( + valueListenable: _selection, + builder: (context, selection, child) { + late Widget child; + if (selection == null) { + child = const SizedBox(); + } else { + final filter = DateFilter(_level, selection.date); + final count = selection.entryCount; + child = Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + AvesFilterChip( + filter: filter, + onTap: widget.onFilterSelection, + ), + const Spacer(), + Text( + '$count', + style: TextStyle( + color: theme.textTheme.caption!.color, + ), + textAlign: TextAlign.end, + ), + ], + ), + ); + } + + return AnimatedSwitcher( + duration: context.read().formTransition, + switchInCurve: Curves.easeInOutCubic, + switchOutCurve: Curves.easeInOutCubic, + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: -1, + child: child, + ), + ), + child: child, + ); + }, + ), + ], + ); + } +} + +@immutable +class EntryByDate extends Equatable { + final DateTime date; + final int entryCount; + + @override + List get props => [date, entryCount]; + + const EntryByDate({ + required this.date, + required this.entryCount, + }); +} diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index b6955616c..1f538c952 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -20,6 +20,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/stats/filter_table.dart'; +import 'package:aves/widgets/stats/histogram.dart'; import 'package:charts_flutter/flutter.dart' as charts; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; @@ -148,6 +149,10 @@ class StatsPage extends StatelessWidget { child = ListView( children: [ mimeDonuts, + Histogram( + entries: entries, + onFilterSelection: (filter) => _onFilterSelection(context, filter), + ), locationIndicator, ..._buildFilterSection(context, context.l10n.statsTopCountries, entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)), ..._buildFilterSection(context, context.l10n.statsTopPlaces, entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)), diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index a874fb942..f672e664b 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -14,7 +14,7 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/enums.dart'; -import 'package:aves/services/media/media_file_service.dart'; +import 'package:aves/services/media/media_edit_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/collection/collection_page.dart'; @@ -246,7 +246,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix source.pauseMonitoring(); await showOpReport( context: context, - opStream: mediaFileService.export( + opStream: mediaEditService.export( selection, options: options, destinationAlbum: destinationAlbum, @@ -336,7 +336,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix MaterialPageRoute( settings: const RouteSettings(name: SourceViewerPage.routeName), builder: (context) => SourceViewerPage( - loader: () => mediaFileService.getSvg(entry.uri, entry.mimeType).then(utf8.decode), + loader: () => mediaFetchService.getSvg(entry.uri, entry.mimeType).then(utf8.decode), ), ), ); diff --git a/lib/widgets/viewer/action/printer.dart b/lib/widgets/viewer/action/printer.dart index 8f2526db6..7e8b9b4be 100644 --- a/lib/widgets/viewer/action/printer.dart +++ b/lib/widgets/viewer/action/printer.dart @@ -74,7 +74,7 @@ class EntryPrinter with FeedbackMixin { Future _buildPageImage(AvesEntry entry) async { if (entry.isSvg) { - final bytes = await mediaFileService.getSvg(entry.uri, entry.mimeType); + final bytes = await mediaFetchService.getSvg(entry.uri, entry.mimeType); if (bytes.isNotEmpty) { return pdf.SvgImage(svg: utf8.decode(bytes)); } diff --git a/lib/widgets/viewer/controller.dart b/lib/widgets/viewer/controller.dart index 5b102e906..bae8ec799 100644 --- a/lib/widgets/viewer/controller.dart +++ b/lib/widgets/viewer/controller.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/widgets/common/magnifier/scale/scale_level.dart'; import 'package:flutter/widgets.dart'; class ViewerController { final ValueNotifier entryNotifier = ValueNotifier(null); + final ScaleLevel initialScale; final ViewerTransition transition; final Duration? autopilotInterval; final bool repeat; @@ -25,6 +27,7 @@ class ViewerController { set autopilot(bool enabled) => _autopilotNotifier.value = enabled; ViewerController({ + this.initialScale = const ScaleLevel(ref: ScaleReference.contained), this.transition = ViewerTransition.parallax, this.repeat = false, bool autopilot = false, diff --git a/lib/widgets/viewer/debug/debug_page.dart b/lib/widgets/viewer/debug/debug_page.dart index a6468fd81..d4bb1e904 100644 --- a/lib/widgets/viewer/debug/debug_page.dart +++ b/lib/widgets/viewer/debug/debug_page.dart @@ -1,7 +1,11 @@ +import 'dart:ui' as ui; + import 'package:aves/app_mode.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/home_widget.dart'; import 'package:aves/widgets/viewer/debug/db.dart'; import 'package:aves/widgets/viewer/debug/metadata.dart'; import 'package:aves/widgets/viewer/info/common.dart'; @@ -27,6 +31,7 @@ class ViewerDebugPage extends StatelessWidget { if (context.select, bool>((vn) => vn.value != AppMode.view)) Tuple2(const Tab(text: 'DB'), DbTab(entry: entry)), Tuple2(const Tab(icon: Icon(AIcons.android)), MetadataTab(entry: entry)), Tuple2(const Tab(icon: Icon(AIcons.image)), _buildThumbnailsTabView()), + Tuple2(const Tab(icon: Icon(AIcons.addShortcut)), _buildWidgetTabView()), ]; return Directionality( textDirection: TextDirection.ltr, @@ -155,7 +160,7 @@ class ViewerDebugPage extends StatelessWidget { padding: const EdgeInsets.all(16), children: entry.cachedThumbnails .expand((provider) => [ - Text('Extent: ${provider.key.extent}'), + Text('Thumb extent: ${provider.key.extent}'), Center( child: Image( image: provider, @@ -177,4 +182,32 @@ class ViewerDebugPage extends StatelessWidget { .toList(), ); } + + Widget _buildWidgetTabView() { + return ListView( + padding: const EdgeInsets.all(16), + children: [303, 636, 972, 1305] + .expand((heightPx) => [ + Text('Widget heightPx: $heightPx'), + FutureBuilder( + future: HomeWidgetPainter( + entry: entry, + devicePixelRatio: ui.window.devicePixelRatio, + ).drawWidget( + widthPx: 978, + heightPx: heightPx, + outline: Colors.amber, + shape: WidgetShape.heart, + format: ui.ImageByteFormat.png, + ), + builder: (context, snapshot) { + final bytes = snapshot.data; + if (bytes == null) return const SizedBox(); + return Image.memory(bytes); + }, + ), + ]) + .toList(), + ); + } } diff --git a/lib/widgets/viewer/entry_horizontal_pager.dart b/lib/widgets/viewer/entry_horizontal_pager.dart index a25307be6..c184a0a47 100644 --- a/lib/widgets/viewer/entry_horizontal_pager.dart +++ b/lib/widgets/viewer/entry_horizontal_pager.dart @@ -90,6 +90,7 @@ class _MultiEntryScrollerState extends State with AutomaticK key: const Key('image_view'), mainEntry: mainEntry, pageEntry: pageEntry ?? mainEntry, + initialScale: viewerController.initialScale, onDisposed: () => widget.onViewDisposed(mainEntry, pageEntry), ); } @@ -100,10 +101,12 @@ class _MultiEntryScrollerState extends State with AutomaticK class SingleEntryScroller extends StatefulWidget { final AvesEntry entry; + final ViewerController viewerController; const SingleEntryScroller({ super.key, required this.entry, + required this.viewerController, }); @override @@ -113,6 +116,8 @@ class SingleEntryScroller extends StatefulWidget { class _SingleEntryScrollerState extends State with AutomaticKeepAliveClientMixin { AvesEntry get mainEntry => widget.entry; + ViewerController get viewerController => widget.viewerController; + @override Widget build(BuildContext context) { super.build(context); @@ -134,6 +139,7 @@ class _SingleEntryScrollerState extends State with Automati return EntryPageView( mainEntry: mainEntry, pageEntry: pageEntry ?? mainEntry, + initialScale: viewerController.initialScale, ); } diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index b7e310155..ddab4e2a2 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -23,6 +23,7 @@ class ViewerVerticalPageView extends StatefulWidget { final CollectionLens? collection; final ValueNotifier entryNotifier; final ViewerController viewerController; + final Animation overlayOpacity; final PageController horizontalPager, verticalPager; final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; final VoidCallback onImagePageRequested; @@ -33,6 +34,7 @@ class ViewerVerticalPageView extends StatefulWidget { required this.collection, required this.entryNotifier, required this.viewerController, + required this.overlayOpacity, required this.verticalPager, required this.horizontalPager, required this.onVerticalPageChanged, @@ -116,7 +118,8 @@ class _ViewerVerticalPageViewState extends State { _buildImagePage(), ]; - if (context.read>().value != AppMode.slideshow) { + final appMode = context.read>().value; + if (!{AppMode.screenSaver, AppMode.slideshow}.contains(appMode)) { final infoPage = NotificationListener( onNotification: (notification) { widget.onImagePageRequested(); @@ -144,9 +147,15 @@ class _ViewerVerticalPageViewState extends State { return ValueListenableBuilder( valueListenable: _backgroundOpacityNotifier, builder: (context, backgroundOpacity, child) { - final background = Theme.of(context).brightness == Brightness.dark ? Colors.black : Colors.white; - return Container( - color: background.withOpacity(backgroundOpacity), + return ValueListenableBuilder( + valueListenable: widget.overlayOpacity, + builder: (context, overlayOpacity, child) { + final background = Theme.of(context).brightness == Brightness.dark ? Colors.black : Color.lerp(Colors.black, Colors.white, overlayOpacity)!; + return Container( + color: background.withOpacity(backgroundOpacity), + child: child, + ); + }, child: child, ); }, @@ -185,6 +194,7 @@ class _ViewerVerticalPageViewState extends State { }; } else if (entry != null) { child = SingleEntryScroller( + viewerController: widget.viewerController, entry: entry!, ); shortcuts = const { diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 12b9eee24..2005f2174 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -62,9 +62,10 @@ class _EntryViewerStackState extends State with EntryViewContr late ValueNotifier _currentVerticalPage; late PageController _horizontalPager, _verticalPager; final AChangeNotifier _verticalScrollNotifier = AChangeNotifier(); + bool _overlayInitialized = false; final ValueNotifier _overlayVisible = ValueNotifier(true); late AnimationController _overlayAnimationController; - late Animation _overlayButtonScale, _overlayVideoControlScale; + late Animation _overlayButtonScale, _overlayVideoControlScale, _overlayOpacity; late Animation _overlayTopOffset; EdgeInsets? _frozenViewInsets, _frozenViewPadding; late VideoActionDelegate _videoActionDelegate; @@ -129,6 +130,10 @@ class _EntryViewerStackState extends State with EntryViewContr // no bounce at the bottom, to avoid video controller displacement curve: Curves.easeOutQuad, ); + _overlayOpacity = CurvedAnimation( + parent: _overlayAnimationController, + curve: Curves.easeOutQuad, + ); _overlayTopOffset = Tween(begin: const Offset(0, -1), end: const Offset(0, 0)).animate(CurvedAnimation( parent: _overlayAnimationController, curve: Curves.easeOutQuad, @@ -177,6 +182,7 @@ class _EntryViewerStackState extends State with EntryViewContr case AppLifecycleState.inactive: case AppLifecycleState.paused: case AppLifecycleState.detached: + viewerController.autopilot = false; pauseVideoControllers(); break; case AppLifecycleState.resumed: @@ -258,6 +264,11 @@ class _EntryViewerStackState extends State with EntryViewContr collection: collection, entryNotifier: entryNotifier, viewerController: viewerController, + overlayOpacity: _overlayInitialized + ? _overlayOpacity + : settings.showOverlayOnOpening + ? kAlwaysCompleteAnimation + : kAlwaysDismissedAnimation, verticalPager: _verticalPager, horizontalPager: _horizontalPager, onVerticalPageChanged: _onVerticalPageChanged, @@ -266,6 +277,7 @@ class _EntryViewerStackState extends State with EntryViewContr onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry), ), ..._buildOverlays(), + const TopGestureAreaProtector(), const SideGestureAreaProtector(), const BottomGestureAreaProtector(), ], @@ -276,14 +288,20 @@ class _EntryViewerStackState extends State with EntryViewContr } List _buildOverlays() { - if (context.read>().value == AppMode.slideshow) { - return [_buildSlideshowBottomOverlay()]; + final appMode = context.read>().value; + switch (appMode) { + case AppMode.screenSaver: + return []; + case AppMode.slideshow: + return [ + _buildSlideshowBottomOverlay(), + ]; + default: + return [ + _buildViewerTopOverlay(), + _buildViewerBottomOverlay(), + ]; } - - return [ - _buildViewerTopOverlay(), - _buildViewerBottomOverlay(), - ]; } Widget _buildSlideshowBottomOverlay() { @@ -637,6 +655,7 @@ class _EntryViewerStackState extends State with EntryViewContr // to show overlay after hero animation is complete await Future.delayed(ModalRoute.of(context)!.transitionDuration * timeDilation); await _onOverlayVisibleChange(); + _overlayInitialized = true; } Future _onOverlayVisibleChange({bool animate = true}) async { diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index a7c11a944..2c949209a 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -189,16 +189,13 @@ class _BasicInfoState extends State<_BasicInfo> { @override void initState() { super.initState(); - if (!entry.trashed) { - final isMediaContent = entry.uri.startsWith('content://media/external/'); - if (isMediaContent) { - _ownerPackageLoader = metadataFetchService.hasContentResolverProp(ownerPackageNamePropKey).then((exists) { - return exists ? metadataFetchService.getContentResolverProp(entry, ownerPackageNamePropKey) : SynchronousFuture(null); - }); - final isViewerMode = context.read>().value == AppMode.view; - if (isViewerMode && settings.isInstalledAppAccessAllowed) { - _appNameLoader = androidFileUtils.initAppNames(); - } + if (!entry.trashed && entry.isMediaStoreContent) { + _ownerPackageLoader = metadataFetchService.hasContentResolverProp(ownerPackageNamePropKey).then((exists) { + return exists ? metadataFetchService.getContentResolverProp(entry, ownerPackageNamePropKey) : SynchronousFuture(null); + }); + final isViewerMode = context.read>().value == AppMode.view; + if (isViewerMode && settings.isInstalledAppAccessAllowed) { + _appNameLoader = androidFileUtils.initAppNames(); } } } diff --git a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart index 8a25664cc..453172941 100644 --- a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart +++ b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart @@ -15,6 +15,7 @@ import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc4xmpext.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/microsoft.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/mwg.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/photoshop.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_ns/plus.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:collection/collection.dart'; @@ -67,6 +68,8 @@ class XmpNamespace extends Equatable { return XmpNoteNamespace(rawProps); case XmpPhotoshopNamespace.ns: return XmpPhotoshopNamespace(rawProps); + case XmpPlusNamespace.ns: + return XmpPlusNamespace(rawProps); case XmpTiffNamespace.ns: return XmpTiffNamespace(rawProps); default: diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/plus.dart b/lib/widgets/viewer/info/metadata/xmp_ns/plus.dart new file mode 100644 index 000000000..ab200ed67 --- /dev/null +++ b/lib/widgets/viewer/info/metadata/xmp_ns/plus.dart @@ -0,0 +1,25 @@ +import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; +import 'package:flutter/material.dart'; + +class XmpPlusNamespace extends XmpNamespace { + static const ns = 'plus'; + + static final licensorPattern = RegExp(ns + r':Licensor\[(\d+)\]/(.*)'); + + final licensor = >{}; + + XmpPlusNamespace(Map rawProps) : super(ns, rawProps); + + @override + bool extractData(XmpProp prop) => extractIndexedStruct(prop, licensorPattern, licensor); + + @override + List buildFromExtractedData() => [ + if (licensor.isNotEmpty) + XmpStructArrayCard( + title: 'Licensor', + structByIndex: licensor, + ), + ]; +} diff --git a/lib/widgets/viewer/overlay/wallpaper_buttons.dart b/lib/widgets/viewer/overlay/wallpaper_buttons.dart index 8b8506773..1e9e0022d 100644 --- a/lib/widgets/viewer/overlay/wallpaper_buttons.dart +++ b/lib/widgets/viewer/overlay/wallpaper_buttons.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:math'; -import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:aves/model/device.dart'; diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 5d97c62ea..f2822b1f8 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -13,7 +13,6 @@ import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:latlong2/latlong.dart'; import 'package:panorama/panorama.dart'; import 'package:provider/provider.dart'; @@ -76,11 +75,7 @@ class _PanoramaPageState extends State { final fullSize = info.fullPanoSize!; final longitude = ((croppedArea.left + croppedArea.width / 2) / fullSize.width - 1 / 2) * 360; return Panorama( - // TODO TLAD [panorama] fork and fix - // as of panorama v0.4.0, doc says `latitude` and `longitude` parameters are in degrees, - // but they are actually converted from radians in state initialization - // as of panorama v0.4.0, state uses longitude in degrees as radians with `Quaternion.axisAngle` - longitude: degToRadian(longitude), + longitude: longitude, sensorControl: sensorControl, croppedArea: croppedArea, croppedFullWidth: fullSize.width, @@ -141,6 +136,8 @@ class _PanoramaPageState extends State { ), ), ), + const TopGestureAreaProtector(), + const SideGestureAreaProtector(), const BottomGestureAreaProtector(), ], ), diff --git a/lib/widgets/viewer/screen_saver_page.dart b/lib/widgets/viewer/screen_saver_page.dart new file mode 100644 index 000000000..9afc79eac --- /dev/null +++ b/lib/widgets/viewer/screen_saver_page.dart @@ -0,0 +1,124 @@ +import 'package:aves/model/filters/mime.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/slideshow_interval.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/enums.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; +import 'package:aves/widgets/common/magnifier/scale/scale_level.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/viewer/controller.dart'; +import 'package:aves/widgets/viewer/entry_viewer_page.dart'; +import 'package:aves/widgets/viewer/entry_viewer_stack.dart'; +import 'package:flutter/material.dart'; + +class ScreenSaverPage extends StatefulWidget { + static const routeName = '/screen_saver'; + + final CollectionSource source; + + const ScreenSaverPage({ + super.key, + required this.source, + }); + + @override + State createState() => _ScreenSaverPageState(); +} + +class _ScreenSaverPageState extends State { + late final ViewerController _viewerController; + CollectionLens? _slideshowCollection; + + CollectionSource get source => widget.source; + + @override + void initState() { + super.initState(); + _viewerController = ViewerController( + initialScale: ScaleLevel(ref: settings.screenSaverFillScreen ? ScaleReference.covered : ScaleReference.contained), + transition: settings.screenSaverTransition, + repeat: true, + autopilot: true, + autopilotInterval: settings.screenSaverInterval.getDuration(), + ); + source.stateNotifier.addListener(_onSourceStateChanged); + _initSlideshowCollection(); + } + + void _onSourceStateChanged() { + if (_slideshowCollection == null) { + _initSlideshowCollection(); + if (_slideshowCollection != null) { + setState(() {}); + } + } + } + + @override + void dispose() { + source.stateNotifier.removeListener(_onSourceStateChanged); + _viewerController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Widget child; + + final collection = _slideshowCollection; + if (collection == null) { + child = const SizedBox(); + } else { + final entries = collection.sortedEntries; + if (entries.isEmpty) { + child = EmptyContent( + icon: AIcons.image, + text: context.l10n.collectionEmptyImages, + alignment: Alignment.center, + ); + } else { + child = ViewStateConductorProvider( + child: VideoConductorProvider( + child: MultiPageConductorProvider( + child: EntryViewerStack( + collection: collection, + initialEntry: entries.first, + viewerController: _viewerController, + ), + ), + ), + ); + } + } + + return MediaQueryDataProvider( + child: Scaffold( + body: child, + ), + ); + } + + void _initSlideshowCollection() { + if (source.stateNotifier.value != SourceState.ready || _slideshowCollection != null) return; + + final originalCollection = CollectionLens( + source: source, + filters: settings.screenSaverCollectionFilters, + ); + var entries = originalCollection.sortedEntries; + if (settings.screenSaverVideoPlayback == SlideshowVideoPlayback.skip) { + entries = entries.where((entry) => !MimeFilter.video.test(entry)).toList(); + } + entries.shuffle(); + _slideshowCollection = CollectionLens( + source: originalCollection.source, + listenToSource: false, + fixedSort: true, + fixedSelection: entries, + ); + } +} diff --git a/lib/widgets/viewer/slideshow_page.dart b/lib/widgets/viewer/slideshow_page.dart index c21b238bf..d62be7afd 100644 --- a/lib/widgets/viewer/slideshow_page.dart +++ b/lib/widgets/viewer/slideshow_page.dart @@ -10,6 +10,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/empty.dart'; +import 'package:aves/widgets/common/magnifier/scale/scale_level.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/viewer/controller.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; @@ -32,32 +33,20 @@ class SlideshowPage extends StatefulWidget { } class _SlideshowPageState extends State { - late final CollectionLens _slideshowCollection; late final ViewerController _viewerController; + late final CollectionLens _slideshowCollection; @override void initState() { super.initState(); - final originalCollection = widget.collection; - var entries = originalCollection.sortedEntries; - if (settings.slideshowVideoPlayback == SlideshowVideoPlayback.skip) { - entries = entries.where((entry) => !MimeFilter.video.test(entry)).toList(); - } - if (settings.slideshowShuffle) { - entries.shuffle(); - } - _slideshowCollection = CollectionLens( - source: originalCollection.source, - listenToSource: false, - fixedSort: true, - fixedSelection: entries, - ); _viewerController = ViewerController( + initialScale: ScaleLevel(ref: settings.slideshowFillScreen ? ScaleReference.covered : ScaleReference.contained), transition: settings.slideshowTransition, repeat: settings.slideshowRepeat, autopilot: true, autopilotInterval: settings.slideshowInterval.getDuration(), ); + _initSlideshowCollection(); } @override @@ -101,6 +90,23 @@ class _SlideshowPageState extends State { ); } + void _initSlideshowCollection() { + final originalCollection = widget.collection; + var entries = originalCollection.sortedEntries; + if (settings.slideshowVideoPlayback == SlideshowVideoPlayback.skip) { + entries = entries.where((entry) => !MimeFilter.video.test(entry)).toList(); + } + if (settings.slideshowShuffle) { + entries.shuffle(); + } + _slideshowCollection = CollectionLens( + source: originalCollection.source, + listenToSource: false, + fixedSort: true, + fixedSelection: entries, + ); + } + void _onActionSelected(SlideshowAction action) { switch (action) { case SlideshowAction.resume: diff --git a/lib/widgets/viewer/video/controller.dart b/lib/widgets/viewer/video/controller.dart index cb07d6e4a..7c02997f4 100644 --- a/lib/widgets/viewer/video/controller.dart +++ b/lib/widgets/viewer/video/controller.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:aves/model/entry.dart'; import 'package:aves/model/video_playback.dart'; import 'package:aves/services/common/services.dart'; diff --git a/lib/widgets/viewer/video_action_delegate.dart b/lib/widgets/viewer/video_action_delegate.dart index d2cfb7e7b..77b66662a 100644 --- a/lib/widgets/viewer/video_action_delegate.dart +++ b/lib/widgets/viewer/video_action_delegate.dart @@ -98,7 +98,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } }; - final newFields = await mediaFileService.captureFrame( + final newFields = await mediaEditService.captureFrame( entry, desiredName: '${entry.bestTitle}_${'$positionMillis'.padLeft(8, '0')}', exif: exif, diff --git a/lib/widgets/viewer/visual/controller_mixin.dart b/lib/widgets/viewer/visual/controller_mixin.dart index bf3aff9d7..ff5eead4c 100644 --- a/lib/widgets/viewer/visual/controller_mixin.dart +++ b/lib/widgets/viewer/visual/controller_mixin.dart @@ -37,20 +37,28 @@ mixin EntryViewControllerMixin on State { } } - bool _isSlideshow(BuildContext context) => context.read>().value == AppMode.slideshow; + SlideshowVideoPlayback? get videoPlaybackOverride { + final appMode = context.read>().value; + switch (appMode) { + case AppMode.screenSaver: + return settings.screenSaverVideoPlayback; + case AppMode.slideshow: + return settings.slideshowVideoPlayback; + default: + return null; + } + } bool _shouldAutoPlay(BuildContext context) { - if (_isSlideshow(context)) { - switch (settings.slideshowVideoPlayback) { - case SlideshowVideoPlayback.skip: - return false; - case SlideshowVideoPlayback.playMuted: - case SlideshowVideoPlayback.playWithSound: - return true; - } + switch (videoPlaybackOverride) { + case SlideshowVideoPlayback.skip: + return false; + case SlideshowVideoPlayback.playMuted: + case SlideshowVideoPlayback.playWithSound: + return true; + case null: + return settings.enableVideoAutoPlay; } - - return settings.enableVideoAutoPlay; } Future _initVideoController(AvesEntry entry) async { @@ -127,7 +135,7 @@ mixin EntryViewControllerMixin on State { // so we play after a delay for increased stability await Future.delayed(const Duration(milliseconds: 300) * timeDilation); - if (_isSlideshow(context) && settings.slideshowVideoPlayback == SlideshowVideoPlayback.playMuted && !videoController.isMuted) { + if (videoPlaybackOverride == SlideshowVideoPlayback.playMuted && !videoController.isMuted) { await videoController.toggleMute(); } diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 3d2ed7c7f..25d8fb156 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -35,6 +35,7 @@ import 'package:tuple/tuple.dart'; class EntryPageView extends StatefulWidget { final AvesEntry mainEntry, pageEntry; + final ScaleLevel initialScale; final VoidCallback? onDisposed; static const decorationCheckSize = 20.0; @@ -43,6 +44,7 @@ class EntryPageView extends StatefulWidget { super.key, required this.mainEntry, required this.pageEntry, + required this.initialScale, this.onDisposed, }); @@ -380,7 +382,7 @@ class _EntryPageViewState extends State { allowOriginalScaleBeyondRange: !isWallpaperMode, minScale: minScale, maxScale: maxScale, - initialScale: minScale, + initialScale: widget.initialScale, scaleStateCycle: scaleStateCycle, applyScale: applyScale, onTap: (c, d, s, o) => _onTap(), diff --git a/lib/widgets/wallpaper_page.dart b/lib/widgets/wallpaper_page.dart index a3346e3f9..390001329 100644 --- a/lib/widgets/wallpaper_page.dart +++ b/lib/widgets/wallpaper_page.dart @@ -5,7 +5,9 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/magnifier/scale/scale_level.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/viewer/controller.dart'; import 'package:aves/widgets/viewer/entry_horizontal_pager.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; @@ -71,6 +73,7 @@ class _EntryEditorState extends State with EntryViewControllerMixin late Animation _overlayVideoControlScale; EdgeInsets? _frozenViewInsets, _frozenViewPadding; late VideoActionDelegate _videoActionDelegate; + late final ViewerController _viewerController; @override final ValueNotifier entryNotifier = ValueNotifier(null); @@ -105,6 +108,9 @@ class _EntryEditorState extends State with EntryViewControllerMixin collection: null, ); + _viewerController = ViewerController( + initialScale: const ScaleLevel(ref: ScaleReference.covered), + ); initEntryControllers(entry); _onOverlayVisibleChange(); } @@ -112,6 +118,7 @@ class _EntryEditorState extends State with EntryViewControllerMixin @override void dispose() { cleanEntryControllers(entry); + _viewerController.dispose(); _videoActionDelegate.dispose(); _overlayAnimationController.dispose(); _overlayVisible.removeListener(_onOverlayVisibleChange); @@ -131,11 +138,13 @@ class _EntryEditorState extends State with EntryViewControllerMixin children: [ SingleEntryScroller( entry: entry, + viewerController: _viewerController, ), Positioned( bottom: 0, child: _buildBottomOverlay(), ), + const TopGestureAreaProtector(), const SideGestureAreaProtector(), const BottomGestureAreaProtector(), ], diff --git a/plugins/aves_map/lib/src/marker/generator.dart b/plugins/aves_map/lib/src/marker/generator.dart index e8a8f77c0..3efff1ebe 100644 --- a/plugins/aves_map/lib/src/marker/generator.dart +++ b/plugins/aves_map/lib/src/marker/generator.dart @@ -1,4 +1,3 @@ -import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:collection/collection.dart'; diff --git a/plugins/aves_map/lib/src/overlay/tile.dart b/plugins/aves_map/lib/src/overlay/tile.dart index fdfb5cb2d..cc5e5a0fa 100644 --- a/plugins/aves_map/lib/src/overlay/tile.dart +++ b/plugins/aves_map/lib/src/overlay/tile.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:flutter/foundation.dart'; @immutable diff --git a/plugins/aves_platform_meta/.gitignore b/plugins/aves_platform_meta/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/plugins/aves_platform_meta/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_platform_meta/.metadata b/plugins/aves_platform_meta/.metadata new file mode 100644 index 000000000..93fddf202 --- /dev/null +++ b/plugins/aves_platform_meta/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 85684f9300908116a78138ea4c6036c35c9a1236 + channel: stable + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 + base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 + - platform: android + create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 + base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/plugins/aves_platform_meta/analysis_options.yaml b/plugins/aves_platform_meta/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/plugins/aves_platform_meta/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/plugins/aves_platform_meta/android/.gitignore b/plugins/aves_platform_meta/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/plugins/aves_platform_meta/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/plugins/aves_platform_meta/android/build.gradle b/plugins/aves_platform_meta/android/build.gradle new file mode 100644 index 000000000..435bd2270 --- /dev/null +++ b/plugins/aves_platform_meta/android/build.gradle @@ -0,0 +1,50 @@ +group 'deckers.thibault.aves.aves_platform_meta' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.2.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 16 + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/plugins/aves_platform_meta/android/gradle/wrapper/gradle-wrapper.jar b/plugins/aves_platform_meta/android/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 000000000..13372aef5 Binary files /dev/null and b/plugins/aves_platform_meta/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/plugins/aves_platform_meta/android/gradle/wrapper/gradle-wrapper.properties b/plugins/aves_platform_meta/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..080650cd5 --- /dev/null +++ b/plugins/aves_platform_meta/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Oct 22 10:54:33 KST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip diff --git a/plugins/aves_platform_meta/android/gradlew b/plugins/aves_platform_meta/android/gradlew new file mode 100755 index 000000000..9d82f7891 --- /dev/null +++ b/plugins/aves_platform_meta/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/plugins/aves_platform_meta/android/gradlew.bat b/plugins/aves_platform_meta/android/gradlew.bat new file mode 100755 index 000000000..aec99730b --- /dev/null +++ b/plugins/aves_platform_meta/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/plugins/aves_platform_meta/android/settings.gradle b/plugins/aves_platform_meta/android/settings.gradle new file mode 100644 index 000000000..d69766b76 --- /dev/null +++ b/plugins/aves_platform_meta/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'aves_platform_meta' diff --git a/plugins/aves_platform_meta/android/src/main/AndroidManifest.xml b/plugins/aves_platform_meta/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..36fa2e6a5 --- /dev/null +++ b/plugins/aves_platform_meta/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/plugins/aves_platform_meta/android/src/main/kotlin/deckers/thibault/aves/aves_platform_meta/AvesPlatformMetaPlugin.kt b/plugins/aves_platform_meta/android/src/main/kotlin/deckers/thibault/aves/aves_platform_meta/AvesPlatformMetaPlugin.kt new file mode 100644 index 000000000..55aca1e2b --- /dev/null +++ b/plugins/aves_platform_meta/android/src/main/kotlin/deckers/thibault/aves/aves_platform_meta/AvesPlatformMetaPlugin.kt @@ -0,0 +1,52 @@ +package deckers.thibault.aves.aves_platform_meta + +import android.content.Context +import android.content.pm.PackageManager +import androidx.annotation.NonNull +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler + +class AvesPlatformMetaPlugin : FlutterPlugin, MethodCallHandler { + private var context: Context? = null + private lateinit var channel: MethodChannel + + override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + context = binding.applicationContext + channel = MethodChannel(binding.binaryMessenger, "deckers.thibault/aves/aves_platform_meta") + channel.setMethodCallHandler(this) + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + context = null + channel.setMethodCallHandler(null) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { + if (call.method == "getMetadata") { + getMetadata(call, result) + } else { + result.notImplemented() + } + } + + + private fun getMetadata(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { + val key = call.argument("key") + if (key == null) { + result.error("getMetadata-args", "missing arguments", null) + return + } + + val ctx = context + if (ctx == null) { + result.error("getMetadata-context", "no context", null) + return + } + + val metadata = ctx.packageManager.getApplicationInfoCompat(ctx.packageName, PackageManager.GET_META_DATA).metaData + val value = metadata.getString(key) + result.success(value) + } +} diff --git a/plugins/aves_platform_meta/android/src/main/kotlin/deckers/thibault/aves/aves_platform_meta/Compat.kt b/plugins/aves_platform_meta/android/src/main/kotlin/deckers/thibault/aves/aves_platform_meta/Compat.kt new file mode 100644 index 000000000..499c0d1ce --- /dev/null +++ b/plugins/aves_platform_meta/android/src/main/kotlin/deckers/thibault/aves/aves_platform_meta/Compat.kt @@ -0,0 +1,14 @@ +package deckers.thibault.aves.aves_platform_meta + +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.os.Build + +fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(flags.toLong())) + } else { + @Suppress("deprecation") + getApplicationInfo(packageName, flags) + } +} diff --git a/plugins/aves_platform_meta/lib/aves_platform_meta.dart b/plugins/aves_platform_meta/lib/aves_platform_meta.dart new file mode 100644 index 000000000..e086a65d5 --- /dev/null +++ b/plugins/aves_platform_meta/lib/aves_platform_meta.dart @@ -0,0 +1,7 @@ +import 'aves_platform_meta_platform_interface.dart'; + +class AvesPlatformMeta { + Future getMetadata(String key) { + return AvesPlatformMetaPlatform.instance.getMetadata(key); + } +} diff --git a/plugins/aves_platform_meta/lib/aves_platform_meta_method_channel.dart b/plugins/aves_platform_meta/lib/aves_platform_meta_method_channel.dart new file mode 100644 index 000000000..6cabfa2e8 --- /dev/null +++ b/plugins/aves_platform_meta/lib/aves_platform_meta_method_channel.dart @@ -0,0 +1,16 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'aves_platform_meta_platform_interface.dart'; + +class MethodChannelAvesPlatformMeta extends AvesPlatformMetaPlatform { + @visibleForTesting + final methodChannel = const MethodChannel('deckers.thibault/aves/aves_platform_meta'); + + @override + Future getMetadata(String key) { + return methodChannel.invokeMethod('getMetadata', { + 'key': key, + }); + } +} diff --git a/plugins/aves_platform_meta/lib/aves_platform_meta_platform_interface.dart b/plugins/aves_platform_meta/lib/aves_platform_meta_platform_interface.dart new file mode 100644 index 000000000..89438ca2f --- /dev/null +++ b/plugins/aves_platform_meta/lib/aves_platform_meta_platform_interface.dart @@ -0,0 +1,22 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'aves_platform_meta_method_channel.dart'; + +abstract class AvesPlatformMetaPlatform extends PlatformInterface { + AvesPlatformMetaPlatform() : super(token: _token); + + static final Object _token = Object(); + + static AvesPlatformMetaPlatform _instance = MethodChannelAvesPlatformMeta(); + + static AvesPlatformMetaPlatform get instance => _instance; + + static set instance(AvesPlatformMetaPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getMetadata(String key) { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/plugins/aves_platform_meta/pubspec.yaml b/plugins/aves_platform_meta/pubspec.yaml new file mode 100644 index 000000000..a604afe86 --- /dev/null +++ b/plugins/aves_platform_meta/pubspec.yaml @@ -0,0 +1,21 @@ +name: aves_platform_meta +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=2.17.5 <3.0.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: + +dev_dependencies: + flutter_lints: + +flutter: + plugin: + platforms: + android: + package: deckers.thibault.aves.aves_platform_meta + pluginClass: AvesPlatformMetaPlugin diff --git a/plugins/aves_services_google/lib/aves_services_platform.dart b/plugins/aves_services_google/lib/aves_services_platform.dart index 4bc46e554..ff24f4e04 100644 --- a/plugins/aves_services_google/lib/aves_services_platform.dart +++ b/plugins/aves_services_google/lib/aves_services_platform.dart @@ -23,7 +23,7 @@ class PlatformMobileServices extends MobileServices { // cf https://github.com/flutter/flutter/issues/23728 // as of google_maps_flutter v2.1.5, Flutter v3.0.1 makes the map hide overlay widgets on API <=22 final androidInfo = await DeviceInfoPlugin().androidInfo; - _canRenderMaps = (androidInfo.version.sdkInt ?? 0) >= 23; + _canRenderMaps = (androidInfo.version.sdkInt ?? 0) >= 21; } @override diff --git a/plugins/aves_services_google/lib/src/map.dart b/plugins/aves_services_google/lib/src/map.dart index 173ee16d3..c371fe973 100644 --- a/plugins/aves_services_google/lib/src/map.dart +++ b/plugins/aves_services_google/lib/src/map.dart @@ -240,6 +240,7 @@ class _EntryGoogleMapState extends State> with WidgetsBindi // sometimes the map does not properly update after changing the widget size, // so we monitor the size and force refreshing after an arbitrary small delay + // TODO TLAD [map] this workaround no longer works with Flutter beta v3.3.0-0.0.pre Future _onSizeChange() async { await Future.delayed(const Duration(milliseconds: 100)); debugPrint('refresh map for size=${_sizeNotifier.value}'); diff --git a/plugins/aves_services_huawei/lib/aves_services_platform.dart b/plugins/aves_services_huawei/lib/aves_services_platform.dart index 6132349fd..528e29e1d 100644 --- a/plugins/aves_services_huawei/lib/aves_services_platform.dart +++ b/plugins/aves_services_huawei/lib/aves_services_platform.dart @@ -1,16 +1,20 @@ library aves_services_platform; import 'package:aves_map/aves_map.dart'; +import 'package:aves_platform_meta/aves_platform_meta_platform_interface.dart'; import 'package:aves_services/aves_services.dart'; import 'package:aves_services_platform/src/map.dart'; import 'package:flutter/widgets.dart'; import 'package:huawei_hmsavailability/huawei_hmsavailability.dart'; +import 'package:huawei_map/map.dart' as hmap; import 'package:latlong2/latlong.dart'; class PlatformMobileServices extends MobileServices { // cf https://developer.huawei.com/consumer/en/doc/development/hmscore-common-References/huaweiapiavailability-0000001050121134#section9492524178 static const int _hmsCoreAvailable = 0; + static const manifestApiKey = 'deckers.thibault.aves.huawei.API_KEY'; + bool _isAvailable = false; @override @@ -18,6 +22,10 @@ class PlatformMobileServices extends MobileServices { final result = await HmsApiAvailability().isHMSAvailable(); _isAvailable = result == _hmsCoreAvailable; debugPrint('Device has Huawei Mobile Services=$_isAvailable'); + + final apiKey = await AvesPlatformMetaPlatform.instance.getMetadata(manifestApiKey); + hmap.HuaweiMapInitializer.setApiKey(apiKey: apiKey ?? ''); + hmap.HuaweiMapInitializer.initializeMap(); } @override diff --git a/plugins/aves_services_huawei/pubspec.yaml b/plugins/aves_services_huawei/pubspec.yaml index 4d2348621..3fecb6458 100644 --- a/plugins/aves_services_huawei/pubspec.yaml +++ b/plugins/aves_services_huawei/pubspec.yaml @@ -10,14 +10,12 @@ dependencies: sdk: flutter aves_map: path: ../aves_map + aves_platform_meta: + path: ../aves_platform_meta aves_services: path: ../aves_services huawei_hmsavailability: huawei_map: - git: - url: https://github.com/deckerst/hms-flutter-plugin.git - path: flutter-hms-map - ref: aves latlong2: provider: diff --git a/pubspec.lock b/pubspec.lock index ae64ed458..4f17f9100 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,21 +7,21 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "40.0.0" + version: "42.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.3.0" archive: dependency: transitive description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.1.11" + version: "3.3.0" args: dependency: transitive description: @@ -35,7 +35,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" aves_map: dependency: "direct main" description: @@ -91,7 +91,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -119,7 +119,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: "direct main" description: @@ -133,7 +133,7 @@ packages: name: connectivity_plus url: "https://pub.dartlang.org" source: hosted - version: "2.3.3" + version: "2.3.6" connectivity_plus_linux: dependency: transitive description: @@ -147,7 +147,7 @@ packages: name: connectivity_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "1.2.3" + version: "1.2.4" connectivity_plus_platform_interface: dependency: transitive description: @@ -161,7 +161,7 @@ packages: name: connectivity_plus_web url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.3" connectivity_plus_windows: dependency: transitive description: @@ -189,14 +189,14 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.5.0" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2" custom_rounded_rectangle_border: dependency: transitive description: @@ -224,7 +224,7 @@ packages: name: device_info_plus url: "https://pub.dartlang.org" source: hosted - version: "3.2.4" + version: "4.0.0" device_info_plus_linux: dependency: transitive description: @@ -296,7 +296,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: @@ -326,35 +326,35 @@ packages: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.17.1" + version: "1.20.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "4.5.0" firebase_core_web: dependency: transitive description: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "1.6.4" + version: "1.7.1" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.6" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.2.7" + version: "3.2.12" flex_color_picker: dependency: "direct main" description: @@ -418,21 +418,21 @@ packages: name: flutter_map url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "2.1.0" flutter_markdown: dependency: "direct main" description: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.6.10+2" + version: "0.6.10+3" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.7" flutter_staggered_animations: dependency: "direct main" description: @@ -475,7 +475,7 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" google_api_availability: dependency: transitive description: @@ -489,14 +489,14 @@ packages: name: google_maps_flutter url: "https://pub.dartlang.org" source: hosted - version: "2.1.7" + version: "2.1.9" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.7" + version: "2.2.1" highlight: dependency: transitive description: @@ -517,7 +517,7 @@ packages: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.2.1" http_parser: dependency: transitive description: @@ -594,28 +594,28 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: "direct main" description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" material_design_icons_flutter: dependency: "direct main" description: name: material_design_icons_flutter url: "https://pub.dartlang.org" source: hosted - version: "5.0.6595" + version: "5.0.6996" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mgrs_dart: dependency: transitive description: @@ -671,7 +671,7 @@ packages: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" package_info_plus: dependency: "direct main" description: @@ -724,17 +724,19 @@ packages: panorama: dependency: "direct main" description: - name: panorama - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.0" + path: "." + ref: aves + resolved-ref: "0050dc1aee451f821961980ea8b9c1eb3d131c01" + url: "https://github.com/deckerst/aves_panorama.git" + source: git + version: "0.4.1" path: dependency: "direct main" description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_parsing: dependency: transitive description: @@ -783,7 +785,7 @@ packages: name: permission_handler url: "https://pub.dartlang.org" source: hosted - version: "9.2.0" + version: "10.0.0" permission_handler_android: dependency: "direct overridden" description: @@ -848,7 +850,7 @@ packages: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.5.1" positioned_tap_detector_2: dependency: transitive description: @@ -948,12 +950,12 @@ packages: source: hosted version: "2.0.15" shared_preferences_android: - dependency: "direct main" + dependency: transitive description: name: shared_preferences_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.10" + version: "2.0.12" shared_preferences_ios: dependency: transitive description: @@ -1002,28 +1004,28 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" shelf_static: dependency: transitive description: name: shelf_static url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" sky_engine: dependency: transitive description: flutter @@ -1049,14 +1051,14 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" sqflite: dependency: "direct main" description: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.2+1" + version: "2.0.3" sqflite_common: dependency: transitive description: @@ -1100,14 +1102,14 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" sync_http: dependency: transitive description: name: sync_http url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.1" synchronized: dependency: transitive description: @@ -1121,28 +1123,28 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test: dependency: "direct dev" description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.1" + version: "1.21.4" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.13" + version: "0.4.16" transparent_image: dependency: "direct main" description: @@ -1163,7 +1165,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" unicode: dependency: transitive description: @@ -1177,7 +1179,7 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.1.3" + version: "6.1.5" url_launcher_android: dependency: transitive description: @@ -1212,14 +1214,14 @@ packages: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.12" url_launcher_windows: dependency: transitive description: @@ -1240,7 +1242,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "8.2.2" + version: "9.0.0" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a75030aa9..66716895f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/deckerst/aves # - github changelog: /CHANGELOG.md # - play changelog: /whatsnew/whatsnew-en-US # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/1XXX.txt -version: 1.6.9+75 +version: 1.6.10+76 publish_to: none environment: @@ -62,8 +62,10 @@ dependencies: overlay_support: package_info_plus: palette_generator: -# TODO TLAD as of 2022/02/22, latest version (v0.4.1) has this issue: https://github.com/zesage/panorama/issues/25 - panorama: 0.4.0 + panorama: + git: + url: https://github.com/deckerst/aves_panorama.git + ref: aves path: pdf: percent_indicator: @@ -73,8 +75,6 @@ dependencies: provider: screen_brightness: shared_preferences: -# TODO TLAD as of 2022/02/22, latest version (v2.0.11) fails to load from analysis service (target wrong channel?) - shared_preferences_android: 2.0.10 sqflite: streams_channel: git: @@ -85,7 +85,7 @@ dependencies: xml: dependency_overrides: - # TODO TLAD as of 2022/06/11, latest version (v9.0.2+1) does not support Android 13 storage permissions + # TODO TLAD as of 2022/06/18, latest version (v10.0.0) does not support Android 13 storage permissions permission_handler_android: git: url: https://github.com/deckerst/flutter-permission-handler diff --git a/shaders_3.0.2.sksl.json b/shaders_3.0.2.sksl.json deleted file mode 100644 index a05d3fa00..000000000 --- a/shaders_3.0.2.sksl.json +++ /dev/null @@ -1 +0,0 @@ -{"platform":"android","name":"SM G970N","engineRevision":"f15f824b57476e369b5e656f53d4c431c5b04b9a","data":{"FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CAAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAADsAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CAAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAACRAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKZmxhdCBpbiBmbG9hdDQgdmdlb21TdWJzZXRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABnZW9tU3Vic2V0AAABAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAWAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQACAAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAADkAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5ID0gbWF4KHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHksIDAuMCk7CgloYWxmIHJpZ2h0QWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVpbm5lclJlY3RfUzEuUiAtIHNrX0ZyYWdDb29yZC54KSk7CgloYWxmIGJvdHRvbUFscGhhID0gaGFsZihzYXR1cmF0ZSh1aW5uZXJSZWN0X1MxLkIgLSBza19GcmFnQ29vcmQueSkpOwoJaGFsZiBhbHBoYSA9IGJvdHRvbUFscGhhICogcmlnaHRBbHBoYSAqIGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAZIA62YSBDACAAAGAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CAAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAAB3BQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBjb3ZlcmFnZTsKCWlmIChpbnQoMSkgPT0ga0ZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZihhbGwoZ3JlYXRlclRoYW4oZmxvYXQ0KHNrX0ZyYWdDb29yZC54eSwgdXJlY3RVbmlmb3JtX1MxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TMS54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJfQoJZWxzZSAKCXsKCQloYWxmNCBkaXN0czQgPSBjbGFtcChoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TMSksIDAuMCwgMS4wKTsKCQloYWxmMiBkaXN0czIgPSAoZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3KSAtIDEuMDsKCQljb3ZlcmFnZSA9IGRpc3RzMi54ICogZGlzdHMyLnk7Cgl9CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJY292ZXJhZ2UgPSAxLjAgLSBjb3ZlcmFnZTsKCX0KCXJldHVybiBoYWxmNChfaW5wdXQgKiBjb3ZlcmFnZSk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CAAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAkAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CAAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACzAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAAHQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAKAAAAaW5RdWFkRWRnZQAAAQAAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CAAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgAAAABdAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CAAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAgBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdCB2Y292ZXJhZ2VfUzA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABnZW9tU3Vic2V0AAABAAAAAAAAAA==","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAABZQA6AAAEAAAAAAAAADUAAAAAAAEAAAAAIDEAA":"CAAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACyAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBoYWxmMyB2aW5TaGFkb3dQYXJhbXNfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAOAAAAaW5TaGFkb3dQYXJhbXMAAAEAAAAAAAAA","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAIAAQAAAAAQGIA":"CAAAAExTS1PlAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc181X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAAAAMAcAAHVuaWZvcm0gaGFsZjQgdXN0YXJ0X1MxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVlbmRfUzFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzFfY29vcmRzID0gX2Nvb3JkczsKCXJldHVybiBoYWxmNChtaXgodXN0YXJ0X1MxX2MwX2MwLCB1ZW5kX1MxX2MwX2MwLCBoYWxmKF90bXBfMV9jb29yZHMueCkpKTsKfQpoYWxmNCBMaW5lYXJMYXlvdXRfUzFfYzBfYzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8yX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8zX2Nvb3JkcyA9IHZUcmFuc2Zvcm1lZENvb3Jkc181X1MwOwoJcmV0dXJuIGhhbGY0KGhhbGY0KGhhbGYoX3RtcF8zX2Nvb3Jkcy54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDYsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglpZiAoYm9vbChpbnQoMSkpKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShoYWxmNCBfaW5wdXQpIAp7CglfaW5wdXQgPSBDbGFtcGVkR3JhZGllbnRfUzFfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfNV9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1MwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSAoaGFsZjQoMS4wKSAtIG91dHB1dF9TMSkgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAcBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAyQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","JABAAAAABAAACAABBYAAAKAAAMAAGEAAAABRAEAAAEHCAAAAAAAABCAAAAAABAEQAEAAAAA":"CAAAAExTS1MbFgAAY29uc3QgZmxvYXQgUFJFQ0lTSU9OID0gNC4wMDAwMDA7CmNvbnN0IGZsb2F0IE1BWF9GSVhFRF9SRVNPTFZFX0xFVkVMID0gNS4wMDAwMDA7CmNvbnN0IGZsb2F0IE1BWF9GSVhFRF9TRUdNRU5UUyA9IDMyLjAwMDAwMDsKdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVhZmZpbmVNYXRyaXhfUzA7CnVuaWZvcm0gZmxvYXQyIHV0cmFuc2xhdGVfUzA7CmluIGZsb2F0MiByZXNvbHZlTGV2ZWxfYW5kX2lkeDsKaW4gZmxvYXQ0IHAwMTsKaW4gZmxvYXQ0IHAyMzsKaW4gZmxvYXQyIGZhblBvaW50QXR0cmliOwovLyBSZXR1cm5zIHRoZSBsZW5ndGggc3F1YXJlZCBvZiB0aGUgbGFyZ2VzdCBmb3J3YXJkIGRpZmZlcmVuY2UgZnJvbSBXYW5nJ3MgY3ViaWMgZm9ybXVsYS4KZmxvYXQgd2FuZ3NfZm9ybXVsYV9tYXhfZmRpZmZfcG93MihmbG9hdDIgcDAsIGZsb2F0MiBwMSwgZmxvYXQyIHAyLCBmbG9hdDIgcDMsICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZmxvYXQyeDIgbWF0cml4KSAKewoJZmxvYXQyIGQwID0gbWF0cml4ICogKGZtYShmbG9hdDIoLTIpLCBwMSwgcDIpICsgcDApOwoJZmxvYXQyIGQxID0gbWF0cml4ICogKGZtYShmbG9hdDIoLTIpLCBwMiwgcDMpICsgcDEpOwoJcmV0dXJuIG1heChkb3QoZDAsZDApLCBkb3QoZDEsZDEpKTsKfQpmbG9hdCB3YW5nc19mb3JtdWxhX2N1YmljKGZsb2F0IF9wcmVjaXNpb25fLCBmbG9hdDIgcDAsIGZsb2F0MiBwMSwgZmxvYXQyIHAyLCBmbG9hdDIgcDMsICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZmxvYXQyeDIgbWF0cml4KSAKewoJZmxvYXQgbSA9IHdhbmdzX2Zvcm11bGFfbWF4X2ZkaWZmX3BvdzIocDAsIHAxLCBwMiwgcDMsIG1hdHJpeCk7CglyZXR1cm4gbWF4KGNlaWwoc3FydCgwLjc1MDAwMCAqIF9wcmVjaXNpb25fICogc3FydChtKSkpLCAxLjApOwp9CmZsb2F0IHdhbmdzX2Zvcm11bGFfY3ViaWNfbG9nMihmbG9hdCBfcHJlY2lzaW9uXywgZmxvYXQyIHAwLCBmbG9hdDIgcDEsIGZsb2F0MiBwMiwgZmxvYXQyIHAzLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZmxvYXQyeDIgbWF0cml4KSAKewoJZmxvYXQgbSA9IHdhbmdzX2Zvcm11bGFfbWF4X2ZkaWZmX3BvdzIocDAsIHAxLCBwMiwgcDMsIG1hdHJpeCk7CglyZXR1cm4gY2VpbChsb2cyKG1heCgwLjU2MjUwMCAqIF9wcmVjaXNpb25fICogX3ByZWNpc2lvbl8gKiBtLCAxLjApKSAqIC4yNSk7Cn0KZmxvYXQgd2FuZ3NfZm9ybXVsYV9jb25pY19wb3cyKGZsb2F0IF9wcmVjaXNpb25fLCBmbG9hdDIgcDAsIGZsb2F0MiBwMSwgZmxvYXQyIHAyLCBmbG9hdCB3KSAKewoJLy8gVHJhbnNsYXRlIHRoZSBib3VuZGluZyBib3ggY2VudGVyIHRvIHRoZSBvcmlnaW4uCglmbG9hdDIgQyA9IChtaW4obWluKHAwLCBwMSksIHAyKSArIG1heChtYXgocDAsIHAxKSwgcDIpKSAqIDAuNTsKCXAwIC09IEM7CglwMSAtPSBDOwoJcDIgLT0gQzsKCS8vIENvbXB1dGUgbWF4IGxlbmd0aC4KCWZsb2F0IG0gPSBzcXJ0KG1heChtYXgoZG90KHAwLHAwKSwgZG90KHAxLHAxKSksIGRvdChwMixwMikpKTsKCS8vIENvbXB1dGUgZm9yd2FyZCBkaWZmZXJlbmNlcy4KCWZsb2F0MiBkcCA9IGZtYShmbG9hdDIoLTIuMCAqIHcpLCBwMSwgcDApICsgcDI7CglmbG9hdCBkdyA9IGFicyhmbWEoLTIuMCwgdywgMi4wKSk7CgkvLyBDb21wdXRlIG51bWVyYXRvciBhbmQgZGVub21pbmF0b3IgZm9yIHBhcmFtZXRyaWMgc3RlcCBzaXplIG9mIGxpbmVhcml6YXRpb24uIEhlcmUsIHRoZQoJLy8gZXBzaWxvbiByZWZlcmVuY2VkIGZyb20gdGhlIGNpdGVkIHBhcGVyIGlzIDEvcHJlY2lzaW9uLgoJZmxvYXQgcnBfbWludXNfMSA9IG1heCgwLjAsIGZtYShtLCBfcHJlY2lzaW9uXywgLTEuMCkpOwoJZmxvYXQgbnVtZXIgPSBsZW5ndGgoZHApICogX3ByZWNpc2lvbl8gKyBycF9taW51c18xICogZHc7CglmbG9hdCBkZW5vbSA9IDQgKiBtaW4odywgMS4wKTsKCXJldHVybiBudW1lci9kZW5vbTsKfQpmbG9hdCB3YW5nc19mb3JtdWxhX2NvbmljKGZsb2F0IF9wcmVjaXNpb25fLCBmbG9hdDIgcDAsIGZsb2F0MiBwMSwgZmxvYXQyIHAyLCBmbG9hdCB3KSAKewoJZmxvYXQgbjIgPSB3YW5nc19mb3JtdWxhX2NvbmljX3BvdzIoX3ByZWNpc2lvbl8sIHAwLCBwMSwgcDIsIHcpOwoJcmV0dXJuIG1heChjZWlsKHNxcnQobjIpKSwgMS4wKTsKfQpmbG9hdCB3YW5nc19mb3JtdWxhX2NvbmljX2xvZzIoZmxvYXQgX3ByZWNpc2lvbl8sIGZsb2F0MiBwMCwgZmxvYXQyIHAxLCBmbG9hdDIgcDIsIGZsb2F0IHcpIAp7CglmbG9hdCBuMiA9IHdhbmdzX2Zvcm11bGFfY29uaWNfcG93MihfcHJlY2lzaW9uXywgcDAsIHAxLCBwMiwgdyk7CglyZXR1cm4gY2VpbChsb2cyKG1heChuMiwgMS4wKSkgKiAuNSk7Cn0KYm9vbCBpc19jb25pY19jdXJ2ZSgpIAp7CglyZXR1cm4gaXNpbmYocDIzLncpOwp9CmJvb2wgaXNfdHJpYW5ndWxhcl9jb25pY19jdXJ2ZSgpIAp7CglyZXR1cm4gaXNpbmYocDIzLnopOwp9CmZsb2F0IGxkZXhwX3BvcnRhYmxlKGZsb2F0IHgsIGZsb2F0IHApIAp7CglyZXR1cm4gbGRleHAoeCwgaW50KHApKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciB0ZXNzZWxsYXRlX01pZGRsZU91dFNoYWRlcgoJZmxvYXQyeDIgQUZGSU5FX01BVFJJWCA9IGZsb2F0MngyKHVhZmZpbmVNYXRyaXhfUzApOwoJZmxvYXQyIFRSQU5TTEFURSA9IHV0cmFuc2xhdGVfUzA7CglmbG9hdCByZXNvbHZlTGV2ZWwgPSByZXNvbHZlTGV2ZWxfYW5kX2lkeC54OwoJZmxvYXQgaWR4SW5SZXNvbHZlTGV2ZWwgPSByZXNvbHZlTGV2ZWxfYW5kX2lkeC55OwoJZmxvYXQyIGxvY2FsY29vcmQ7CgkvLyBBIG5lZ2F0aXZlIHJlc29sdmUgbGV2ZWwgbWVhbnMgdGhpcyBpcyB0aGUgZmFuIHBvaW50LgoJaWYgKHJlc29sdmVMZXZlbCA8IDApIAoJewoJCWxvY2FsY29vcmQgPSBmYW5Qb2ludEF0dHJpYjsKCX0KCWVsc2UgICAgICAgICAgICBpZiAoaXNfdHJpYW5ndWxhcl9jb25pY19jdXJ2ZSgpKSAKCXsKCQkvLyBUaGlzIHBhdGNoIGlzIGFuIGV4YWN0IHRyaWFuZ2xlLgoJCWxvY2FsY29vcmQgPSAocmVzb2x2ZUxldmVsICE9IDApICAgICAgPyBwMDEuencgICAgICAgICAgICAgICAgICAgICAgICAgICA6IChpZHhJblJlc29sdmVMZXZlbCAhPSAwKSA/IHAyMy54eSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDogcDAxLnh5OwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgcDA9cDAxLnh5LCBwMT1wMDEuencsIHAyPXAyMy54eSwgcDM9cDIzLnp3OwoJCWZsb2F0IHcgPSAtMTsKCQkvLyB3IDwgMCB0ZWxscyB1cyB0byB0cmVhdCB0aGUgaW5zdGFuY2UgYXMgYW4gaW50ZWdyYWwgY3ViaWMuCgkJZmxvYXQgbWF4UmVzb2x2ZUxldmVsOwoJCWlmIChpc19jb25pY19jdXJ2ZSgpKSAKCQl7CgkJCS8vIENvbmljcyBhcmUgMyBwb2ludHMsIHdpdGggdGhlIHdlaWdodCBpbiBwMy4KCQkJdyA9IHAzLng7CgkJCW1heFJlc29sdmVMZXZlbCA9IHdhbmdzX2Zvcm11bGFfY29uaWNfbG9nMihQUkVDSVNJT04sIEFGRklORV9NQVRSSVggKiBwMCwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFGRklORV9NQVRSSVggKiBwMSwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFGRklORV9NQVRSSVggKiBwMiwgdyk7CgkJCXAxICo9IHc7CgkJCS8vIFVucHJvamVjdCBwMS4KCQkJcDMgPSBwMjsKCQkJLy8gRHVwbGljYXRlIHRoZSBlbmRwb2ludCBmb3Igc2hhcmVkIGNvZGUgdGhhdCBhbHNvIHJ1bnMgb24gY3ViaWNzLgoJCX0KCQllbHNlIAoJCXsKCQkJLy8gVGhlIHBhdGNoIGlzIGFuIGludGVncmFsIGN1YmljLgoJCQltYXhSZXNvbHZlTGV2ZWwgPSB3YW5nc19mb3JtdWxhX2N1YmljX2xvZzIoUFJFQ0lTSU9OLCBwMCwgcDEsIHAyLCBwMywgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBBRkZJTkVfTUFUUklYKTsKCQl9CgkJaWYgKHJlc29sdmVMZXZlbCA+IG1heFJlc29sdmVMZXZlbCkgCgkJewoJCQkvLyBUaGlzIHZlcnRleCBpcyBhdCBhIGhpZ2hlciByZXNvbHZlIGxldmVsIHRoYW4gd2UgbmVlZC4gRGVtb3RlIHRvIGEgbG93ZXIKCQkJLy8gcmVzb2x2ZUxldmVsLCB3aGljaCB3aWxsIHByb2R1Y2UgYSBkZWdlbmVyYXRlIHRyaWFuZ2xlLgoJCQlpZHhJblJlc29sdmVMZXZlbCA9IGZsb29yKGxkZXhwX3BvcnRhYmxlKGlkeEluUmVzb2x2ZUxldmVsLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhSZXNvbHZlTGV2ZWwgLSByZXNvbHZlTGV2ZWwpKTsKCQkJcmVzb2x2ZUxldmVsID0gbWF4UmVzb2x2ZUxldmVsOwoJCX0KCQkvLyBQcm9tb3RlIG91ciBsb2NhdGlvbiB0byBhIGRpc2NyZXRlIHBvc2l0aW9uIGluIHRoZSBtYXhpbXVtIGZpeGVkIHJlc29sdmUgbGV2ZWwuCgkJLy8gVGhpcyBpcyBleHRyYSBwYXJhbm9pYSB0byBlbnN1cmUgd2UgZ2V0IHRoZSBleGFjdCBzYW1lIGZwMzIgY29vcmRpbmF0ZXMgZm9yCgkJLy8gY29sb2NhdGVkIHBvaW50cyBmcm9tIGRpZmZlcmVudCByZXNvbHZlIGxldmVscyAoZS5nLiwgdGhlIHZlcnRpY2VzIFQ9My80IGFuZAoJCS8vIFQ9Ni84IHNob3VsZCBiZSBleGFjdGx5IGNvbG9jYXRlZCkuCgkJZmxvYXQgZml4ZWRWZXJ0ZXhJRCA9IGZsb29yKC41ICsgbGRleHBfcG9ydGFibGUoICAgICAgICAgICAgICAgICAgICAgICAgaWR4SW5SZXNvbHZlTGV2ZWwsIE1BWF9GSVhFRF9SRVNPTFZFX0xFVkVMIC0gcmVzb2x2ZUxldmVsKSk7CgkJaWYgKDAgPCBmaXhlZFZlcnRleElEICYmIGZpeGVkVmVydGV4SUQgPCBNQVhfRklYRURfU0VHTUVOVFMpIAoJCXsKCQkJZmxvYXQgVCA9IGZpeGVkVmVydGV4SUQgKiAoMSAvIE1BWF9GSVhFRF9TRUdNRU5UUyk7CgkJCS8vIEV2YWx1YXRlIGF0IFQuIFVzZSBEZSBDYXN0ZWxqYXUncyBmb3IgaXRzIGFjY3VyYWN5IGFuZCBzdGFiaWxpdHkuCgkJCWZsb2F0MiBhYiA9IG1peChwMCwgcDEsIFQpOwoJCQlmbG9hdDIgYmMgPSBtaXgocDEsIHAyLCBUKTsKCQkJZmxvYXQyIGNkID0gbWl4KHAyLCBwMywgVCk7CgkJCWZsb2F0MiBhYmMgPSBtaXgoYWIsIGJjLCBUKTsKCQkJZmxvYXQyIGJjZCA9IG1peChiYywgY2QsIFQpOwoJCQlmbG9hdDIgYWJjZCA9IG1peChhYmMsIGJjZCwgVCk7CgkJCS8vIEV2YWx1YXRlIHRoZSBjb25pYyB3ZWlnaHQgYXQgVC4KCQkJZmxvYXQgdSA9IG1peCgxLjAsIHcsIFQpOwoJCQlmbG9hdCB2ID0gdyArIDEgLSB1OwoJCQkvLyA9PSBtaXgodywgMSwgVCkKCQkJZmxvYXQgdXYgPSBtaXgodSwgdiwgVCk7CgkJCWxvY2FsY29vcmQgPSAodyA8IDApID8gLypjdWJpYyovIGFiY2QgOiAvKmNvbmljKi8gYWJjL3V2OwoJCX0KCQllbHNlIAoJCXsKCQkJbG9jYWxjb29yZCA9IChmaXhlZFZlcnRleElEID09IDApID8gcDAueHkgOiBwMy54eTsKCQl9Cgl9CglmbG9hdDIgdmVydGV4cG9zID0gQUZGSU5FX01BVFJJWCAqIGxvY2FsY29vcmQgKyBUUkFOU0xBVEU7Cglza19Qb3NpdGlvbiA9IHZlcnRleHBvcy54eTAxOwp9CgAAAAAA4QAAAHVuaWZvcm0gaGFsZjQgdWNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgdGVzc2VsbGF0ZV9NaWRkbGVPdXRTaGFkZXIKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gdWNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogRGlzYWJsZSBDb2xvcgoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAQAAAAUAAAAcmVzb2x2ZUxldmVsX2FuZF9pZHgDAAAAcDAxAAMAAABwMjMADgAAAGZhblBvaW50QXR0cmliAAABAAAAAAAAAA==","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIBSQB5VRECGAEAAAMAAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAA4wQAAGNvbnN0IGludCBrRmlsbEJXX1MxID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIGNvdmVyYWdlKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABGAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAKQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAGUDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CAAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAZQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACtBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZAAAAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CAAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAAA/BAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkgPSBtYXgodWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eSwgMC4wKTsKCWhhbGYgcmlnaHRBbHBoYSA9IGhhbGYoc2F0dXJhdGUodWlubmVyUmVjdF9TMS5SIC0gc2tfRnJhZ0Nvb3JkLngpKTsKCWhhbGYgYm90dG9tQWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVpbm5lclJlY3RfUzEuQiAtIHNrX0ZyYWdDb29yZC55KSk7CgloYWxmIGFscGhhID0gYm90dG9tQWxwaGEgKiByaWdodEFscGhhICogaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAD4AQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACRAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","CIAAAAAAQAARQAAYQAAAAGFYQAABRAAAAEEAAAAAAARAEAEABYAAAAEAAAAAAAEEBQAAAAA":"CAAAAExTS1NVAwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVhdGxhc19hZGp1c3RfUzA7CmluIGZsb2F0NCBmaWxsQm91bmRzOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQ0IGxvY2F0aW9uczsKb3V0IGZsb2F0MiB2YXRsYXNDb29yZF9TMDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEcmF3QXRsYXNQYXRoU2hhZGVyCglmbG9hdDIgdW5pdENvb3JkID0gZmxvYXQyKHNrX1ZlcnRleElEICYgMSwgc2tfVmVydGV4SUQgPj4gMSk7CglmbG9hdDIgZGV2Q29vcmQgPSBtaXgoZmlsbEJvdW5kcy54eSwgZmlsbEJvdW5kcy56dywgdW5pdENvb3JkKTsKCS8vIEEgbmVnYXRpdmUgeCBjb29yZGluYXRlIGluIHRoZSBhdGxhcyBpbmRpY2F0ZXMgdGhhdCB0aGUgcGF0aCBpcyB0cmFuc3Bvc2VkLgoJLy8gV2UgYWxzbyBhZGRlZCAxIHNpbmNlIHdlIGNhbid0IG5lZ2F0ZSB6ZXJvLgoJZmxvYXQyIGF0bGFzVG9wTGVmdCA9IGZsb2F0MihhYnMobG9jYXRpb25zLngpIC0gMSwgbG9jYXRpb25zLnkpOwoJZmxvYXQyIGRldlRvcExlZnQgPSBsb2NhdGlvbnMuenc7Cglib29sIHRyYW5zcG9zZWQgPSBsb2NhdGlvbnMueCA8IDA7CglmbG9hdDIgYXRsYXNDb29yZCA9IGRldkNvb3JkIC0gZGV2VG9wTGVmdDsKCWlmICh0cmFuc3Bvc2VkKSAKCXsKCQlhdGxhc0Nvb3JkID0gYXRsYXNDb29yZC55eDsKCX0KCWF0bGFzQ29vcmQgKz0gYXRsYXNUb3BMZWZ0OwoJdmF0bGFzQ29vcmRfUzAgPSBhdGxhc0Nvb3JkICogdWF0bGFzX2FkanVzdF9TMDsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBkZXZDb29yZC54eTAxOwp9CgAAAAAAAADKAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2YXRsYXNDb29yZF9TMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEcmF3QXRsYXNQYXRoU2hhZGVyCgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZiBhdGxhc0NvdmVyYWdlID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2YXRsYXNDb29yZF9TMCkuMDAwci5hOwoJb3V0cHV0Q292ZXJhZ2VfUzAgKj0gYXRsYXNDb3ZlcmFnZTsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAZmlsbEJvdW5kcwAABQAAAGNvbG9yAAAACQAAAGxvY2F0aW9ucwAAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADhAwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CAAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgxKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADOAgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADEGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbNF07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbNF07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueCwgdWNsYW1wX1MxX2MwX2MwX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzVfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF82X2kgPSAwOyAoXzZfaSA8IDEzKTsgXzZfaSsrKSAoXzNfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChfNV9jb29yZCArIGZsb2F0MigodV8yX09mZnNldHNfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0gKiB1XzBfSW5jcmVtZW50X1MxX2MwKSkpKSAqIHVfMV9LZXJuZWxfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0pKTsKCXJldHVybiBfM19jb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAYEIBAAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAATQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJCXNrX0ZyYWdDb2xvciA9IHNrX0ZyYWdDb2xvci5hMDAwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAKPABAAAAAAB2AAAAAAACAAAAAEBSAAAAAAA":"CAAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAACzBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBFbGxpcHRpY2FsUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CglmbG9hdDIgWiA9IGR4eSAqIHVpbnZSYWRpaVhZX1MxLnh5OwoJaGFsZiBpbXBsaWNpdCA9IGhhbGYoZG90KFosIGR4eSkgLSAxLjApOwoJaGFsZiBncmFkX2RvdCA9IGhhbGYoNC4wICogZG90KFosIFopKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjBlLTQpOwoJaGFsZiBhcHByb3hfZGlzdCA9IGltcGxpY2l0ICogaGFsZihpbnZlcnNlc3FydChncmFkX2RvdCkpOwoJaGFsZiBhbHBoYSA9IGNsYW1wKDAuNSArIGFwcHJveF9kaXN0LCAwLjAsIDEuMCk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEVsbGlwdGljYWxSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAQAAABza2V3GQAAAHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUAAAAFAAAAY29sb3IAAAABAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAARAGQWMHGBRIAAAAABQAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABhBAAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjbGVfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxLncpIC0gMS4wKSAqIHVjaXJjbGVfUzEueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJaWYgKGludCgxKSA9PSBrRmlsbEFBX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIHNhdHVyYXRlKGQpKTsKCX0KCWVsc2UgCgl7CgkJcmV0dXJuIGhhbGY0KGQgPiAwLjUgPyBfaW5wdXQgOiBoYWxmNCgwLjApKTsKCX0KfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJZmxvYXQyIHRleENvb3JkOwoJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TMDsKCW91dHB1dENvbG9yX1MwID0gKChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHRleENvb3JkKSAqIGhhbGY0KDEpKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY2xlX1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAFBQATAAAAAAFAAMAAAABAAAAAAABBAMAAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAABYBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgRWxsaXB0aWNhbFJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJZmxvYXQyIFogPSBkeHkgKiB1aW52UmFkaWlYWV9TMS54eTsKCWhhbGYgaW1wbGljaXQgPSBoYWxmKGRvdChaLCBkeHkpIC0gMS4wKTsKCWhhbGYgZ3JhZF9kb3QgPSBoYWxmKDQuMCAqIGRvdChaLCBaKSk7CglncmFkX2RvdCA9IG1heChncmFkX2RvdCwgMS4wZS00KTsKCWhhbGYgYXBwcm94X2Rpc3QgPSBpbXBsaWNpdCAqIGhhbGYoaW52ZXJzZXNxcnQoZ3JhZF9kb3QpKTsKCWhhbGYgYWxwaGEgPSBjbGFtcCgwLjUgKyBhcHByb3hfZGlzdCwgMC4wLCAxLjApOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRWxsaXB0aWNhbFJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAB3QA6AAAEAAAAAAAMAAPEAEAAABAAAAAAB2AAAAAAACAAAAAEBSAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAA2BQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzI7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfUzIoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MyLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MyLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzIueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCWhhbGY0IG91dHB1dF9TMjsKCW91dHB1dF9TMiA9IENpcmN1bGFyUlJlY3RfUzIob3V0cHV0X1MxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMjsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CAAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAjAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAuAIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAABAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAIAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CAAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgwKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACnAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAACTAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CAAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADoAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYTRVAADQAAAOAEARAFQJAABBADAAAILBYAACCYUQD777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAD1AwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAFAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2ULAAAAaW5DbGlwUGxhbmUADAAAAGluSXNlY3RQbGFuZQEAAAAAAAAA","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAACEA2X4PLOGEAAAAAAAAACAAAAAVQQAAQAAAAAQCDIBCAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAAAAAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAACwQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMS54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzEudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzEueikgKiB1Y2lyY2xlRGF0YV9TMS53OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIE1hdHJpeEVmZmVjdF9TMV9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZUJsdXJfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADEGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbNF07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbNF07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAueSwgdWNsYW1wX1MxX2MwX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzVfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF82X2kgPSAwOyAoXzZfaSA8IDEzKTsgXzZfaSsrKSAoXzNfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChfNV9jb29yZCArIGZsb2F0MigodV8yX09mZnNldHNfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0gKiB1XzBfSW5jcmVtZW50X1MxX2MwKSkpKSAqIHVfMV9LZXJuZWxfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0pKTsKCXJldHVybiBfM19jb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAPIEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdV8wX0luY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1XzFfS2VybmVsX1MxX2MwWzRdOwp1bmlmb3JtIGhhbGY0IHVfMl9PZmZzZXRzX1MxX2MwWzRdOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIF9jb29yZHMpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF8zX2NvbG9yID0gaGFsZjQoMC4wKTsKCWZsb2F0MiBfNV9jb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgXzZfaSA9IDA7IChfNl9pIDwgMTMpOyBfNl9pKyspIChfM19jb2xvciArPSAoTWF0cml4RWZmZWN0X1MxX2MwX2MwKF9pbnB1dCwgKF81X2Nvb3JkICsgZmxvYXQyKCh1XzJfT2Zmc2V0c19TMV9jMFsoXzZfaSAvIDQpXVsoXzZfaSAmIDMpXSAqIHVfMF9JbmNyZW1lbnRfUzFfYzApKSkpICogdV8xX0tlcm5lbF9TMV9jMFsoXzZfaSAvIDQpXVsoXzZfaSAmIDMpXSkpOwoJcmV0dXJuIF8zX2NvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAEQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAZCBRE4GNEACAAAOAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CAAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAAAHBQAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY2xlX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBkOwoJaWYgKGludCgzKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWQgPSBoYWxmKChsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSAtIDEuMCkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJZWxzZSAKCXsKCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1MxLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzEudykpICogdWNpcmNsZV9TMS56KTsKCX0KCWlmIChpbnQoMykgPT0ga0ZpbGxBQV9TMSB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCXJldHVybiBoYWxmNChfaW5wdXQgKiBzYXR1cmF0ZShkKSk7Cgl9CgllbHNlIAoJewoJCXJldHVybiBoYWxmNChkID4gMC41ID8gX2lucHV0IDogaGFsZjQoMC4wKSk7Cgl9Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZV9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAQAAABza2V3GQAAAHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUAAAAFAAAAY29sb3IAAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAAAQAAAAGQCBAMQACAAAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAIgDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzAueCwgdWNsYW1wX1MxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQAAEAQAAAAGQCBAMQAAAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAIgDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzAueSwgdWNsYW1wX1MxX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CAAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAHgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IAAQAAAAAAAAA="}} \ No newline at end of file diff --git a/shaders_3.3.0-0.0.pre.sksl.json b/shaders_3.3.0-0.0.pre.sksl.json new file mode 100644 index 000000000..b23df92f3 --- /dev/null +++ b/shaders_3.3.0-0.0.pre.sksl.json @@ -0,0 +1 @@ +{"platform":"android","name":"SM G970N","engineRevision":"1388adb442192ce155630eeb6806b74db07dd15e","data":{"FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADRAAYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAABJBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0IGR5MCA9IHVpbm5lclJlY3RfUzEuVCAtIHNrX0ZyYWdDb29yZC55OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChmbG9hdDIoZHh5MS54LCBtYXgoZHkwLCBkeHkxLnkpKSwgMC4wKTsKCWhhbGYgbGVmdEFscGhhID0gaGFsZihzYXR1cmF0ZShza19GcmFnQ29vcmQueCAtIHVpbm5lclJlY3RfUzEuTCkpOwoJaGFsZiBhbHBoYSA9IGxlZnRBbHBoYSAqIGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIBSQB5VRECGAEAAAMAAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CQAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAA4wQAAGNvbnN0IGludCBrRmlsbEJXX1MxID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIGNvdmVyYWdlKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CQAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAHgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAEAAAAAAAAA","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CQAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADoAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAyQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQKAAAAAAABAEAAAABJUQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAEcGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzExXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TMV9jMF9jMF9jMC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDExOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQAAEAQAAAAGQCBAMQAAAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAIgDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzAueSwgdWNsYW1wX1MxX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAD4AQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQAMAAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAADuAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdCBkeTAgPSB1aW5uZXJSZWN0X1MxLlQgLSBza19GcmFnQ29vcmQueTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgoZmxvYXQyKGR4eTEueCwgbWF4KGR5MCwgZHh5MS55KSksIDAuMCk7CgloYWxmIGxlZnRBbHBoYSA9IGhhbGYoc2F0dXJhdGUoc2tfRnJhZ0Nvb3JkLnggLSB1aW5uZXJSZWN0X1MxLkwpKTsKCWhhbGYgYWxwaGEgPSBsZWZ0QWxwaGEgKiBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACnAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAIAAQAAAAAQGIA":"CQAAAExTS1PlAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc181X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAAAAMAcAAHVuaWZvcm0gaGFsZjQgdXN0YXJ0X1MxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVlbmRfUzFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzFfY29vcmRzID0gX2Nvb3JkczsKCXJldHVybiBoYWxmNChtaXgodXN0YXJ0X1MxX2MwX2MwLCB1ZW5kX1MxX2MwX2MwLCBoYWxmKF90bXBfMV9jb29yZHMueCkpKTsKfQpoYWxmNCBMaW5lYXJMYXlvdXRfUzFfYzBfYzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8yX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8zX2Nvb3JkcyA9IHZUcmFuc2Zvcm1lZENvb3Jkc181X1MwOwoJcmV0dXJuIGhhbGY0KGhhbGY0KGhhbGYoX3RtcF8zX2Nvb3Jkcy54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDYsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglpZiAoYm9vbChpbnQoMSkpKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShoYWxmNCBfaW5wdXQpIAp7CglfaW5wdXQgPSBDbGFtcGVkR3JhZGllbnRfUzFfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfNV9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1MwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSAoaGFsZjQoMS4wKSAtIG91dHB1dF9TMSkgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CMRQCIAABBYAAAEIXBAAACDQMAABRAFAAAAAAAAAAAAAAAEABYAAAAEAAAAAAAEEBQAAAAA":"CQAAAExTS1MyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0MiBpbkVsbGlwc2VPZmZzZXQ7CmluIGZsb2F0NCBpbkVsbGlwc2VSYWRpaTsKb3V0IGZsb2F0MiB2RWxsaXBzZU9mZnNldHNfUzA7Cm91dCBmbG9hdDQgdkVsbGlwc2VSYWRpaV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBFbGxpcHNlR2VvbWV0cnlQcm9jZXNzb3IKCXZFbGxpcHNlT2Zmc2V0c19TMCA9IGluRWxsaXBzZU9mZnNldDsKCXZFbGxpcHNlUmFkaWlfUzAgPSBpbkVsbGlwc2VSYWRpaTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAIoDAABpbiBmbG9hdDIgdkVsbGlwc2VPZmZzZXRzX1MwOwppbiBmbG9hdDQgdkVsbGlwc2VSYWRpaV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBFbGxpcHNlR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0MiBvZmZzZXQgPSB2RWxsaXBzZU9mZnNldHNfUzAueHk7CglvZmZzZXQgKj0gdkVsbGlwc2VSYWRpaV9TMC54eTsKCWZsb2F0IHRlc3QgPSBkb3Qob2Zmc2V0LCBvZmZzZXQpIC0gMS4wOwoJZmxvYXQyIGdyYWQgPSAyLjAqb2Zmc2V0KnZFbGxpcHNlUmFkaWlfUzAueHk7CglmbG9hdCBncmFkX2RvdCA9IGRvdChncmFkLCBncmFkKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjE3NTVlLTM4KTsKCWZsb2F0IGludmxlbiA9IGludmVyc2VzcXJ0KGdyYWRfZG90KTsKCWZsb2F0IGVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNS10ZXN0Kmludmxlbik7CglvZmZzZXQgPSB2RWxsaXBzZU9mZnNldHNfUzAueHkqdkVsbGlwc2VSYWRpaV9TMC56dzsKCXRlc3QgPSBkb3Qob2Zmc2V0LCBvZmZzZXQpIC0gMS4wOwoJZ3JhZCA9IDIuMCpvZmZzZXQqdkVsbGlwc2VSYWRpaV9TMC56dzsKCWdyYWRfZG90ID0gZG90KGdyYWQsIGdyYWQpOwoJaW52bGVuID0gaW52ZXJzZXNxcnQoZ3JhZF9kb3QpOwoJZWRnZUFscGhhICo9IHNhdHVyYXRlKDAuNSt0ZXN0Kmludmxlbik7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGhhbGYoZWRnZUFscGhhKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpbkVsbGlwc2VPZmZzZXQADgAAAGluRWxsaXBzZVJhZGlpAAABAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAFBQATAAAAAAFAAMAAAABAAAAAAABBAMAAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAABYBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgRWxsaXB0aWNhbFJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJZmxvYXQyIFogPSBkeHkgKiB1aW52UmFkaWlYWV9TMS54eTsKCWhhbGYgaW1wbGljaXQgPSBoYWxmKGRvdChaLCBkeHkpIC0gMS4wKTsKCWhhbGYgZ3JhZF9kb3QgPSBoYWxmKDQuMCAqIGRvdChaLCBaKSk7CglncmFkX2RvdCA9IG1heChncmFkX2RvdCwgMS4wZS00KTsKCWhhbGYgYXBwcm94X2Rpc3QgPSBpbXBsaWNpdCAqIGhhbGYoaW52ZXJzZXNxcnQoZ3JhZF9kb3QpKTsKCWhhbGYgYWxwaGEgPSBjbGFtcCgwLjUgKyBhcHByb3hfZGlzdCwgMC4wLCAxLjApOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRWxsaXB0aWNhbFJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACRAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAGIBIAAABAAAAANAEAAAAAAAAAAAAAABAAOAAAABAAAAAAABBAMAAAAA":"CQAAAExTS1N0AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIsCAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwKS5ycnJyOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAEcGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TMV9jMF9jMF9jMC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueik7CgljbGFtcGVkQ29vcmQueSA9IHN1YnNldENvb3JkLnk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CQAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAkAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAACEA2X4PLOGEAAAAAAAAACAAAAAVQQAAQAAAAAQCDIBCAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAAAAAAA":"CQAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAACwQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMS54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzEudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzEueikgKiB1Y2lyY2xlRGF0YV9TMS53OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIE1hdHJpeEVmZmVjdF9TMV9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZUJsdXJfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAZCBRE4GNEACAAAOAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAAAHBQAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY2xlX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBkOwoJaWYgKGludCgzKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWQgPSBoYWxmKChsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSAtIDEuMCkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJZWxzZSAKCXsKCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1MxLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzEudykpICogdWNpcmNsZV9TMS56KTsKCX0KCWlmIChpbnQoMykgPT0ga0ZpbGxBQV9TMSB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCXJldHVybiBoYWxmNChfaW5wdXQgKiBzYXR1cmF0ZShkKSk7Cgl9CgllbHNlIAoJewoJCXJldHVybiBoYWxmNChkID4gMC41ID8gX2lucHV0IDogaGFsZjQoMC4wKSk7Cgl9Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZV9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CQAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAjAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAOAAAAaW5TaGFkb3dQYXJhbXMAAAEAAAAAAAAA","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMADSAB4QAAAAAEAAAAAAHIAAAAAAAIAAAAAQGIAAAAAAA":"CQAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAAB3AwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAZIA62YSBDACAAAGAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAAB3BQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBjb3ZlcmFnZTsKCWlmIChpbnQoMSkgPT0ga0ZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZihhbGwoZ3JlYXRlclRoYW4oZmxvYXQ0KHNrX0ZyYWdDb29yZC54eSwgdXJlY3RVbmlmb3JtX1MxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TMS54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJfQoJZWxzZSAKCXsKCQloYWxmNCBkaXN0czQgPSBjbGFtcChoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TMSksIDAuMCwgMS4wKTsKCQloYWxmMiBkaXN0czIgPSAoZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3KSAtIDEuMDsKCQljb3ZlcmFnZSA9IGRpc3RzMi54ICogZGlzdHMyLnk7Cgl9CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJY292ZXJhZ2UgPSAxLjAgLSBjb3ZlcmFnZTsKCX0KCXJldHVybiBoYWxmNChfaW5wdXQgKiBjb3ZlcmFnZSk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADOAgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgAAAABdAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAEAAAAAAAAA","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CQAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACzAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","EABQAAAAAEAAAAAQAABQAAIOAAABCFYIAAKAUDAAAAAAAAABAAAAAAAAAAANAAIAAAABAAAAACAJAAIAAAAA":"CQAAAExTS1OhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJdkludFRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8yX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfUzA7CgloYWxmNCB0ZXhDb2xvcjsKCXsKCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLnJycnI7Cgl9CgloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJaGFsZiBhZndpZHRoOwoJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeCh2SW50VGV4dHVyZUNvb3Jkc19TMC54KSkpOwoJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KHZhbCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQKAAAAAAIAAEAAAABJUQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAEcGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzExXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TMV9jMF9jMF9jMC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueik7CgljbGFtcGVkQ29vcmQueSA9IHN1YnNldENvb3JkLnk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDExOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAcBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CQAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAgBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdCB2Y292ZXJhZ2VfUzA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAEcGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TMV9jMF9jMF9jMC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAKPABAAAAAAB2AAAAAAACAAAAAEBSAAAAAAA":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAACzBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBFbGxpcHRpY2FsUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CglmbG9hdDIgWiA9IGR4eSAqIHVpbnZSYWRpaVhZX1MxLnh5OwoJaGFsZiBpbXBsaWNpdCA9IGhhbGYoZG90KFosIGR4eSkgLSAxLjApOwoJaGFsZiBncmFkX2RvdCA9IGhhbGYoNC4wICogZG90KFosIFopKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjBlLTQpOwoJaGFsZiBhcHByb3hfZGlzdCA9IGltcGxpY2l0ICogaGFsZihpbnZlcnNlc3FydChncmFkX2RvdCkpOwoJaGFsZiBhbHBoYSA9IGNsYW1wKDAuNSArIGFwcHJveF9kaXN0LCAwLjAsIDEuMCk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEVsbGlwdGljYWxSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBQSSNO73OAAAAABQAAAAAYAAAAAMAAAAAGAZYKT6MIAAAAEAQCAAAAABAEM5ROPCEYDAAAAABAEAQAAAAFMJBAEAAAAAAAA2AMEBQAAIBAAAAAAB2AAAAAAACAAAAAEBSAAAAAAA":"CQAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNF9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzApICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIoGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNHg0IHVtX1MxOwp1bmlmb3JtIGhhbGY0IHV2X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc180X1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMF9jMF9jMC54eSwgdWNsYW1wX1MxX2MwX2MwX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglfaW5wdXQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCk7Cn0KaGFsZjQgdW5wcmVtdWxfUzEoaGFsZjQgY29sb3IpOwpoYWxmNCB1bnByZW11bF9TMShoYWxmNCBjb2xvcikgCnsKCXJldHVybiBoYWxmNChjb2xvci54eXogLyBtYXgoY29sb3IudywgOS45OTk5OTk3NDczNzg3NTE2ZS0wNSksIGNvbG9yLncpOwp9CmhhbGY0IENvbG9yTWF0cml4X1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzFfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfMV9pbkNvbG9yID0gX2lucHV0OwoJaWYgKGJvb2woaW50KDEpKSkgCgl7CgkJX2lucHV0ID0gdW5wcmVtdWwoX2lucHV0KTsKCX0KCV9pbnB1dCA9IHVtX1MxICogX2lucHV0ICsgdXZfUzE7CglpZiAoYm9vbChpbnQoMSkpKSAKCXsKCQlfaW5wdXQgPSBzYXR1cmF0ZShfaW5wdXQpOwoJfQoJZWxzZSAKCXsKCQlfaW5wdXQudyA9IHNhdHVyYXRlKF9pbnB1dC53KTsKCX0KCWlmIChib29sKGludCgxKSkpIAoJewoJCV9pbnB1dC54eXogKj0gX2lucHV0Lnc7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENvbG9yTWF0cml4X1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADhAwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CQAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAACRAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKZmxhdCBpbiBmbG9hdDQgdmdlb21TdWJzZXRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAQAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAEQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADBAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CQAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAAAOAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdCBkeDAgPSB1aW5uZXJSZWN0X1MxLkwgLSBza19GcmFnQ29vcmQueDsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgoZmxvYXQyKG1heChkeDAsIGR4eTEueCksIGR4eTEueSksIDAuMCk7CgloYWxmIHRvcEFscGhhID0gaGFsZihzYXR1cmF0ZShza19GcmFnQ29vcmQueSAtIHVpbm5lclJlY3RfUzEuVCkpOwoJaGFsZiBhbHBoYSA9IHRvcEFscGhhICogaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQACAAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAADkAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5ID0gbWF4KHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHksIDAuMCk7CgloYWxmIHJpZ2h0QWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVpbm5lclJlY3RfUzEuUiAtIHNrX0ZyYWdDb29yZC54KSk7CgloYWxmIGJvdHRvbUFscGhhID0gaGFsZihzYXR1cmF0ZSh1aW5uZXJSZWN0X1MxLkIgLSBza19GcmFnQ29vcmQueSkpOwoJaGFsZiBhbHBoYSA9IGJvdHRvbUFscGhhICogcmlnaHRBbHBoYSAqIGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAAgFAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmMiB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFsxM107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgX2Nvb3Jkcyk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChfaW5wdXQsIGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzApICogX2Nvb3Jkcy54eTEpOwp9CmhhbGY0IFNtb290aF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBjb29yZCwgaGFsZjIgb2Zmc2V0QW5kS2VybmVsKSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChjb29yZCArIG9mZnNldEFuZEtlcm5lbC54ICogdUluY3JlbWVudF9TMV9jMCkpICogb2Zmc2V0QW5kS2VybmVsLnk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBjb2xvciA9IGhhbGY0KDApOwoJZmxvYXQyIGNvb3JkID0gdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cglmb3IgKGludCBpPTA7IGk8MTM7ICsraSkgCgl7CgkJY29sb3IgKz0gU21vb3RoX1MxX2MwKF9pbnB1dCwgY29vcmQsIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwW2ldKTsKCX0KCXJldHVybiBjb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAABAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAIAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CQAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgwKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CQAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAZQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAB3QA6AAAEAAAAAAAMAAPEAEAAABAAAAAAB2AAAAAAACAAAAAEBSAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAA2BQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzI7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfUzIoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MyLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MyLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzIueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCWhhbGY0IG91dHB1dF9TMjsKCW91dHB1dF9TMiA9IENpcmN1bGFyUlJlY3RfUzIob3V0cHV0X1MxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMjsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAARAGQWMHGBRIAAAAABQAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABhBAAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjbGVfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxLncpIC0gMS4wKSAqIHVjaXJjbGVfUzEueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJaWYgKGludCgxKSA9PSBrRmlsbEFBX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIHNhdHVyYXRlKGQpKTsKCX0KCWVsc2UgCgl7CgkJcmV0dXJuIGhhbGY0KGQgPiAwLjUgPyBfaW5wdXQgOiBoYWxmNCgwLjApKTsKCX0KfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJZmxvYXQyIHRleENvb3JkOwoJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TMDsKCW91dHB1dENvbG9yX1MwID0gKChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHRleENvb3JkKSAqIGhhbGY0KDEpKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY2xlX1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AYTRVAADQAAAOAEARAFQJAABBADAAAILBYAACCYUQD777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAD1AwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlCwAAAGluQ2xpcFBsYW5lAAwAAABpbklzZWN0UGxhbmUBAAAAAAAAAA==","B2ABSAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CQAAAExTS1N4AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gdUNvbG9yX1MwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8zX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzFfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAeAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAWAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAADsAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAEAAAAAAAAA","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAAHQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAGUDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAACTAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABGAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CQAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAKQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACtBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA=="}} \ No newline at end of file diff --git a/test/fake/media_file_service.dart b/test/fake/media_file_service.dart deleted file mode 100644 index 6ae81b5af..000000000 --- a/test/fake/media_file_service.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:aves/model/entry.dart'; -import 'package:aves/services/common/image_op_events.dart'; -import 'package:aves/services/media/media_file_service.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'media_store_service.dart'; - -class FakeMediaFileService extends Fake implements MediaFileService { - @override - Stream rename({ - String? opId, - required Map entriesToNewName, - }) { - final contentId = FakeMediaStoreService.nextId; - final kv = entriesToNewName.entries.first; - final entry = kv.key; - final newName = kv.value; - return Stream.value(MoveOpEvent( - success: true, - skipped: false, - uri: entry.uri, - newFields: { - 'uri': 'content://media/external/images/media/$contentId', - 'contentId': contentId, - 'path': '${entry.directory}/$newName', - 'dateModifiedSecs': FakeMediaStoreService.dateSecs, - }, - deleted: false, - )); - } -} diff --git a/test/fake/window_service.dart b/test/fake/window_service.dart index aff923d3e..a87e5c2dc 100644 --- a/test/fake/window_service.dart +++ b/test/fake/window_service.dart @@ -4,6 +4,9 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; class FakeWindowService extends Fake implements WindowService { + @override + Future isActivity() => SynchronousFuture(true); + @override Future keepScreenOn(bool on) => SynchronousFuture(null); diff --git a/test/model/collection_source_test.dart b/test/model/collection_source_test.dart index eebf5c197..029244b25 100644 --- a/test/model/collection_source_test.dart +++ b/test/model/collection_source_test.dart @@ -15,7 +15,6 @@ import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/device_service.dart'; -import 'package:aves/services/media/media_file_service.dart'; import 'package:aves/services/media/media_store_service.dart'; import 'package:aves/services/metadata/metadata_fetch_service.dart'; import 'package:aves/services/storage_service.dart'; @@ -30,7 +29,6 @@ import 'package:path/path.dart' as p; import '../fake/android_app_service.dart'; import '../fake/availability.dart'; import '../fake/device_service.dart'; -import '../fake/media_file_service.dart'; import '../fake/media_store_service.dart'; import '../fake/metadata_db.dart'; import '../fake/metadata_fetch_service.dart'; @@ -59,7 +57,6 @@ void main() { getIt.registerLazySingleton(FakeAndroidAppService.new); getIt.registerLazySingleton(FakeDeviceService.new); - getIt.registerLazySingleton(FakeMediaFileService.new); getIt.registerLazySingleton(FakeMediaStoreService.new); getIt.registerLazySingleton(FakeMetadataFetchService.new); getIt.registerLazySingleton(FakeReportService.new); diff --git a/test/model/filters_test.dart b/test/model/filters_test.dart index b8eaf5ce5..d1107457a 100644 --- a/test/model/filters_test.dart +++ b/test/model/filters_test.dart @@ -1,5 +1,6 @@ import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/coordinate.dart'; +import 'package:aves/model/filters/date.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; @@ -36,6 +37,12 @@ void main() { final bounds = CoordinateFilter(LatLng(29.979167, 28.223615), LatLng(36.451000, 31.134167)); expect(bounds, jsonRoundTrip(bounds)); + final date = DateFilter(DateLevel.ym, DateTime(1969, 7)); + expect(date, jsonRoundTrip(date)); + + final onThisDay = DateFilter.onThisDay; + expect(onThisDay, jsonRoundTrip(onThisDay)); + const fav = FavouriteFilter.instance; expect(fav, jsonRoundTrip(fav)); diff --git a/test/model/video/metadata_test.dart b/test/model/video/metadata_test.dart index b3b3a3c13..dd009e43c 100644 --- a/test/model/video/metadata_test.dart +++ b/test/model/video/metadata_test.dart @@ -9,6 +9,7 @@ void main() { expect(VideoMetadataFormatter.parseVideoDate('UTC 2021-05-30 19:14:21'), DateTime(2021, 5, 30, 19, 14, 21).millisecondsSinceEpoch); expect(VideoMetadataFormatter.parseVideoDate('2021/10/31 21:23:17'), DateTime(2021, 10, 31, 21, 23, 17).millisecondsSinceEpoch); expect(VideoMetadataFormatter.parseVideoDate('2021-09-10T7:14:49 pmZ'), DateTime(2021, 9, 10, 19, 14, 49).millisecondsSinceEpoch); + expect(VideoMetadataFormatter.parseVideoDate('2012-1-1T12:00:00Z'), DateTime(2012, 1, 1, 12, 0, 0).millisecondsSinceEpoch); }); test('Ambiguous date', () { diff --git a/test/utils/time_utils_test.dart b/test/utils/time_utils_test.dart index 1e5cefb69..b6c49cdef 100644 --- a/test/utils/time_utils_test.dart +++ b/test/utils/time_utils_test.dart @@ -19,13 +19,15 @@ void main() { test('Parse dates', () { final localOffset = DateTime.now().timeZoneOffset; - expect(parseUnknownDateFormat('1600995564713'), DateTime(2020, 09, 25, 0, 59, 24, 713).add(localOffset)); - expect(parseUnknownDateFormat('pre1600995564713suf'), DateTime(2020, 09, 25, 0, 59, 24, 713).add(localOffset)); + expect(parseUnknownDateFormat('1600995564713'), DateTime(2020, 9, 25, 0, 59, 24, 713).add(localOffset)); + expect(parseUnknownDateFormat('pre1600995564713suf'), DateTime(2020, 9, 25, 0, 59, 24, 713).add(localOffset)); - expect(parseUnknownDateFormat('1600995564'), DateTime(2020, 09, 25, 0, 59, 24, 0).add(localOffset)); - expect(parseUnknownDateFormat('pre1600995564suf'), DateTime(2020, 09, 25, 0, 59, 24, 0).add(localOffset)); + expect(parseUnknownDateFormat('1600995564'), DateTime(2020, 9, 25, 0, 59, 24, 0).add(localOffset)); + expect(parseUnknownDateFormat('pre1600995564suf'), DateTime(2020, 9, 25, 0, 59, 24, 0).add(localOffset)); - expect(parseUnknownDateFormat('IMG_20210901_142523_783'), DateTime(2021, 09, 1, 14, 25, 23, 783)); + expect(parseUnknownDateFormat('IMG_20210901_142523_783'), DateTime(2021, 9, 1, 14, 25, 23, 783)); expect(parseUnknownDateFormat('Screenshot_20211028-115056_Aves'), DateTime(2021, 10, 28, 11, 50, 56, 0)); + + expect(parseUnknownDateFormat('Screenshot_2022-05-14-15-40-29-164_uri'), DateTime(2022, 5, 14, 15, 40, 29, 164)); }); } diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart index e08ed8d56..b13e1e5bc 100644 --- a/test_driver/driver_screenshots.dart +++ b/test_driver/driver_screenshots.dart @@ -28,7 +28,7 @@ Future configureAndLaunch() async { // navigation ..keepScreenOn = KeepScreenOn.always ..homePage = HomePageSetting.collection - ..showBottomNavigationBar = true + ..enableBottomNavigationBar = true // collection ..collectionSectionFactor = EntryGroupFactor.month ..collectionSortFactor = EntrySortFactor.date diff --git a/test_driver/driver_shaders.dart b/test_driver/driver_shaders.dart index 168bb3cc9..4901d9df8 100644 --- a/test_driver/driver_shaders.dart +++ b/test_driver/driver_shaders.dart @@ -27,7 +27,7 @@ Future configureAndLaunch() async { // navigation ..keepScreenOn = KeepScreenOn.always ..homePage = HomePageSetting.collection - ..showBottomNavigationBar = true + ..enableBottomNavigationBar = true // collection ..collectionBrowsingQuickActions = SettingsDefaults.collectionBrowsingQuickActions // viewer diff --git a/untranslated.json b/untranslated.json index a28413c14..3a5674b38 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,94 +1,32 @@ { - "id": [ - "slideshowActionResume", - "slideshowActionShowInCollection", - "slideshowVideoPlaybackSkip", - "slideshowVideoPlaybackMuted", - "slideshowVideoPlaybackWithSound", - "viewerTransitionSlide", - "viewerTransitionParallax", - "viewerTransitionFade", - "viewerTransitionZoomIn", - "wallpaperTargetHome", - "wallpaperTargetLock", - "wallpaperTargetHomeLock", - "menuActionSlideshow", - "collectionEmptyGrantAccessButtonLabel", - "settingsViewerSlideshowTile", - "settingsViewerSlideshowTitle", - "settingsSlideshowRepeat", - "settingsSlideshowShuffle", - "settingsSlideshowTransitionTile", - "settingsSlideshowTransitionTitle", - "settingsSlideshowIntervalTile", - "settingsSlideshowIntervalTitle", - "settingsSlideshowVideoPlaybackTile", - "settingsSlideshowVideoPlaybackTitle", - "settingsThemeEnableDynamicColor", - "viewerSetWallpaperButtonLabel" + "es": [ + "filterOnThisDayLabel", + "settingsSlideshowFillScreen", + "settingsScreenSaverPageTitle", + "settingsWidgetPageTitle", + "settingsWidgetShowOutline" ], - "ja": [ - "slideshowActionResume", - "slideshowActionShowInCollection", - "slideshowVideoPlaybackSkip", - "slideshowVideoPlaybackMuted", - "slideshowVideoPlaybackWithSound", - "viewerTransitionSlide", - "viewerTransitionParallax", - "viewerTransitionFade", - "viewerTransitionZoomIn", - "wallpaperTargetHome", - "wallpaperTargetLock", - "wallpaperTargetHomeLock", - "menuActionSlideshow", - "collectionEmptyGrantAccessButtonLabel", - "settingsViewerSlideshowTile", - "settingsViewerSlideshowTitle", - "settingsSlideshowRepeat", - "settingsSlideshowShuffle", - "settingsSlideshowTransitionTile", - "settingsSlideshowTransitionTitle", - "settingsSlideshowIntervalTile", - "settingsSlideshowIntervalTitle", - "settingsSlideshowVideoPlaybackTile", - "settingsSlideshowVideoPlaybackTitle", - "settingsThemeEnableDynamicColor", - "viewerSetWallpaperButtonLabel" + "id": [ + "filterOnThisDayLabel", + "settingsSlideshowFillScreen", + "settingsScreenSaverPageTitle", + "settingsWidgetPageTitle", + "settingsWidgetShowOutline" ], "ru": [ - "slideshowActionResume", - "slideshowActionShowInCollection", - "slideshowVideoPlaybackSkip", - "slideshowVideoPlaybackMuted", - "slideshowVideoPlaybackWithSound", - "viewerTransitionSlide", - "viewerTransitionParallax", - "viewerTransitionFade", - "viewerTransitionZoomIn", - "wallpaperTargetHome", - "wallpaperTargetLock", - "wallpaperTargetHomeLock", - "menuActionSlideshow", - "collectionEmptyGrantAccessButtonLabel", - "settingsViewerSlideshowTile", - "settingsViewerSlideshowTitle", - "settingsSlideshowRepeat", - "settingsSlideshowShuffle", - "settingsSlideshowTransitionTile", - "settingsSlideshowTransitionTitle", - "settingsSlideshowIntervalTile", - "settingsSlideshowIntervalTitle", - "settingsSlideshowVideoPlaybackTile", - "settingsSlideshowVideoPlaybackTitle", - "settingsThemeEnableDynamicColor", - "viewerSetWallpaperButtonLabel" + "filterOnThisDayLabel", + "settingsSlideshowFillScreen", + "settingsScreenSaverPageTitle", + "settingsWidgetPageTitle", + "settingsWidgetShowOutline" ], "tr": [ "slideshowActionResume", "slideshowActionShowInCollection", + "filterOnThisDayLabel", "slideshowVideoPlaybackSkip", "slideshowVideoPlaybackMuted", "slideshowVideoPlaybackWithSound", @@ -104,12 +42,16 @@ "settingsViewerSlideshowTitle", "settingsSlideshowRepeat", "settingsSlideshowShuffle", + "settingsSlideshowFillScreen", "settingsSlideshowTransitionTile", "settingsSlideshowTransitionTitle", "settingsSlideshowIntervalTile", "settingsSlideshowIntervalTitle", "settingsSlideshowVideoPlaybackTile", "settingsSlideshowVideoPlaybackTitle", + "settingsScreenSaverPageTitle", + "settingsWidgetPageTitle", + "settingsWidgetShowOutline", "viewerSetWallpaperButtonLabel" ] } diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 31d4d7931..2f40fd87a 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,5 +1,5 @@ -In v1.6.9: -- start slideshows -- change your wallpaper -- enjoy the app in Turkish +In v1.6.10: +- add the photo frame widget to your home +- use your photos as screen saver +- search photos taken "on this day" Full changelog available on GitHub \ No newline at end of file