diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..4aa03bd --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,21 @@ +name: Java CI + +on: + push: + branches: + - main + - develop + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + - name: Build with Gradle + run: ./gradlew build diff --git a/.gitignore b/.gitignore index fd45b12..46712b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ *.iml .gradle /local.properties -/.idea/caches/build_file_checksums.ser -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml +/.idea .DS_Store /build /captures .externalNativeBuild +sign.jks +signing.properties +lint-baseline.xml \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index d59ce13..c764937 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,37 +1,35 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' - -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt'//kapt 插件 +apply from: "${rootDir}/common.gradle" android { + namespace "com.wrbug.developerhelper" lintOptions { checkReleaseBuilds false abortOnError false } - signingConfigs { - releaseConfig - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + if (isSignFileValid()) { + signingConfigs { + releaseConfig + } } - compileSdkVersion 28 + compileSdk 34 defaultConfig { applicationId "com.wrbug.developerhelper" - minSdkVersion 21 - targetSdkVersion 27 + minSdkVersion 23 + targetSdkVersion 34 versionCode 100040 versionName "1.0.4" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" ndk { - abiFilters "armeabi", "armeabi-v7a", "arm64-v8a" + abiFilters "arm64-v8a","x86_64" } } buildTypes { debug { versionNameSuffix "-debug" - if (signingConfigs.releaseConfig != null) { + if (isSignFileValid()) { signingConfig signingConfigs.releaseConfig } } @@ -40,41 +38,39 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } - dataBinding { - enabled = true - } - } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'net.dongliu:apk-parser:2.6.10' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' - implementation 'com.google.code.gson:gson:2.8.5' + implementation 'com.google.code.gson:gson:2.10' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' implementation 'com.github.yhaolpz:FloatWindow:1.0.9' - implementation project(':basecommon') kapt "com.android.databinding:compiler:3.1.4" implementation 'com.elvishew:xlog:1.6.1' implementation 'org.jetbrains.anko:anko-commons:0.10.8' - //dagger2 - implementation 'com.google.dagger:dagger:2.16' + implementation 'com.airbnb.android:lottie:6.5.2' // https://mvnrepository.com/artifact/dom4j/dom4j implementation 'dom4j:dom4j:1.6.1' + implementation 'com.drakeet.multitype:multitype:4.3.0' implementation 'com.evrencoskun.library:tableview:0.8.8' + implementation 'com.yanzhenjie.recyclerview:x:1.3.2' implementation 'gdut.bsx:share2:0.9.3' - kapt 'com.google.dagger:dagger-compiler:2.16' - implementation project(':xposedmodule') implementation 'de.blox:graphview:0.5.0' - implementation project(':basemoduleimport') + implementation 'com.github.bumptech.glide:glide:4.16.0' + implementation "com.google.android.material:material:1.12.0" implementation project(':ipc') + implementation project(':commonutil') + implementation project(':mmkv') } kapt { generateStubs = true } - Properties props = new Properties() def propFile = file('../signing.properties') if (propFile.canRead()) { @@ -94,3 +90,7 @@ if (propFile.canRead()) { println 'release build not found signing file' android.buildTypes.release.signingConfig = null } + +def isSignFileValid() { + return file('../signing.properties').canRead() +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 25f34e2..3856b16 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,14 +1,22 @@ + xmlns:tools="http://schemas.android.com/tools"> - - + + + + + @@ -32,6 +41,7 @@ @@ -50,14 +60,21 @@ android:name=".service.FloatWindowService" android:enabled="true" android:exported="true" + android:foregroundServiceType="dataSync" android:process=":floatWindow" /> - - + android:name=".ui.activity.sharedpreferencesedit.SharedPreferenceEditActivity" + android:exported="false" /> + + + - - - \ No newline at end of file diff --git a/app/src/main/assets/zip.dex b/app/src/main/assets/zip.dex deleted file mode 100644 index 8cb90dd..0000000 Binary files a/app/src/main/assets/zip.dex and /dev/null differ diff --git a/basemoduleimport/src/main/java/com/wrbug/developerhelper/basewidgetimport/BaseModule.kt b/app/src/main/java/com/wrbug/developerhelper/BaseModule.kt similarity index 63% rename from basemoduleimport/src/main/java/com/wrbug/developerhelper/basewidgetimport/BaseModule.kt rename to app/src/main/java/com/wrbug/developerhelper/BaseModule.kt index c0df8bd..7f045cc 100644 --- a/basemoduleimport/src/main/java/com/wrbug/developerhelper/basewidgetimport/BaseModule.kt +++ b/app/src/main/java/com/wrbug/developerhelper/BaseModule.kt @@ -1,7 +1,6 @@ -package com.wrbug.developerhelper.basewidgetimport +package com.wrbug.developerhelper import android.app.Application -import com.github.megatronking.netbare.NetBare import com.wrbug.developerhelper.commonutil.CommonUtils import com.wrbug.developerhelper.mmkv.manager.MMKVManager @@ -9,6 +8,5 @@ object BaseModule { fun init(application: Application) { MMKVManager.register(application) CommonUtils.register(application) - NetBare.get().attachApplication(application, BuildConfig.DEBUG) } } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/DeveloperApplication.kt b/app/src/main/java/com/wrbug/developerhelper/DeveloperApplication.kt index e80eaea..76b99d7 100644 --- a/app/src/main/java/com/wrbug/developerhelper/DeveloperApplication.kt +++ b/app/src/main/java/com/wrbug/developerhelper/DeveloperApplication.kt @@ -3,33 +3,21 @@ package com.wrbug.developerhelper import android.app.Activity import android.content.Context import android.os.Bundle -import android.os.Handler -import android.os.Looper +import android.os.Process import com.elvishew.xlog.LogConfiguration import com.elvishew.xlog.LogLevel import com.elvishew.xlog.XLog import com.elvishew.xlog.internal.DefaultsFactory -import com.wrbug.developerhelper.basecommon.BaseApp -import com.wrbug.developerhelper.basewidgetimport.BaseModule -import java.io.File -import java.io.FileOutputStream -import kotlin.concurrent.thread -import com.wrbug.developerhelper.commonwidget.flexibletoast.FlexibleToast +import com.wrbug.developerhelper.base.BaseApp +import com.wrbug.developerhelper.commonutil.ProcessUtil +import com.wrbug.developerhelper.commonutil.print +import com.wrbug.developerhelper.ipcserver.IpcManager import com.wrbug.developerhelper.ui.activity.main.MainActivity - +import com.wrbug.developerhelper.util.AppStatusRegister class DeveloperApplication : BaseApp() { - // 全局的 handler 对象 - private val appHandler = Handler() - // 全局的 Toast 对象 - private val flexibleToast: FlexibleToast by lazy { - FlexibleToast(this) - } - private val builder: FlexibleToast.Builder by lazy { - FlexibleToast.Builder(this).setGravity(FlexibleToast.GRAVITY_BOTTOM) - } - companion object { + private lateinit var instance: DeveloperApplication fun getInstance(): DeveloperApplication { return instance @@ -38,82 +26,62 @@ class DeveloperApplication : BaseApp() { override fun attachBaseContext(base: Context?) { super.attachBaseContext(base) + instance = this } + override fun onCreate() { super.onCreate() - BaseModule.init(this) - instance = this XLog.init( - LogConfiguration.Builder().logLevel(LogLevel.ALL).tag("developerHelper.print-->").build(), + LogConfiguration.Builder().logLevel(LogLevel.ALL).tag("developerHelper.print-->") + .build(), DefaultsFactory.createPrinter() ) - releaseAssetsFile() + registerIpcServer() + BaseModule.init(this) registerLifecycle() + AppStatusRegister.init(this) + } + + private fun registerIpcServer() { + val name = ProcessUtil.readProcName(Process.myPid()) + if (name != "$packageName:floatWindow") { + "ignore registerIpcServer : $name".print() + return + } + IpcManager.init() + "registerIpcServer: ${Process.myPid()}".print() } private fun registerLifecycle() { registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { private var count = 0 - override fun onActivityPaused(activity: Activity?) { + override fun onActivityPaused(activity: Activity) { } - override fun onActivityResumed(activity: Activity?) { + override fun onActivityResumed(activity: Activity) { } - override fun onActivityStarted(activity: Activity?) { + override fun onActivityStarted(activity: Activity) { count++ } - override fun onActivityDestroyed(activity: Activity?) { + override fun onActivityDestroyed(activity: Activity) { } - override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) { + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { } - override fun onActivityStopped(activity: Activity?) { + override fun onActivityStopped(activity: Activity) { count-- - activity?.let { - if (count == 0 && activity is MainActivity) { - activity.finish() - } + if (count == 0 && activity is MainActivity) { + activity.finish() } - } - override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) { + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { } }) } - - - - private fun releaseAssetsFile() { - thread { - val inputStream = BaseApp.instance.assets.open("zip.dex") - val file = File(BaseApp.instance.cacheDir, "zip.dex") - if (file.exists().not()) { - file.createNewFile() - } - val fileOutputStream = FileOutputStream(file) - fileOutputStream.write(inputStream.readBytes()) - fileOutputStream.flush() - fileOutputStream.close() - } - } - - - fun showToast(builder: FlexibleToast.Builder) { - if (Looper.myLooper() !== Looper.getMainLooper()) { - appHandler.post { flexibleToast.toastShow(builder) } - } else { - flexibleToast.toastShow(builder) - } - } - - override fun showToast(msg: String) { - builder.setSecondText(msg) - showToast(builder) - } } diff --git a/app/src/main/java/com/wrbug/developerhelper/base/AndroidExt.kt b/app/src/main/java/com/wrbug/developerhelper/base/AndroidExt.kt new file mode 100644 index 0000000..f410d8a --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/base/AndroidExt.kt @@ -0,0 +1,11 @@ +package com.wrbug.developerhelper.base + +import android.content.pm.PackageInfo +import android.os.Build + +inline val PackageInfo.versionCodeLong: Long + get() = if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.P) { + longVersionCode + } else { + versionCode.toLong() + } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/base/AppCompatActivityExt.kt b/app/src/main/java/com/wrbug/developerhelper/base/AppCompatActivityExt.kt new file mode 100644 index 0000000..2fa0857 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/base/AppCompatActivityExt.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.wrbug.developerhelper.base + +import android.Manifest +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import androidx.annotation.IdRes +import androidx.appcompat.app.ActionBar +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.base.activityresultcallback.ActResultRequest +import com.wrbug.developerhelper.base.activityresultcallback.ActivityResultCallback + +/** + * Various extension functions for AppCompatActivity. + */ + + +fun AppCompatActivity.setupActionBar(@IdRes toolbarId: Int, action: ActionBar.() -> Unit = {}) { + setupActionBar(findViewById(toolbarId), action) +} + +fun AppCompatActivity.setupActionBar(toolbar: Toolbar, action: ActionBar.() -> Unit = {}) { + setSupportActionBar(toolbar) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(false) + action() + } +} + +/** + * Runs a FragmentTransaction, then calls commit(). + */ +private inline fun FragmentManager.transact(action: FragmentTransaction.() -> Unit) { + beginTransaction().apply { + action() + }.commit() +} + +fun AppCompatActivity.startActivityForResult(intent: Intent, callback: ActivityResultCallback) { + ActResultRequest(this).startForResult(intent, callback) +} + +fun AppCompatActivity.startActivityForResultOk( + intent: Intent, action: Intent?.() -> Unit +) { + ActResultRequest(this).startForResult(intent, object : ActivityResultCallback() { + override fun onActivityResultOk(data: Intent?) { + action(data) + } + }) +} + +fun Context.requestStoragePermission(callback: () -> Unit) { + if (this !is BaseActivity) { + return + } + when { + Build.VERSION.SDK_INT>= Build.VERSION_CODES.R -> { + if (!Environment.isExternalStorageManager()) { + AlertDialog.Builder(this).setTitle(R.string.notice) + .setMessage(getString(R.string.request_write_sdcard_notice)) + .setNegativeButton(R.string.cancel, null).setPositiveButton( + R.string.ok + ) { _, _ -> + try { + val intent = + Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + intent.addCategory("android.intent.category.DEFAULT") + intent.data = Uri.parse(String.format("package:%s", packageName)) + startActivity(intent) + } catch (e: Exception) { + val intent = Intent() + intent.action = Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION + startActivity(intent) + } + }.create().show() + } else { + callback() + } + } + + else -> { + requestPermission(arrayOf( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ), object : BaseActivity.PermissionCallback() { + override fun granted() { + callback() + } + }) + } + } +} + +fun Context.registerReceiverComp( + broadcastReceiver: BroadcastReceiver, + intentFilter: IntentFilter, + flags: Int = AppCompatActivity.RECEIVER_NOT_EXPORTED +) { + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.O) { + registerReceiver(broadcastReceiver, intentFilter, flags) + } else { + registerReceiver(broadcastReceiver, intentFilter) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/base/BaseActivity.kt b/app/src/main/java/com/wrbug/developerhelper/base/BaseActivity.kt new file mode 100644 index 0000000..5bb12e6 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/base/BaseActivity.kt @@ -0,0 +1,215 @@ +package com.wrbug.developerhelper.base + +import android.content.DialogInterface +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.View +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.viewbinding.ViewBinding +import com.google.android.material.snackbar.Snackbar +import io.reactivex.rxjava3.disposables.CompositeDisposable +import java.util.ArrayList + +abstract class BaseActivity : AppCompatActivity() { + + private lateinit var toastRootView: View + protected lateinit var context: BaseActivity + private var mPermissionCallback: PermissionCallback? = null + protected lateinit var disposable: CompositeDisposable + + companion object { + private const val PERMISSION_REQUEST_CODE = 0xAADF1 + } + + override fun onCreate(savedInstanceState: Bundle?) { + context = this + super.onCreate(savedInstanceState) + disposable = CompositeDisposable() + } + + override fun setContentView(layoutResID: Int) { + setContentView(layoutInflater.inflate(layoutResID, null)) + } + + override fun onDestroy() { + super.onDestroy() + disposable.dispose() + } + + override fun setContentView(view: View?) { + view?.let { + toastRootView = view + } + super.setContentView(view) + } + + fun showSnack(msg: String) { + Snackbar.make(toastRootView, msg, Snackbar.LENGTH_SHORT).show() + } + + fun showSnack(id: Int) { + Snackbar.make(toastRootView, id, Snackbar.LENGTH_SHORT).show() + } + + fun requestPermission(permissions: Array, callback: PermissionCallback) { + val list = ArrayList() + for (permission in permissions) { + val hasPermission = checkSelfPermission(permission) + if (hasPermission != PackageManager.PERMISSION_GRANTED) { + list.add(permission) + } + } + if (list.isEmpty()) { + callback.granted() + return + } + mPermissionCallback = callback + requestPermissions(list.toTypedArray(), PERMISSION_REQUEST_CODE) + } + + fun showDialog( + @StringRes title: Int, + @StringRes msg: Int, + @StringRes positiveText: Int, + positiveListener: DialogInterface.OnClickListener + ) { + showDialog(title, msg, positiveText, 0, positiveListener, null) + } + + fun showDialog( + title: String, + msg: String, + positiveText: String, + positiveListener: DialogInterface.OnClickListener + ) { + showDialog(title, msg, positiveText, null, positiveListener, null) + } + + fun showDialog( + @StringRes title: Int, + @StringRes msg: Int, + @StringRes positiveText: Int, + @StringRes negativeText: Int, + onPositiveClick: DialogInterface.(Int) -> Unit, + onNegativeClick: DialogInterface.(Int) -> Unit? = { + + } + ) { + showDialog(title, + msg, + positiveText, + negativeText, + { dialog, which -> dialog.onPositiveClick(which) }, + { dialog, which -> dialog.onNegativeClick(which) }) + } + + fun showDialog( + @StringRes title: Int, + msg: String, + @StringRes positiveText: Int, + @StringRes negativeText: Int, + onPositiveClick: DialogInterface?.(Int) -> Unit, + onNegativeClick: DialogInterface?.(Int) -> Unit? = { + + } + ) { + showDialog(title, + msg, + positiveText, + negativeText, + { dialog, which -> dialog.onPositiveClick(which) }, + { dialog, which -> dialog.onNegativeClick(which) }) + } + + private fun showDialog( + @StringRes title: Int, + @StringRes msg: Int, + @StringRes positiveText: Int, + @StringRes negativeText: Int, + positiveListener: DialogInterface.OnClickListener, + negativeListener: DialogInterface.OnClickListener? + ) { + showDialog( + getString(title), + getString(msg), + getString(positiveText), + if (negativeText == 0) null else getString(negativeText), + positiveListener, + negativeListener + ) + } + + private fun showDialog( + @StringRes title: Int, + msg: String, + @StringRes positiveText: Int, + @StringRes negativeText: Int, + positiveListener: DialogInterface.OnClickListener, + negativeListener: DialogInterface.OnClickListener? + ) { + showDialog( + getString(title), + msg, + getString(positiveText), + if (negativeText == 0) null else getString(negativeText), + positiveListener, + negativeListener + ) + } + + fun showDialog( + title: String, + msg: String, + positiveText: String, + negativeText: String?, + positiveListener: DialogInterface.OnClickListener, + negativeListener: DialogInterface.OnClickListener? + ) { + val builder = AlertDialog.Builder(this).setMessage(msg).setTitle(title) + .setPositiveButton(positiveText, positiveListener) + if (negativeText.isNullOrEmpty().not()) { + builder.setNegativeButton(negativeText, negativeListener) + } + builder.show() + } + + fun hasPermission(permissions: String): Boolean { + return checkSelfPermission(permissions) == PackageManager.PERMISSION_GRANTED + } + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray + ) { + if (requestCode == PERMISSION_REQUEST_CODE) { + val list = ArrayList() + for (i in grantResults.indices) { + if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { + list.add(permissions[i]) + } + } + if (list.isEmpty()) { + mPermissionCallback?.granted() + return + } + mPermissionCallback?.denied(list) + return + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + abstract class PermissionCallback { + + abstract fun granted() + + open fun denied(permissions: List) { + + } + } + + protected fun T.inject(): T { + setContentView(this.root) + return this + } +} \ No newline at end of file diff --git a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseApp.kt b/app/src/main/java/com/wrbug/developerhelper/base/BaseApp.kt similarity index 59% rename from basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseApp.kt rename to app/src/main/java/com/wrbug/developerhelper/base/BaseApp.kt index 3239594..86221e7 100644 --- a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseApp.kt +++ b/app/src/main/java/com/wrbug/developerhelper/base/BaseApp.kt @@ -1,4 +1,4 @@ -package com.wrbug.developerhelper.basecommon +package com.wrbug.developerhelper.base import android.app.Application @@ -12,11 +12,4 @@ abstract class BaseApp : Application() { instance = this } - - abstract fun showToast(msg: String) - - fun showToast(id: Int){ - showToast(getString(id)) - } - } \ No newline at end of file diff --git a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseFragment.kt b/app/src/main/java/com/wrbug/developerhelper/base/BaseFragment.kt similarity index 64% rename from basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseFragment.kt rename to app/src/main/java/com/wrbug/developerhelper/base/BaseFragment.kt index 894989c..e7d2360 100644 --- a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseFragment.kt +++ b/app/src/main/java/com/wrbug/developerhelper/base/BaseFragment.kt @@ -1,4 +1,4 @@ -package com.wrbug.developerhelper.basecommon +package com.wrbug.developerhelper.base import androidx.fragment.app.Fragment diff --git a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseVMActivity.kt b/app/src/main/java/com/wrbug/developerhelper/base/BaseVMActivity.kt similarity index 92% rename from basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseVMActivity.kt rename to app/src/main/java/com/wrbug/developerhelper/base/BaseVMActivity.kt index fb50ab6..51bff35 100644 --- a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseVMActivity.kt +++ b/app/src/main/java/com/wrbug/developerhelper/base/BaseVMActivity.kt @@ -1,4 +1,4 @@ -package com.wrbug.developerhelper.basecommon +package com.wrbug.developerhelper.base import android.os.Bundle diff --git a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseViewModel.kt b/app/src/main/java/com/wrbug/developerhelper/base/BaseViewModel.kt similarity index 90% rename from basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseViewModel.kt rename to app/src/main/java/com/wrbug/developerhelper/base/BaseViewModel.kt index 3928d9a..a0e716a 100644 --- a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseViewModel.kt +++ b/app/src/main/java/com/wrbug/developerhelper/base/BaseViewModel.kt @@ -1,4 +1,4 @@ -package com.wrbug.developerhelper.basecommon +package com.wrbug.developerhelper.base import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.MutableLiveData diff --git a/app/src/main/java/com/wrbug/developerhelper/base/ExtraKey.kt b/app/src/main/java/com/wrbug/developerhelper/base/ExtraKey.kt new file mode 100644 index 0000000..bec3ba2 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/base/ExtraKey.kt @@ -0,0 +1,13 @@ +package com.wrbug.developerhelper.base + +object ExtraKey { + + const val PACKAGE_NAME = "packageName" + const val DATA = "data" + const val SELECTED = "selected" + const val KEY_1 = "key1" + const val KEY_2 = "key2" + const val KEY_3 = "key3" + const val KEY_4 = "key4" + const val KEY_5 = "key5" +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/base/ThreadExt.kt b/app/src/main/java/com/wrbug/developerhelper/base/ThreadExt.kt new file mode 100644 index 0000000..6278628 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/base/ThreadExt.kt @@ -0,0 +1,21 @@ +package com.wrbug.developerhelper.base + +import android.os.Handler +import android.os.Looper +import kotlin.concurrent.thread + +private val handler by lazy { + Handler(Looper.getMainLooper()) +} + +fun doAsync(callback: () -> Unit) { + thread { + callback() + } +} + +fun uiThread(callback: () -> Unit) { + handler.post { + callback() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/base/ToastExts.kt b/app/src/main/java/com/wrbug/developerhelper/base/ToastExts.kt new file mode 100644 index 0000000..872bcf7 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/base/ToastExts.kt @@ -0,0 +1,32 @@ +package com.wrbug.developerhelper.base + +import android.app.Activity +import android.content.Context +import android.view.View +import android.widget.Toast +import androidx.annotation.StringRes +import com.wrbug.developerhelper.ui.widget.flexibletoast.FlexibleToast + +fun View.showToast(msg: CharSequence?) { + Toast.makeText(this.context, msg, Toast.LENGTH_SHORT).show() +} + +fun View.showToast(id: Int) { + Toast.makeText(this.context, id, Toast.LENGTH_SHORT).show() +} + + +fun Context.showToast(msg: CharSequence?) { + FlexibleToast.toastShow(this, msg.toString()) +} + + +fun Activity.showToast(msg: CharSequence) { + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() +} + +fun Activity.showToast(@StringRes id: Int) { + Toast.makeText(this, id, Toast.LENGTH_SHORT).show() +} + + diff --git a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/activityresultcallback/ActResultRequest.kt b/app/src/main/java/com/wrbug/developerhelper/base/activityresultcallback/ActResultRequest.kt similarity index 84% rename from basecommon/src/main/java/com/wrbug/developerhelper/basecommon/activityresultcallback/ActResultRequest.kt rename to app/src/main/java/com/wrbug/developerhelper/base/activityresultcallback/ActResultRequest.kt index 522648a..8301892 100644 --- a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/activityresultcallback/ActResultRequest.kt +++ b/app/src/main/java/com/wrbug/developerhelper/base/activityresultcallback/ActResultRequest.kt @@ -1,4 +1,4 @@ -package com.wrbug.developerhelper.basecommon.activityresultcallback +package com.wrbug.developerhelper.base.activityresultcallback import android.content.Intent import android.os.Bundle @@ -7,7 +7,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager class ActResultRequest { - private lateinit var mFragment: OnActResultEventDispatcherFragment + private val mFragment: OnActResultEventDispatcherFragment constructor(activity: AppCompatActivity) { mFragment = getEventDispatchFragment(activity) @@ -29,10 +29,12 @@ class ActResultRequest { } private fun addFragment(fragmentManager: FragmentManager): OnActResultEventDispatcherFragment { - var fragment: OnActResultEventDispatcherFragment? = this.findEventDispatchFragment(fragmentManager) + var fragment: OnActResultEventDispatcherFragment? = + this.findEventDispatchFragment(fragmentManager) if (fragment == null) { fragment = OnActResultEventDispatcherFragment() - fragmentManager.beginTransaction().add(fragment, "on_act_result_event_dispatcher").commitAllowingStateLoss() + fragmentManager.beginTransaction().add(fragment, "on_act_result_event_dispatcher") + .commitAllowingStateLoss() fragmentManager.executePendingTransactions() } diff --git a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/activityresultcallback/ActivityResultCallback.kt b/app/src/main/java/com/wrbug/developerhelper/base/activityresultcallback/ActivityResultCallback.kt similarity index 54% rename from basecommon/src/main/java/com/wrbug/developerhelper/basecommon/activityresultcallback/ActivityResultCallback.kt rename to app/src/main/java/com/wrbug/developerhelper/base/activityresultcallback/ActivityResultCallback.kt index 8306c3d..42ef08b 100644 --- a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/activityresultcallback/ActivityResultCallback.kt +++ b/app/src/main/java/com/wrbug/developerhelper/base/activityresultcallback/ActivityResultCallback.kt @@ -1,21 +1,22 @@ -package com.wrbug.developerhelper.basecommon.activityresultcallback +package com.wrbug.developerhelper.base.activityresultcallback import android.app.Activity import android.content.Intent -abstract class ActivityResultCallback() { - fun dispatchActivityResult(resultCode: Int, data: Intent) { +abstract class ActivityResultCallback { + + fun dispatchActivityResult(resultCode: Int, data: Intent?) { onActivityResult(resultCode, data) if (resultCode == Activity.RESULT_OK) { onActivityResultOk(data) } } - protected open fun onActivityResult(resultCode: Int, data: Intent) { + protected open fun onActivityResult(resultCode: Int, data: Intent?) { } - protected open fun onActivityResultOk(data: Intent) { + protected open fun onActivityResultOk(data: Intent?) { } } diff --git a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/activityresultcallback/OnActResultEventDispatcherFragment.kt b/app/src/main/java/com/wrbug/developerhelper/base/activityresultcallback/OnActResultEventDispatcherFragment.kt similarity index 90% rename from basecommon/src/main/java/com/wrbug/developerhelper/basecommon/activityresultcallback/OnActResultEventDispatcherFragment.kt rename to app/src/main/java/com/wrbug/developerhelper/base/activityresultcallback/OnActResultEventDispatcherFragment.kt index 70311d2..b44f5ba 100644 --- a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/activityresultcallback/OnActResultEventDispatcherFragment.kt +++ b/app/src/main/java/com/wrbug/developerhelper/base/activityresultcallback/OnActResultEventDispatcherFragment.kt @@ -1,4 +1,4 @@ -package com.wrbug.developerhelper.basecommon.activityresultcallback +package com.wrbug.developerhelper.base.activityresultcallback import android.content.Intent import android.os.Bundle @@ -15,12 +15,10 @@ class OnActResultEventDispatcherFragment : Fragment() { this.startActivityForResult(intent, requestCode, options) } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) val callback = this.mCallbacks.get(requestCode) as ActivityResultCallback this.mCallbacks.remove(requestCode) callback.dispatchActivityResult(resultCode, data) - } } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/base/entry/HierarchyNode.kt b/app/src/main/java/com/wrbug/developerhelper/base/entry/HierarchyNode.kt new file mode 100644 index 0000000..0356af7 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/base/entry/HierarchyNode.kt @@ -0,0 +1,33 @@ +package com.wrbug.developerhelper.base.entry + +import android.graphics.Rect +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import java.io.Serializable + +@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") +@Parcelize +data class HierarchyNode( + var id: Long = -1L, + var screenBounds: Rect? = null, + var parentBounds: Rect? = null, + var checkable: Boolean = false, + var checked: Boolean = false, + var widget: String = "", + var clickable: Boolean = false, + var contentDesc: String = "", + var enabled: Boolean = false, + var focusable: Boolean = false, + var focused: Boolean = false, + var index: String = "", + var longClickable: Boolean = false, + var packageName: String = "", + var password: Boolean = false, + var scrollable: Boolean = false, + var selected: Boolean = false, + var text: String = "", + var resourceId: String = "", + var idHex: String? = null, + var parentId: Long = -1L, + var childId: ArrayList = arrayListOf() +): Parcelable, Serializable \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ipcserver/FileProcessDataImpl.kt b/app/src/main/java/com/wrbug/developerhelper/ipcserver/FileProcessDataImpl.kt new file mode 100644 index 0000000..8371bf1 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ipcserver/FileProcessDataImpl.kt @@ -0,0 +1,31 @@ +package com.wrbug.developerhelper.ipcserver + +import com.wrbug.developerhelper.base.BaseApp +import com.wrbug.developerhelper.commonutil.Base64 +import com.wrbug.developerhelper.ipc.processshare.TcpUrl +import com.wrbug.developerhelper.ipcserver.annotation.Controller + + +/** + * + * class: FileProcessDataImpl.kt + * author: wrbug + * date: 2020年05月19日 + * description: + * + */ +class FileProcessDataImpl { + @Controller(TcpUrl.FileProcessDataUrl.GET_DATA_FINDER_ZIP_FILE) + fun getDataFinderZipFile(): String { + val data = BaseApp.instance.assets.open("datafinder-web/web-static.zip").readBytes() + val base64 = Base64.encodeAsString(data) + return base64 + } + + @Controller(TcpUrl.FileProcessDataUrl.GET_DUMP_SO_ZIP_FILE) + fun getDumpSoZipFile(): String { + val data = BaseApp.instance.assets.open("nativeDump.zip").readBytes() + val base64 = Base64.encodeAsString(data) + return base64 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ipcserver/IpcManager.kt b/app/src/main/java/com/wrbug/developerhelper/ipcserver/IpcManager.kt new file mode 100644 index 0000000..75a3920 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ipcserver/IpcManager.kt @@ -0,0 +1,41 @@ +package com.wrbug.developerhelper.ipcserver + +import com.wrbug.developerhelper.ipc.processshare.tcp.MessageHandler +import com.wrbug.developerhelper.ipc.processshare.tcp.TcpManager +import com.wrbug.developerhelper.ipcserver.annotation.Controller + +object IpcManager { + private val map = HashMap() + + fun init() { + registerController(FileProcessDataImpl()) + TcpManager.messageHandler = object : MessageHandler { + override fun handle(action: String, message: String): String { + if (!map.containsKey(action)) { + return "" + } + val info = map[action] ?: return "" + info.method.run { + return try { + if (parameterTypes.isEmpty()) { + invoke(info.obj)?.toString() ?: "" + } else { + invoke(info.obj, message)?.toString() ?: "" + } + } catch (t: Throwable) { + "" + } + } + } + + } + TcpManager.startServer() + } + + private fun registerController(obj: Any) { + obj.javaClass.declaredMethods.forEach { + val router = it.getAnnotation(Controller::class.java)?.value ?: return@forEach + map[router] = MethodInfo(obj, it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ipcserver/MethodInfo.kt b/app/src/main/java/com/wrbug/developerhelper/ipcserver/MethodInfo.kt new file mode 100644 index 0000000..f37777c --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ipcserver/MethodInfo.kt @@ -0,0 +1,5 @@ +package com.wrbug.developerhelper.ipcserver + +import java.lang.reflect.Method + +internal class MethodInfo(val obj: Any, val method: Method) diff --git a/app/src/main/java/com/wrbug/developerhelper/ipcserver/annotation/Controller.kt b/app/src/main/java/com/wrbug/developerhelper/ipcserver/annotation/Controller.kt new file mode 100644 index 0000000..a231ded --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ipcserver/annotation/Controller.kt @@ -0,0 +1,14 @@ +package com.wrbug.developerhelper.ipcserver.annotation + + +/** + * + * class: Controller.kt + * author: wrbug + * date: 2020年05月15日 + * description: + * + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Controller(val value: String) \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppData.kt b/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppData.kt new file mode 100644 index 0000000..3c1ac5b --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppData.kt @@ -0,0 +1,13 @@ +package com.wrbug.developerhelper.model.entity + +import kotlinx.parcelize.Parcelize +import java.io.File +import java.io.Serializable + +data class BackupAppData( + val appName: String, + val packageName: String, + val rootDir: File, + val backupMap: HashMap, + val iconPath: File? +) : Serializable diff --git a/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppInfo.kt b/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppInfo.kt new file mode 100644 index 0000000..91db0d0 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppInfo.kt @@ -0,0 +1,12 @@ +package com.wrbug.developerhelper.model.entity + +import com.google.gson.annotations.SerializedName + +data class BackupAppInfo( + @SerializedName("appName") + var appName: String = "", + @SerializedName("packageName") + var packageName: String = "", + @SerializedName("backupMap") + val backupMap: HashMap = hashMapOf() +) diff --git a/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppItemInfo.kt b/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppItemInfo.kt new file mode 100644 index 0000000..613b9f3 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppItemInfo.kt @@ -0,0 +1,38 @@ +package com.wrbug.developerhelper.model.entity + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize +import java.io.Serializable + +@Parcelize +data class BackupAppItemInfo( + @SerializedName("backupFile") + var backupFile: String = "", + @SerializedName("backupApk") + var backupApk: Boolean = false, + @SerializedName("backupData") + var backupData: Boolean = false, + @SerializedName("backupAndroidData") + var backupAndroidData: Boolean = false, + @SerializedName("apkFile") + var apkFile: String = "", + @SerializedName("versionName") + var versionName: String = "", + @SerializedName("versionCode") + var versionCode: Long = 0, + @SerializedName("packageName") + var packageName: String = "", + @SerializedName("time") + var time: Long = 0, + @SerializedName("dataFile") + var dataFile: String = "", + @SerializedName("androidDataFile") + var androidDataFile: String = "", + @SerializedName("memo") + var memo: String = "" +) : Parcelable, Serializable { + companion object { + val EMPTY = BackupAppItemInfo() + } +} diff --git a/app/src/main/java/com/wrbug/developerhelper/model/entity/SharedPreferenceItemInfo.kt b/app/src/main/java/com/wrbug/developerhelper/model/entity/SharedPreferenceItemInfo.kt index 6bf2927..49859ce 100644 --- a/app/src/main/java/com/wrbug/developerhelper/model/entity/SharedPreferenceItemInfo.kt +++ b/app/src/main/java/com/wrbug/developerhelper/model/entity/SharedPreferenceItemInfo.kt @@ -10,7 +10,7 @@ class SharedPreferenceItemInfo { fun isValueValid(): Boolean { - when (type.toLowerCase()) { + when (type.lowercase()) { "string" -> { return JsonHelper.fromJson(value) == null || JsonHelper.fromJson(newValue) != null } diff --git a/app/src/main/java/com/wrbug/developerhelper/service/AccessibilityManager.kt b/app/src/main/java/com/wrbug/developerhelper/service/AccessibilityManager.kt index 418707e..3c9c6ce 100644 --- a/app/src/main/java/com/wrbug/developerhelper/service/AccessibilityManager.kt +++ b/app/src/main/java/com/wrbug/developerhelper/service/AccessibilityManager.kt @@ -4,32 +4,23 @@ import android.content.Context import android.content.Intent import android.provider.Settings import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.showToast -import com.wrbug.developerhelper.commonutil.shell.Callback +import com.wrbug.developerhelper.base.showToast import com.wrbug.developerhelper.commonutil.shell.ShellManager +import com.wrbug.developerhelper.util.getString +import io.reactivex.rxjava3.core.Single object AccessibilityManager { - fun startService(context: Context?, callback: Callback? = null) { - context?.run { - ShellManager.openAccessibilityService(object : Callback { - override fun onSuccess(data: Boolean) { - if (!data) { - showToast(getString(R.string.please_open_accessbility_service)) - startAccessibilitySetting(context) - } - callback?.onSuccess(data) - } - - override fun onFailed(msg: String) { - callback?.onFailed(msg) - startAccessibilitySetting(context) - } - - }) + fun startService(context: Context?): Single { + return ShellManager.openAccessibilityService().doOnSuccess { + if (!it) { + context?.showToast(getString(R.string.please_open_accessbility_service)) + } } } - fun startAccessibilitySetting(context: Context) { + + fun startAccessibilitySetting(context: Context?) { + context ?: return val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) diff --git a/app/src/main/java/com/wrbug/developerhelper/service/DeveloperHelperAccessibilityService.kt b/app/src/main/java/com/wrbug/developerhelper/service/DeveloperHelperAccessibilityService.kt index 8ca5662..7c8e99f 100644 --- a/app/src/main/java/com/wrbug/developerhelper/service/DeveloperHelperAccessibilityService.kt +++ b/app/src/main/java/com/wrbug/developerhelper/service/DeveloperHelperAccessibilityService.kt @@ -1,27 +1,27 @@ package com.wrbug.developerhelper.service import android.accessibilityservice.AccessibilityService +import android.annotation.SuppressLint import android.content.BroadcastReceiver +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter import android.graphics.Rect +import android.os.Build import android.provider.Settings import android.text.TextUtils import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.BaseApp -import com.wrbug.developerhelper.basecommon.showToast +import com.wrbug.developerhelper.base.BaseApp +import com.wrbug.developerhelper.base.entry.HierarchyNode +import com.wrbug.developerhelper.base.registerReceiverComp import com.wrbug.developerhelper.commonutil.AppInfoManager +import com.wrbug.developerhelper.commonutil.UiUtils import com.wrbug.developerhelper.commonutil.entity.ApkInfo import com.wrbug.developerhelper.commonutil.entity.TopActivityInfo import com.wrbug.developerhelper.constant.ReceiverConstant -import com.wrbug.developerhelper.basecommon.entry.HierarchyNode -import com.wrbug.developerhelper.commonutil.shell.Callback -import com.wrbug.developerhelper.commonutil.shell.ShellManager import com.wrbug.developerhelper.ui.activity.hierachy.HierarchyActivity -import com.wrbug.developerhelper.commonutil.UiUtils class DeveloperHelperAccessibilityService : AccessibilityService() { @@ -29,18 +29,20 @@ class DeveloperHelperAccessibilityService : AccessibilityService() { private var nodeId = 0L private var currentAppInfo: ApkInfo? = null private var topActivity: TopActivityInfo? = null + private val activityMap = hashMapOf() companion object { internal var serviceRunning = false fun isAccessibilitySettingsOn(): Boolean { var accessibilityEnabled = 0 - val service = "com.wrbug.developerhelper/" + DeveloperHelperAccessibilityService::class.java.canonicalName + val service = + "com.wrbug.developerhelper/" + DeveloperHelperAccessibilityService::class.java.canonicalName try { accessibilityEnabled = Settings.Secure.getInt( BaseApp.instance.applicationContext.contentResolver, android.provider.Settings.Secure.ACCESSIBILITY_ENABLED ) - } catch (e: Settings.SettingNotFoundException) { + } catch (_: Settings.SettingNotFoundException) { } val mStringColonSplitter = TextUtils.SimpleStringSplitter(':') @@ -63,7 +65,6 @@ class DeveloperHelperAccessibilityService : AccessibilityService() { return false } - //todo java.lang.RuntimeException:android.os.TransactionTooLargeException val nodeMap: HashMap = hashMapOf() } @@ -71,7 +72,22 @@ class DeveloperHelperAccessibilityService : AccessibilityService() { } override fun onAccessibilityEvent(event: AccessibilityEvent?) { - + if (event?.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + || event.className.isNullOrEmpty() + || event.className?.startsWith("android.") == true + || event.packageName.isNullOrEmpty() + ) { + return + } + runCatching { + val info = packageManager.getActivityInfo( + ComponentName( + event.packageName.toString(), + event.className.toString() + ), 0 + ) + activityMap[info.packageName] = info.name + } } fun readNode(): ArrayList { @@ -87,6 +103,7 @@ class DeveloperHelperAccessibilityService : AccessibilityService() { return hierarchyNodes } + private fun getDecorViewNode(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { for (index in 0 until node.childCount) { val child = node.getChild(index) @@ -101,16 +118,18 @@ class DeveloperHelperAccessibilityService : AccessibilityService() { return null } + @SuppressLint("UnspecifiedRegisterReceiverFlag") override fun onCreate() { super.onCreate() val filter = IntentFilter() filter.addAction(ReceiverConstant.ACTION_HIERARCHY_VIEW) - registerReceiver(receiver, filter) + registerReceiverComp(receiver, filter) sendStatusBroadcast(true) serviceRunning = true nodeMap.clear() } + override fun onDestroy() { super.onDestroy() nodeMap.clear() @@ -140,7 +159,7 @@ class DeveloperHelperAccessibilityService : AccessibilityService() { } for (index in 0 until accessibilityNodeInfo.childCount) { val child = accessibilityNodeInfo.getChild(index) - if (child.isVisibleToUser.not()) { + if (child?.isVisibleToUser != true) { continue } val screenRect = Rect() @@ -202,17 +221,11 @@ class DeveloperHelperAccessibilityService : AccessibilityService() { inner class DeveloperHelperAccessibilityReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, data: Intent?) { - showToast(getString(R.string.getting_app_info)) - ShellManager.getTopActivity(object : Callback { - - override fun onSuccess(data: TopActivityInfo?) { - topActivity = data - val nodesInfo = readNode() - HierarchyActivity.start(context, currentAppInfo, nodesInfo, topActivity) - } - }) - + override fun onReceive(context: Context, data: Intent?) { + val nodesInfo = readNode() + currentAppInfo?.topActivity = + activityMap[currentAppInfo?.packageInfo?.packageName].orEmpty() + HierarchyActivity.start(context, currentAppInfo, nodesInfo) } } diff --git a/app/src/main/java/com/wrbug/developerhelper/service/FloatWindowService.kt b/app/src/main/java/com/wrbug/developerhelper/service/FloatWindowService.kt index ba11265..5404f33 100644 --- a/app/src/main/java/com/wrbug/developerhelper/service/FloatWindowService.kt +++ b/app/src/main/java/com/wrbug/developerhelper/service/FloatWindowService.kt @@ -1,28 +1,41 @@ package com.wrbug.developerhelper.service +import android.annotation.SuppressLint import android.app.* import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder +import android.util.Log import android.view.LayoutInflater import android.widget.RemoteViews import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.showToast +import com.wrbug.developerhelper.base.registerReceiverComp +import com.wrbug.developerhelper.commonutil.UiUtils +import com.wrbug.developerhelper.commonutil.addTo +import com.wrbug.developerhelper.commonutil.dpInt import com.wrbug.developerhelper.constant.ReceiverConstant -import com.wrbug.developerhelper.commonutil.shell.Callback import com.wrbug.developerhelper.commonutil.shell.ShellManager +import com.wrbug.developerhelper.util.setOnDoubleCheckClickListener import com.wrbug.developerhelper.ui.activity.main.MainActivity +import com.wrbug.developerhelper.util.DeviceUtils +import com.wrbug.developerhelper.util.isPortrait import com.yhao.floatwindow.FloatWindow import com.yhao.floatwindow.Screen - +import com.yhao.floatwindow.ViewStateListener +import com.yhao.floatwindow.ViewStateListenerAdapter +import io.reactivex.rxjava3.disposables.CompositeDisposable +import org.jetbrains.anko.toast class FloatWindowService : Service() { companion object { + const val FLOAT_BUTTON = "floatButton" private const val CHANNEL_ID = "DEMON" fun start(context: Context) { @@ -44,84 +57,116 @@ class FloatWindowService : Service() { RemoteViews(packageName, R.layout.view_float_custom).apply { setOnClickPendingIntent( R.id.adbWifiContainer, PendingIntent.getBroadcast( - applicationContext, 0, - Intent(ReceiverConstant.ACTION_ADB_WIFI_CLICKED), PendingIntent.FLAG_UPDATE_CURRENT + applicationContext, + 0, + Intent(ReceiverConstant.ACTION_ADB_WIFI_CLICKED), + PendingIntent.FLAG_UPDATE_CURRENT ) ) } } + private lateinit var disposable: CompositeDisposable private lateinit var notification: Notification private val receiver = Receiver() override fun onCreate() { super.onCreate() + disposable = CompositeDisposable() initReceiver() - LayoutInflater.from(this).inflate(R.layout.layout_float_window_button, null)?.let { it -> - it.setOnClickListener { + LayoutInflater.from(this).inflate(R.layout.layout_float_window_button, null)?.let { + it.setOnDoubleCheckClickListener { if (!DeveloperHelperAccessibilityService.isAccessibilitySettingsOn()) { - AccessibilityManager.startService(this, object : Callback { - override fun onSuccess(data: Boolean) { - if (data) { - it.postDelayed({ - sendBroadcast(Intent(ReceiverConstant.ACTION_HIERARCHY_VIEW)) - }, 500) - } + AccessibilityManager.startService(this).subscribe({ data -> + if (data) { + it.postDelayed({ + sendBroadcast( + Intent(ReceiverConstant.ACTION_HIERARCHY_VIEW).setPackage( + packageName + ) + ) + }, 500) } + }, { - }) - return@setOnClickListener + }).addTo(disposable) + return@setOnDoubleCheckClickListener } - sendBroadcast(Intent(ReceiverConstant.ACTION_HIERARCHY_VIEW)) + sendBroadcast(Intent(ReceiverConstant.ACTION_HIERARCHY_VIEW).setPackage(packageName)) } - FloatWindow - .with(applicationContext) - .setView(it) - .setWidth(Screen.width, 0.1f) //设置控件宽高 - .setHeight(Screen.width, 0.1f) - .setY(Screen.height, 0.3f) - .setTag(FLOAT_BUTTON) - .setDesktopShow(true) //桌面显示 - .build() + val screen = if (isPortrait()) { + UiUtils.getDeviceWidth() * 0.1 + } else { + UiUtils.getDeviceHeight() * 0.1 + }.toInt() + FloatWindow.with(applicationContext).setView(it).setWidth(screen) + .setHeight(screen).setY(Screen.height, 0.3f).setTag(FLOAT_BUTTON) + .setDesktopShow(true).setViewStateListener(object : ViewStateListenerAdapter() { + override fun onPositionUpdate(x: Int, y: Int) { + val minY = UiUtils.getStatusHeight() + 10.dpInt(applicationContext) + val maxY = UiUtils.getDeviceHeight() - 60.dpInt(applicationContext) + if (y < minY) { + FloatWindow.get(FLOAT_BUTTON).updateY(y) + return + } + if (y> maxY) { + FloatWindow.get(FLOAT_BUTTON).updateY(maxY) + } + } + }).build() + } + } + private fun updateNotification() { + val type = if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.R) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else { + 0 } + runCatching { + ServiceCompat.startForeground(this, 0x10000, notification, type) + }.getOrElse { + it.printStackTrace() + } + } + + private fun updateNotificationContent(text: String) { + floatCustomView.setTextViewText(R.id.contentTv, text) + updateNotification() + } + + private fun updateNotificationWifi(id: Int) { + floatCustomView.setImageViewResource(R.id.adbWifiIv, id) + updateNotification() } + @SuppressLint("UnspecifiedImmutableFlag") private fun initNotification() { - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.O) { - val channel = - NotificationChannel(CHANNEL_ID, getString(R.string.demon_process), NotificationManager.IMPORTANCE_LOW) + val channel = NotificationChannel( + CHANNEL_ID, getString(R.string.demon_process), NotificationManager.IMPORTANCE_LOW + ) channel.enableLights(true) channel.setShowBadge(true) notificationManager.createNotificationChannel(channel) } val intent = Intent(this, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) - val builder = NotificationCompat.Builder(this, CHANNEL_ID) - .setAutoCancel(false) - .setContentIntent(pendingIntent) - .setContentTitle(getString(R.string.app_name)) + val pendingIntent = if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.M) { + PendingIntent.getActivity( + this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } else { + PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) + } + val builder = NotificationCompat.Builder(this, CHANNEL_ID).setAutoCancel(false) + .setContentIntent(pendingIntent).setContentTitle(getString(R.string.app_name)) .setContentText(getString(R.string.demon_process_content)) - .setSmallIcon(R.drawable.ic_launcher_notify) - .setVibrate(null) + .setSmallIcon(R.drawable.ic_launcher_notify).setVibrate(null) notification = builder.build() - notification.flags = Notification.FLAG_ONGOING_EVENT or Notification.FLAG_NO_CLEAR or - Notification.FLAG_FOREGROUND_SERVICE - updateNotification() - } - - private fun updateNotification() { - startForeground(0x10000, notification) - } - - private fun updateNotificationContent(text: String) { - floatCustomView.setTextViewText(R.id.contentTv, text) - updateNotification() - } - - private fun updateNotificationWifi(id: Int) { - floatCustomView.setImageViewResource(R.id.adbWifiIv, id) + notification.flags = + Notification.FLAG_ONGOING_EVENT or Notification.FLAG_NO_CLEAR or Notification.FLAG_FOREGROUND_SERVICE updateNotification() } @@ -136,7 +181,7 @@ class FloatWindowService : Service() { private fun initReceiver() { val filter = IntentFilter(ReceiverConstant.ACTION_SET_FLOAT_BUTTON_VISIBLE) filter.addAction(ReceiverConstant.ACTION_ADB_WIFI_CLICKED) - registerReceiver(receiver, filter) + registerReceiverComp(receiver, filter) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -147,6 +192,7 @@ class FloatWindowService : Service() { override fun onDestroy() { try { + disposable.dispose() FloatWindow.destroy(FLOAT_BUTTON) unregisterReceiver(receiver) stopForeground(true) @@ -159,8 +205,8 @@ class FloatWindowService : Service() { override fun onBind(intent: Intent): IBinder = null!! - private inner class Receiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { ReceiverConstant.ACTION_SET_FLOAT_BUTTON_VISIBLE -> { @@ -171,6 +217,7 @@ class FloatWindowService : Service() { hideFloatButton() } } + ReceiverConstant.ACTION_ADB_WIFI_CLICKED -> { updateNotificationContent("正在开启adb wifi") val success = ShellManager.openAdbWifi() diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/AppBackupDetailActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/AppBackupDetailActivity.kt new file mode 100644 index 0000000..0dd9509 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/AppBackupDetailActivity.kt @@ -0,0 +1,125 @@ +package com.wrbug.developerhelper.ui.activity.appbackup + +import android.app.ActionBar.LayoutParams +import android.app.backup.BackupManager +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.core.content.IntentCompat +import androidx.recyclerview.widget.LinearLayoutManager +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.base.BaseActivity +import com.wrbug.developerhelper.base.ExtraKey +import com.wrbug.developerhelper.commonutil.dpInt +import com.wrbug.developerhelper.databinding.ActivityAppBackupDetailBinding +import com.wrbug.developerhelper.model.entity.BackupAppData +import com.wrbug.developerhelper.model.entity.BackupAppItemInfo +import com.wrbug.developerhelper.ui.adapter.ExMultiTypeAdapter +import com.wrbug.developerhelper.ui.decoration.SpaceItemDecoration +import com.wrbug.developerhelper.util.BackupUtils +import com.wrbug.developerhelper.util.loadImage +import com.yanzhenjie.recyclerview.SwipeMenuItem + +class AppBackupDetailActivity : BaseActivity() { + + companion object { + fun start(context: Context, info: BackupAppData) { + context.startActivity(Intent(context, AppBackupDetailActivity::class.java).apply { + putExtra(ExtraKey.DATA, info) + }) + } + } + + + private val adapter by ExMultiTypeAdapter.get() + + private val info by lazy { + IntentCompat.getSerializableExtra(intent, ExtraKey.DATA, BackupAppData::class.java) + } + private val binding by lazy { + ActivityAppBackupDetailBinding.inflate(layoutInflater) + } + private var listCache = arrayListOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + initView() + loadData(info?.backupMap) + } + + private fun loadData(map: Map?) { + val list = map?.values?.sortedByDescending { it.time } ?: emptyList() + listCache.clear() + listCache.addAll(list) + setupList() + } + + private fun setupList() { + if (listCache.isEmpty()) { + adapter.showEmpty() + } else { + adapter.loadData(listCache) + } + } + + private fun initView() { + binding.appBar.setSubTitle(info?.appName) + binding.rvAppBackupList.layoutManager = LinearLayoutManager(this) + binding.rvAppBackupList.addItemDecoration(SpaceItemDecoration.standard) + adapter.register(BackupDetailDelegate(info?.appName.orEmpty())) + binding.rvAppBackupList.setSwipeMenuCreator { leftMenu, rightMenu, position -> + rightMenu.addMenuItem(SwipeMenuItem(this).apply { + text = getString(R.string.item_swipe_menu_meme) + width = 56.dpInt() + height = LayoutParams.MATCH_PARENT + setTextColorResource(R.color.material_text_color_white_text) + setBackgroundColorResource(R.color.colorAccent) + }) + rightMenu.addMenuItem(SwipeMenuItem(this).apply { + text = getString(R.string.item_swipe_menu_delete) + width = 56.dpInt() + height = LayoutParams.MATCH_PARENT + setTextColorResource(R.color.material_text_color_white_text) + setBackgroundColorResource(R.color.material_color_red_600) + }) + } + binding.rvAppBackupList.setOnItemMenuClickListener { menuBridge, adapterPosition -> + menuBridge.closeMenu() + when (menuBridge.position) { + 0 -> { + + } + + 1 -> { + showDialog( + R.string.notice, + R.string.delete_backup_notice, + R.string.ok, + R.string.cancel, + { + deleteBackup(adapter.items[adapterPosition] as? BackupAppItemInfo) + dismiss() + }, + { + dismiss() + }) + } + + else -> {} + } + } + binding.rvAppBackupList.adapter = adapter + } + + private fun deleteBackup(backupAppItemInfo: BackupAppItemInfo?) { + backupAppItemInfo ?: return + BackupUtils.deleteBackupItem(backupAppItemInfo.packageName, backupAppItemInfo.backupFile) + .subscribe({ + loadData(it.backupMap) + }, { + showSnack(getString(R.string.delete_backup_failed_retry)) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupAppActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupAppActivity.kt new file mode 100644 index 0000000..535cfd6 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupAppActivity.kt @@ -0,0 +1,55 @@ +package com.wrbug.developerhelper.ui.activity.appbackup + +import android.os.Bundle +import androidx.recyclerview.widget.LinearLayoutManager +import com.drakeet.multitype.MultiTypeAdapter +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.base.BaseActivity +import com.wrbug.developerhelper.base.setupActionBar +import com.wrbug.developerhelper.commonutil.addTo +import com.wrbug.developerhelper.commonutil.dpInt +import com.wrbug.developerhelper.commonutil.runOnIO +import com.wrbug.developerhelper.databinding.ActivityBackupAppBinding +import com.wrbug.developerhelper.ui.adapter.ExMultiTypeAdapter +import com.wrbug.developerhelper.ui.decoration.SpaceItemDecoration +import com.wrbug.developerhelper.util.BackupUtils + +class BackupAppActivity : BaseActivity() { + private val binding by lazy { + ActivityBackupAppBinding.inflate(layoutInflater) + } + private val adapter by ExMultiTypeAdapter.get() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + initView() + } + + private fun initView() { + binding.rvAppList.layoutManager = LinearLayoutManager(this) + binding.rvAppList.adapter = adapter + binding.rvAppList.addItemDecoration(SpaceItemDecoration.standard) + adapter.register(BackupInfoItemDelegate { + AppBackupDetailActivity.start(this, it) + }) + } + + override fun onStart() { + super.onStart() + loadData() + } + + private fun loadData() { + adapter.showLoading() + BackupUtils.getAllBackupInfo().runOnIO().subscribe({ + if (it.isEmpty()) { + adapter.showEmpty() + return@subscribe + } + adapter.loadData(it) + }, { + adapter.showEmpty() + }).addTo(disposable) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupDetailDelegate.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupDetailDelegate.kt new file mode 100644 index 0000000..ff9a1c9 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupDetailDelegate.kt @@ -0,0 +1,45 @@ +package com.wrbug.developerhelper.ui.activity.appbackup + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.databinding.ItemBackupDetailInfoBinding +import com.wrbug.developerhelper.model.entity.BackupAppItemInfo +import com.wrbug.developerhelper.ui.adapter.delegate.BaseItemViewBindingDelegate +import com.wrbug.developerhelper.util.format +import com.wrbug.developerhelper.util.getColor +import com.wrbug.developerhelper.util.getString + +class BackupDetailDelegate(private val appName: String) : + BaseItemViewBindingDelegate() { + override fun onBindViewHolder(binding: ItemBackupDetailInfoBinding, item: BackupAppItemInfo) { + binding.tvTime.text = item.time.format() + binding.tvTitle.text = item.memo.ifEmpty { + binding.root.context.getString( + R.string.backup_default_meme, + appName + ) + } + binding.tvVersion.text = "${item.versionName}(${item.versionCode})" + binding.tvBackupApk.setStatusColor(item.backupApk) + binding.tvBackupAndroidData.setStatusColor(item.backupAndroidData) + binding.tvBackupData.setStatusColor(item.backupData) + } + + private fun TextView.setStatusColor(enable: Boolean) { + val color = if (enable) { + R.color.material_color_green_500.getColor() + } else { + R.color.material_color_red_500.getColor() + } + setTextColor(color) + } + + override fun onCreateViewBinding( + inflater: LayoutInflater, + parent: ViewGroup + ): ItemBackupDetailInfoBinding { + return ItemBackupDetailInfoBinding.inflate(inflater, parent, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupInfoItemDelegate.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupInfoItemDelegate.kt new file mode 100644 index 0000000..f5cf1b1 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupInfoItemDelegate.kt @@ -0,0 +1,39 @@ +package com.wrbug.developerhelper.ui.activity.appbackup + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.commonutil.SpannableBuilder +import com.wrbug.developerhelper.databinding.ItemBackupAppInfoBinding +import com.wrbug.developerhelper.model.entity.BackupAppData +import com.wrbug.developerhelper.ui.adapter.delegate.BaseItemViewBindingDelegate +import com.wrbug.developerhelper.util.format +import com.wrbug.developerhelper.util.getString +import com.wrbug.developerhelper.util.loadImage +import com.wrbug.developerhelper.util.setOnDoubleCheckClickListener + +class BackupInfoItemDelegate(private val listener: (BackupAppData) -> Unit) : + BaseItemViewBindingDelegate() { + override fun onBindViewHolder(binding: ItemBackupAppInfoBinding, item: BackupAppData) { + binding.tvAppName.text = item.appName + binding.tvAppPackageName.text = item.packageName + val size = item.backupMap.size + binding.tvBackupCount.text = SpannableBuilder.with( + binding.root.context, + R.string.app_info_backup_count.getString(size) + ).addSpanWithBold(size.toString()).build() + val time = item.backupMap.values.maxByOrNull { it.time }?.time ?: 0 + binding.tvLastBackupTime.text = R.string.last_backup_time.getString(time.format()) + binding.ivIcon.loadImage(item.iconPath, R.drawable.ic_default_app_ico_place_holder) + binding.root.setOnDoubleCheckClickListener { + listener(item) + } + } + + override fun onCreateViewBinding( + inflater: LayoutInflater, + parent: ViewGroup + ): ItemBackupAppInfoBinding { + return ItemBackupAppInfoBinding.inflate(inflater, parent, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/databaseedit/DatabaseEditActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/databaseedit/DatabaseEditActivity.kt index cdfe075..d8f805a 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/databaseedit/DatabaseEditActivity.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/databaseedit/DatabaseEditActivity.kt @@ -10,23 +10,23 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.evrencoskun.tableview.listener.ITableViewListener import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.BaseActivity -import com.wrbug.developerhelper.basecommon.setupActionBar -import com.wrbug.developerhelper.basecommon.showToast +import com.wrbug.developerhelper.base.* import com.wrbug.developerhelper.model.entity.DatabaseTableInfo import com.wrbug.developerhelper.commonutil.shell.ShellManager import com.wrbug.developerhelper.util.DatabaseUtils import com.wrbug.developerhelper.commonutil.dp2px -import kotlinx.android.synthetic.main.activity_database_edit.* +import com.wrbug.developerhelper.databinding.ActivityDatabaseEditBinding import org.jetbrains.anko.doAsync import org.jetbrains.anko.uiThread import java.io.File import java.util.* import kotlin.collections.ArrayList -class DatabaseEditActivity : BaseActivity() { +class DatabaseEditActivity: BaseActivity() { + private var filePath: String? = "" private lateinit var dbPath: File + private lateinit var dataBinding: ActivityDatabaseEditBinding private val tableNames = ArrayList() private var dbMap: Map = TreeMap() private val adapter = DatabaseTableAdapter(this) @@ -40,6 +40,7 @@ class DatabaseEditActivity : BaseActivity() { } companion object { + fun start(context: Context, filePath: String) { val intent = Intent(context, DatabaseEditActivity::class.java) intent.putExtra("filePath", filePath) @@ -49,7 +50,8 @@ class DatabaseEditActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_database_edit) + dataBinding = ActivityDatabaseEditBinding.inflate(layoutInflater) + setContentView(dataBinding.root) intent?.run { filePath = getStringExtra("filePath") } @@ -67,8 +69,8 @@ class DatabaseEditActivity : BaseActivity() { } private fun initTableView() { - tableView.adapter = adapter - tableView.tableViewListener = object : ITableViewListener { + dataBinding.tableView.adapter = adapter + dataBinding.tableView.tableViewListener = object: ITableViewListener { override fun onCellLongPressed(p0: RecyclerView.ViewHolder, p1: Int, p2: Int) { } @@ -96,7 +98,8 @@ class DatabaseEditActivity : BaseActivity() { if (dstDir.exists().not()) { dstDir.mkdir() } - val success = ShellManager.cpFile(dbPath.absolutePath, "${dstDir.absolutePath}/${dbPath.name}") + val success = + ShellManager.cpFile(dbPath.absolutePath, "${dstDir.absolutePath}/${dbPath.name}") if (!success) { uiThread { showToast(getString(R.string.get_database_failed)) @@ -115,11 +118,13 @@ class DatabaseEditActivity : BaseActivity() { } } - private fun setTableContainer() { - tableNameContainer.removeAllViews() + dataBinding.tableNameContainer.removeAllViews() val layoutParams = - LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) val dp10 = dp2px(10F) val dp20 = dp2px(20F) tableNames.forEachIndexed { index, name -> @@ -135,7 +140,7 @@ class DatabaseEditActivity : BaseActivity() { tv.setOnClickListener { selectTable(it.tag as Int) } - tableNameContainer.addView(tv, layoutParams) + dataBinding.tableNameContainer.addView(tv, layoutParams) } } @@ -161,9 +166,9 @@ class DatabaseEditActivity : BaseActivity() { uiThread { setHasData(!cell.isEmpty()) adapter.setAllItems(keyList, rowHeaders, cell) - var tv = tableNameContainer.getChildAt(selectedIndex) as TextView + var tv = dataBinding.tableNameContainer.getChildAt(selectedIndex) as TextView tv.setTextColor(resources.getColor(R.color.text_color_666666)) - tv = tableNameContainer.getChildAt(position) as TextView + tv = dataBinding.tableNameContainer.getChildAt(position) as TextView tv.setTextColor(resources.getColor(R.color.colorPrimary)) selectedIndex = position } @@ -172,7 +177,7 @@ class DatabaseEditActivity : BaseActivity() { } private fun setHasData(hasData: Boolean) { - noDataTv.visibility = if (hasData) View.GONE else View.VISIBLE - tableView.visibility = if (hasData) View.VISIBLE else View.GONE + dataBinding.noDataTv.visibility = if (hasData) View.GONE else View.VISIBLE + dataBinding.tableView.visibility = if (hasData) View.VISIBLE else View.GONE } } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/databaseedit/DatabaseTableAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/databaseedit/DatabaseTableAdapter.kt index 9ee8514..680b799 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/databaseedit/DatabaseTableAdapter.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/databaseedit/DatabaseTableAdapter.kt @@ -1,5 +1,6 @@ package com.wrbug.developerhelper.ui.activity.databaseedit +import android.annotation.SuppressLint import android.content.Context import android.view.View import android.view.ViewGroup @@ -55,6 +56,7 @@ class DatabaseTableAdapter(val context: Context) : AbstractTableAdapter? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_guide) - mSectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager) - viewPager.adapter = mSectionsPagerAdapter - viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { - override fun onPageSelected(position: Int) { - currentPosition = position - updateIndicators(position) - viewPager.setBackgroundColor(bgColors[position]) - buttonPre.visibility = if (position == 0) View.GONE else View.VISIBLE - buttonNext.visibility = if (position == 2) View.GONE else View.VISIBLE - buttonFinish.visibility = if (position == 2) View.VISIBLE else View.GONE - } - - override fun onPageScrollStateChanged(position: Int) { - - } - - override fun onPageScrolled(p0: Int, p1: Float, p2: Int) { - val colorUpdate = ArgbEvaluator().evaluate( - p1, - bgColors[p0], - bgColors[if (p0 == 2) p0 else p0 + 1] - ) as Int - viewPager.setBackgroundColor(colorUpdate) - } - - }) - indicators = arrayOf(imageViewIndicator0 as View, imageViewIndicator1 as View, imageViewIndicator2 as View) - } - - private fun updateIndicators(position: Int) { - for (i in 0 until indicators?.size!!) { - indicators?.get(i)?.setBackgroundResource( - if (i == position) R.drawable.onboarding_indicator_selected else R.drawable.onboarding_indicator_unselected - ) - } - } -} diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepAccessibilityFragment.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepAccessibilityFragment.kt deleted file mode 100644 index 1466d40..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepAccessibilityFragment.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.guide - -import android.content.Intent -import android.os.Bundle -import android.provider.Settings -import android.view.View -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.service.AccessibilityManager -import com.wrbug.developerhelper.service.DeveloperHelperAccessibilityService -import kotlinx.android.synthetic.main.fragment_guide.* - -class GuideStepAccessibilityFragment : GuideStepFragment() { - override fun getLabelText(): String { - return "无障碍功能" - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - checkIsOpen() - icoIv.setImageResource(R.drawable.ic_accessibility_black) - contentTv.setOnClickListener { - if (DeveloperHelperAccessibilityService.serviceRunning) { - return@setOnClickListener - } - AccessibilityManager.startService(activity) - - } - } - - private fun checkIsOpen() { - contentTv.text = - if (DeveloperHelperAccessibilityService.serviceRunning) "无障碍辅助已开启" else "无障碍辅助已关闭,将无法分析布局和页面信息,点击开启" - } - - override fun onResume() { - super.onResume() - checkIsOpen() - } - - companion object { - fun instance(): GuideStepAccessibilityFragment { - val fragment = GuideStepAccessibilityFragment() - return fragment - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepFloatWindowFragment.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepFloatWindowFragment.kt deleted file mode 100644 index 3e5fd57..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepFloatWindowFragment.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.guide - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.provider.Settings -import android.view.View -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.service.FloatWindowService -import com.wrbug.developerhelper.util.DeviceUtils -import kotlinx.android.synthetic.main.fragment_guide.* - -class GuideStepFloatWindowFragment : GuideStepFragment() { - override fun getLabelText(): String { - return getString(R.string.float_window_permission) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - icoIv.setImageResource(R.drawable.ic_float_air_bubble) - contentTv.setOnClickListener { - if (DeviceUtils.isFloatWindowOpened(activity!!)) { - return@setOnClickListener - } - startActivityForResult( - Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:${activity?.packageName}")), - 0 - ) - } - } - - override fun onResume() { - super.onResume() - checkFloatWindow() - } - - private fun checkFloatWindow() { - if (activity != null && DeviceUtils.isFloatWindowOpened(activity!!)) { - contentTv.text = getString(R.string.float_window_opened) - FloatWindowService.start(activity!!) - } else { - contentTv.text = getString(R.string.float_window_closed) - } - } - - companion object { - fun instance(): GuideStepFloatWindowFragment { - val fragment = GuideStepFloatWindowFragment() - return fragment - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == 0) { - checkFloatWindow() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepFragment.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepFragment.kt deleted file mode 100644 index 772c1d3..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepFragment.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.guide - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.BaseFragment -import kotlinx.android.synthetic.main.fragment_guide.view.* - - - abstract class GuideStepFragment : BaseFragment() { - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val rootView = inflater.inflate(R.layout.fragment_guide, container, false) - rootView.labelTv.text = getLabelText() - return rootView - } - abstract fun getLabelText(): String -} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/SectionsPagerAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/SectionsPagerAdapter.kt deleted file mode 100644 index 9515409..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/SectionsPagerAdapter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.guide - -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter - -class SectionsPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) { - private val fragments = arrayOf( - GuideStepFloatWindowFragment.instance(), - GuideStepAccessibilityFragment.instance() - ) - - override fun getItem(position: Int): Fragment { - return fragments[position] - } - - override fun getCount(): Int { - return fragments.size - } -} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoDialog.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoDialog.kt index 4d4e06e..0e85d4c 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoDialog.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoDialog.kt @@ -9,64 +9,74 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.wrbug.developerhelper.R import com.wrbug.developerhelper.commonutil.entity.ApkInfo -import com.wrbug.developerhelper.commonutil.entity.TopActivityInfo import com.wrbug.developerhelper.commonutil.UiUtils import com.wrbug.developerhelper.commonutil.dp2px -import kotlinx.android.synthetic.main.dialog_apk_info.* +import com.wrbug.developerhelper.databinding.DialogApkInfoBinding +import com.wrbug.developerhelper.util.isPortrait +import io.reactivex.rxjava3.disposables.CompositeDisposable class AppInfoDialog : DialogFragment() { - private var apkInfo: ApkInfo? = null - private var topActivity: TopActivityInfo? = null + + private val apkInfo: ApkInfo? by lazy { + arguments?.getParcelable("apkInfo") + } private var listener: AppInfoDialogEventListener? = null + private lateinit var binding: DialogApkInfoBinding + private lateinit var disposable: CompositeDisposable + private val pagerAdapter by lazy { + AppInfoPagerAdapter(this, disposable) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(DialogFragment.STYLE_NORMAL, R.style.FullScreenDialog) - arguments?.let { - apkInfo = it.getParcelable("apkInfo") - topActivity = it.getParcelable("topActivity") - } + setStyle(STYLE_NORMAL, R.style.FullScreenDialog) } - override fun onAttach(activity: Activity?) { + override fun onAttach(activity: Activity) { super.onAttach(activity) if (activity is AppInfoDialogEventListener) { listener = activity } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val view = inflater.inflate(R.layout.dialog_apk_info, container, false) - dialog.window.takeUnless { - it == null - }?.run { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + disposable = CompositeDisposable() + binding = DialogApkInfoBinding.inflate(inflater, container, false) + dialog?.window?.run { val layoutParams = attributes - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT - layoutParams.height = UiUtils.getDeviceHeight() / 2 + dp2px(40F) + if (isPortrait()) { + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + layoutParams.height = UiUtils.getDeviceHeight() / 2 + dp2px(40F) + setGravity(Gravity.TOP) + } else { + layoutParams.width = UiUtils.getDeviceWidth() / 2 + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + setGravity(Gravity.END) + } attributes = layoutParams - setGravity(Gravity.TOP) } - return view + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - titleContainer.setPadding(0, UiUtils.getStatusHeight(), 0, 0) - activity?.let { - val pagerAdapter = AppInfoPagerAdapter(this, apkInfo, topActivity) - pagerAdapter.listener = listener - viewPager.adapter = pagerAdapter - tabLayout.setupWithViewPager(viewPager) - } - apkInfo?.let { it -> - logoIv.setImageDrawable(it.getIco()) - titleTv.text = it.getAppName() - subTitleTv.text = it.applicationInfo.packageName + binding.titleContainer.setPadding(0, UiUtils.getStatusHeight(), 0, 0) + pagerAdapter.listener = listener + binding.viewPager.adapter = pagerAdapter + binding.tabLayout.setupWithViewPager(binding.viewPager) + apkInfo?.let { + binding.logoIv.setImageDrawable(it.getIco()) + binding.titleTv.text = it.getAppName() + binding.subTitleTv.text = it.applicationInfo.packageName } + pagerAdapter.loadData(apkInfo) } - override fun onDestroyView() { - super.onDestroyView() listener?.close() + disposable.dispose() + super.onDestroyView() } } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoPagerAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoPagerAdapter.kt index 529efab..44079d1 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoPagerAdapter.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoPagerAdapter.kt @@ -3,61 +3,71 @@ package com.wrbug.developerhelper.ui.activity.hierachy import android.content.Context import android.view.View import android.view.ViewGroup +import androidx.annotation.Keep import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager.widget.PagerAdapter import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.base.versionCodeLong import com.wrbug.developerhelper.commonutil.entity.ApkInfo -import com.wrbug.developerhelper.commonutil.entity.TopActivityInfo import com.wrbug.developerhelper.commonutil.shell.ShellManager import com.wrbug.developerhelper.ui.decoration.SpaceItemDecoration import com.wrbug.developerhelper.ui.widget.appdatainfoview.AppDataInfoView import com.wrbug.developerhelper.ui.widget.appsettingview.AppSettingView import com.wrbug.developerhelper.ui.widget.layoutinfoview.infopage.InfoAdapter import com.wrbug.developerhelper.ui.widget.layoutinfoview.infopage.ItemInfo -import com.wrbug.developerhelper.util.EnforceUtils import com.wrbug.developerhelper.commonutil.UiUtils import com.wrbug.developerhelper.util.format import com.wrbug.developerhelper.util.getString -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.uiThread +import io.reactivex.rxjava3.disposables.CompositeDisposable import java.util.ArrayList +@Keep class AppInfoPagerAdapter( - private val dialog: AppInfoDialog, - private val apkInfo: ApkInfo?, - private val topActivity: TopActivityInfo? -) : - PagerAdapter() { - private val context: Context = dialog.activity!! + private val dialog: AppInfoDialog, private val disposable: CompositeDisposable +) : PagerAdapter() { + + private val context: Context = dialog.requireContext() private val tabList = arrayListOf() private val viewList = arrayListOf() - private val adapter = InfoAdapter(context) - private val enforceItem = ItemInfo(context.getString(R.string.enforce_type), context.getString(R.string.analyzing)) + private val adapter by lazy { + InfoAdapter(context, analyzeItem) + } var listener: AppInfoDialogEventListener? = null - private val itemInfos = ArrayList() + private val itemInfos = ArrayList() + private val analyzeItem by lazy { + ItemInfo(getString(R.string.page_analyze), getString(R.string.click_to_analyze)).apply { + showCopy = false + textColor = context.resources.getColor(R.color.colorPrimaryDark) + setOnClickListener(View.OnClickListener { + listener?.showHierachyView() + dialog.dismissAllowingStateLoss() + }) + } + } - init { - initAppInfoTab() - initAppDataInfoTab() - initAppSettingTab() + fun loadData(apkInfo: ApkInfo?) { + initAppInfoTab(apkInfo) + initAppDataInfoTab(apkInfo) + initAppSettingTab(apkInfo) + notifyDataSetChanged() } - private fun initAppSettingTab() { + private fun initAppSettingTab(apkInfo: ApkInfo?) { tabList.add(context.getString(R.string.app_setting)) val view = AppSettingView(context) - view.apkInfo = apkInfo + view.setApkInfo(apkInfo) viewList.add(view) } - private fun initAppDataInfoTab() { + private fun initAppDataInfoTab(apkInfo: ApkInfo?) { tabList.add(context.getString(R.string.data_info)) val appDataInfoView = AppDataInfoView(context) viewList.add(appDataInfoView) appDataInfoView.apkInfo = apkInfo } - private fun initAppInfoTab() { + private fun initAppInfoTab(apkInfo: ApkInfo?) { tabList.add(context.getString(R.string.base_info)) val rv = RecyclerView(context) viewList.add(rv) @@ -68,65 +78,31 @@ class AppInfoPagerAdapter( itemDecoration.setFirstTopPadding(UiUtils.dp2px(context, 10F)) rv.addItemDecoration(itemDecoration) apkInfo?.let { it -> - val item = ItemInfo(getString(R.string.page_analyze), getString(R.string.click_to_analyze)) - item.setOnClickListener(View.OnClickListener { - listener?.showHierachyView() - dialog.dismissAllowingStateLoss() - }) - item.textColor = context.resources.getColor(R.color.colorPrimaryDark) - itemInfos.add(item) - itemInfos.add(ItemInfo("VersionCode", it.packageInfo.versionCode)) - itemInfos.add(ItemInfo("VersionName", it.packageInfo.versionName)) - topActivity?.let { - itemInfos.add(ItemInfo("Activity", it.activity)) - it.fragments.takeUnless { fragments -> - fragments.isNullOrEmpty() - }?.forEach { - if (it.hidden.not()) { - itemInfos.add(ItemInfo("Fragment", it.name)) - } - } - } + itemInfos.add(ItemInfo("PackageName", it.packageInfo.packageName)) it.applicationInfo.className?.let { name -> itemInfos.add(ItemInfo("Application", name)) } - itemInfos.add(enforceItem) + it.topActivity.takeIf { it.isNotEmpty() }?.let { + itemInfos.add(ItemInfo("Activity", it)) + } + itemInfos.add(ItemInfo("VersionName", it.packageInfo.versionName)) + itemInfos.add(ItemInfo("VersionCode", it.packageInfo.versionCodeLong)) itemInfos.add(ItemInfo("uid", it.applicationInfo.uid)) - ShellManager.getPid(it.packageInfo.packageName).takeUnless { - it.isEmpty() - }?.let { + ShellManager.getPid(it.packageInfo.packageName).takeUnless { it.isEmpty() }?.let { itemInfos.add(ItemInfo("Pid", it)) } itemInfos.add( ItemInfo( - getString(R.string.first_install_time), - it.packageInfo.firstInstallTime.format() + getString(R.string.first_install_time), it.packageInfo.firstInstallTime.format() ) ) itemInfos.add( ItemInfo( - getString(R.string.last_update_time), - it.packageInfo.lastUpdateTime.format() + getString(R.string.last_update_time), it.packageInfo.lastUpdateTime.format() ) ) itemInfos.add(ItemInfo("DataDir", it.applicationInfo.dataDir)) adapter.setItems(itemInfos) - getEnforce(it.packageInfo.packageName) - } - } - - private fun setEnforceType(type: EnforceUtils.EnforceType) { - enforceItem.content = type.type - } - - private fun getEnforce(packageName: String) { - doAsync { - val type = EnforceUtils.getEnforceType(packageName) - uiThread { - setEnforceType(type) - adapter.notifyItemChanged(enforceItem) - } - } } @@ -138,7 +114,6 @@ class AppInfoPagerAdapter( return tabList.size } - override fun instantiateItem(container: ViewGroup, position: Int): Any { container.addView(viewList[position]) return viewList[position] diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/HierarchyActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/HierarchyActivity.kt index 797b45d..b148447 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/HierarchyActivity.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/HierarchyActivity.kt @@ -7,44 +7,38 @@ import android.content.Intent import android.content.IntentFilter import android.os.Bundle import android.view.View -import com.google.gson.reflect.TypeToken -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.BaseActivity -import com.wrbug.developerhelper.constant.ReceiverConstant +import com.wrbug.developerhelper.base.BaseActivity +import com.wrbug.developerhelper.base.entry.HierarchyNode +import com.wrbug.developerhelper.base.registerReceiverComp import com.wrbug.developerhelper.commonutil.entity.ApkInfo -import com.wrbug.developerhelper.basecommon.entry.HierarchyNode -import com.wrbug.developerhelper.commonutil.entity.TopActivityInfo -import com.wrbug.developerhelper.ui.widget.hierarchyView.HierarchyView -import com.wrbug.developerhelper.commonutil.JsonHelper import com.wrbug.developerhelper.constant.ReceiverConstant.ACTION_FINISH_HIERACHY_Activity +import com.wrbug.developerhelper.databinding.ActivityHierarchyBinding import com.wrbug.developerhelper.service.FloatWindowService -import com.wrbug.developerhelper.ui.widget.layoutinfoview.LayoutInfoView -import com.wrbug.developerhelper.ui.widget.layoutinfoview.OnNodeChangedListener -import kotlinx.android.synthetic.main.activity_hierarchy.* +import com.wrbug.developerhelper.ui.widget.hierarchyView.HierarchyView +import com.wrbug.developerhelper.ui.widget.layoutinfoview.LayoutInfoDialog import java.lang.ref.WeakReference -class HierarchyActivity : BaseActivity(), AppInfoDialogEventListener, OnNodeChangedListener { - +class HierarchyActivity : BaseActivity(), AppInfoDialogEventListener { - private var apkInfo: ApkInfo? = null - private var nodeList: List? = null - private var nodeMap: HashMap? = null + private val apkInfo: ApkInfo? by lazy { + intent?.getParcelableExtra("apkInfo") + } + private val nodeList: ArrayList? by lazy { + intent?.getParcelableArrayListExtra("node") + } private var showHierachyView = false - private var topActivity: TopActivityInfo? = null + private lateinit var binding: ActivityHierarchyBinding companion object { + fun start( - context: Context?, - apkInfo: ApkInfo?, - node: ArrayList?, - topActivity: TopActivityInfo? + context: Context?, apkInfo: ApkInfo?, node: ArrayList ) { val intent = Intent(context, HierarchyActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val bundle = Bundle() bundle.putParcelable("apkInfo", apkInfo) bundle.putParcelableArrayList("node", node) - bundle.putParcelable("topActivity", topActivity) intent.putExtras(bundle) context?.startActivity(intent) } @@ -57,7 +51,7 @@ class HierarchyActivity : BaseActivity(), AppInfoDialogEventListener, OnNodeChan override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { - ReceiverConstant.ACTION_FINISH_HIERACHY_Activity -> { + ACTION_FINISH_HIERACHY_Activity -> { reference?.get()?.finish() } @@ -69,53 +63,63 @@ class HierarchyActivity : BaseActivity(), AppInfoDialogEventListener, OnNodeChan override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_hierarchy) - intent?.run { - apkInfo = getParcelableExtra("apkInfo") - nodeList = getParcelableArrayListExtra("node") - topActivity = getParcelableExtra("topActivity") - val json = getStringExtra("nodeMap") - nodeMap = JsonHelper.fromJson(json, object : TypeToken>() {}.type) - } + binding = ActivityHierarchyBinding.inflate(layoutInflater) + setContentView(binding.root) + checkNodeList() val filter = IntentFilter(ACTION_FINISH_HIERACHY_Activity) receiver.setActivity(this) - registerReceiver(receiver, filter) + registerReceiverComp(receiver, filter) showAppInfoDialog() FloatWindowService.setFloatButtonVisible(this, false) } + private fun checkNodeList() { + nodeList ?: return + if ((nodeList?.size ?: 0) <= 1) { + return + } + var hierarchyNode: HierarchyNode? = null + nodeList?.forEach { + if (hierarchyNode == null) { + hierarchyNode = it + } else if (hierarchyNode?.screenBounds?.let { it1 -> it.screenBounds?.contains(it1) } == true) { + hierarchyNode = it + } + } + nodeList?.clear() + hierarchyNode?.let { + nodeList?.add(it) + } + } + private fun showAppInfoDialog() { val dialog = AppInfoDialog() val bundle = Bundle() bundle.putParcelable("apkInfo", apkInfo) - bundle.putParcelable("topActivity", topActivity) dialog.arguments = bundle dialog.show(supportFragmentManager, "") } override fun showHierachyView() { showHierachyView = true - hierarchyView.setHierarchyNodes(nodeList) - hierarchyView.setOnHierarchyNodeClickListener(object : HierarchyView.OnHierarchyNodeClickListener { + binding.hierarchyView.setHierarchyNodes(nodeList) + binding.hierarchyView.setOnHierarchyNodeClickListener(object : + HierarchyView.OnHierarchyNodeClickListener { override fun onClick(node: HierarchyNode, parentNode: HierarchyNode?) { - hierarchyDetailView.visibility = View.VISIBLE - hierarchyDetailView.setNode(node, parentNode) - val layoutInfoView = LayoutInfoView(context, nodeList, node) - layoutInfoView.setOnNodeChangedListener(this@HierarchyActivity) - layoutInfoView.show() + binding.hierarchyDetailView.visibility = View.VISIBLE + binding.hierarchyDetailView.setNode(node, parentNode) + LayoutInfoDialog.show(supportFragmentManager, nodeList, node) { node, parentNode -> + binding.hierarchyDetailView.setNode(node, parentNode) + } } override fun onSelectedNodeChanged(node: HierarchyNode, parentNode: HierarchyNode?) { - hierarchyDetailView.visibility = View.VISIBLE - hierarchyDetailView.setNode(node, parentNode) + binding.hierarchyDetailView.visibility = View.VISIBLE + binding.hierarchyDetailView.setNode(node, parentNode) } }) } - override fun onChanged(node: HierarchyNode, parentNode: HierarchyNode?) { - hierarchyDetailView.setNode(node, parentNode) - } - override fun onDestroy() { FloatWindowService.setFloatButtonVisible(this, true) unregisterReceiver(receiver) @@ -129,18 +133,17 @@ class HierarchyActivity : BaseActivity(), AppInfoDialogEventListener, OnNodeChan } override fun onBackPressed() { - if (hierarchyDetailView.visibility == View.VISIBLE) { - hierarchyDetailView.visibility = View.GONE + if (binding.hierarchyDetailView.visibility == View.VISIBLE) { + binding.hierarchyDetailView.visibility = View.GONE return } if (showHierachyView) { showHierachyView = false - hierarchyView.visibility = View.GONE + binding.hierarchyView.visibility = View.GONE showAppInfoDialog() return } super.onBackPressed() } - } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/MainActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/MainActivity.kt index 0f1e136..d6bb8b7 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/MainActivity.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/MainActivity.kt @@ -1,211 +1,220 @@ package com.wrbug.developerhelper.ui.activity.main -import android.content.* +import android.Manifest +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.net.Uri +import android.os.Build import android.os.Bundle import android.provider.Settings import android.view.Menu import android.view.MenuItem -import android.widget.CompoundButton import androidx.appcompat.app.AlertDialog -import androidx.databinding.DataBindingUtil -import com.wrbug.developerhelper.util.NetBarStarter -import com.wrbug.developerhelper.BuildConfig +import androidx.core.view.isVisible import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.* +import com.wrbug.developerhelper.base.BaseActivity +import com.wrbug.developerhelper.base.registerReceiverComp +import com.wrbug.developerhelper.base.requestStoragePermission +import com.wrbug.developerhelper.base.setupActionBar import com.wrbug.developerhelper.commonutil.ClipboardUtils -import com.wrbug.developerhelper.commonutil.shell.Callback +import com.wrbug.developerhelper.commonutil.shell.ShellManager +import com.wrbug.developerhelper.util.setOnDoubleCheckClickListener import com.wrbug.developerhelper.constant.ReceiverConstant import com.wrbug.developerhelper.databinding.ActivityMainBinding +import com.wrbug.developerhelper.mmkv.ConfigKv +import com.wrbug.developerhelper.mmkv.manager.MMKVManager +import com.wrbug.developerhelper.model.entity.VersionInfo import com.wrbug.developerhelper.service.AccessibilityManager +import com.wrbug.developerhelper.service.DeveloperHelperAccessibilityService import com.wrbug.developerhelper.service.FloatWindowService -import com.wrbug.developerhelper.commonutil.shell.ShellManager -import com.wrbug.developerhelper.ui.activity.main.viewmodel.MainViewModel -import com.wrbug.developerhelper.ui.widget.settingitemview.SettingItemView +import com.wrbug.developerhelper.ui.activity.appbackup.BackupAppActivity import com.wrbug.developerhelper.util.DeviceUtils -import com.wrbug.developerhelper.commonutil.toInt -import com.wrbug.developerhelper.model.entity.VersionInfo -import com.wrbug.developerhelper.ui.activity.xposed.xposedsetting.XposedSettingActivity -import com.wrbug.developerhelper.util.UpdateUtils -import kotlinx.android.synthetic.main.activity_main.* - - -class MainActivity : BaseVMActivity() { +class MainActivity : BaseActivity() { + private val configKv: ConfigKv = MMKVManager.get(ConfigKv::class.java) + private val binding: ActivityMainBinding by lazy { + ActivityMainBinding.inflate(layoutInflater) + } - lateinit var binding: ActivityMainBinding - lateinit var xposedSettingView: SettingItemView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (DeviceUtils.isFloatWindowOpened()) { FloatWindowService.start(this) - "".toInt() - } - binding = DataBindingUtil.setContentView(this, R.layout.activity_main) - binding.presenter = Presenter() - xposedSettingView = binding.xposedSettingView - binding.mainVm = vm - setupActionBar(R.id.toolbar) { - } + setContentView(binding.root) + setupActionBar(R.id.toolbar) ShellManager.openAccessibilityService() + initView() initListener() val filter = IntentFilter(ReceiverConstant.ACTION_ACCESSIBILITY_SERVICE_STATUS_CHANGED) - registerReceiver(receiver, filter) + registerReceiverComp(receiver, filter) + } + + private fun initView() { + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.TIRAMISU) { + binding.notificationSettingView.checkable = false + binding.notificationSettingView.isVisible = true + binding.notificationSettingView.checked = + hasPermission(Manifest.permission.POST_NOTIFICATIONS) + } else { + binding.notificationSettingView.isVisible = false + } } private fun initListener() { - floatWindowSettingView.setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener { _, isChecked -> + binding.floatWindowSettingView.setOnCheckedChangeListener { _, isChecked -> if (isChecked && DeviceUtils.isFloatWindowOpened()) { FloatWindowService.start(this) } else { FloatWindowService.stop(this) } - }) - rootSettingView.setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener { _, _ -> - - }) - xposedSettingView.setOnClickListener { - if (xposedSettingView.isChecked().not()) { - showSnack(getString(R.string.open_xposed_first)) - return@setOnClickListener - } - startActivity(Intent(this, XposedSettingActivity::class.java)) } - captureSettingView.setOnSwitcherClickListener { - val intent = NetBarStarter.prepareVpn() - if (intent == null) { - NetBarStarter.toggle(this@MainActivity) - return@setOnSwitcherClickListener - } - startActivityForResultOk(intent) { - NetBarStarter.toggle(this@MainActivity) + binding.backupAppSettingView.setOnDoubleCheckClickListener { + requestStoragePermission { + startActivity(Intent(this, BackupAppActivity::class.java)) } } - } - - override fun getViewModel(): MainViewModel { - return obtainViewModel(MainViewModel::class.java) - } - - override fun onDestroy() { - super.onDestroy() - unregisterReceiver(receiver) - } - - inner class Presenter { - fun onAccessibilityClick() { - if (!accessibilitySettingView.checked) { + binding.accessibilitySettingView.setOnDoubleCheckClickListener { + if (!binding.accessibilitySettingView.checked) { AccessibilityManager.startAccessibilitySetting(context) } else { showSnack(getString(R.string.accessibility_service_opened)) } } - - fun onFloatWindowClick() { - if (!floatWindowSettingView.checked) { + binding.floatWindowSettingView.setOnDoubleCheckClickListener { + if (!binding.floatWindowSettingView.checked) { startActivityForResult( - Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")), - 0 + Intent( + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName") + ), 0 ) } else { showSnack(getString(R.string.float_window_opened)) } } - - fun onRootClick() { - vm.toggleRootPermission() + binding.rootSettingView.setOnDoubleCheckClickListener { + if (!DeviceUtils.isRoot()) { + showSnack(R.string.devices_is_not_root) + return@setOnDoubleCheckClickListener + } + val isRootEnable = binding.rootSettingView.isChecked() + binding.rootSettingView.checked = !isRootEnable + configKv.setOpenRoot(!isRootEnable) } + binding.notificationSettingView.setOnSwitcherClickListener { + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.TIRAMISU) { + requestPermission(arrayOf(Manifest.permission.POST_NOTIFICATIONS), + object : PermissionCallback() { + override fun granted() { + binding.notificationSettingView.checked = true + } + }) + } + } + } + - fun onCaptureClick() { - showSnack("hehehe") + override fun onResume() { + super.onResume() + checkStatus() + } + + + private fun checkStatus() { + binding.accessibilitySettingView.checked = + DeveloperHelperAccessibilityService.serviceRunning + binding.floatWindowSettingView.checked = DeviceUtils.isFloatWindowOpened() + if (configKv.isOpenRoot()) { + if (DeviceUtils.isRoot()) { + binding.rootSettingView.checked = true + } else { + binding.rootSettingView.checked = false + configKv.setOpenRoot(false) + } + } + if (DeviceUtils.isFloatWindowOpened()) { + FloatWindowService.start(application) } } + override fun onDestroy() { + super.onDestroy() + unregisterReceiver(receiver) + } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_main, menu) return super.onCreateOptionsMenu(menu) } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - item?.let { - when (it.itemId) { - R.id.about_menu -> { - showAboutDialog() - } - R.id.exit_menu -> { - showExitMenuDialog() - } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.about_menu -> { + showAboutDialog() } - } + R.id.exit_menu -> { + showExitMenuDialog() + } + } return super.onOptionsItemSelected(item) } - private fun showExitMenuDialog() = AlertDialog.Builder(this) - .setTitle(R.string.notice) + private fun showExitMenuDialog() = AlertDialog.Builder(this).setTitle(R.string.notice) .setMessage(getString(R.string.exit_content)) .setPositiveButton(getString(R.string.ok)) { _, _ -> FloatWindowService.stop(this) finish() - } - .setNegativeButton(getString(R.string.cancel), null) - .create() - .show() + }.setNegativeButton(getString(R.string.cancel), null).create().show() - private fun showAboutDialog() = AlertDialog.Builder(this) - .setTitle(R.string.about) + private fun showAboutDialog() = AlertDialog.Builder(this).setTitle(R.string.about) .setMessage(getString(R.string.about_content)) .setPositiveButton(getString(R.string.copy_group_number)) { _, _ -> ClipboardUtils.saveClipboardText(this, "627962572") showSnack(R.string.copy_success) - } - .setNeutralButton(getString(R.string.check_update)) { _, _ -> + }.setNeutralButton(getString(R.string.check_update)) { _, _ -> checkUpdate(true) - } - .create().show() - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - - } + }.create().show() private fun checkUpdate(showSnack: Boolean = false) { - if (showSnack) { - showSnack(getString(R.string.checking_update)) - } - UpdateUtils.checkUpdate(object : Callback { - override fun onSuccess(data: VersionInfo) { - if (BuildConfig.VERSION_NAME == data.versionName) { - showSnack(getString(R.string.no_new_version)) - return - } - showUpdateDialog(data) - } - - override fun onFailed(msg: String) { - if (showSnack) { - showSnack(getString(R.string.check_update_failed)) - } - } - }) +// if (showSnack) { +// showSnack(getString(R.string.checking_update)) +// } +// UpdateUtils.checkUpdate(object : Callback { +// override fun onSuccess(data: VersionInfo) { +// if (BuildConfig.VERSION_NAME == data.versionName) { +// showSnack(getString(R.string.no_new_version)) +// return +// } +// showUpdateDialog(data) +// } +// +// override fun onFailed(msg: String) { +// if (showSnack) { +// showSnack(getString(R.string.check_update_failed)) +// } +// } +// }) } - private fun showUpdateDialog(data: VersionInfo) = AlertDialog.Builder(this) - .setTitle(getString(R.string.find_new_version)) - .setMessage("版本号:${data.versionName}\n更新时间:${data.updateDate}\n大小:${data.size}\n版本说明:\n${data.feature}") - .setPositiveButton(getString(R.string.download)) { _, _ -> - val intent = Intent(Intent.ACTION_VIEW) - val uri = Uri.parse(data.downloadUrl) - intent.data = uri - startActivity(intent) - } - .create().show() + private fun showUpdateDialog(data: VersionInfo) = + AlertDialog.Builder(this).setTitle(getString(R.string.find_new_version)) + .setMessage("版本号:${data.versionName}\n更新时间:${data.updateDate}\n大小:${data.size}\n版本说明:\n${data.feature}") + .setPositiveButton(getString(R.string.download)) { _, _ -> + val intent = Intent(Intent.ACTION_VIEW) + val uri = Uri.parse(data.downloadUrl) + intent.data = uri + startActivity(intent) + }.create().show() private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { intent?.run { if (action == ReceiverConstant.ACTION_ACCESSIBILITY_SERVICE_STATUS_CHANGED) { - accessibilitySettingView.checked = intent.getBooleanExtra("status", false) + binding.accessibilitySettingView.checked = + intent.getBooleanExtra("status", false) } } } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/viewmodel/MainViewModel.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/viewmodel/MainViewModel.kt deleted file mode 100644 index c6959ae..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/viewmodel/MainViewModel.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.main.viewmodel - -import androidx.databinding.ObservableBoolean -import androidx.databinding.ObservableField -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.OnLifecycleEvent -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.BaseViewModel -import com.wrbug.developerhelper.mmkv.ConfigKv -import com.wrbug.developerhelper.mmkv.manager.MMKVManager -import com.wrbug.developerhelper.service.DeveloperHelperAccessibilityService -import com.wrbug.developerhelper.service.FloatWindowService -import com.wrbug.developerhelper.util.DeviceUtils -import javax.inject.Inject - -class MainViewModel @Inject constructor() : BaseViewModel() { - val openAccessibility = ObservableBoolean() - val openFloatWindow = ObservableBoolean() - val openRoot = ObservableBoolean() - private val configKv: ConfigKv = MMKVManager.get(ConfigKv::class.java) - private fun checkStatus() { - openAccessibility.set(DeveloperHelperAccessibilityService.serviceRunning) - openFloatWindow.set(DeviceUtils.isFloatWindowOpened()) - if (configKv.isOpenRoot()) { - if (DeviceUtils.isRoot()) { - openRoot.set(true) - } else { - openRoot.set(false) - configKv.setOpenRoot(false) - } - } - if (DeviceUtils.isFloatWindowOpened()) { - FloatWindowService.start(application) - } - } - - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - fun onResume() { - checkStatus() - } - - fun toggleRootPermission() { - if (!DeviceUtils.isRoot()) { - showSnack(R.string.devices_is_not_root) - return - } - openRoot.set(!openRoot.get()) - configKv.setOpenRoot(openRoot.get()) - } -} diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/sharedpreferencesedit/SharedPreferenceEditActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/sharedpreferencesedit/SharedPreferenceEditActivity.kt index 1516235..04089b9 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/sharedpreferencesedit/SharedPreferenceEditActivity.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/sharedpreferencesedit/SharedPreferenceEditActivity.kt @@ -6,31 +6,48 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.BaseActivity -import com.wrbug.developerhelper.basecommon.setupActionBar -import com.wrbug.developerhelper.commonutil.shell.ShellManager +import com.wrbug.developerhelper.base.BaseActivity +import com.wrbug.developerhelper.base.setupActionBar +import com.wrbug.developerhelper.commonutil.AppManagerUtils +import com.wrbug.developerhelper.commonutil.addTo +import com.wrbug.developerhelper.commonutil.dpInt +import com.wrbug.developerhelper.commonutil.runOnIO +import com.wrbug.developerhelper.util.startPageLoading +import com.wrbug.developerhelper.util.stopPageLoading +import com.wrbug.developerhelper.databinding.ActivitySharedPreferenceEditBinding import com.wrbug.developerhelper.ui.decoration.SpaceItemDecoration -import com.wrbug.developerhelper.util.OutSharedPreferenceManager -import com.wrbug.developerhelper.util.XmlUtil -import com.wrbug.developerhelper.commonutil.dp2px -import kotlinx.android.synthetic.main.activity_shared_preference_edit.* -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.uiThread +import com.wrbug.developerhelper.util.OutSharedPreference import java.io.File -class SharedPreferenceEditActivity : BaseActivity(), SharedPreferenceListAdapter.OnValueChangedListener { +class SharedPreferenceEditActivity : BaseActivity() { - - private var filePath: String = "" + private val filePath by lazy { + intent?.getStringExtra(KEY_FILE_PATH).orEmpty() + } + private val filePackageName by lazy { + intent?.getStringExtra(KEY_PACKAGE_NAME).orEmpty() + } + private val appName by lazy { + intent?.getStringExtra(KEY_APP_NAME).orEmpty() + } private lateinit var adapter: SharedPreferenceListAdapter private var saveMenuItem: MenuItem? = null + private lateinit var binding: ActivitySharedPreferenceEditBinding + private lateinit var outSharedPreference: OutSharedPreference companion object { - fun start(context: Context, filePath: String) { + + private const val KEY_FILE_PATH = "filePath" + private const val KEY_PACKAGE_NAME = "packageName" + private const val KEY_APP_NAME = "appName" + fun start(context: Context, filePath: String, packageName: String, appName: String) { val intent = Intent(context, SharedPreferenceEditActivity::class.java) - intent.putExtra("filePath", filePath) + intent.putExtra(KEY_FILE_PATH, filePath) + intent.putExtra(KEY_PACKAGE_NAME, packageName) + intent.putExtra(KEY_APP_NAME, appName) context.startActivity(intent) } @@ -38,39 +55,42 @@ class SharedPreferenceEditActivity : BaseActivity(), SharedPreferenceListAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_shared_preference_edit) - intent?.let { - filePath = it.getStringExtra("filePath") - } + binding = ActivitySharedPreferenceEditBinding.inflate(layoutInflater).inject() setupActionBar(R.id.toolbar) { if (filePath.isNotEmpty()) { title = File(filePath).name } } - - sprefRv.layoutManager = LinearLayoutManager(this) + outSharedPreference = OutSharedPreference(this, filePath) + binding.sprefRv.layoutManager = LinearLayoutManager(this) adapter = SharedPreferenceListAdapter(this) - sprefRv.adapter = adapter - adapter.setOnValueChangedListener(this) - val spaceItemDecoration = SpaceItemDecoration(dp2px(10F)) - sprefRv.addItemDecoration(spaceItemDecoration) + binding.sprefRv.adapter = adapter + adapter.setOnValueChangedListener { + saveMenuItem?.isVisible = it + } + val spaceItemDecoration = SpaceItemDecoration( + 24.dpInt(context), + 16.dpInt(context), + 24.dpInt(context), + 0, + 16.dpInt(context), + 40.dpInt(context) + ) + binding.sprefRv.addItemDecoration(spaceItemDecoration) parseXml() - - } - - override fun onChanged(changed: Boolean) { - saveMenuItem?.isVisible = changed } private fun parseXml() { - doAsync { - val xml = ShellManager.catFile(filePath) - val list = XmlUtil.parseSharedPreference(xml) - uiThread { - saveMenuItem?.isVisible = false - adapter.setData(list) - } - } + binding.flLoading.startPageLoading() + outSharedPreference.parse().runOnIO().subscribe({ + binding.emptyView.isVisible = it.isEmpty() + binding.sprefRv.isVisible = it.isNotEmpty() + saveMenuItem?.isVisible = false + adapter.setData(it) + binding.flLoading.stopPageLoading() + }, { + binding.flLoading.stopPageLoading() + }).addTo(disposable) } override fun onBackPressed() { @@ -85,44 +105,50 @@ class SharedPreferenceEditActivity : BaseActivity(), SharedPreferenceListAdapter private fun showSaveDialog() { AlertDialog.Builder(this).setMessage(getString(R.string.confirm_shared_preference_save)) - .setTitle(R.string.notice) - .setPositiveButton(R.string.save_and_exit) { _, _ -> - if (doSave()) { - showSnack(getString(R.string.save_shared_preference_success)) - finish() - } else { - showSnack(getString(R.string.save_shared_preference_failed)) - } - } - .setNegativeButton(getString(R.string.do_not_save)) { _, _ -> + .setTitle(R.string.notice).setPositiveButton(R.string.save_and_exit) { _, _ -> + doSave() + }.setNegativeButton(getString(R.string.do_not_save)) { _, _ -> finish() }.show() } - private fun doSave(): Boolean { + private fun doSave() { val data = adapter.getData() - val file = OutSharedPreferenceManager.saveToFile(this, data) - val success = ShellManager.catFile(file.absolutePath, filePath, "666") - file.delete() - return success + outSharedPreference.saveToFile(this, data).runOnIO().subscribe({ + if (it) { + parseXml() + showDialog(R.string.notice, + getString(R.string.save_shared_preference_success, appName), + R.string.ok, + R.string.cancel, + { + AppManagerUtils.restartApp( + this@SharedPreferenceEditActivity, filePackageName + ) + finish() + }) + } else { + showSnack(getString(R.string.save_shared_preference_failed)) + } + }, { + + }).addTo(disposable) } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - item?.run { - when (itemId) { - R.id.save_menu -> { - if (!doSave()) { - showSnack(getString(R.string.save_shared_preference_failed)) - return@run - } - showSnack(getString(R.string.save_shared_preference_success)) - parseXml() - } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.save_menu -> { + doSave() } } return super.onOptionsItemSelected(item) } + override fun onDestroy() { + super.onDestroy() + outSharedPreference.deleteTmpFile() + } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_shared_preference_edit, menu) saveMenuItem = menu?.findItem(R.id.save_menu) diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/sharedpreferencesedit/SharedPreferenceListAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/sharedpreferencesedit/SharedPreferenceListAdapter.kt index a952e6e..0e68495 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/sharedpreferencesedit/SharedPreferenceListAdapter.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/sharedpreferencesedit/SharedPreferenceListAdapter.kt @@ -4,33 +4,34 @@ import android.content.Context import android.text.Editable import android.text.TextWatcher import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.EditText -import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.util.inVisible +import com.wrbug.developerhelper.databinding.ItemSharedPreferenceInfoBinding import com.wrbug.developerhelper.model.entity.SharedPreferenceItemInfo class SharedPreferenceListAdapter(val context: Context) : RecyclerView.Adapter() { private val data: ArrayList = arrayListOf() - private var onValueChangedListener: OnValueChangedListener? = null + private var onValueChangedListener: ((Boolean) -> Unit)? = null private var changedFlag: Long = 0 - fun setOnValueChangedListener(listener: OnValueChangedListener) { + fun setOnValueChangedListener(listener: (Boolean) -> Unit) { onValueChangedListener = listener } fun setData(array: Array) { data.clear() changedFlag = 0 - data.addAll(array.sortedBy { it.key }) + data.addAll(array.sortedBy { it.key.lowercase() }) notifyDataSetChanged() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_shared_preference_info, parent, false)) + return ViewHolder( + ItemSharedPreferenceInfoBinding.inflate(LayoutInflater.from(context), parent, false) + ) } override fun getItemCount(): Int { @@ -39,18 +40,15 @@ class SharedPreferenceListAdapter(val context: Context) : override fun onBindViewHolder(holder: ViewHolder, position: Int) { val sharedPreferenceItemInfo = data[position] - holder.titleTv?.text = sharedPreferenceItemInfo.key - holder.contentTv?.setText(sharedPreferenceItemInfo.value) - holder.restoreTv?.visibility = - if (sharedPreferenceItemInfo.value == sharedPreferenceItemInfo.newValue) { - View.INVISIBLE - } else { - View.VISIBLE - } + holder.removeTextChangedListener() + holder.binding.titleTv.text = sharedPreferenceItemInfo.key + holder.binding.contentEt.setText(sharedPreferenceItemInfo.newValue) + holder.binding.restoreTv.inVisible = + sharedPreferenceItemInfo.value == sharedPreferenceItemInfo.newValue if (sharedPreferenceItemInfo.isValueValid()) { - holder.contentTv?.error = null + holder.binding.contentEt.error = null } else { - holder.contentTv?.error = context.getString(R.string.input_error) + holder.binding.contentEt.error = context.getString(R.string.input_error) } holder.tag = sharedPreferenceItemInfo holder.addTextChangedListener() @@ -61,21 +59,26 @@ class SharedPreferenceListAdapter(val context: Context) : } - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val titleTv: TextView? = itemView.findViewById(R.id.titleTv) - val contentTv: EditText? = itemView.findViewById(R.id.contentEt) - var restoreTv: TextView? = itemView.findViewById(R.id.restoreTv) + inner class ViewHolder(val binding: ItemSharedPreferenceInfoBinding) : + RecyclerView.ViewHolder(binding.root) { var tag: SharedPreferenceItemInfo? = null var textWatcher: TextWatcher? = null init { - restoreTv?.setOnClickListener { + binding.restoreTv.setOnClickListener { tag?.run { - contentTv?.setText(value) + binding.contentEt.setText(value) } } } + fun removeTextChangedListener() { + if (textWatcher != null) { + binding.contentEt.removeTextChangedListener(textWatcher) + textWatcher = null + } + } + fun addTextChangedListener() { if (textWatcher != null) { return @@ -90,9 +93,9 @@ class SharedPreferenceListAdapter(val context: Context) : tag?.let { it.newValue = toString() if (it.isValueValid()) { - contentTv?.error = null + binding.contentEt.error = null } else { - contentTv?.error = context.getString(R.string.input_error) + binding.contentEt.error = context.getString(R.string.input_error) } } changedFlag = if (toString() == tag?.value) { @@ -100,30 +103,22 @@ class SharedPreferenceListAdapter(val context: Context) : } else { changedFlag or 1L shl index } - onValueChangedListener?.onChanged(changedFlag != 0L) + onValueChangedListener?.invoke(changedFlag != 0L) } - restoreTv?.visibility = - if (tag?.value == tag?.newValue) { - View.INVISIBLE - } else { - View.VISIBLE - } + binding.restoreTv.inVisible = tag?.value == tag?.newValue } - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + override fun beforeTextChanged( + s: CharSequence?, start: Int, count: Int, after: Int + ) { } override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { } } - contentTv?.addTextChangedListener(textWatcher) + binding.contentEt.addTextChangedListener(textWatcher) } } - - - interface OnValueChangedListener { - fun onChanged(changed: Boolean) - } } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/xposed/shellmanager/ShellAppListAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/xposed/shellmanager/ShellAppListAdapter.kt deleted file mode 100644 index f902d9c..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/xposed/shellmanager/ShellAppListAdapter.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.xposed.shellmanager - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.commonutil.entity.ApkInfo -import com.wrbug.developerhelper.ui.widget.bottommenu.BottomMenu -import com.wrbug.developerhelper.ui.widget.bottommenu.OnItemClickListener -import com.wrbug.developerhelper.ipc.processshare.DumpDexListProcessData -import com.wrbug.developerhelper.ipc.processshare.ProcessDataManager - -class ShellAppListAdapter(val context: Context) : RecyclerView.Adapter() { - private val list = ArrayList() - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_shell_app_info, parent, false)) - - fun setData(data: List) { - list.clear() - list.addAll(data) - notifyDataSetChanged() - } - - override fun getItemCount(): Int = list.size - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val apkInfo = list[position] - holder.icoIv.setImageDrawable(apkInfo.getIco()) - holder.appNameTv.text = apkInfo.getAppName() - holder.packageNameTv.text = apkInfo.packageInfo.packageName - holder.apkInfo = apkInfo - } - - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var icoIv: ImageView = itemView.findViewById(R.id.icoIv) - var appNameTv: TextView = itemView.findViewById(R.id.appNameTv) - var packageNameTv: TextView = itemView.findViewById(R.id.packageNameTv) - var apkInfo: ApkInfo? = null - - init { - itemView.setOnClickListener { - apkInfo?.apply { - showDialog(this) - } - } - } - - private fun showDialog(apkInfo: ApkInfo) { - val bottomMenu = BottomMenu.Builder(context) - .menuItems(arrayOf(context.getString(R.string.remove_item))) - .onItemClickListener(object : OnItemClickListener { - override fun onClick(position: Int) { - when (position) { - 0 -> { - removeItem(apkInfo) - } - } - } - }) - .build() - bottomMenu.show() - } - } - - private fun removeItem(apkInfo: ApkInfo) { - val dexListProcessData = ProcessDataManager.get(DumpDexListProcessData::class.java) - val packageNames = dexListProcessData.getData() - packageNames?.apply { - remove(apkInfo.packageInfo.packageName) - dexListProcessData.setData(this) - } - val index = list.indexOf(apkInfo) - list.remove(apkInfo) - notifyItemRemoved(index) - } - -} diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/xposed/shellmanager/ShellAppManagerActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/xposed/shellmanager/ShellAppManagerActivity.kt deleted file mode 100644 index ff8cfb0..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/xposed/shellmanager/ShellAppManagerActivity.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.xposed.shellmanager - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.View -import androidx.recyclerview.widget.LinearLayoutManager -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.BaseActivity -import com.wrbug.developerhelper.basecommon.setupActionBar -import com.wrbug.developerhelper.commonutil.AppInfoManager -import com.wrbug.developerhelper.commonutil.entity.ApkInfo -import com.wrbug.developerhelper.ipc.processshare.DumpDexListProcessData -import com.wrbug.developerhelper.ipc.processshare.ProcessDataManager -import kotlinx.android.synthetic.main.activity_shell_app_manager.* -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.uiThread - -class ShellAppManagerActivity : BaseActivity() { - private var tmpList = ArrayList() - private val adapter: ShellAppListAdapter by lazy { - ShellAppListAdapter(this) - } - - companion object { - fun start(context: Context) { - context.startActivity(Intent(context, ShellAppManagerActivity::class.java)) - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_shell_app_manager) - setupActionBar(R.id.toolbar) { - title = getString(R.string.shell_app_manager) - } - swipeLayout.setOnRefreshListener { - getShellApp() - } - initRv() - getShellApp() - } - - private fun initRv() { - shellAppRv.layoutManager = LinearLayoutManager(this) - shellAppRv.adapter = adapter - } - - private fun getShellApp() { - swipeLayout.isRefreshing = true - doAsync { - val dexListProcessData = ProcessDataManager.get(DumpDexListProcessData::class.java) - val packageNames = dexListProcessData.getData() - if (packageNames == null || packageNames.isEmpty()) { - uiThread { - swipeLayout.isRefreshing = false - emptyView.visibility = View.VISIBLE - shellAppRv.visibility = View.INVISIBLE - } - tmpList.clear() - return@doAsync - } - if (tmpList.size != packageNames.size || !tmpList.containsAll(packageNames)) { - val data = ArrayList() - packageNames.forEach { it -> - AppInfoManager.getAppByPackageName(it)?.let { - data.add(it) - } - } - tmpList = packageNames - uiThread { - setData(data) - } - } else { - uiThread { - swipeLayout.isRefreshing = false - } - } - } - } - - private fun setData(list: List) { - swipeLayout.isRefreshing = false - emptyView.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE - shellAppRv.visibility = if (list.isEmpty()) View.INVISIBLE else View.VISIBLE - if (list.isEmpty().not()) { - adapter.setData(list) - } - } -} diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/xposed/xposedsetting/XposedSettingActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/xposed/xposedsetting/XposedSettingActivity.kt deleted file mode 100644 index 4a491ad..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/xposed/xposedsetting/XposedSettingActivity.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.xposed.xposedsetting - -import android.os.Bundle -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.BaseActivity -import com.wrbug.developerhelper.basecommon.setupActionBar -import com.wrbug.developerhelper.ui.activity.xposed.shellmanager.ShellAppManagerActivity -import kotlinx.android.synthetic.main.activity_xposed_setting.* - -class XposedSettingActivity : BaseActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_xposed_setting) - setupActionBar(R.id.toolbar) { - title = getString(R.string.xposed_setting) - } - shellSettingItemView.setOnClickListener { - ShellAppManagerActivity.start(this) - } - } -} diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/adapter/ExMultiTypeAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/ExMultiTypeAdapter.kt new file mode 100644 index 0000000..fc192c7 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/ExMultiTypeAdapter.kt @@ -0,0 +1,36 @@ +package com.wrbug.developerhelper.ui.adapter + +import com.drakeet.multitype.MultiTypeAdapter +import com.wrbug.developerhelper.ui.adapter.bean.EmptyViewItemData +import com.wrbug.developerhelper.ui.adapter.bean.LoadingViewItemData +import com.wrbug.developerhelper.ui.adapter.delegate.EmptyViewItemDelegate +import com.wrbug.developerhelper.ui.adapter.delegate.LoadingItemDelegate + +class ExMultiTypeAdapter : MultiTypeAdapter() { + + companion object { + fun get() = lazy { + ExMultiTypeAdapter() + } + } + + init { + register(EmptyViewItemDelegate()) + register(LoadingItemDelegate()) + } + + fun showEmpty(title: String = "") { + items = listOf(EmptyViewItemData(title)) + notifyDataSetChanged() + } + + fun showLoading() { + items = listOf(LoadingViewItemData) + notifyDataSetChanged() + } + + fun loadData(list: List) { + items = list.toList() + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/adapter/bean/EmptyViewItemData.kt b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/bean/EmptyViewItemData.kt new file mode 100644 index 0000000..190efdb --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/bean/EmptyViewItemData.kt @@ -0,0 +1,5 @@ +package com.wrbug.developerhelper.ui.adapter.bean + +class EmptyViewItemData(val title: String = "") + +object LoadingViewItemData \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/BaseItemViewBindingDelegate.kt b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/BaseItemViewBindingDelegate.kt new file mode 100644 index 0000000..ad8b57c --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/BaseItemViewBindingDelegate.kt @@ -0,0 +1,27 @@ +package com.wrbug.developerhelper.ui.adapter.delegate + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.viewbinding.ViewBinding +import com.drakeet.multitype.ItemViewDelegate + +abstract class BaseItemViewBindingDelegate : + ItemViewDelegate>() { + + final override fun onBindViewHolder(holder: ViewBindingHolder, item: T) { + onBindViewHolder(holder.binding, item) + } + + final override fun onCreateViewHolder( + context: Context, + parent: ViewGroup + ): ViewBindingHolder { + return ViewBindingHolder(onCreateViewBinding(LayoutInflater.from(context), parent)) + } + + abstract fun onCreateViewBinding(inflater: LayoutInflater, parent: ViewGroup): B + + abstract fun onBindViewHolder(binding: B, item: T) + +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/EmptyViewItemDelegate.kt b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/EmptyViewItemDelegate.kt new file mode 100644 index 0000000..e191a21 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/EmptyViewItemDelegate.kt @@ -0,0 +1,27 @@ +package com.wrbug.developerhelper.ui.adapter.delegate + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.databinding.ItemEmptyDataBinding +import com.wrbug.developerhelper.ui.adapter.bean.EmptyViewItemData +import com.wrbug.developerhelper.util.getString + +class EmptyViewItemDelegate : + BaseItemViewBindingDelegate() { + override fun onBindViewHolder(binding: ItemEmptyDataBinding, item: EmptyViewItemData) { + if (item.title.isNotEmpty()) { + binding.emptyView.setTitle(item.title) + } else { + binding.emptyView.setTitle(R.string.no_data.getString()) + } + } + + override fun onCreateViewBinding( + inflater: LayoutInflater, + parent: ViewGroup + ): ItemEmptyDataBinding { + return ItemEmptyDataBinding.inflate(inflater, parent, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/LoadingItemDelegate.kt b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/LoadingItemDelegate.kt new file mode 100644 index 0000000..d778443 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/LoadingItemDelegate.kt @@ -0,0 +1,20 @@ +package com.wrbug.developerhelper.ui.adapter.delegate + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.wrbug.developerhelper.databinding.ItemLoadingDataBinding +import com.wrbug.developerhelper.ui.adapter.bean.LoadingViewItemData + +class LoadingItemDelegate : + BaseItemViewBindingDelegate() { + override fun onBindViewHolder(binding: ItemLoadingDataBinding, item: LoadingViewItemData) { + + } + + override fun onCreateViewBinding( + inflater: LayoutInflater, + parent: ViewGroup + ): ItemLoadingDataBinding { + return ItemLoadingDataBinding.inflate(inflater, parent, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/ViewBindingHolder.kt b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/ViewBindingHolder.kt new file mode 100644 index 0000000..4eb89fe --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/ViewBindingHolder.kt @@ -0,0 +1,7 @@ +package com.wrbug.developerhelper.ui.adapter.delegate + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding + +class ViewBindingHolder(val binding: T) : + RecyclerView.ViewHolder(binding.root) \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/decoration/SpaceItemDecoration.kt b/app/src/main/java/com/wrbug/developerhelper/ui/decoration/SpaceItemDecoration.kt index d31ab71..5fb18cf 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/decoration/SpaceItemDecoration.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/decoration/SpaceItemDecoration.kt @@ -3,6 +3,7 @@ package com.wrbug.developerhelper.ui.decoration import android.graphics.Rect import android.view.View import androidx.recyclerview.widget.RecyclerView +import com.wrbug.developerhelper.commonutil.dpInt /** * SpaceItemDecoration @@ -10,15 +11,37 @@ import androidx.recyclerview.widget.RecyclerView * @author wrbug * @since 2017年9月29日 */ -class SpaceItemDecoration : RecyclerView.ItemDecoration { - private var leftSpace: Int = 0 - private var topSpace: Int = 0 - private var rightSpace: Int = 0 - private var bottomSpace: Int = 0 - private var firstTopSpace = 0 - private var lastBottomSpace: Int = 0 - - override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { +class SpaceItemDecoration( + private val leftSpace: Int = 0, + private val topSpace: Int = 0, + private val rightSpace: Int = 0, + private val bottomSpace: Int = 0, + private var firstTopSpace: Int = 0, + private var lastBottomSpace: Int = 0, +) : RecyclerView.ItemDecoration() { + companion object { + val standard = SpaceItemDecoration( + 24.dpInt(), + 12.dpInt(), + 24.dpInt(), + 12.dpInt(), + 24.dpInt(), + 40.dpInt() + ) + } + + constructor(space: Int) : this( + bottomSpace = space, + topSpace = space, + rightSpace = space, + leftSpace = space, + lastBottomSpace = space, + firstTopSpace = space + ) + + override fun getItemOffsets( + outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State + ) { super.getItemOffsets(outRect, view, parent, state) outRect.left = leftSpace outRect.right = rightSpace @@ -35,14 +58,6 @@ class SpaceItemDecoration : RecyclerView.ItemDecoration { } - constructor(space: Int) { - bottomSpace = space - topSpace = space - rightSpace = space - leftSpace = space - lastBottomSpace = space - firstTopSpace = space - } fun setLastBottomPadding(space: Int) { lastBottomSpace = space @@ -51,13 +66,4 @@ class SpaceItemDecoration : RecyclerView.ItemDecoration { fun setFirstTopPadding(space: Int) { firstTopSpace = space } - - constructor(leftSpace: Int, topSpace: Int, rightSpace: Int, bottomSpace: Int) { - this.leftSpace = leftSpace - this.topSpace = topSpace - this.rightSpace = rightSpace - this.bottomSpace = bottomSpace - lastBottomSpace = bottomSpace - firstTopSpace = topSpace - } } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/appbar/AppBar.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appbar/AppBar.kt new file mode 100644 index 0000000..c208920 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appbar/AppBar.kt @@ -0,0 +1,88 @@ +package com.wrbug.developerhelper.ui.widget.appbar + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentActivity +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.databinding.ViewAppBarBinding +import com.wrbug.developerhelper.util.getColor +import com.wrbug.developerhelper.util.setOnDoubleCheckClickListener + +class AppBar @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { + + private val binding = ViewAppBarBinding.inflate(LayoutInflater.from(context), this, false) + + init { + setBackgroundColor(R.color.colorPrimaryDark.getColor(context)) + addView(binding.root) + binding.ivBack.setOnDoubleCheckClickListener { + if (context is FragmentActivity) { + context.onBackPressedDispatcher.onBackPressed() + } + } + attrs?.let { + initAttr(it) + } + } + + private fun initAttr(attrs: AttributeSet) { + with(context.obtainStyledAttributes(attrs, R.styleable.AppBar)) { + val title = getString(R.styleable.AppBar_title) + setTitle(title) + val subTitle = getString(R.styleable.AppBar_subTitle) + setSubTitle(subTitle) + val showBack = getBoolean(R.styleable.AppBar_showBack, false) + showBack(showBack) + recycle() + } + } + + fun setSubTitle(subTitle: CharSequence?) { + binding.tvSubTitle.text = subTitle + binding.tvSubTitle.isGone = subTitle.isNullOrEmpty() + } + + fun showBack(showBack: Boolean) { + binding.ivBack.isInvisible = !showBack + } + + fun setTitle(title: CharSequence?) { + binding.tvTitle.text = title + } + + override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) { + if (child == binding.root) { + super.addView(child, index, params) + return + } + binding.flRight.addView(child, index, params) + } + + override fun addViewInLayout( + child: View?, + index: Int, + params: ViewGroup.LayoutParams?, + preventRequestLayout: Boolean + ): Boolean { + return super.addViewInLayout(child, index, params, preventRequestLayout) + } + + +// fun setMenu(title: String?, listener: (View) -> Unit) { +// binding.tvRightBtn.text = title +// binding.tvRightBtn.isGone = title.isNullOrEmpty() +// binding.tvRightBtn.setOnDoubleCheckClickListener { +// listener(it) +// } +// } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/appdatainfoview/AppDataInfoView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appdatainfoview/AppDataInfoView.kt index 1d844e6..292ab52 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/appdatainfoview/AppDataInfoView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appdatainfoview/AppDataInfoView.kt @@ -14,37 +14,39 @@ import com.wrbug.developerhelper.commonutil.shell.ShellManager import com.wrbug.developerhelper.ui.activity.databaseedit.DatabaseEditActivity import com.wrbug.developerhelper.ui.activity.sharedpreferencesedit.SharedPreferenceEditActivity import com.wrbug.developerhelper.commonutil.UiUtils -import kotlinx.android.synthetic.main.view_app_data_info.view.* +import com.wrbug.developerhelper.databinding.ViewAppDataInfoBinding import org.jetbrains.anko.doAsync import org.jetbrains.anko.uiThread import java.io.File -class AppDataInfoView : FrameLayout { +class AppDataInfoView: FrameLayout { + + private lateinit var binding: ViewAppDataInfoBinding var apkInfo: ApkInfo? = null set(value) { field = value loadData() } - constructor(context: Context) : super(context) { + constructor(context: Context): super(context) { initView() } - constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) { + constructor(context: Context, attributeSet: AttributeSet): super(context, attributeSet) { initView() } private fun initView() { - LayoutInflater.from(context).inflate(R.layout.view_app_data_info, this) + binding = ViewAppDataInfoBinding.inflate(LayoutInflater.from(context), this, true) } private fun loadData() { apkInfo?.run { - apkPathTv.text = applicationInfo.publicSourceDir + binding.apkPathTv.text = applicationInfo.publicSourceDir val apkSignInfo = ApkUtils.getApkSignInfo(context, applicationInfo.packageName) - apkSha1Tv.text = apkSignInfo.sha1 - apkMd5Tv.text = apkSignInfo.md5 + binding.apkSha1Tv.text = apkSignInfo.sha1 + binding.apkMd5Tv.text = apkSignInfo.md5 getSharedPreferencesFiles(applicationInfo.packageName) getDatabaseFiles(applicationInfo.packageName) } @@ -55,11 +57,12 @@ class AppDataInfoView : FrameLayout { val sqliteFiles = ShellManager.getSqliteFiles(packageName) uiThread { if (sqliteFiles.isEmpty()) { - defaultDbTv.setText(R.string.none) + binding.defaultDbTv.setText(R.string.none) return@uiThread } - databaseContainer.removeAllViews() - val params = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + binding.databaseContainer.removeAllViews() + val params = + LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) for (sqliteFile in sqliteFiles) { val textView = AppCompatTextView(context) textView.text = sqliteFile.name @@ -67,7 +70,7 @@ class AppDataInfoView : FrameLayout { textView.setTextColor(resources.getColor(R.color.item_content_text)) textView.setPadding(0, UiUtils.dp2px(context, 8F), 0, 0) textView.tag = sqliteFile - databaseContainer.addView(textView, params) + binding.databaseContainer.addView(textView, params) textView.setOnClickListener { DatabaseEditActivity.start(context, (it.tag as File).absolutePath) } @@ -82,11 +85,12 @@ class AppDataInfoView : FrameLayout { val files = AppInfoManager.getSharedPreferencesFiles(packageName) uiThread { if (files.isEmpty()) { - defaultSpTv.setText(R.string.none) + binding.defaultSpTv.setText(R.string.none) return@uiThread } - sharedPreferenceContainer.removeAllViews() - val params = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + binding.sharedPreferenceContainer.removeAllViews() + val params = + LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) files.forEach { val textView = AppCompatTextView(context) textView.text = it.name @@ -94,9 +98,14 @@ class AppDataInfoView : FrameLayout { textView.setTextColor(resources.getColor(R.color.item_content_text)) textView.setPadding(0, UiUtils.dp2px(context, 8F), 0, 0) textView.tag = it - sharedPreferenceContainer.addView(textView, params) + binding.sharedPreferenceContainer.addView(textView, params) textView.setOnClickListener { - SharedPreferenceEditActivity.start(context, (it.tag as File).absolutePath) + SharedPreferenceEditActivity.start( + context, + (it.tag as File).absolutePath, + packageName, + apkInfo?.getAppName() ?: "" + ) } } } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/AppSettingView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/AppSettingView.kt index 5ea318f..aaeb840 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/AppSettingView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/AppSettingView.kt @@ -1,39 +1,32 @@ package com.wrbug.developerhelper.ui.widget.appsettingview -import android.Manifest import android.app.Activity -import android.app.AlertDialog import android.content.Context -import android.content.DialogInterface import android.util.AttributeSet import android.view.LayoutInflater import android.widget.ScrollView +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.FragmentActivity +import com.wrbug.developerhelper.BuildConfig import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.showToast +import com.wrbug.developerhelper.base.requestStoragePermission +import com.wrbug.developerhelper.base.showToast import com.wrbug.developerhelper.commonutil.AppManagerUtils import com.wrbug.developerhelper.commonutil.entity.ApkInfo +import com.wrbug.developerhelper.util.setOnRootCheckClickListener +import com.wrbug.developerhelper.util.visible +import com.wrbug.developerhelper.databinding.DialogBackupAppSelectBinding +import com.wrbug.developerhelper.databinding.ViewAppSettingBinding import com.wrbug.developerhelper.mmkv.ConfigKv import com.wrbug.developerhelper.mmkv.manager.MMKVManager -import kotlinx.android.synthetic.main.view_app_setting.view.* -import android.content.Intent -import android.net.Uri -import androidx.appcompat.widget.AppCompatButton -import com.wrbug.developerhelper.basecommon.BaseActivity -import com.wrbug.developerhelper.commonutil.shell.ShellManager -import com.wrbug.developerhelper.commonutil.zip -import com.wrbug.developerhelper.util.BackupUtils -import com.wrbug.developerhelper.commonutil.toUri -import gdut.bsx.share2.Share2 -import gdut.bsx.share2.ShareContentType -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.uiThread -import java.io.File - +import io.reactivex.rxjava3.disposables.CompositeDisposable class AppSettingView : ScrollView { - var apkInfo: ApkInfo? = null + + private var apkInfo: ApkInfo? = null private val configKv = MMKVManager.get(ConfigKv::class.java) - private var exportDexBtn: AppCompatButton? = null + private lateinit var binding: ViewAppSettingBinding + private lateinit var disposable: CompositeDisposable constructor(context: Context) : super(context) { initView() @@ -43,89 +36,85 @@ class AppSettingView : ScrollView { initView() } - private fun initView() { - LayoutInflater.from(context).inflate(R.layout.view_app_setting, this) - exportDexBtn = findViewById(R.id.exportDexBtn) - if (configKv.isOpenRoot().not()) { - backupApkBtn.isEnabled = false - backupApkDataDirBtn.isEnabled = false - restartAppBtn.isEnabled = false - stopAppBtn.isEnabled = false - deleteAppDataBtn.isEnabled = false - exportDexBtn?.isEnabled = false + fun setApkInfo(apkInfo: ApkInfo?) { + this.apkInfo = apkInfo + if (apkInfo?.applicationInfo?.packageName == BuildConfig.APPLICATION_ID) { + binding.uninstallAppBtn.visible = false } + } + + private fun initView() { + binding = ViewAppSettingBinding.inflate(LayoutInflater.from(context), this, true) initListener() } private fun initListener() { - backupApkBtn.setOnClickListener { - doBackupApk() + binding.backupAppBtn.setOnRootCheckClickListener { + showBackupSelect() } - backupApkDataDirBtn.setOnClickListener { - doBackupDataDir() + binding.restoreAppBtn.setOnRootCheckClickListener { } - restartAppBtn.setOnClickListener { + binding.restartAppBtn.setOnRootCheckClickListener { doRestartApp() } - stopAppBtn.setOnClickListener { + binding.stopAppBtn.setOnRootCheckClickListener { doStopApp() } - deleteAppDataBtn.setOnClickListener { + binding.deleteAppDataBtn.setOnRootCheckClickListener { doDeleteAppData() } - uninstallAppBtn.setOnClickListener { + binding.uninstallAppBtn.setOnRootCheckClickListener { doUninstallApp() } - - exportDexBtn?.setOnClickListener { - doBackupDexData() - } } - private fun doBackupDexData() { - apkInfo?.apply { - showToast(context.getString(R.string.packing_files)) - doAsync { - val dir = File(context.externalCacheDir, "dex/${applicationInfo.packageName}") - if (dir.exists()) { - ShellManager.rmFile(dir.absolutePath) - } - dir.mkdirs() - val dexDir = "/data/data/${applicationInfo.packageName}/dump" - val lsDir = ShellManager.lsDir(dexDir) - if (lsDir.isEmpty()) { - uiThread { - showToast(context.getString(R.string.no_dex_files)) - } - return@doAsync - } - if (ShellManager.cpFile(dexDir, dir.absolutePath)) { - val zipFile = File(context.externalCacheDir, "${apkInfo?.getAppName() ?: ""}-dex.zip") - dir.zip(zipFile) - val uri = zipFile.toUri(context) - if (uri == null) { - showToast(R.string.export_failed) - return@doAsync - } - uiThread { - showShareDexNotice(uri) - } - } else { - uiThread { - showToast(context.getString(R.string.export_failed)) - } - } + private fun showBackupSelect() { + context.requestStoragePermission { + val selected = booleanArrayOf(false, false, false) + val binding = DialogBackupAppSelectBinding.inflate(LayoutInflater.from(context)) + binding.cbApk.setOnCheckedChangeListener { _, isChecked -> + selected[0] = isChecked + } + binding.cbData.setOnCheckedChangeListener { _, isChecked -> + selected[1] = isChecked } + binding.cbAndroidData.setOnCheckedChangeListener { _, isChecked -> + selected[2] = isChecked + } + AlertDialog.Builder(context) + .setTitle(R.string.backup_app_file) + .setView(binding.root) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton( + R.string.ok + ) { _, _ -> + doBackup(selected) + }.create().show() } } - private fun showShareDexNotice(uri: Uri) { - Share2.Builder(context as Activity) - .setContentType(ShareContentType.FILE) - .setShareFileUri(uri) - .setOnActivityResult(10) - .build() - .shareBySystem() + override fun onAttachedToWindow() { + super.onAttachedToWindow() + disposable = CompositeDisposable() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + disposable.dispose() + } + + private fun doBackup(selected: BooleanArray) { + val activity = context as? FragmentActivity ?: return + if (selected.find { it } == null) { + return + } + BackupAppDialog.show( + activity.supportFragmentManager, + apkInfo, + selected[0], + selected[1], + selected[2] + ) } private fun doUninstallApp() { @@ -140,13 +129,13 @@ class AppSettingView : ScrollView { } apkInfo?.apply { showNotice( - context.getString(R.string.confirm_delete_app_data), - DialogInterface.OnClickListener { _, _ -> - if (AppManagerUtils.clearAppData(applicationInfo.packageName)) { - activityFinish() - showToast(context.getString(R.string.clear_complete)) - } - }) + context.getString(R.string.confirm_delete_app_data) + ) { + if (AppManagerUtils.clearAppData(applicationInfo.packageName)) { + activityFinish() + showToast(context.getString(R.string.clear_complete)) + } + } } } @@ -156,11 +145,11 @@ class AppSettingView : ScrollView { return } apkInfo?.apply { - showNotice(context.getString(R.string.confirm_stop_app), DialogInterface.OnClickListener { _, _ -> + showNotice(context.getString(R.string.confirm_stop_app)) { if (AppManagerUtils.forceStopApp(applicationInfo.packageName)) { activityFinish() } - }) + } } } @@ -169,112 +158,15 @@ class AppSettingView : ScrollView { return } apkInfo?.apply { - showNotice(context.getString(R.string.confirm_restart_app), DialogInterface.OnClickListener { _, _ -> - if (!AppManagerUtils.forceStopApp(applicationInfo.packageName)) { - showToast(context.getString(R.string.restart_failed)) - return@OnClickListener - } + showNotice( + context.getString(R.string.confirm_restart_app) + ) { + AppManagerUtils.restartApp(context, applicationInfo.packageName) activityFinish() - val intent = context.packageManager.getLaunchIntentForPackage(applicationInfo.packageName) - intent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) - }) - } - } - - private fun doBackupDataDir() { - if (checkRoot().not()) { - return - } - apkInfo?.apply { - val backupAppData = BackupUtils.backupAppData(applicationInfo.packageName, applicationInfo.dataDir) - if (backupAppData == null) { - showToast(context.getString(R.string.backup_failed)) - return } - if (context !is BaseActivity) { - showToast(context.getString(R.string.backup_success_msg)) - return - } - showShareDataNotice(backupAppData) } } - private fun showShareDataNotice(backupAppData: File) { - showNotice( - context.getString(R.string.backup_success_and_share_msg), - DialogInterface.OnClickListener { _, _ -> - (context as BaseActivity).requestPermission(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), - object : BaseActivity.PermissionCallback() { - override fun granted() { - val zipFile = File(context.externalCacheDir, "${apkInfo?.getAppName() ?: ""}-data.zip") - backupAppData.zip(zipFile) - val uri = zipFile.toUri(context) - if (uri == null) { - showToast(context.getString(R.string.share_failed)) - return - } - activityFinish() - Share2.Builder(context as Activity) - .setContentType(ShareContentType.FILE) - .setShareFileUri(uri) - .setOnActivityResult(10) - .build() - .shareBySystem() - } - - }) - - }) - } - - private fun doBackupApk() { - if (checkRoot().not()) { - return - } - apkInfo?.apply { - val uri = BackupUtils.backupApk( - applicationInfo.packageName, - applicationInfo.publicSourceDir, - "${getAppName()}_${packageInfo.versionName}.apk" - ) - if (uri == null) { - showToast(context.getString(R.string.backup_failed)) - return - } - if (context !is BaseActivity) { - showToast(context.getString(R.string.backup_success_msg)) - return - } - showShareApkDialog(uri) - - } - } - - private fun showShareApkDialog(uri: Uri) { - showNotice( - context.getString(R.string.backup_success_and_share_msg), - DialogInterface.OnClickListener { _, _ -> - (context as BaseActivity).requestPermission(arrayOf( - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ), - object : BaseActivity.PermissionCallback() { - override fun granted() { - activityFinish() - Share2.Builder(context as Activity) - .setContentType(ShareContentType.FILE) - .setShareFileUri(uri) - .setOnActivityResult(10) - .build() - .shareBySystem() - } - - }) - - }) - } - private fun checkRoot(): Boolean { if (configKv.isOpenRoot().not()) { showToast(context.getString(R.string.please_open_root)) @@ -283,18 +175,17 @@ class AppSettingView : ScrollView { return true } - fun activityFinish() { + private fun activityFinish() { if (context is Activity) { (context as Activity).finish() } } - private fun showNotice(msg: String, listener: DialogInterface.OnClickListener) { - AlertDialog.Builder(context) - .setTitle(R.string.notice) - .setMessage(msg) - .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.ok, listener) - .create().show() + private fun showNotice(msg: String, listener: () -> Unit) { + AlertDialog.Builder(context).setTitle(R.string.notice).setMessage(msg) + .setNegativeButton(R.string.cancel, null).setPositiveButton( + R.string.ok + ) { _, _ -> listener() }.create().show() } + } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/BackupAppDialog.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/BackupAppDialog.kt new file mode 100644 index 0000000..2173665 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/BackupAppDialog.kt @@ -0,0 +1,270 @@ +package com.wrbug.developerhelper.ui.widget.appsettingview + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.base.ExtraKey +import com.wrbug.developerhelper.base.versionCodeLong +import com.wrbug.developerhelper.commonutil.addTo +import com.wrbug.developerhelper.commonutil.entity.ApkInfo +import com.wrbug.developerhelper.commonutil.observeOnMain +import com.wrbug.developerhelper.commonutil.shell.ShellManager +import com.wrbug.developerhelper.util.setOnDoubleCheckClickListener +import com.wrbug.developerhelper.databinding.DialogBackupAppBinding +import com.wrbug.developerhelper.model.entity.BackupAppItemInfo +import com.wrbug.developerhelper.ui.widget.backupprogress.BackupProgressView +import com.wrbug.developerhelper.util.BackupUtils +import com.wrbug.developerhelper.util.format +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.schedulers.Schedulers + + +class BackupAppDialog : BottomSheetDialogFragment() { + companion object { + fun show( + fragmentManager: FragmentManager, + apkInfo: ApkInfo?, + backupApk: Boolean, + backupData: Boolean, + backupAndroidData: Boolean + ) { + BackupAppDialog().apply { + arguments = Bundle().apply { + putParcelable(ExtraKey.DATA, apkInfo) + putBoolean(ExtraKey.KEY_1, backupApk) + putBoolean(ExtraKey.KEY_2, backupData) + putBoolean(ExtraKey.KEY_3, backupAndroidData) + } + }.show(fragmentManager, "BackupAppDialog") + } + } + + private lateinit var binding: DialogBackupAppBinding + private lateinit var disposable: CompositeDisposable + private lateinit var dateDir: String + private var backupTimeStamp: Long = 0 + + private val apkInfo: ApkInfo? by lazy { + arguments?.getParcelable(ExtraKey.DATA) + } + + private val backupApk by lazy { + arguments?.getBoolean(ExtraKey.KEY_1) ?: false + } + + + private val backupData by lazy { + arguments?.getBoolean(ExtraKey.KEY_2) ?: false + } + + + private val backupAndroidData by lazy { + arguments?.getBoolean(ExtraKey.KEY_3) ?: false + } + + private var successCount = 0 + set(value) { + field = value + checkIsAllSuccess() + } + private var hasError = false + set(value) { + field = value + checkIsAllSuccess() + } + + private fun checkIsAllSuccess() { + if (hasError) { + binding.tvNotice.isVisible = false + binding.btnExit.isVisible = true + binding.zipFileProgress.setStatus(BackupProgressView.Status.Canceled) + return + } + if (successCount < 3) { + return + } + createInfoFile() + } + + private fun createInfoFile() { + binding.zipFileProgress.setStatus(BackupProgressView.Status.Processing) + runOnIo().map { apkInfo -> + BackupUtils.zipBackupFile(apkInfo.packageInfo.packageName.orEmpty(), dateDir) + ?.let { apkInfo to it } + ?: throw Exception() + }.map { + val info = BackupAppItemInfo( + it.second.name, + backupApk, + backupData, + backupAndroidData, + it.first.generateBackupApkFileName(), + it.first.packageInfo.versionName.orEmpty(), + it.first.packageInfo.versionCodeLong, + it.first.packageInfo.packageName.orEmpty(), + backupTimeStamp + ) + val success = + BackupUtils.saveBackupInfo(it.first, info, it.second.name) + if (success) { + return@map it.second.absolutePath + } + throw Exception() + }.observeOnMain().subscribe({ + binding.zipFileProgress.setStatus(BackupProgressView.Status.Success, it) + showSuccess() + }, { + hasError = true + binding.zipFileProgress.setStatus(BackupProgressView.Status.Failed) + }).addTo(disposable) + } + + private fun showSuccess() { + binding.btnExit.isVisible = true + binding.tvNotice.text = getString( + R.string.backup_success_notice, + BackupUtils.getAppBackupDir( + apkInfo?.packageInfo?.packageName.orEmpty() + ).absolutePath + ) + binding.tvNotice.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.material_color_green_600 + ) + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + successCount = 0 + backupTimeStamp = System.currentTimeMillis() + dateDir = backupTimeStamp.format("yyyy-MM-dd-HH_mm_ss") + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + disposable = CompositeDisposable() + binding = DialogBackupAppBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + setCancelable(false) + setCanceledOnTouchOutside(false) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.apkProgress.setTitle(R.string.item_backup_apk) + binding.androidDataProgress.setTitle(R.string.item_backup_android_data) + binding.dataProgress.setTitle(R.string.item_backup_data) + binding.zipFileProgress.setTitle(R.string.item_zip_backup_file) + binding.zipFileProgress.setStatus(BackupProgressView.Status.Waiting) + binding.btnExit.setOnDoubleCheckClickListener { + dismissAllowingStateLoss() + } + backupApk() + backupData() + backupAndroidData() + } + + private fun backupApk() { + if (!backupApk) { + successCount++ + binding.apkProgress.setStatus(BackupProgressView.Status.Ignore) + return + } + binding.apkProgress.setStatus(BackupProgressView.Status.Processing) + runOnIo().map { + BackupUtils.backupApk( + it.applicationInfo.packageName, + dateDir, + it.applicationInfo.publicSourceDir, + it.generateBackupApkFileName() + ) ?: throw Exception() + }.observeOnMain().subscribe({ + successCount++ + binding.apkProgress.setStatus(BackupProgressView.Status.Success, it) + }, { + hasError = true + binding.apkProgress.setStatus(BackupProgressView.Status.Failed) + }).addTo(disposable) + } + + override fun onDestroyView() { + super.onDestroyView() + disposable.dispose() + } + + override fun onDestroy() { + super.onDestroy() + if (hasError) { + ShellManager.rmFile( + BackupUtils.getCurrentAppBackupDir( + apkInfo?.packageInfo?.packageName.orEmpty(), + dateDir + ).absolutePath + ) + } + } + + private fun backupAndroidData() { + if (!backupAndroidData) { + successCount++ + binding.androidDataProgress.setStatus(BackupProgressView.Status.Ignore) + return + } + binding.androidDataProgress.setStatus(BackupProgressView.Status.Processing) + runOnIo().map { + BackupUtils.backupAppAndroidData(dateDir, it.applicationInfo.packageName) + ?: throw Exception() + }.observeOnMain().subscribe({ + successCount++ + binding.androidDataProgress.setStatus( + BackupProgressView.Status.Success, + it.ifEmpty { getString(R.string.no_need_to_backup) } + ) + }, { + hasError = true + binding.androidDataProgress.setStatus(BackupProgressView.Status.Failed) + }).addTo(disposable) + } + + private fun backupData() { + if (!backupData) { + successCount++ + binding.dataProgress.setStatus(BackupProgressView.Status.Ignore) + return + } + binding.dataProgress.setStatus(BackupProgressView.Status.Processing) + runOnIo().map { + BackupUtils.backupAppData(dateDir, it.applicationInfo.packageName) ?: throw Exception() + }.observeOnMain().subscribe({ + successCount++ + binding.dataProgress.setStatus(BackupProgressView.Status.Success, it.absolutePath) + }, { + hasError = true + binding.dataProgress.setStatus(BackupProgressView.Status.Failed) + }).addTo(disposable) + } + + + private fun runOnIo() = if (apkInfo == null) { + Single.error(Exception()) + } else { + Single.just(apkInfo!!).subscribeOn(Schedulers.newThread()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/backupprogress/BackupProgressView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/backupprogress/BackupProgressView.kt new file mode 100644 index 0000000..698bf87 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/backupprogress/BackupProgressView.kt @@ -0,0 +1,98 @@ +package com.wrbug.developerhelper.ui.widget.backupprogress + +import android.content.Context +import android.content.res.ColorStateList +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.databinding.ViewBackupProgressBinding +import com.wrbug.developerhelper.util.getString + +class BackupProgressView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : ConstraintLayout(context, attrs) { + private val binding = ViewBackupProgressBinding.inflate(LayoutInflater.from(context), this) + + + fun setTitle(title: Int) { + binding.tvTitle.text = getString(title) + } + + fun setStatus(status: Status, successPath: String = "") { + when (status) { + Status.Processing -> { + binding.progress.isVisible = true + binding.ivStatus.isVisible = false + binding.tvStatus.text = context.getString(R.string.backuping_data) + } + + Status.Success -> { + binding.progress.isVisible = false + binding.ivStatus.isVisible = true + binding.ivStatus.setImageResource(R.drawable.ic_success) + binding.ivStatus.imageTintList = ColorStateList.valueOf( + ContextCompat.getColor( + context, + R.color.material_color_green_400 + ) + ) + binding.tvStatus.text = successPath + } + + Status.Failed -> { + binding.progress.isVisible = false + binding.ivStatus.isVisible = true + binding.tvStatus.text = context.getString(R.string.backup_fail) + binding.ivStatus.setImageResource(R.drawable.ic_fail) + binding.ivStatus.imageTintList = ColorStateList.valueOf( + ContextCompat.getColor( + context, + R.color.material_color_red_400 + ) + ) + + } + + Status.Ignore -> { + binding.progress.isVisible = false + binding.ivStatus.isVisible = true + binding.tvStatus.text = context.getString(R.string.ignore_backup) + binding.ivStatus.setImageResource(R.drawable.ic_ignore) + binding.ivStatus.imageTintList = ColorStateList.valueOf( + ContextCompat.getColor( + context, + R.color.material_color_grey_400 + ) + ) + } + + Status.Waiting -> { + binding.progress.isVisible = true + binding.ivStatus.isVisible = true + binding.tvStatus.text = context.getString(R.string.backup_waiting) + binding.ivStatus.setImageResource(R.drawable.ic_waiting) + binding.ivStatus.imageTintList = ColorStateList.valueOf( + ContextCompat.getColor(context, R.color.material_color_grey_400) + ) + } + + Status.Canceled -> { + binding.progress.isVisible = false + binding.ivStatus.isVisible = true + binding.ivStatus.setImageResource(R.drawable.ic_canceled) + binding.tvStatus.text = context.getString(R.string.canceled) + binding.ivStatus.imageTintList = ColorStateList.valueOf( + ContextCompat.getColor(context, R.color.material_color_grey_400) + ) + } + } + } + + + enum class Status { + Processing, Success, Failed, Ignore, Waiting, Canceled + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/bottommenu/BottomMenu.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/bottommenu/BottomMenu.kt deleted file mode 100644 index fcbc77c..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/bottommenu/BottomMenu.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.wrbug.developerhelper.ui.widget.bottommenu - -import android.content.Context -import android.graphics.drawable.ColorDrawable -import android.os.Bundle -import android.view.View -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.commonutil.UiUtils -import com.wrbug.developerhelper.ui.decoration.SpaceItemDecoration -import kotlinx.android.synthetic.main.dialog_bottom_menu.* - -class BottomMenu(context: Context) : BottomSheetDialog(context), OnItemClickListener { - - private var title = "" - val adapter: BottomMenuItemAdapter by lazy { - BottomMenuItemAdapter(context) - } - - private var items: Array = arrayOf() - private var listener: OnItemClickListener? = null - - init { - setContentView(R.layout.dialog_bottom_menu) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - initRv() - initView() - } - - private fun initView() { - titleTv.text = title - titleTv.visibility = if (title.isEmpty()) View.GONE else View.VISIBLE - } - - private fun initRv() { - menuListRv.layoutManager = LinearLayoutManager(context) - val decoration = SpaceItemDecoration(UiUtils.dp2px(context, 0F)) - decoration.setLastBottomPadding(UiUtils.dp2px(context, 10F)) - decoration.setFirstTopPadding(UiUtils.dp2px(context, 10F)) - menuListRv.addItemDecoration(decoration) - val dividerItemDecoration = DividerItemDecoration(context, DividerItemDecoration.VERTICAL) - dividerItemDecoration.setDrawable(ColorDrawable(context.resources.getColor(R.color.divider_color))) - menuListRv.addItemDecoration(dividerItemDecoration) - adapter.setOnItemClickListener(this) - menuListRv.adapter = adapter - adapter.list = items - } - - override fun onClick(position: Int) { - listener?.onClick(position) - dismiss() - } - - class Builder(val context: Context) { - private var title = "" - private var items: Array = arrayOf() - private var listener: OnItemClickListener? = null - - fun title(id: Int): Builder { - title = context.getString(id) - return this - } - - fun title(title: String): Builder { - this.title = title - return this - } - - fun menuItems(items: Array): Builder { - this.items = items - return this - } - - fun onItemClickListener(listener: OnItemClickListener?): Builder { - this.listener = listener - return this - } - - fun build(): BottomMenu { - val menu = BottomMenu(context) - menu.items = items - menu.title = title - menu.listener = listener - return menu - } - } - -} diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/bottommenu/BottomMenuItemAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/bottommenu/BottomMenuItemAdapter.kt deleted file mode 100644 index a8e5b9c..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/bottommenu/BottomMenuItemAdapter.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.wrbug.developerhelper.ui.widget.bottommenu - -import android.content.Context -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.commonutil.UiUtils - -class BottomMenuItemAdapter(val context: Context) : RecyclerView.Adapter() { - var list: Array = arrayOf() - set(value) { - field = value - notifyDataSetChanged() - } - var listener: OnItemClickListener? = null - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val tv = TextView(context) - tv.setTextColor(context.resources.getColor(R.color.text_color_666666)) - tv.gravity = Gravity.CENTER - val dp10 = UiUtils.dp2px(context, 10F) - tv.setPadding(dp10, dp10, dp10, dp10) - val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - tv.layoutParams = params - return ViewHolder(tv) - } - - override fun getItemCount(): Int = list.size - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.tv.text = list[position] - holder.index = position - } - - fun setOnItemClickListener(listener: OnItemClickListener) { - this.listener = listener - } - - inner class ViewHolder(val tv: TextView) : RecyclerView.ViewHolder(tv) { - var index = 0 - - init { - tv.setOnClickListener { - listener?.onClick(index) - } - } - - } -} diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/bottommenu/OnItemClickListener.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/bottommenu/OnItemClickListener.kt deleted file mode 100644 index 2a4f90b..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/bottommenu/OnItemClickListener.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.wrbug.developerhelper.ui.widget.bottommenu - -interface OnItemClickListener { - fun onClick(position: Int) -} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/boundsinfoview/BoundsInfoView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/boundsinfoview/BoundsInfoView.kt index 20d5d2e..828526c 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/boundsinfoview/BoundsInfoView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/boundsinfoview/BoundsInfoView.kt @@ -17,9 +17,9 @@ class BoundsInfoView : View { field = value invalidate() } - private val edgeLineSize = UiUtils.dp2px(context,4F).toFloat() + private val edgeLineSize = UiUtils.dp2px(context, 4F).toFloat() private val paint = Paint() - var unit: Unit = Unit.DP + private var unit: Unit = Unit.DP set(value) { if (field == value) { return @@ -27,13 +27,21 @@ class BoundsInfoView : View { field = value invalidate() } - private val textMargin = UiUtils.dp2px(context,3F) + private val textMargin = UiUtils.dp2px(context, 3F) - constructor(context: Context) : super(context) + constructor(context: Context) : this(context, null) - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + setOnClickListener { + toggleUnit() + } + } - override fun onDraw(canvas: Canvas?) { + private fun toggleUnit() { + unit = if (unit == Unit.DP) Unit.PX else Unit.DP + } + + override fun onDraw(canvas: Canvas) { val rect = drawRect(canvas) val lineRect = drawEdge(rect, canvas) drawAl(rect, lineRect, canvas) @@ -45,16 +53,16 @@ class BoundsInfoView : View { bounds?.let { paint.reset() paint.isAntiAlias = true - paint.textSize = UiUtils.dp2px(context,14F).toFloat() + paint.textSize = UiUtils.dp2px(context, 14F).toFloat() var leftMargin = it.left var topMargin = it.top var rightMargin = UiUtils.getDeviceWidth() - it.right var bottomMargin = UiUtils.getDeviceHeight() - it.bottom if (unit == Unit.DP) { - leftMargin = UiUtils.px2dp(context,leftMargin.toFloat()).toInt() - topMargin = UiUtils.px2dp(context,topMargin.toFloat()).toInt() - rightMargin = UiUtils.px2dp(context,rightMargin.toFloat()).toInt() - bottomMargin = UiUtils.px2dp(context,bottomMargin.toFloat()).toInt() + leftMargin = UiUtils.px2dp(context, leftMargin.toFloat()).toInt() + topMargin = UiUtils.px2dp(context, topMargin.toFloat()).toInt() + rightMargin = UiUtils.px2dp(context, rightMargin.toFloat()).toInt() + bottomMargin = UiUtils.px2dp(context, bottomMargin.toFloat()).toInt() } val leftMarginText = "$leftMargin ${unit.s}" val topMarginText = "$topMargin ${unit.s}" @@ -100,13 +108,13 @@ class BoundsInfoView : View { var width = it.right - it.left var height = it.bottom - it.top if (unit == Unit.DP) { - width = UiUtils.px2dp(context,width.toFloat()).toInt() - height = UiUtils.px2dp(context,height.toFloat()).toInt() + width = UiUtils.px2dp(context, width.toFloat()).toInt() + height = UiUtils.px2dp(context, height.toFloat()).toInt() } val text = "$width ${unit.s} ×ばつ $height ${unit.s}" paint.reset() paint.isAntiAlias = true - paint.textSize = UiUtils.dp2px(context,14F).toFloat() + paint.textSize = UiUtils.dp2px(context, 14F).toFloat() val bounds = Rect() paint.getTextBounds(text, 0, text.length, bounds) val textWidth = bounds.width() @@ -127,7 +135,7 @@ class BoundsInfoView : View { paint.style = Paint.Style.STROKE paint.isAntiAlias = true paint.color = resources.getColor(R.color.colorAccent) - paint.strokeWidth = UiUtils.dp2px(context,1F).toFloat() + paint.strokeWidth = UiUtils.dp2px(context, 1F).toFloat() CanvasHelper.drawAL( rect.left, lineRect.left + edgeLineSize / 2, @@ -174,7 +182,12 @@ class BoundsInfoView : View { val lineWidth = measuredWidth / 6 val lineHeight = measuredHeight / 6 val lineRect = - RectF(rect.left / 2F, rect.top / 2F, measuredWidth - rect.left / 2F, measuredHeight - rect.top / 2F) + RectF( + rect.left / 2F, + rect.top / 2F, + measuredWidth - rect.left / 2F, + measuredHeight - rect.top / 2F + ) canvas?.run { drawLine( lineRect.left, @@ -213,7 +226,7 @@ class BoundsInfoView : View { paint.style = Paint.Style.STROKE paint.isAntiAlias = true paint.color = resources.getColor(R.color.colorAccent) - paint.strokeWidth = UiUtils.dp2px(context,2F).toFloat() + paint.strokeWidth = UiUtils.dp2px(context, 2F).toFloat() val width = measuredWidth / 3F val height = measuredHeight / 4F val top = measuredHeight * 3 / 8F @@ -224,6 +237,6 @@ class BoundsInfoView : View { enum class Unit(var s: String) { - DP("dp"), PX("dp") + DP("dp"), PX("px") } } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/emptyview/EmptyView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/emptyview/EmptyView.kt index e5c3504..92af967 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/emptyview/EmptyView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/emptyview/EmptyView.kt @@ -3,48 +3,21 @@ package com.wrbug.developerhelper.ui.widget.emptyview import android.content.Context import android.util.AttributeSet import android.view.Gravity -import android.view.View +import android.view.LayoutInflater import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout import com.wrbug.developerhelper.R import com.wrbug.developerhelper.commonutil.UiUtils +import com.wrbug.developerhelper.databinding.ViewEmptyViewBinding -class EmptyView : LinearLayout { - val icoIv: ImageView by lazy { - val icoIv = ImageView(context) - icoIv.setImageResource(R.drawable.ic_ic_empty_666666) - icoIv - } - val textTv: TextView by lazy { - val tv = TextView(context) - tv.setTextColor(resources.getColor(R.color.text_color_666666)) - tv.text = "无数据" - tv.textSize = 20F - tv.gravity = Gravity.CENTER - val params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) - params.topMargin = UiUtils.dp2px(context, 20F) - tv.layoutParams = params - tv - } - - constructor(context: Context) : super(context) { - } - - constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet) { - init() - initView() - } - - private fun init() { - orientation = VERTICAL - gravity = Gravity.CENTER - } +class EmptyView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : ConstraintLayout(context, attrs) { + private val binding = ViewEmptyViewBinding.inflate(LayoutInflater.from(context), this) - private fun initView() { - addView(icoIv) - addView(textTv) + fun setTitle(title: String) { + binding.tvTitle.text = title } - - } \ No newline at end of file diff --git a/commonwidget/src/main/java/com/wrbug/developerhelper/commonwidget/flexibletoast/FlexibleToast.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/flexibletoast/FlexibleToast.kt similarity index 63% rename from commonwidget/src/main/java/com/wrbug/developerhelper/commonwidget/flexibletoast/FlexibleToast.kt rename to app/src/main/java/com/wrbug/developerhelper/ui/widget/flexibletoast/FlexibleToast.kt index f492466..ddd5aa0 100644 --- a/commonwidget/src/main/java/com/wrbug/developerhelper/commonwidget/flexibletoast/FlexibleToast.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/flexibletoast/FlexibleToast.kt @@ -1,6 +1,8 @@ -package com.wrbug.developerhelper.commonwidget.flexibletoast +package com.wrbug.developerhelper.ui.widget.flexibletoast import android.content.Context +import android.os.Handler +import android.os.Looper import android.view.Gravity import android.view.LayoutInflater import android.view.View @@ -8,9 +10,36 @@ import android.widget.ImageView import android.widget.TextView import android.widget.Toast import com.wrbug.developerhelper.commonutil.UiUtils -import com.wrbug.developerhelper.commonwidget.R +import com.wrbug.developerhelper.databinding.LayoutToastFlexibleBinding -class FlexibleToast(private val mContext: Context) { +class FlexibleToast private constructor(private val mContext: Context) { + + companion object { + const val GRAVITY_BOTTOM = 0 + const val GRAVITY_CENTER = 1 + const val GRAVITY_TOP = 2 + const val TOAST_SHORT = 0 + const val TOAST_LONG = 1 + private var instance: FlexibleToast? = null + private fun getInstance(context: Context): FlexibleToast { + if (instance == null) { + instance = FlexibleToast(context.applicationContext) + } + return instance as FlexibleToast + + } + + fun toastShow(context: Context, msg: String) { + val toast = getInstance(context) + val builder = Builder(context).setGravity(GRAVITY_BOTTOM) + builder.setSecondText(msg) + if (Looper.myLooper() != Looper.getMainLooper()) { + Handler(Looper.getMainLooper()).post { toast.toastShow(builder) } + } else { + toast.toastShow(builder) + } + } + } private val flexibleToast: Toast by lazy { Toast(mContext) @@ -23,11 +52,13 @@ class FlexibleToast(private val mContext: Context) { 0, 0 ) + builder.mGravity == GRAVITY_TOP -> flexibleToast.setGravity( Gravity.TOP or Gravity.CENTER_VERTICAL, 0, UiUtils.dp2px(mContext, 20F) ) + else -> flexibleToast.setGravity( Gravity.BOTTOM or Gravity.CENTER_VERTICAL, 0, @@ -42,7 +73,7 @@ class FlexibleToast(private val mContext: Context) { if (builder.hasCustomerView && builder.mCustomerView != null) { flexibleToast.view = builder.mCustomerView } else { - flexibleToast.view = builder.mDefaultView + flexibleToast.view = builder.binding.root } flexibleToast.show() } @@ -51,46 +82,31 @@ class FlexibleToast(private val mContext: Context) { * 控制Toast的显示样式 */ class Builder(context: Context) { - val mDefaultView: View = LayoutInflater.from(context) - .inflate(R.layout.layout_toast_flexible, null) + val binding = LayoutToastFlexibleBinding.inflate(LayoutInflater.from(context)) var mCustomerView: View? = null - private val mIvImage: ImageView - private val mTvFirst: TextView - private val mTvSecond: TextView - - private val dividerFirst: View - private val dividerSecond: View var mDuration = Toast.LENGTH_SHORT// 0 short, 1 long var mGravity = 0 var hasCustomerView = false // 是否使用自定义layout - init { - mIvImage = mDefaultView.findViewById(R.id.imgIv) - mTvFirst = mDefaultView.findViewById(R.id.firstTv) - mTvSecond = mDefaultView.findViewById(R.id.secondTv) - dividerFirst = mDefaultView.findViewById(R.id.firstDividerView) - dividerSecond = mDefaultView.findViewById(R.id.secondDividerView) - } - fun setImageResource(resId: Int): Builder { - this.mIvImage.setImageResource(resId) - this.mIvImage.visibility = View.VISIBLE - this.dividerFirst.visibility = View.VISIBLE + binding.imgIv.setImageResource(resId) + binding.imgIv.visibility = View.VISIBLE + binding.firstDividerView.visibility = View.VISIBLE return this } fun setFirstText(firstText: String): Builder { - this.mTvFirst.text = firstText - this.mTvFirst.visibility = View.VISIBLE - this.dividerSecond.visibility = View.VISIBLE + binding.firstTv.text = firstText + binding.firstTv.visibility = View.VISIBLE + binding.secondDividerView.visibility = View.VISIBLE return this } fun setSecondText(secondText: String): Builder { - this.mTvSecond.text = secondText - this.mTvSecond.visibility = View.VISIBLE + binding.secondTv.text = secondText + binding.secondTv.visibility = View.VISIBLE return this } @@ -116,14 +132,4 @@ class FlexibleToast(private val mContext: Context) { } } - companion object { - - const val GRAVITY_BOTTOM = 0 - const val GRAVITY_CENTER = 1 - const val GRAVITY_TOP = 2 - const val TOAST_SHORT = 0 - const val TOAST_LONG = 1 - } - - } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyDetailView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyDetailView.kt index 9b869db..fb54b6e 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyDetailView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyDetailView.kt @@ -6,15 +6,16 @@ import android.graphics.Color import android.graphics.Paint import android.util.AttributeSet import android.widget.FrameLayout -import com.wrbug.developerhelper.basecommon.entry.HierarchyNode +import com.wrbug.developerhelper.base.entry.HierarchyNode import com.wrbug.developerhelper.R import com.wrbug.developerhelper.ui.widget.helper.CanvasHelper import com.wrbug.developerhelper.commonutil.UiUtils class HierarchyDetailView : FrameLayout { + private val paint: Paint by lazy { val paint = Paint() - paint.color = context.resources.getColor(R.color.colorAccent) + paint.color = context.resources.getColor(R.color.material_color_blue_800) paint.style = Paint.Style.STROKE paint.strokeWidth = 3F paint @@ -34,7 +35,6 @@ class HierarchyDetailView : FrameLayout { initView() } - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initView() } @@ -50,14 +50,13 @@ class HierarchyDetailView : FrameLayout { setWillNotDraw(false) } - - override fun onDraw(canvas: Canvas?) { + override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (hierarchyNode != null) { - drawNode(canvas) if (hierarchyNode?.parentId!!> -1) { drawParentNode(canvas) } + drawNode(canvas) } } @@ -65,7 +64,7 @@ class HierarchyDetailView : FrameLayout { if (parentHierarchyNode == null) { return } - val bounds = parentHierarchyNode?.screenBounds + val bounds = parentHierarchyNode?.screenBounds ?: return canvas?.drawRect(bounds, parentpPaint) } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyView.kt index 5ffd740..93d95f5 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyView.kt @@ -7,8 +7,9 @@ import android.graphics.Paint import android.util.AttributeSet import android.view.MotionEvent import android.view.View -import com.wrbug.developerhelper.basecommon.entry.HierarchyNode +import com.wrbug.developerhelper.base.entry.HierarchyNode import com.wrbug.developerhelper.service.DeveloperHelperAccessibilityService +import kotlin.math.pow class HierarchyView(context: Context, attrs: AttributeSet?) : View(context, attrs) { private val strokePaint = Paint() @@ -18,6 +19,7 @@ class HierarchyView(context: Context, attrs: AttributeSet?) : View(context, attr private var onHierarchyNodeClickListener: OnHierarchyNodeClickListener? = null private var selectedNode: HierarchyNode? = null private var selectedParentNode: HierarchyNode? = null + private var isFromLeftDown = false constructor(context: Context) : this(context, null) @@ -36,7 +38,7 @@ class HierarchyView(context: Context, attrs: AttributeSet?) : View(context, attr mHierarchyNodes.addAll(it) } nodeMap.putAll(DeveloperHelperAccessibilityService.nodeMap) - visibility=View.VISIBLE + visibility = View.VISIBLE invalidate() } @@ -47,15 +49,25 @@ class HierarchyView(context: Context, attrs: AttributeSet?) : View(context, attr override fun onTouchEvent(event: MotionEvent?): Boolean { when (event?.action) { MotionEvent.ACTION_DOWN -> { + isFromLeftDown = event.x <= 15 + if (isFromLeftDown) { + return super.onTouchEvent(event) + } getSelectedNode(event.x, event.y) return true } + MotionEvent.ACTION_MOVE -> { - getSelectedNode(event.x, event.y) + if (!isFromLeftDown) { + getSelectedNode(event.x, event.y) + } } + MotionEvent.ACTION_UP -> { - selectedNode?.let { - onHierarchyNodeClickListener?.onClick(it, selectedParentNode) + if (!isFromLeftDown) { + selectedNode?.let { + onHierarchyNodeClickListener?.onClick(it, selectedParentNode) + } } } } @@ -70,15 +82,17 @@ class HierarchyView(context: Context, attrs: AttributeSet?) : View(context, attr } selectedNode = hierarchyNode.selectedNode selectedParentNode = hierarchyNode.parentNode - onHierarchyNodeClickListener?.onSelectedNodeChanged(hierarchyNode.selectedNode, hierarchyNode.parentNode) + onHierarchyNodeClickListener?.onSelectedNodeChanged( + hierarchyNode.selectedNode, hierarchyNode.parentNode + ) } } - override fun onDraw(canvas: Canvas?) { + override fun onDraw(canvas: Canvas) { drawRect(canvas, mHierarchyNodes) } - private fun drawRect(canvas: Canvas?, hierarchyNodes: List) { + private fun drawRect(canvas: Canvas, hierarchyNodes: List) { for (hierarchyNode in hierarchyNodes) { drawWidget(canvas, hierarchyNode) drawRect(canvas, hierarchyNode.childId) @@ -108,10 +122,10 @@ class HierarchyView(context: Context, attrs: AttributeSet?) : View(context, attr } else { var left = info.selectedNode.screenBounds?.left ?: 0 var top = info.selectedNode.screenBounds?.top ?: 0 - val len = Math.pow(x.toDouble() - left, 2.0) + Math.pow(y.toDouble() - top, 2.0) + val len = (x.toDouble() - left).pow(2.0) + (y.toDouble() - top).pow(2.0) left = list[index].selectedNode.screenBounds?.left ?: 0 top = list[index].selectedNode.screenBounds?.top ?: 0 - val len1 = Math.pow(x.toDouble() - left, 2.0) + Math.pow(y.toDouble() - top, 2.0) + val len1 = (x.toDouble() - left).pow(2.0) + (y.toDouble() - top).pow(2.0) if (len1 < len) { info = list[index] } @@ -120,10 +134,12 @@ class HierarchyView(context: Context, attrs: AttributeSet?) : View(context, attr return info } - private fun getNode(x: Float, y: Float, hierarchyNode: HierarchyNode, list: ArrayList) { + private fun getNode( + x: Float, y: Float, hierarchyNode: HierarchyNode, list: ArrayList + ) { val rect = hierarchyNode.screenBounds ?: return if (rect.contains(x.toInt(), y.toInt())) { - if (!hierarchyNode.childId.isEmpty()) { + if (hierarchyNode.childId.isNotEmpty()) { for (child in hierarchyNode.childId.reversed()) { getNode(x, y, child, list) } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/SelectedNodeInfo.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/SelectedNodeInfo.kt index b3a16b8..c960bfd 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/SelectedNodeInfo.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/SelectedNodeInfo.kt @@ -1,6 +1,6 @@ package com.wrbug.developerhelper.ui.widget.hierarchyView -import com.wrbug.developerhelper.basecommon.entry.HierarchyNode +import com.wrbug.developerhelper.base.entry.HierarchyNode class SelectedNodeInfo(var selectedNode: HierarchyNode, var parentNode: HierarchyNode?) { diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoDialog.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoDialog.kt new file mode 100644 index 0000000..96bdc1f --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoDialog.kt @@ -0,0 +1,100 @@ +package com.wrbug.developerhelper.ui.widget.layoutinfoview + +import android.app.Dialog +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.base.ExtraKey +import com.wrbug.developerhelper.base.entry.HierarchyNode +import com.wrbug.developerhelper.commonutil.UiUtils +import com.wrbug.developerhelper.commonutil.dp2px +import com.wrbug.developerhelper.databinding.ViewLayoutInfoBinding +import com.wrbug.developerhelper.util.isPortrait + +class LayoutInfoDialog : DialogFragment() { + + companion object { + fun show( + fragmentManager: FragmentManager, + list: List?, + selectedNode: HierarchyNode, + listener: ((HierarchyNode, HierarchyNode?) -> Unit) + ) { + LayoutInfoDialog().apply { + arguments = Bundle().apply { + putParcelableArrayList(ExtraKey.DATA, list?.let { ArrayList(it) }) + putParcelable(ExtraKey.SELECTED, selectedNode) + } + onNodeChangedListener = listener + }.show(fragmentManager, "LayoutInfoDialog") + } + } + + private val nodeList: List? by lazy { + arguments?.getParcelableArrayList(ExtraKey.DATA) + } + private val hierarchyNode: HierarchyNode? by lazy { + arguments?.getParcelable(ExtraKey.SELECTED) + } + private var onNodeChangedListener: ((HierarchyNode, HierarchyNode?) -> Unit)? = null + + val adapter by lazy { + LayoutInfoViewPagerAdapter(requireContext(), nodeList, hierarchyNode!!) + } + + private lateinit var binding: ViewLayoutInfoBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.FullScreenDialog) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ViewLayoutInfoBinding.inflate(layoutInflater) + return binding.root + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + setCanceledOnTouchOutside(true) + window?.run { + val layoutParams = attributes + if (isPortrait()) { + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + layoutParams.height = UiUtils.getDeviceHeight() / 2 + setGravity(Gravity.BOTTOM) + } else { + layoutParams.width = UiUtils.getDeviceWidth() / 2 + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + setGravity(Gravity.END) + } + attributes = layoutParams + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (dialog?.window?.attributes?.gravity != Gravity.BOTTOM) { + binding.layoutInfoContainer.setPadding(0, UiUtils.getStatusHeight(), 0, 0) + } + onNodeChangedListener?.let { + adapter.setOnNodeChangedListener(it) + } + initViewpager() + } + + private fun initViewpager() { + binding.viewPager.adapter = adapter + binding.tabLayout.setupWithViewPager(binding.viewPager) + } +} diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoView.kt deleted file mode 100644 index 874b6fd..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoView.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.wrbug.developerhelper.ui.widget.layoutinfoview - -import android.content.Context -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.entry.HierarchyNode -import com.wrbug.developerhelper.commonutil.UiUtils -import kotlinx.android.synthetic.main.view_layout_info.* - - -class LayoutInfoView( - context: Context, - private val nodeList: List?, - private val hierarchyNode: HierarchyNode -) : BottomSheetDialog(context) { - val adapter = LayoutInfoViewPagerAdapter(context, nodeList, hierarchyNode) - - init { - init() - } - - fun setOnNodeChangedListener(listener: OnNodeChangedListener) { - adapter.setOnNodeChangedListener(listener) - } - - private fun init() { - setContentView(R.layout.view_layout_info) - val layoutParams = layoutInfoContainer.layoutParams - layoutParams.height = UiUtils.getDeviceHeight(context) / 2 - layoutInfoContainer.layoutParams = layoutParams - initViewpager() - } - - private fun initViewpager() { - viewPager.adapter = adapter - tabLayout.setupWithViewPager(viewPager) - } -} diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoViewPagerAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoViewPagerAdapter.kt index 051790d..bb9148e 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoViewPagerAdapter.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoViewPagerAdapter.kt @@ -9,7 +9,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager.widget.PagerAdapter import androidx.viewpager.widget.ViewPager import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.basecommon.entry.HierarchyNode +import com.wrbug.developerhelper.base.entry.HierarchyNode import com.wrbug.developerhelper.ui.widget.boundsinfoview.BoundsInfoView import com.wrbug.developerhelper.ui.widget.layoutinfoview.infopage.InfoAdapter import com.wrbug.developerhelper.ui.widget.layoutinfoview.infopage.ItemInfo @@ -30,7 +30,7 @@ class LayoutInfoViewPagerAdapter( private val infoAdapter: InfoAdapter = InfoAdapter(context) private lateinit var graphView: GraphView private val boundsInfoView = BoundsInfoView(context) - private var onNodeChangedListener: OnNodeChangedListener? = null + private var onNodeChangedListener: ((HierarchyNode, HierarchyNode?) -> Unit)? = null init { initInfoTab() @@ -38,14 +38,14 @@ class LayoutInfoViewPagerAdapter( initViewTreeTab() } - fun setOnNodeChangedListener(listener: OnNodeChangedListener) { + fun setOnNodeChangedListener(listener: (HierarchyNode, HierarchyNode?) -> Unit) { onNodeChangedListener = listener } private fun initViewTreeTab() { tabList.add("ViewTree") graphView = - LayoutInflater.from(context).inflate(R.layout.layout_hierarchy_tree, null) as GraphView + LayoutInflater.from(context).inflate(R.layout.layout_hierarchy_tree, null) as GraphView val adapter = ViewTreeGraphAdapter(context, R.layout.item_tree_node_view) graphView.adapter = adapter adapter.setOnItemClickListener(object : ViewTreeGraphAdapter.OnItemClickListener { @@ -54,7 +54,7 @@ class LayoutInfoViewPagerAdapter( resetInfoTab() resetLayoutTable() resetViewTreeTab(node) - onNodeChangedListener?.onChanged(node.node, node.parent?.node) + onNodeChangedListener?.invoke(node.node, node.parent?.node) } }) val configuration = BuchheimWalkerConfiguration.Builder() @@ -150,7 +150,7 @@ class LayoutInfoViewPagerAdapter( "${resourceId.replace(packageName, "app")}[${idHex?.replace("#", "0x") ?: "NO_ID"}]" ) ) - if (!text.isEmpty()) { + if (text.isNotEmpty()) { list.add(ItemInfo("Text", text)) } list.add(ItemInfo("Enable", enabled)) diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/OnNodeChangedListener.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/OnNodeChangedListener.kt deleted file mode 100644 index 70af503..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/OnNodeChangedListener.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.wrbug.developerhelper.ui.widget.layoutinfoview - -import com.wrbug.developerhelper.basecommon.entry.HierarchyNode - -interface OnNodeChangedListener { - fun onChanged(node: HierarchyNode, parentNode: HierarchyNode?) -} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/ViewTreeGraphAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/ViewTreeGraphAdapter.kt index 8a3e153..1871acd 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/ViewTreeGraphAdapter.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/ViewTreeGraphAdapter.kt @@ -9,7 +9,7 @@ import androidx.cardview.widget.CardView import com.wrbug.developerhelper.R import de.blox.graphview.BaseGraphAdapter -class ViewTreeGraphAdapter(@NonNull val context: Context, @LayoutRes val layoutRes: Int) : +class ViewTreeGraphAdapter(val context: Context, @LayoutRes val layoutRes: Int) : BaseGraphAdapter(context, layoutRes) { private var listener: OnItemClickListener? = null override fun onCreateViewHolder(view: View?) = ViewHolder(view!!) diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/ViewTreeGraphNode.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/ViewTreeGraphNode.kt index 866b0b2..21a3846 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/ViewTreeGraphNode.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/ViewTreeGraphNode.kt @@ -1,6 +1,6 @@ package com.wrbug.developerhelper.ui.widget.layoutinfoview -import com.wrbug.developerhelper.basecommon.entry.HierarchyNode +import com.wrbug.developerhelper.base.entry.HierarchyNode class ViewTreeGraphNode(val node: HierarchyNode) { var selected: Boolean = false diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/infopage/InfoAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/infopage/InfoAdapter.kt index f442ae2..f37bfe6 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/infopage/InfoAdapter.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/infopage/InfoAdapter.kt @@ -2,25 +2,56 @@ package com.wrbug.developerhelper.ui.widget.layoutinfoview.infopage import android.content.Context import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.wrbug.developerhelper.R +import com.airbnb.lottie.LottieDrawable +import com.wrbug.developerhelper.commonutil.ClipboardUtils import com.wrbug.developerhelper.commonutil.print -import kotlinx.android.synthetic.main.item_view_info.view.* +import com.wrbug.developerhelper.util.setOnDoubleCheckClickListener +import com.wrbug.developerhelper.util.visible +import com.wrbug.developerhelper.databinding.ItemInfoLoadingBinding +import com.wrbug.developerhelper.databinding.ItemViewInfoBinding -class InfoAdapter(val context: Context) : RecyclerView.Adapter() { - private val list = arrayListOf() - override fun onCreateViewHolder(p0: ViewGroup, p1: Int): ViewHolder { - return ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_view_info, p0, false)) +class InfoAdapter(val context: Context, private val topItem: ItemInfo? = null) : + RecyclerView.Adapter() { + companion object { + private const val VIEW_TYPE_ITEM = 0 + private const val VIEW_TYPE_LOADING = 1 + } + + private val list = arrayListOf() + override fun onCreateViewHolder(p0: ViewGroup, p1: Int): RecyclerView.ViewHolder { + if (p1 == VIEW_TYPE_LOADING) { + return LoadingViewHolder( + ItemInfoLoadingBinding.inflate( + LayoutInflater.from(p0.context), p0, false + ) + ) + } + return InfoViewHolder( + ItemViewInfoBinding.inflate( + LayoutInflater.from(p0.context), p0, false + ) + ) + } + + override fun getItemViewType(position: Int): Int { + return if (list[position] is LoadingItem) { + VIEW_TYPE_LOADING + } else { + VIEW_TYPE_ITEM + } } override fun getItemCount(): Int { return list.size } - fun setItems(list: ArrayList) { + fun setItems(list: List) { this.list.clear() + topItem?.let { + this.list.add(it) + } if (list.isEmpty().not()) { this.list.addAll(list) } @@ -35,16 +66,27 @@ class InfoAdapter(val context: Context) : RecyclerView.Adapter Unit) { + setOnClickListener(View.OnClickListener { v -> v.onclick() }) + } } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/infopage/LoadingItem.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/infopage/LoadingItem.kt new file mode 100644 index 0000000..a0dc06e --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/infopage/LoadingItem.kt @@ -0,0 +1,4 @@ +package com.wrbug.developerhelper.ui.widget.layoutinfoview.infopage + +object LoadingItem { +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/settingitemview/SettingItemView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/settingitemview/SettingItemView.kt index ef4f764..8fed9fa 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/settingitemview/SettingItemView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/settingitemview/SettingItemView.kt @@ -1,17 +1,25 @@ package com.wrbug.developerhelper.ui.widget.settingitemview import android.content.Context +import android.content.res.ColorStateList import android.graphics.drawable.Drawable -import android.text.TextUtils import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.CompoundButton -import android.widget.FrameLayout +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.core.view.updatePadding import com.wrbug.developerhelper.R -import kotlinx.android.synthetic.main.view_setting_item.view.* +import com.wrbug.developerhelper.commonutil.dpInt +import com.wrbug.developerhelper.util.setOnDoubleCheckClickListener +import com.wrbug.developerhelper.databinding.ViewSettingItemBinding + +class SettingItemView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : ConstraintLayout(context, attrs) { -class SettingItemView : FrameLayout { var checkable: Boolean = true set(value) { field = value @@ -21,35 +29,34 @@ class SettingItemView : FrameLayout { var checked: Boolean = false set(value) { field = value - switcher.isChecked = checked + binding.switcher.isChecked = checked } private var switcherMaskViewClicked = false + private val binding = ViewSettingItemBinding.inflate(LayoutInflater.from(context), this) - constructor(context: Context) : super(context) { - initView() - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init { initView() initAttrs(attrs) } private fun initView() { - LayoutInflater.from(context).inflate(R.layout.view_setting_item, this) + updatePadding(top = 8.dpInt(context), bottom = 8.dpInt(context)) setBackgroundResource(R.drawable.ripple_with_color_mask) } - private fun initAttrs(attrs: AttributeSet) { + private fun initAttrs(attrs: AttributeSet?) { + attrs ?: return with(context.obtainStyledAttributes(attrs, R.styleable.SettingItemView)) { val src = getDrawable(R.styleable.SettingItemView_src) - src?.let { - setImage(it) + setImage(src) + getColorStateList(R.styleable.SettingItemView_icoTint)?.let { + setIconTint(it) } val title = getString(R.styleable.SettingItemView_title) - titleTv.text = title + binding.titleTv.text = title setSummary(getString(R.styleable.SettingItemView_summary)) val switchVisible = getBoolean(R.styleable.SettingItemView_switchVisible, true) - switcher.visibility = if (switchVisible) View.VISIBLE else View.GONE + binding.switcher.visibility = if (switchVisible) View.VISIBLE else View.GONE checked = getBoolean(R.styleable.SettingItemView_checked, false) checkable = getBoolean(R.styleable.SettingItemView_checkable, true) recycle() @@ -57,47 +64,40 @@ class SettingItemView : FrameLayout { } + private fun setIconTint(colorStateList: ColorStateList?) { + binding.icoIv.imageTintList = colorStateList + } + override fun setOnClickListener(l: OnClickListener?) { super.setOnClickListener(l) if (switcherMaskViewClicked.not()) { - switcherMaskView.setOnClickListener(l) + binding.switcherMaskView.setOnClickListener(l) } } fun setOnSwitcherClickListener(listener: View.() -> Unit) { switcherMaskViewClicked = true - switcherMaskView.setOnClickListener(listener) + binding.switcherMaskView.setOnDoubleCheckClickListener(clickListener = listener) } - fun isChecked() = switcher.isChecked + fun isChecked() = binding.switcher.isChecked private fun setSwitchCheckable() { - if (checkable) { -// switcher.setOnTouchListener(null) - switcherMaskView.visibility = View.GONE - } else { -// switcher.setOnTouchListener { _, _ -> true } - switcherMaskView.visibility = View.VISIBLE - } + binding.switcherMaskView.isGone = checkable } fun setOnCheckedChangeListener(listener: CompoundButton.OnCheckedChangeListener) { - switcher.setOnCheckedChangeListener(listener) + binding.switcher.setOnCheckedChangeListener(listener) } - fun setImage(drawable: Drawable) { - icoIv.setImageDrawable(drawable) - icoIv.visibility = View.VISIBLE + fun setImage(drawable: Drawable?) { + binding.icoIv.setImageDrawable(drawable) + binding.icoIv.isVisible = drawable != null } fun setSummary(summary: String?) { - summary.takeIf { - !TextUtils.isEmpty(it) - }?.let { - summaryTv.text = it - summaryTv.visibility = View.VISIBLE - } + binding.summaryTv.text = summary + binding.summaryTv.isVisible = !summary.isNullOrEmpty() } - } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/util/AppStatusRegister.kt b/app/src/main/java/com/wrbug/developerhelper/util/AppStatusRegister.kt new file mode 100644 index 0000000..1d49973 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/util/AppStatusRegister.kt @@ -0,0 +1,56 @@ +package com.wrbug.developerhelper.util + +import android.app.Activity +import android.app.Application +import android.os.Bundle + +object AppStatusRegister { + private var count = 0 + private val backgroundMap = hashMapOf Unit>() + fun init(application: Application) { + application.registerActivityLifecycleCallbacks(object : + Application.ActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + + } + + override fun onActivityStarted(activity: Activity) { + count++ + } + + override fun onActivityResumed(activity: Activity) { + } + + override fun onActivityPaused(activity: Activity) { + } + + override fun onActivityStopped(activity: Activity) { + count-- + backgroundCheck() + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + } + + override fun onActivityDestroyed(activity: Activity) { + } + + }) + } + + + fun registerBackgroundListener(key: String, listener: () -> Unit) { + backgroundMap[key] = listener + } + + fun removeBackgroundListener(key: String) { + backgroundMap.remove(key) + } + + private fun backgroundCheck() { + if (count == 0) { + backgroundMap.values.forEach { it() } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/util/BackupUtils.kt b/app/src/main/java/com/wrbug/developerhelper/util/BackupUtils.kt index d037393..e77a499 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/BackupUtils.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/BackupUtils.kt @@ -1,36 +1,159 @@ package com.wrbug.developerhelper.util -import android.net.Uri import android.os.Environment -import com.wrbug.developerhelper.basecommon.BaseApp +import com.wrbug.developerhelper.commonutil.Constant +import com.wrbug.developerhelper.commonutil.entity.ApkInfo +import com.wrbug.developerhelper.commonutil.fromJson +import com.wrbug.developerhelper.commonutil.safeCreateSingle +import com.wrbug.developerhelper.commonutil.safeRead import com.wrbug.developerhelper.commonutil.shell.ShellManager -import com.wrbug.developerhelper.commonutil.toUri -import com.wrbug.developerhelper.xposed.developerhelper.DeveloperHelper +import com.wrbug.developerhelper.commonutil.toJson +import com.wrbug.developerhelper.model.entity.BackupAppData +import com.wrbug.developerhelper.model.entity.BackupAppInfo +import com.wrbug.developerhelper.model.entity.BackupAppItemInfo +import io.reactivex.rxjava3.core.Single +import net.dongliu.apk.parser.ApkFile import java.io.File object BackupUtils { - private val backupDir: File by lazy { - val file = File(Environment.getExternalStorageDirectory(), "com.wrbug.developerHelper/backup") + private const val ANDROID_DATA_TAR = "android_data.tar" + private const val DATA_TAR = "data.tar" + private const val CONFIG_JSON = "config.json" + private const val ICON_PNG = "icon.png" + private val backupRootDir: File by lazy { + val file = + File(Environment.getExternalStorageDirectory(), "DeveloperHelper/backup") if (file.exists().not()) { file.mkdirs() } file } - fun backupApk(packageName: String, apkPath: String, fileName: String): Uri? { - val apkDir = File(backupDir, "apks/$packageName/$fileName") + fun getCurrentAppBackupDir(packageName: String, dateDir: String): File { + return File(getAppBackupDir(packageName), dateDir) + } + + fun getAppBackupDir(packageName: String): File { + return File(backupRootDir, packageName).apply { + if (!exists()) { + mkdirs() + } + } + } + + fun backupApk( + packageName: String, + dateDir: String, + apkPath: String, + fileName: String + ): String? { + val apkDir = File(getCurrentAppBackupDir(packageName, dateDir), fileName) if (ShellManager.cpFile(apkPath, apkDir.absolutePath)) { - return apkDir.toUri(BaseApp.instance) + return apkDir.absolutePath } return null } - fun backupAppData(packageName: String, dataDir: String): File? { - val backupDataDir = - File(backupDir, "datas/$packageName/${System.currentTimeMillis().format("yyyy-MM-dd-HH_mm_ss")}") - if (ShellManager.cpFile(dataDir, backupDataDir.absolutePath)) { + fun backupAppData(dateDir: String, packageName: String): File? { + val backupDataDir = File(getCurrentAppBackupDir(packageName, dateDir), DATA_TAR) + val dataDir = Constant.getDataDir(packageName) + if (ShellManager.tarCF(backupDataDir.absolutePath, dataDir)) { return backupDataDir } return null } + + fun backupAppAndroidData(dateDir: String, packageName: String): String? { + val backupDataDir = File( + getCurrentAppBackupDir(packageName, dateDir), ANDROID_DATA_TAR + ) + val dataDir = + Environment.getExternalStorageDirectory().absolutePath + "/Android/data/" + packageName + if (!File(dataDir).exists()) { + return "" + } + if (ShellManager.tarCF(backupDataDir.absolutePath, dataDir)) { + return backupDataDir.absolutePath + } + return null + } + + fun saveBackupInfo( + apkInfo: ApkInfo, + backupAppItemInfo: BackupAppItemInfo, + tarFile: String + ): Boolean { + val apkFile = apkInfo.applicationInfo.publicSourceDir + val tmpApkFile = File(getAppBackupDir(apkInfo.applicationInfo.packageName), "tmp.apk") + ShellManager.cpFile(apkFile, tmpApkFile.absolutePath) + runCatching { + ApkFile(tmpApkFile).allIcons.find { it.isFile }?.data?.let { + File(getAppBackupDir(apkInfo.applicationInfo.packageName), ICON_PNG).writeBytes(it) + } + } + tmpApkFile.delete() + backupAppItemInfo.androidDataFile = ANDROID_DATA_TAR + backupAppItemInfo.dataFile = DATA_TAR + val configFile = File(getAppBackupDir(apkInfo.applicationInfo.packageName), CONFIG_JSON) + val info = configFile.safeRead().fromJson() ?: BackupAppInfo() + info.appName = apkInfo.getAppName() + info.packageName = apkInfo.applicationInfo.packageName + info.backupMap[tarFile] = backupAppItemInfo + configFile.writeText(info.toJson().orEmpty()) + return true + } + + + fun zipBackupFile(packageName: String, dateDir: String): File? { + val target = File(getAppBackupDir(packageName), "$dateDir.tar") + val src = getCurrentAppBackupDir(packageName, dateDir).absolutePath + if (ShellManager.tarCF(target.absolutePath, src)) { + ShellManager.rmFile(src) + return target + } + return null + } + + fun getAllBackupInfo(): Single> { + return safeCreateSingle { + val list = arrayListOf() + backupRootDir.listFiles()?.forEach { root -> + val configJson = File(root, CONFIG_JSON) + if (!configJson.exists()) { + return@forEach + } + val info = configJson.safeRead().fromJson() ?: return@forEach + val map = info.backupMap.filter { File(root, it.key).exists() } + val icoFile = File(root, ICON_PNG).takeIf { it.exists() } + list.add(BackupAppData(info.appName, info.packageName, root, HashMap(map), icoFile)) + } + it.onSuccess(list) + } + } + + fun deleteBackupItem(packageName: String, tarFile: String): Single { + return safeCreateSingle { + val dir = backupRootDir.listFiles { _, name -> name == packageName }?.getOrNull(0) + val backupArch = dir?.let { File(it, tarFile) } + if (dir == null || !dir.exists() || backupArch?.exists() != true) { + it.onError(Exception()) + return@safeCreateSingle + } + backupArch.delete() + val configJson = File(dir, CONFIG_JSON) + val info = configJson.safeRead().fromJson() + if (info == null) { + it.onError(Exception()) + return@safeCreateSingle + } + val newInfo = info.copy( + backupMap = info.backupMap.apply { remove(tarFile) } + ) + configJson.writeText(newInfo.toJson().orEmpty()) + it.onSuccess(newInfo) + } + + } + + } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/util/DateUtilsExt.kt b/app/src/main/java/com/wrbug/developerhelper/util/DateUtilsExt.kt index 3e6e0cd..6fc9513 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/DateUtilsExt.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/DateUtilsExt.kt @@ -1,9 +1,11 @@ package com.wrbug.developerhelper.util +import android.annotation.SuppressLint import java.text.SimpleDateFormat import java.util.* +@SuppressLint("SimpleDateFormat") fun Date.format(format: String="yyyy-MM-dd HH:mm:ss"): String { val simpleDateFormat = SimpleDateFormat(format) return simpleDateFormat.format(this) diff --git a/app/src/main/java/com/wrbug/developerhelper/util/DeviceUtils.kt b/app/src/main/java/com/wrbug/developerhelper/util/DeviceUtils.kt index b44b4c9..59742b1 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/DeviceUtils.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/DeviceUtils.kt @@ -3,9 +3,10 @@ package com.wrbug.developerhelper.util import android.content.Context import android.os.Build import android.provider.Settings -import android.text.TextUtils -import com.wrbug.developerhelper.basecommon.BaseApp -import com.wrbug.developerhelper.commonutil.ShellUtils +import android.util.DisplayMetrics +import android.view.WindowManager +import com.wrbug.developerhelper.base.BaseApp +import com.wrbug.developerhelper.commonutil.shell.ShellUtils object DeviceUtils { @@ -14,10 +15,27 @@ object DeviceUtils { } fun isFloatWindowOpened(): Boolean { - return isFloatWindowOpened(BaseApp.instance!!) + return isFloatWindowOpened(BaseApp.instance) } fun isFloatWindowOpened(context: Context): Boolean { return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(context) } + + + fun getScreenWidth(): Int { + val manager: WindowManager = + BaseApp.instance.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val outMetrics = DisplayMetrics() + manager.defaultDisplay.getMetrics(outMetrics) + return outMetrics.widthPixels + } + + fun getScreenHeight(): Int { + val manager: WindowManager = + BaseApp.instance.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val outMetrics = DisplayMetrics() + manager.defaultDisplay.getMetrics(outMetrics) + return outMetrics.heightPixels + } } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/util/EnforceUtils.kt b/app/src/main/java/com/wrbug/developerhelper/util/EnforceUtils.kt deleted file mode 100644 index c4d389e..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/util/EnforceUtils.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.wrbug.developerhelper.util -import com.wrbug.developerhelper.commonutil.shell.ShellManager - -object EnforceUtils { - fun getEnforceType(packageName: String): EnforceType { - val files = ShellManager.getZipFileList(ShellManager.findApkDir(packageName)).toString().trim() - return when { - isIjiaMi(files) -> EnforceType.I_JIA_MI - is360(files) -> EnforceType.QI_HOO - isLeGu(files) -> EnforceType.LE_GU - isBangcle(files) -> EnforceType.BANGCLE - else -> EnforceType.UN_KNOWN - } - } - - private fun isIjiaMi(files: String): Boolean { - return files.contains("ijm") or files.contains("ijiami") - - } - - private fun is360(files: String): Boolean { - return files.contains("libjiagu.so") - } - - private fun isLeGu(files: String): Boolean { - return files.contains("tencent_stub") - } - - private fun isBangcle(files: String): Boolean { - return files.contains("bangcle") || files.contains("SecShell") - } - - - enum class EnforceType(val type: String) { - QI_HOO("360"), - BAI_DU("百度"), - I_JIA_MI("爱加密"), - LE_GU("乐固"), - BANGCLE("梆梆"), - UN_KNOWN("未知/无加固"), - } -} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/util/NetBarStarter.java b/app/src/main/java/com/wrbug/developerhelper/util/NetBarStarter.java deleted file mode 100644 index f88efb4..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/util/NetBarStarter.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.wrbug.developerhelper.util; - -import android.content.Context; -import android.content.Intent; -import com.github.megatronking.netbare.NetBare; -import com.github.megatronking.netbare.NetBareConfig; -import com.github.megatronking.netbare.http.HttpInjectInterceptor; -import com.github.megatronking.netbare.http.HttpInterceptorFactory; -import com.github.megatronking.netbare.ssl.JKS; - -import java.util.List; - -public class NetBarStarter { - private Object mlock = new Object(); - private static NetBarStarter instance; - private NetBare netBare; - - private NetBarStarter() { - netBare = NetBare.get(); - } - - private static NetBarStarter getInstance() { - if (instance == null) { - synchronized (NetBarStarter.class) { - if (instance == null) { - instance = new NetBarStarter(); - } - } - - } - return instance; - } - - public static boolean isJksInstalled(Context context) { - return JKS.isInstalled(context, JKS.getJskAlias()); - } - - public static boolean installJks(Context context) { - if (isJksInstalled(context)) { - return true; - } - try { - JKS.install(context, JKS.getJskAlias(), JKS.getJskAlias()); - return true; - } catch (Exception e) { - e.printStackTrace(); - // 安装失败 - } - return false; - } - - public static Intent prepareVpn() { - return getInstance().netBare.prepare(); - } - - public static void toggle(Context context) { - if (getInstance().netBare.isActive()) { - getInstance().netBare.stop(); - } else { - start(context); - } - } - - private static void start(Context context) { - // 安装自签证书 - if (!installJks(context)) { - return; - } - // 配置VPN - Intent intent = prepareVpn(); - if (intent != null) { - return; - } - // 启动NetBare服务 - getInstance().netBare.start( - NetBareConfig.defaultHttpConfig( - JKS.getJks(), - interceptorFactories() - ).newBuilder().dumpUid(true).build() - ); - } - - private static List interceptorFactories() { - return null; - } -} diff --git a/app/src/main/java/com/wrbug/developerhelper/util/OutSharedPreferenceManager.kt b/app/src/main/java/com/wrbug/developerhelper/util/OutSharedPreferenceManager.kt index cc64336..5a6c852 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/OutSharedPreferenceManager.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/OutSharedPreferenceManager.kt @@ -1,37 +1,95 @@ package com.wrbug.developerhelper.util +import android.annotation.SuppressLint import android.content.Context +import com.wrbug.developerhelper.commonutil.Constant +import com.wrbug.developerhelper.commonutil.shell.ShellManager import com.wrbug.developerhelper.commonutil.toBoolean import com.wrbug.developerhelper.commonutil.toInt import com.wrbug.developerhelper.commonutil.toLong import com.wrbug.developerhelper.model.entity.SharedPreferenceItemInfo +import io.reactivex.rxjava3.core.Single import java.io.File -object OutSharedPreferenceManager { - fun saveToFile(context: Context, list: Array): File { - val name = System.currentTimeMillis().toString() - val sp = context.applicationContext.getSharedPreferences(name, Context.MODE_PRIVATE) - val edit = sp.edit() - for (sharedPreferenceItemInfo in list) { - when (sharedPreferenceItemInfo.type.toLowerCase()) { - "string" -> { - edit.putString(sharedPreferenceItemInfo.key, sharedPreferenceItemInfo.getValidValue()) - } - "int" -> { - edit.putInt(sharedPreferenceItemInfo.key, sharedPreferenceItemInfo.getValidValue().toInt()) - } - "long" -> { - edit.putLong(sharedPreferenceItemInfo.key, sharedPreferenceItemInfo.getValidValue().toLong()) - } - "boolean" -> { - edit.putBoolean(sharedPreferenceItemInfo.key, sharedPreferenceItemInfo.getValidValue().toBoolean()) - } - "float" -> { - edit.putFloat(sharedPreferenceItemInfo.key, sharedPreferenceItemInfo.getValidValue().toFloat()) - } +class OutSharedPreference(private val context: Context, private val filePath: String) { + private val fileName by lazy { + System.currentTimeMillis().toString() + } + private val tmpSpFile by lazy { + File("${Constant.getDataDir(context.packageName)}/shared_prefs/${fileName}.xml") + } + private val tmpFile by lazy { + File(context.cacheDir, "$fileName.xml") + } + + fun parse(): Single> { + return Single.just(filePath).map { + if (!ShellManager.catFile(it, tmpFile.absolutePath, "777")) { + return@map emptyArray() } + val xml = tmpFile.readText() + XmlUtil.parseSharedPreference(xml) } - edit.commit() - return File("/data/data/${context.packageName}/shared_prefs/$name.xml") + } + + @SuppressLint("ApplySharedPref") + fun saveToFile(context: Context, list: Array): Single { + return Single.just(list).map { + val sp = context.applicationContext.getSharedPreferences(fileName, Context.MODE_PRIVATE) + val edit = sp.edit() + for (sharedPreferenceItemInfo in list) { + when (sharedPreferenceItemInfo.type.lowercase()) { + "string" -> { + edit.putString( + sharedPreferenceItemInfo.key, + sharedPreferenceItemInfo.getValidValue() + ) + } + + "int" -> { + edit.putInt( + sharedPreferenceItemInfo.key, + sharedPreferenceItemInfo.getValidValue().toInt() + ) + } + + "long" -> { + edit.putLong( + sharedPreferenceItemInfo.key, + sharedPreferenceItemInfo.getValidValue().toLong() + ) + } + + "boolean" -> { + edit.putBoolean( + sharedPreferenceItemInfo.key, + sharedPreferenceItemInfo.getValidValue().toBoolean() + ) + } + + "float" -> { + edit.putFloat( + sharedPreferenceItemInfo.key, + sharedPreferenceItemInfo.getValidValue().toFloat() + ) + } + + "set" -> { + edit.putStringSet( + sharedPreferenceItemInfo.key, + sharedPreferenceItemInfo.getValidValue().split(",").toSet() + ) + } + } + } + edit.commit() + ShellManager.catFile(tmpSpFile.absolutePath, filePath, "666") + }.onErrorReturn { false } + } + + + fun deleteTmpFile() { + tmpSpFile.delete() + tmpFile.delete() } } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/util/ResourceUtilsExt.kt b/app/src/main/java/com/wrbug/developerhelper/util/ResourceUtilsExt.kt index 4effcd8..a5083dc 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/ResourceUtilsExt.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/ResourceUtilsExt.kt @@ -1,12 +1,27 @@ package com.wrbug.developerhelper.util import android.content.Context -import com.wrbug.developerhelper.basecommon.BaseApp +import android.content.res.Configuration +import androidx.core.content.ContextCompat +import com.wrbug.developerhelper.base.BaseApp -fun Int.toResString(context: Context = BaseApp.instance): String { +fun Int.getString(context: Context = BaseApp.instance): String { return context.getString(this) } +fun Int.getColor(context: Context = BaseApp.instance): Int { + return ContextCompat.getColor(context, this) +} + +fun Int.getString(vararg formatArgs: Any): String { + return BaseApp.instance.getString(this, *formatArgs) +} + fun getString(resId: Int): String { return BaseApp.instance.getString(resId) +} + + +fun isPortrait(): Boolean { + return BaseApp.instance.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/util/UpdateUtils.kt b/app/src/main/java/com/wrbug/developerhelper/util/UpdateUtils.kt index 3010f62..c991959 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/UpdateUtils.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/UpdateUtils.kt @@ -1,42 +1,11 @@ package com.wrbug.developerhelper.util -import com.wrbug.developerhelper.commonutil.HttpUtil -import com.wrbug.developerhelper.commonutil.shell.Callback import org.jetbrains.anko.doAsync -import org.jetbrains.anko.uiThread -import com.wrbug.developerhelper.commonutil.OkhttpUtils -import com.wrbug.developerhelper.model.entity.VersionInfo -import org.jsoup.Jsoup -import java.lang.Exception object UpdateUtils { - private const val URL = "https://www.coolapk.com/apk/com.wrbug.developerhelper" - fun checkUpdate(callback: Callback) { + fun checkUpdate() { doAsync { - try { - val document = Jsoup.connect(URL) - .sslSocketFactory(OkhttpUtils.createSSLSocketFactory()).get() - val versionName = document.getElementsByClass("list_app_info").text() ?: "" - val feature = document.getElementsByClass("apk_left_title_info").first().html().replace("
", "\n") - val size = document.getElementsByClass("apk_topba_message").html().split("/")[0].trim() - val updateTime = - document.getElementsByClass("apk_left_title_info")[2].html().split("
")[1].replace( - "更新时间:", - "" - ) - val info = VersionInfo() - info.versionName = versionName - info.feature = feature - info.size = size - info.updateDate = updateTime - info.downloadUrl = URL - uiThread { - callback.onSuccess(info) - } - } catch (e: Exception) { - uiThread { callback.onFailed() } - } } diff --git a/app/src/main/java/com/wrbug/developerhelper/util/ViewExt.kt b/app/src/main/java/com/wrbug/developerhelper/util/ViewExt.kt new file mode 100644 index 0000000..4a39531 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/util/ViewExt.kt @@ -0,0 +1,94 @@ +package com.wrbug.developerhelper.util + +import android.app.ActionBar.LayoutParams +import android.os.SystemClock +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.Toast +import androidx.core.view.isVisible +import com.airbnb.lottie.LottieAnimationView +import com.airbnb.lottie.LottieDrawable +import com.bumptech.glide.Glide +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.mmkv.ConfigKv +import com.wrbug.developerhelper.mmkv.manager.MMKVManager + +private val configKv by lazy { + MMKVManager.get(ConfigKv::class.java) +} + +fun View?.setOnDoubleCheckClickListener(duration: Long = 800, clickListener: (View) -> Unit) { + this?.setOnClickListener { + val time = SystemClock.elapsedRealtime() + val lastTime = (it.getTag(R.id.double_check_click) as? Long) ?: 0 + if (time - lastTime> duration) { + it.setTag(R.id.double_check_click, time) + clickListener(it) + } + } +} + + +fun View?.setOnRootCheckClickListener(clickListener: (View) -> Unit) { + setOnDoubleCheckClickListener { + if (!configKv.isOpenRoot()) { + Toast.makeText(it.context, R.string.open_root_notice, Toast.LENGTH_SHORT).show() + return@setOnDoubleCheckClickListener + } + clickListener(it) + } +} + +fun FrameLayout.startPageLoading() { + val lottieView = LottieAnimationView(context) + lottieView.id = R.id.lottie_loading + lottieView.setAnimation(R.raw.lottie_page_loading) + lottieView.playAnimation() + lottieView.repeatCount = LottieDrawable.INFINITE + removeAllViews() + isVisible = true + addView( + lottieView, + FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + ) +} + +fun FrameLayout.stopPageLoading() { + val lottieView: LottieAnimationView? = findViewById(R.id.lottie_loading) + lottieView?.cancelAnimation() + removeAllViews() + isVisible = false +} + + +fun ImageView.loadImage(url: Any?, default: Int? = null) { + Glide.with(this).load(url).apply { + if (default != null) { + error(default) + } + }.into(this) +} + + +inline var View.visible: Boolean + set(value) { + visibility = if (value) { + View.VISIBLE + } else { + View.GONE + } + } + get() = visibility == View.VISIBLE + + +inline var View.inVisible: Boolean + set(value) { + visibility = if (value) { + View.INVISIBLE + } else { + View.VISIBLE + } + } + get() = visibility == View.INVISIBLE \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/util/XmlUtil.kt b/app/src/main/java/com/wrbug/developerhelper/util/XmlUtil.kt index 1347a91..4b89580 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/XmlUtil.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/XmlUtil.kt @@ -3,6 +3,7 @@ package com.wrbug.developerhelper.util import com.wrbug.developerhelper.model.entity.SharedPreferenceItemInfo import org.dom4j.DocumentHelper import org.dom4j.Element +import org.dom4j.tree.DefaultElement /** * xml相关的工具类 @@ -28,6 +29,11 @@ object XmlUtil { if (type == "string") { info.value = child.text info.newValue = child.text + } else if (type == "set") { + info.value = child.content().filterIsInstance(DefaultElement::class.java) + .joinToString(",") { it.text } + info.newValue = child.content().filterIsInstance(DefaultElement::class.java) + .joinToString(",") { it.text } } else { info.value = child.attributeValue("value") info.newValue = child.attributeValue("value") diff --git a/app/src/main/res/drawable/bg_round_8dp_white.xml b/app/src/main/res/drawable/bg_round_8dp_white.xml new file mode 100644 index 0000000..3edf0a7 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_8dp_white.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_accessibility.xml b/app/src/main/res/drawable/ic_accessibility.xml new file mode 100644 index 0000000..c872987 --- /dev/null +++ b/app/src/main/res/drawable/ic_accessibility.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_accessibility_666666.xml b/app/src/main/res/drawable/ic_accessibility_666666.xml deleted file mode 100644 index 0204f1a..0000000 --- a/app/src/main/res/drawable/ic_accessibility_666666.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_accessibility_black.xml b/app/src/main/res/drawable/ic_accessibility_black.xml deleted file mode 100644 index 3239379..0000000 --- a/app/src/main/res/drawable/ic_accessibility_black.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_application.xml b/app/src/main/res/drawable/ic_application.xml new file mode 100644 index 0000000..630ea85 --- /dev/null +++ b/app/src/main/res/drawable/ic_application.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_canceled.xml b/app/src/main/res/drawable/ic_canceled.xml new file mode 100644 index 0000000..99b4c15 --- /dev/null +++ b/app/src/main/res/drawable/ic_canceled.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_capture_666666.xml b/app/src/main/res/drawable/ic_capture_666666.xml deleted file mode 100644 index 06e142e..0000000 --- a/app/src/main/res/drawable/ic_capture_666666.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_chevron_left.xml b/app/src/main/res/drawable/ic_chevron_left.xml new file mode 100644 index 0000000..2fd7c1c --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_left.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_left_white_24dp.xml b/app/src/main/res/drawable/ic_chevron_left_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_chevron_left_white_24dp.xml rename to app/src/main/res/drawable/ic_chevron_left_24dp.xml diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right.xml new file mode 100644 index 0000000..ce2744c --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_right_white_24dp.xml b/app/src/main/res/drawable/ic_chevron_right_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_chevron_right_white_24dp.xml rename to app/src/main/res/drawable/ic_chevron_right_24dp.xml diff --git a/app/src/main/res/drawable/ic_copy.xml b/app/src/main/res/drawable/ic_copy.xml new file mode 100644 index 0000000..5b54247 --- /dev/null +++ b/app/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_default_app_ico_place_holder.xml b/app/src/main/res/drawable/ic_default_app_ico_place_holder.xml new file mode 100644 index 0000000..0eaded5 --- /dev/null +++ b/app/src/main/res/drawable/ic_default_app_ico_place_holder.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_fail.xml b/app/src/main/res/drawable/ic_fail.xml new file mode 100644 index 0000000..65ffd33 --- /dev/null +++ b/app/src/main/res/drawable/ic_fail.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_float_air_bubble_666666.xml b/app/src/main/res/drawable/ic_float_air_bubble_666666.xml deleted file mode 100644 index 31149a0..0000000 --- a/app/src/main/res/drawable/ic_float_air_bubble_666666.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_ic_empty_666666.xml b/app/src/main/res/drawable/ic_ic_empty.xml similarity index 100% rename from app/src/main/res/drawable/ic_ic_empty_666666.xml rename to app/src/main/res/drawable/ic_ic_empty.xml diff --git a/app/src/main/res/drawable/ic_ignore.xml b/app/src/main/res/drawable/ic_ignore.xml new file mode 100644 index 0000000..04dbaad --- /dev/null +++ b/app/src/main/res/drawable/ic_ignore.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 0000000..b60ed3d --- /dev/null +++ b/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_root_666666.xml b/app/src/main/res/drawable/ic_root.xml similarity index 100% rename from app/src/main/res/drawable/ic_root_666666.xml rename to app/src/main/res/drawable/ic_root.xml diff --git a/app/src/main/res/drawable/ic_search_white_24dp.xml b/app/src/main/res/drawable/ic_search_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_search_white_24dp.xml rename to app/src/main/res/drawable/ic_search_24.xml diff --git a/app/src/main/res/drawable/ic_sheldon_root.xml b/app/src/main/res/drawable/ic_sheldon_root.xml new file mode 100644 index 0000000..ef66512 --- /dev/null +++ b/app/src/main/res/drawable/ic_sheldon_root.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_shield_666666.xml b/app/src/main/res/drawable/ic_shield.xml similarity index 100% rename from app/src/main/res/drawable/ic_shield_666666.xml rename to app/src/main/res/drawable/ic_shield.xml diff --git a/app/src/main/res/drawable/ic_success.xml b/app/src/main/res/drawable/ic_success.xml new file mode 100644 index 0000000..309b83d --- /dev/null +++ b/app/src/main/res/drawable/ic_success.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_switch.xml b/app/src/main/res/drawable/ic_switch.xml new file mode 100644 index 0000000..fce0772 --- /dev/null +++ b/app/src/main/res/drawable/ic_switch.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_waiting.xml b/app/src/main/res/drawable/ic_waiting.xml new file mode 100644 index 0000000..b090eb1 --- /dev/null +++ b/app/src/main/res/drawable/ic_waiting.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_xposed_666666.xml b/app/src/main/res/drawable/ic_xposed_666666.xml deleted file mode 100644 index 247f7a8..0000000 --- a/app/src/main/res/drawable/ic_xposed_666666.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_xposed_custom_666666.xml b/app/src/main/res/drawable/ic_xposed_custom_666666.xml deleted file mode 100644 index 80f08b0..0000000 --- a/app/src/main/res/drawable/ic_xposed_custom_666666.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/onboarding_indicator_selected.xml b/app/src/main/res/drawable/onboarding_indicator_selected.xml deleted file mode 100644 index 3d6e9fa..0000000 --- a/app/src/main/res/drawable/onboarding_indicator_selected.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/onboarding_indicator_unselected.xml b/app/src/main/res/drawable/onboarding_indicator_unselected.xml deleted file mode 100644 index 7461fa9..0000000 --- a/app/src/main/res/drawable/onboarding_indicator_unselected.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/commonwidget/src/main/res/drawable/toast_bg_circle_cornor_rect.xml b/app/src/main/res/drawable/toast_bg_circle_cornor_rect.xml similarity index 100% rename from commonwidget/src/main/res/drawable/toast_bg_circle_cornor_rect.xml rename to app/src/main/res/drawable/toast_bg_circle_cornor_rect.xml diff --git a/app/src/main/res/drawable/widget_ic_no_data.xml b/app/src/main/res/drawable/widget_ic_no_data.xml new file mode 100644 index 0000000..35be377 --- /dev/null +++ b/app/src/main/res/drawable/widget_ic_no_data.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_app_backup_detail.xml b/app/src/main/res/layout/activity_app_backup_detail.xml new file mode 100644 index 0000000..6960008 --- /dev/null +++ b/app/src/main/res/layout/activity_app_backup_detail.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_backup_app.xml b/app/src/main/res/layout/activity_backup_app.xml new file mode 100644 index 0000000..bc844e7 --- /dev/null +++ b/app/src/main/res/layout/activity_backup_app.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_database_edit.xml b/app/src/main/res/layout/activity_database_edit.xml index bfacaa4..4492843 100644 --- a/app/src/main/res/layout/activity_database_edit.xml +++ b/app/src/main/res/layout/activity_database_edit.xml @@ -25,7 +25,8 @@ android:padding="20dp" android:text="@string/table_data_is_empty" android:textSize="18sp" - android:visibility="gone" /> + android:visibility="gone" + app:layout_constraintTop_toTopOf="parent" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4979553..7325ea0 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,110 +1,114 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".ui.activity.main.MainActivity"> - - + - - + + - - - + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - - - - + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingHorizontal="24dp"> - - - + android:paddingTop="16dp" + android:paddingBottom="8dp" + android:text="@string/base_setting" + android:textColor="@color/colorAccent" /> - + - + - + - + - + + + + + - + - - - - - \ No newline at end of file + + + + diff --git a/app/src/main/res/layout/activity_shared_preference_edit.xml b/app/src/main/res/layout/activity_shared_preference_edit.xml index 3bec598..8f95392 100644 --- a/app/src/main/res/layout/activity_shared_preference_edit.xml +++ b/app/src/main/res/layout/activity_shared_preference_edit.xml @@ -17,5 +17,19 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:listitem="@layout/item_shared_preference_info" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_shell_app_manager.xml b/app/src/main/res/layout/activity_shell_app_manager.xml deleted file mode 100644 index 5b1cc8a..0000000 --- a/app/src/main/res/layout/activity_shell_app_manager.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_xposed_setting.xml b/app/src/main/res/layout/activity_xposed_setting.xml deleted file mode 100644 index 8b6e95f..0000000 --- a/app/src/main/res/layout/activity_xposed_setting.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_apk_info.xml b/app/src/main/res/layout/dialog_apk_info.xml index 7d8a32a..e3d19ea 100644 --- a/app/src/main/res/layout/dialog_apk_info.xml +++ b/app/src/main/res/layout/dialog_apk_info.xml @@ -1,4 +1,5 @@ + - \ No newline at end of file + + diff --git a/app/src/main/res/layout/dialog_backup_app.xml b/app/src/main/res/layout/dialog_backup_app.xml new file mode 100644 index 0000000..9211711 --- /dev/null +++ b/app/src/main/res/layout/dialog_backup_app.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_backup_app_select.xml b/app/src/main/res/layout/dialog_backup_app_select.xml new file mode 100644 index 0000000..21d3dc6 --- /dev/null +++ b/app/src/main/res/layout/dialog_backup_app_select.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_guide.xml b/app/src/main/res/layout/fragment_guide.xml index e67d07d..4e15b75 100644 --- a/app/src/main/res/layout/fragment_guide.xml +++ b/app/src/main/res/layout/fragment_guide.xml @@ -1,4 +1,5 @@ @@ -48,5 +49,4 @@ tools:text="Your awesome description about the app" /> - - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_backup_app_info.xml b/app/src/main/res/layout/item_backup_app_info.xml new file mode 100644 index 0000000..9fdd236 --- /dev/null +++ b/app/src/main/res/layout/item_backup_app_info.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_backup_detail_info.xml b/app/src/main/res/layout/item_backup_detail_info.xml new file mode 100644 index 0000000..b35cff6 --- /dev/null +++ b/app/src/main/res/layout/item_backup_detail_info.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_empty_data.xml b/app/src/main/res/layout/item_empty_data.xml new file mode 100644 index 0000000..c90c36a --- /dev/null +++ b/app/src/main/res/layout/item_empty_data.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_info_loading.xml b/app/src/main/res/layout/item_info_loading.xml new file mode 100644 index 0000000..89661e5 --- /dev/null +++ b/app/src/main/res/layout/item_info_loading.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_loading_data.xml b/app/src/main/res/layout/item_loading_data.xml new file mode 100644 index 0000000..54f2089 --- /dev/null +++ b/app/src/main/res/layout/item_loading_data.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_shared_preference_info.xml b/app/src/main/res/layout/item_shared_preference_info.xml index 44c755d..6b6eb94 100644 --- a/app/src/main/res/layout/item_shared_preference_info.xml +++ b/app/src/main/res/layout/item_shared_preference_info.xml @@ -16,8 +16,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:textColor="@color/item_title_text" - android:textSize="16sp" + android:textAppearance="@style/TextAppearance.ItemTitle" tools:text="标题" /> + android:textColor="@color/material_color_light_blue_500" + android:visibility="gone" /> diff --git a/app/src/main/res/layout/item_shell_app_info.xml b/app/src/main/res/layout/item_shell_app_info.xml index 3e0a3d0..c24e1f3 100644 --- a/app/src/main/res/layout/item_shell_app_info.xml +++ b/app/src/main/res/layout/item_shell_app_info.xml @@ -1,5 +1,6 @@ - + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:background="@color/material_color_red_500" /> - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_view_info.xml b/app/src/main/res/layout/item_view_info.xml index a92cd12..57ce62b 100644 --- a/app/src/main/res/layout/item_view_info.xml +++ b/app/src/main/res/layout/item_view_info.xml @@ -1,28 +1,45 @@ - + android:paddingHorizontal="24dp"> - \ No newline at end of file + + + \ No newline at end of file diff --git a/commonwidget/src/main/res/layout/layout_toast_flexible.xml b/app/src/main/res/layout/layout_toast_flexible.xml similarity index 100% rename from commonwidget/src/main/res/layout/layout_toast_flexible.xml rename to app/src/main/res/layout/layout_toast_flexible.xml diff --git a/app/src/main/res/layout/layout_toolbar.xml b/app/src/main/res/layout/layout_toolbar.xml index 5532922..9b16398 100644 --- a/app/src/main/res/layout/layout_toolbar.xml +++ b/app/src/main/res/layout/layout_toolbar.xml @@ -1,13 +1,13 @@ - \ No newline at end of file diff --git a/app/src/main/res/layout/view_app_bar.xml b/app/src/main/res/layout/view_app_bar.xml new file mode 100644 index 0000000..ba968b3 --- /dev/null +++ b/app/src/main/res/layout/view_app_bar.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_app_data_info.xml b/app/src/main/res/layout/view_app_data_info.xml index bca1bbc..104f635 100644 --- a/app/src/main/res/layout/view_app_data_info.xml +++ b/app/src/main/res/layout/view_app_data_info.xml @@ -17,8 +17,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Apk目录" - android:textColor="@color/item_title_text" - android:textSize="16sp" /> + android:textAppearance="@style/TextAppearance.ItemTitle" /> + android:textAppearance="@style/TextAppearance.ItemTitle" /> + android:textAppearance="@style/TextAppearance.ItemTitle" /> + android:textAppearance="@style/TextAppearance.ItemTitle" /> + android:textAppearance="@style/TextAppearance.ItemTitle" /> - + android:paddingTop="24dp" + app:constraint_referenced_ids="backupAppBtn,restoreAppBtn,restartAppBtn,stopAppBtn,deleteAppDataBtn,uninstallAppBtn" + app:flow_verticalGap="16dp" + app:flow_wrapMode="aligned" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + android:text="@string/backup_app_file" + app:layout_constraintWidth_percent="0.45" /> + android:text="@string/restore_app_file" + app:layout_constraintWidth_percent="0.45" /> + app:layout_constraintWidth_percent="0.45" /> + app:layout_constraintWidth_percent="0.45" /> + app:layout_constraintWidth_percent="0.45" /> - - + app:layout_constraintWidth_percent="0.45" /> \ No newline at end of file diff --git a/app/src/main/res/layout/view_backup_progress.xml b/app/src/main/res/layout/view_backup_progress.xml new file mode 100644 index 0000000..102ea64 --- /dev/null +++ b/app/src/main/res/layout/view_backup_progress.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_empty_view.xml b/app/src/main/res/layout/view_empty_view.xml new file mode 100644 index 0000000..6b19298 --- /dev/null +++ b/app/src/main/res/layout/view_empty_view.xml @@ -0,0 +1,31 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_layout_info.xml b/app/src/main/res/layout/view_layout_info.xml index 381235f..531797c 100644 --- a/app/src/main/res/layout/view_layout_info.xml +++ b/app/src/main/res/layout/view_layout_info.xml @@ -3,12 +3,17 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/layoutInfoContainer" android:layout_width="match_parent" - android:layout_height="300dp"> + android:layout_height="match_parent" + android:background="@android:color/white"> - + android:paddingVertical="8dp" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> - + tools:src="@drawable/ic_accessibility" /> - + app:layout_constraintTop_toTopOf="@+id/titleTv" /> + + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/raw/lottie_list_loading.json b/app/src/main/res/raw/lottie_list_loading.json new file mode 100644 index 0000000..8fa429d --- /dev/null +++ b/app/src/main/res/raw/lottie_list_loading.json @@ -0,0 +1 @@ +{"v":"5.7.4","fr":29.9700012207031,"ip":0,"op":48.0000019550801,"w":400,"h":400,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1L 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,289,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.928477328431,0.15922648112,0.723155182483,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":21,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":29,"s":[50]},{"t":36.0000014663101,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":21,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":29,"s":[25]},{"t":36.0000014663101,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":21.0000008553475,"op":55.0000022401959,"st":21.0000008553475,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1L 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,289,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.928477328431,0.15922648112,0.723155182483,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":17,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[50]},{"t":32.0000013033867,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":17,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[25]},{"t":32.0000013033867,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":17.0000006924242,"op":51.0000020772726,"st":17.0000006924242,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 1L","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,289,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.928477328431,0.15922648112,0.723155182483,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[50]},{"t":20.0000008146167,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[25]},{"t":20.0000008146167,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":5.00000020365417,"op":39.0000015885026,"st":5.00000020365417,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 2R 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,339,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.482111194087,0.119379574645,0.946568627451,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":17,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[50]},{"t":32.0000013033867,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":17,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[25]},{"t":32.0000013033867,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":17.0000006924242,"op":52.0000021180034,"st":17.0000006924242,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 2R 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,339,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.482111194087,0.119379574645,0.946568627451,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[50]},{"t":37.0000015070409,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[25]},{"t":37.0000015070409,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":22.0000008960784,"op":57.0000023216576,"st":22.0000008960784,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 2R","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,339,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.482111194087,0.119379574645,0.946568627451,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":17,"s":[50]},{"t":24.00000097754,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":17,"s":[25]},{"t":24.00000097754,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":9.00000036657752,"op":44.0000017921567,"st":9.00000036657752,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 3L 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,389,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.096529941933,0.306382272758,0.935631127451,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[50]},{"t":37.0000015070409,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[25]},{"t":37.0000015070409,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":22.0000008960784,"op":56.0000022809268,"st":22.0000008960784,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 3L 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,389,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.096529941933,0.306382272758,0.935631127451,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":26,"s":[50]},{"t":33.0000013441176,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":26,"s":[25]},{"t":33.0000013441176,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":18.000000733155,"op":52.0000021180034,"st":18.000000733155,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 3L","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,389,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.096529941933,0.306382272758,0.935631127451,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[50]},{"t":20.0000008146167,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[25]},{"t":20.0000008146167,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":5.00000020365417,"op":39.0000015885026,"st":5.00000020365417,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 4R 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,439,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.069122576246,0.9015625,0.230790366378,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":21,"s":[50]},{"t":28.0000011404634,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":21,"s":[25]},{"t":28.0000011404634,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":13.0000005295009,"op":48.0000019550801,"st":13.0000005295009,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Shape Layer 4R 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,439,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.069122576246,0.9015625,0.230790366378,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[50]},{"t":37.0000015070409,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[25]},{"t":37.0000015070409,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":22.0000008960784,"op":57.0000023216576,"st":22.0000008960784,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 4R","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,439,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.069122576246,0.9015625,0.230790366378,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":3,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[50]},{"t":18.000000733155,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":3,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[25]},{"t":18.000000733155,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":3.00000012219251,"op":38.0000015477717,"st":3.00000012219251,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Shape Layer 5L 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,179,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.078822124706,0.739201085708,0.917202818627,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[50]},{"t":27.0000010997325,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[25]},{"t":27.0000010997325,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":12.00000048877,"op":46.0000018736184,"st":12.00000048877,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Shape Layer 5L 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,179,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.078822124706,0.739201085708,0.917202818627,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":27,"s":[50]},{"t":34.0000013848484,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":27,"s":[25]},{"t":34.0000013848484,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":19.0000007738859,"op":53.0000021587343,"st":19.0000007738859,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Shape Layer 5L 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,179,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.078822124706,0.739201085708,0.917202818627,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[50]},{"t":16.0000006516934,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[25]},{"t":16.0000006516934,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1.00000004073083,"op":35.0000014255792,"st":1.00000004073083,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Shape Layer 5L","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,179,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.078822124706,0.739201085708,0.917202818627,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[50]},{"t":23.0000009368092,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[25]},{"t":23.0000009368092,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":8.00000032584668,"op":42.0000017106951,"st":8.00000032584668,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Shape Layer 6R 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,239,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.524494904163,0.884911151961,0.087406652114,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":23,"s":[50]},{"t":30.0000012219251,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":23,"s":[25]},{"t":30.0000012219251,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":15.0000006109625,"op":50.0000020365418,"st":15.0000006109625,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Shape Layer 6R 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,239,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.524494904163,0.884911151961,0.087406652114,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[50]},{"t":37.0000015070409,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[25]},{"t":37.0000015070409,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":22.0000008960784,"op":57.0000023216576,"st":22.0000008960784,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"Shape Layer 6R","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,239,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.524494904163,0.884911151961,0.087406652114,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[50]},{"t":20.0000008146167,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[25]},{"t":20.0000008146167,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":5.00000020365417,"op":40.0000016292334,"st":5.00000020365417,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"Shape Layer 7L 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,489,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.611406034582,0.891253063725,0.090217530494,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0],"y":[0]},"t":21,"s":[50]},{"t":28.0000011404634,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":21,"s":[25]},{"t":28.0000011404634,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":13.0000005295009,"op":47.0000019143492,"st":13.0000005295009,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"Shape Layer 7L 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,489,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.611406034582,0.891253063725,0.090217530494,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":17,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0],"y":[0]},"t":25,"s":[50]},{"t":32.0000013033867,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":17,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[25]},{"t":32.0000013033867,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":17.0000006924242,"op":51.0000020772726,"st":17.0000006924242,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"Shape Layer 7L","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,489,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.611406034582,0.891253063725,0.090217530494,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":4,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0],"y":[0]},"t":12,"s":[50]},{"t":19.0000007738859,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":4,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":12,"s":[25]},{"t":19.0000007738859,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":4.00000016292334,"op":38.0000015477717,"st":4.00000016292334,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"Shape Layer 8R 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,532,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.954534313725,0.324577391381,0.102267328898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[50]},{"t":27.0000010997325,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[25]},{"t":27.0000010997325,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":12.00000048877,"op":47.0000019143492,"st":12.00000048877,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"Shape Layer 8R 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,532,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.954534313725,0.324577391381,0.102267328898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[50]},{"t":37.0000015070409,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[25]},{"t":37.0000015070409,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":22.0000008960784,"op":57.0000023216576,"st":22.0000008960784,"bm":0},{"ddd":0,"ind":25,"ty":4,"nm":"Shape Layer 8R","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[197.852,532,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-198,-154],[200,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.954534313725,0.324577391381,0.102267328898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[114.784,101.093],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[50]},{"t":21.0000008553475,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[25]},{"t":21.0000008553475,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":6.00000024438501,"op":41.0000016699642,"st":6.00000024438501,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/res/raw/lottie_loading.json b/app/src/main/res/raw/lottie_loading.json new file mode 100644 index 0000000..6347c41 --- /dev/null +++ b/app/src/main/res/raw/lottie_loading.json @@ -0,0 +1 @@ +{"nm":"Main Scene","ddd":0,"h":100,"w":180,"meta":{"g":"@lottiefiles/creator 1.25.0"},"layers":[{"ty":0,"nm":" Comp 1","sr":1,"st":0,"op":31.00000126265584,"ip":4.00000016292334,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[63,24]},"s":{"a":0,"k":[250,250]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"w":200,"h":100,"refId":"precomp_Comp 1_ff1747d4-671c-4717-aeaa-23476b2a6335","ind":1}],"v":"5.7.0","fr":29.9700012207031,"op":31,"ip":0,"assets":[{"nm":"","id":"comp_0_85618a3e-3dd7-43d3-a8a7-28422c05ed7d","layers":[{"ty":4,"nm":"Shape Layer 2","sr":1,"st":-4.00000016292334,"op":90.00000366577514,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-32.5,5,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[20,20,100],"t":9.000000162923339},{"s":[100,100,100],"t":18.00000073315504}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[72,74.8,0],"t":10.000000162923339},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[72,62.8,0],"t":16.00000016292334},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[72,66.1,0],"t":22.00000016292334},{"o":{"x":0.167,"y":0},"i":{"x":0.667,"y":1},"s":[72,64.5,0],"t":28.00000016292334},{"s":[72,64.8,0],"t":34.00000138484844}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":9.000000162923339},{"s":[100],"t":18.00000073315504}],"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[15,15],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.498,0.4902,0.5176],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-32.618,4.916],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Shape Layer 1","sr":1,"st":0,"op":94.00000382869844,"ip":4.00000016292334,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-32.5,5,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100,100,100],"t":6.00000016292334},{"s":[120,120,100],"t":15.000000610962541}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[72,64.75,0],"t":4.00000016292334},{"s":[72,44.8,0],"t":13.00000052950086}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100],"t":4.00000016292334},{"s":[0],"t":13.00000052950086}],"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[15,15],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.498,0.4902,0.5176],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-32.618,4.916],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2}]},{"nm":"Comp 1","id":"precomp_Comp 1_ff1747d4-671c-4717-aeaa-23476b2a6335","fr":29.9700012207031,"layers":[{"ty":0,"nm":"单ä ̧aç‚1","sr":1,"st":0,"op":94.00000382869844,"ip":4.00000016292334,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[50,50,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[49.75,38.5,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"w":100,"h":100,"refId":"comp_0_85618a3e-3dd7-43d3-a8a7-28422c05ed7d","ind":1},{"ty":0,"nm":"单ä ̧aç‚1","sr":1,"st":2.00000008146167,"op":96.00000391016015,"ip":6.00000024438501,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[50,50,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[74.75,38.5,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"w":100,"h":100,"refId":"comp_0_85618a3e-3dd7-43d3-a8a7-28422c05ed7d","ind":2},{"ty":4,"nm":"Shape Layer 3","sr":1,"st":0,"op":7.000000285115849,"ip":4.00000016292334,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-32.5,5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[96.75,53.25,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[15,15],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.498,0.4902,0.5176],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-32.618,4.916],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3},{"ty":4,"nm":"Shape Layer 2","sr":1,"st":0,"op":10.000000407308349,"ip":4.00000016292334,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-32.5,5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[121.75,53.25,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[15,15],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.498,0.4902,0.5176],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-32.618,4.916],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":4},{"ty":0,"nm":"单ä ̧aç‚1","sr":1,"st":4.00000016292334,"op":98.00000399162184,"ip":8.00000032584668,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[50,50,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[99.75,38.5,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"w":100,"h":100,"refId":"comp_0_85618a3e-3dd7-43d3-a8a7-28422c05ed7d","ind":5}]}]} \ No newline at end of file diff --git a/app/src/main/res/raw/lottie_page_loading.json b/app/src/main/res/raw/lottie_page_loading.json new file mode 100644 index 0000000..016f5fd --- /dev/null +++ b/app/src/main/res/raw/lottie_page_loading.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.21","a":"Syed Mubarak","k":"Loading","d":"Loading Animation","tc":"#FFFFFF"},"fr":29.9700012207031,"ip":0,"op":71.0000028918893,"w":1080,"h":1080,"nm":"Loading2","ddd":0,"assets":[{"id":"image_0","w":1080,"h":1080,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAQ4CAYAAADsEGyPAAAgAElEQVR4XuzdT6sdV7of4FXaGtxBwO5PYDU3cIdWk0kQp463ByGZBKs/gWVIRglY/gDBx5C51ZCMEmj5E7QMGSSZ9NZZmwOBQEuzEC60PMi4pdGdeKtCqbeu/0unalftXe9azwFzB7f+rPd5X7mt36la1SQ/BAgQIECAAAECBAgQIECAAIHgAk3w9Vs+AQIECBAgQIAAAQIECBAgQCAJOAwBAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQI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AgQIECBAgEABAgKOApqoBAIECBAgQIAAAQIECBAgULuAgKP2CVA/AQIECBAgQIAAAQIECBAoQEDAUUATlUCAAAECBAgQIECAAAECBGoXEHDUPgHqJ0CAAAECBAgQIECAAAECBQgIOApoohIIECBAgAABAgQIECBAgEDtAgKO2idA/QQIECBAgAABAgQIECBAoAABAUcBTVQCAQIECBAgQIAAAQIECBCoXUDAUfsEqJ8AAQIECBAgQIAAAQIECBQgIOAooIlKIECAAAECBAgQIECAAAECtQsIOGqfAPUTIECAAAECBAgQIECAAIECBAQcBTRRCQQIECBAgAABAgQIECBAoHYBAUftE6B+AgQIECBAgAABAgQIECBQgICAo4AmKoEAAQIECBAgQIAAAQIECNQuIOCofQLUT4AAAQIECBAgQIAAAQIEChAQcBTQRCUQIECAAAECBAgQIECAAIHaBQQctU+A+gkQIECAAAECBAgQIECAQAECAo4CmqgEAgQIECBAgAABAgQIECBQu4CAo/YJUD8BAgQIECBAgAABAgQIEChAQMBRQBOVQIAAAQIECBAgQIAAAQIEahcQcNQ+AeonQIAAAQIECBAgQIAAAQIFCAg4CmiiEggQIECAAAECBAgQIECAQO0CAo7aJ0D9BAgQIECAAAECBAgQIECgAAEBRwFNVAIBAgQIECBAgAABAgQIEKhdQMBR+wSonwABAgQIECBAgAABAgQIFCAg4CigiUogQIAAAQIECBAgQIAAAQK1Cwg4ap8A9RMgQIAAAQIECBAgQIAAgQIEBBwFNFEJBAgQIECAAAECBAgQIECgdgEBR+0ToH4CBAgQIECAAAECBAgQIFCAgICjgCYqgQABAgQIECBAgAABAgQI1C4g4Kh9AtRPgAABAgQIECBAgAABAgQKEBBwFNBEJRAgQIAAAQIECBAgQIAAgdoFBBy1T4D6CRAgQIAAAQIECBAgQIBAAQICjgKaqAQCBAgQIECAAAECBAgQIFC7gICj9glQPwECBAgQIECAAAECBAgQKEBAwFFAE5VAgAABAgQIECBAgAABAgRqFxBw1D4B6idAgAABAgQIECBAgAABAgUICDgKaKISCBAgQIAAAQIECBAgQIBA7QICjtonQP0ECBAgQIAAAQIECBAgQKAAAQFHAU1UAgECBAgQIECAAAECBAgQqF1AwFH7BKifAAECBAgQIECAAAECBAgUICDgKKCJSiBAgAABAgQIECBAgAABArULCDhqnwD1EyBAgAABAgQIECBAgACBAgQEHAU0UQkECBAgQIAAAQIECBAgQKB2AQFH7ROgfgIECBAgQIAAAQIECBAgUICAgKOAJiqBAAECBAgQIECAAAECBAjULiDgqH0C1E+AAAECBAgQIECAAAECBAoQEHAU0EQlECBAgAABAgQIECBAgACB2gUEHLVPgPoJECBAgAABAgQIECBAgEABAgKOApqoBAIECBAgQIAAAQIECBAgULuAgKP2CVA/AQIECBAgQIAAAQIECBAoQEDAUUATlUCAAAECBAgQIECAAAECBGoXEHDUPgHqJ0CAAAECBAgQIECAAAECBQgIOApoohIIECBAgAABAgQIECBAgEDtAgKO2idA/QQIECBAgAABAgQIECBAoAABAUcBTVQCAQIECBAgQIAAAQIECBCoXUDAUfsEqJ8AAQIECBAgQIAAAQIECBQgIOAooIlKIECAAAECBAgQIECAAAECtQsIOGqfAPUTIECAAAECBAgQIECAAIECBAQcBTRRCQQIECBAgAABAgQIECBAoHYBAUftE6B+AgQIECBAgAABAgQIECBQgICAo4AmKoEAAQIECBAgQIAAAQIECNQuIOCofQLUT4AAAQIECBAgQIAAAQIEChAQcBTQRCUQIECAAAECBAgQIECAAIHaBQQctU+A+gkQIECAAAECBAgQIECAQAECAo4CmqgEAgQIECBAgAABAgQIECBQu4CAo/YJUD8BAgQIECBAgAABAgQIEChAQMBRQBOVQIAAAQIECBAgQIAAAQIEahcQcNQ+AeonQIAAAQIECBAgQIAAAQIFCAg4CmiiEggQIECAAAECBAgQIECAQO0CAo7aJ0D9BAgQIECAAAECBAgQIECgAAEBRwFNVAIBAgQIECBAgAABAgQIEKhdQMBR+wSonwABAgQIECBAgAABAgQIFCAg4CigiUogQIAAAQIECBAgQIAAAQK1Cwg4ap8A9RMgQIAAAQIECBAgQIAAgQIEBBwFNFEJBAgQIECAAAECBAgQIECgdgEBR+0ToH4CBAgQIECAAAECBAgQIFCAgICjgCYqgQABAgQIECBAgAABAgQI1C4g4Kh9AtRPgAABAgQIECBAgAABAgQKEBBwFNBEJRAgQIAAAQIECBAgQIAAgdoFBBy1T4D6CRAgQIAAAQIECBAgQIBAAQICjgKaqAQCBAgQIECAAAECBAgQIFC7gICj9glQPwECBAgQIECAAAECBAgQKEBAwFFAE5VAgAABAgQIECBAgAABAgRqFxBw1D4B6idAgAABAgQIECBAgAABAgUICDgKaKISCBAgQIAAAQIECBAgQIBA7QICjtonQP0ECBAgQIAAAQIECBAgQKAAAQFHAU1UAgECBAgQIECAAAECBAgQqF1AwFH7BKifAAECBAgQIECAAAECBAgUICDgKKCJSiBAgAABAgQIECBAgAABArULCDhqnwD1EyBAgAABAgQIECBAgACBAgQEHAU0UQkECBAgQIAAAQIECBAgQKB2AQFH7ROgfgIECBAgQIAAAQIECBAgUICAgKOAJiqBAAECBAgQIECAAAECBAjULiDgqH0C1E+AAAECBAgQIECAAAECBAoQEHAU0EQlECBAgAABAgQIECBAgACB2gUEHLVPgPoJECBAgAABAgQIECBAgEABAgKOApqoBAIECBAgQIAAAQIECBAgULuAgKP2CVA/AQIECBAgQIAAAQIECBAoQEDAUUATlUCAAAECBAgQIECAAAECBGoXEHDUPgHqJ0CAAAECBAgQIECAAAECBQgIOApoohIIECBAgAABAgQIECBAgEDtAgKO2idA/QQIECBAgAABAgQIECBAoAABAUcBTVQCAQIECBAgQIAAAQIECBCoXUDAUfsEqJ8AAQIECBAgQIAAAQIECBQgIOAooIlKIECAAAECBAgQIECAAAECtQsIOGqfAPUTIECAAAECBAgQIECAAIECBAQcBTRRCQQIECBAgAABAgQIECBAoHYBAUftE6B+AgQIECBAgAABAgQIECBQgICAo4AmKoEAAQIECBAgQIAAAQIECNQuIOCofQLUT4AAAQIECBAgQIAAAQIEChAQcBTQRCUQIECAAAECBAgQIECAAIHaBQQctU+A+gkQIECAAAECBAgQIECAQAECAo4CmqgEAgQIECBAgAABAgQIECBQu4CAo/YJUD8BAgQIECBAgAABAgQIEChAQMBRQBOVQIAAAQIECBAg8P/bsYMCAAAAAmL9W+vhFsF4IUCAAAECdQEHR30B8hMgQIAAAQIECBAgQIAAgQMBB8dBiSIQIECAAAECBAgQIECAAIG6gIOjvgD5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","e":1},{"id":"image_1","w":1080,"h":1080,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAQ4CAYAAADsEGyPAAAgAElEQVR4XuzdXZIU15kG4JOk7qVZgdAKxNwSla3yCoRXAFqB8AqEViC0goEVCK3Apc4Obo1WYLQCw72LnMh2y20hZPIU/ffmeTrCMRdzsvp8z/v573V1VVf8ECBAgAABAgQIECBAgAABAgTCBbrw+7s+AQIECBAgQIAAAQIECBAgQKAoOCwBAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAAAECBAgQIECAAAEC8QIKjvgIDUCAAAECBAgQIECAAAECBAgoOOwAAQIECBAgQIAAAQIECBAgEC+g4IiP0AAECBAgQIAAAQIECBAgQICAgsMOECBAgAABAgQIECBAgAABAvECCo74CA1AgAABAgQIECBAgAABAgQIKDjsAAECBAgQIECAAAECBAgQIBAvoOCIj9AABAgQIECAAAECBAgQIECAgILDDhAgQIAAAQIECBAgQIAAAQLxAgqO+AgNQIAAAQIECBAgQIAAAQIECCg47AABAgQIECBAgAABAgQIECAQL6DgiI/QAAQIECBAgAABAgQIECBAgICCww4QIECAwJUJbDab7ZX9Mr+IQJjARx999HK3270Mu7brEiBAgACBGyOg4LgxUbgIAQIE1iOw3W5v7/f77TRNd7quu1NK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AAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACB/2/HjmkAAAAQhvl3PR1LKoHCxV7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGKKR1pAAAI3klEQVSvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF7AwbGvUAACBAgQIECAAAECBAgQIEDAwWEDBAgQIECAAAECBAgQIECAwF4gdmkykxIWeegAAAAASUVORK5CYII=","e":1},{"id":"image_2","w":1080,"h":1080,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAQ4CAYAAADsEGyPAAAgAElEQVR4Xuzdf4xl53kf9vc9596ZoahaQyAN16ldDeumWsGNNazTcJ3U1RAJqg2CVKMijagoNndRJFq1ELSEY0u2a3kZwVYiO6a0iWwkMEARbaIi/4T6j0BRaJkgCeu6zTJIE6o/wGWbOoyQ1rsOJc7uzj1vce7uypS4uzNz5/44zzmfAQjZ4j3nvM/neeZo7veee05OfggQIECAAAECBAgQIECAAAECwQVy8PVbPgECBAgQIECAAAECBAgQIEAgCTgMAQECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIJSAE/8AACAASURBVECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDBAgQIAAAQIECBAgQIAAAQLhBQQc4VuoAAIECBAgQIAAAQIECBAgQEDAYQYIECBAgAABAgQIECBAgACB8AICjvAtVAABAgQIECBAgAABAgQIECAg4DADBAgQIECAAAECBAgQIECAQHgBAUf4FiqAAAECBAgQIECAAAECBAgQEHCYAQIECBAgQIAAAQIECBAgQCC8gIAjfAsVQIAAAQIECBAgQIAAAQIECAg4zAABAgQIECBAgAABAgQIECAQXkDAEb6FCiBAgAABAgQIECBAgAABAgQEHGaAAAECBAgQIECAAAECBAgQCC8g4AjfQgUQIECAAAECBAgQIECAAAECAg4zQIAAAQIECBAgQIAAAQIECIQXEHCEb6ECCBAgQIAAAQIECBAgQIAAAQGHGSBAgAABAgQIECBAgAABAgTCCwg4wrdQAQQIECBAgAABAgQIECBAgICAwwwQIECAAAECBAgQIECAAAEC4QUEHOFbqAACBAgQIECAAAECBAgQIEBAwGEGCBAgQIAAAQIECBAgQIAAgfACAo7wLVQAAQIECBAgQIAAAQIECBAgIOAwAwQIECBAgAABAgQIECBAgEB4AQFH+BYqgAABAgQIECBAgAABAgQIEBBwmAECBAgQIECAAAECBAgQIEAgvICAI3wLFUCAAAECBAgQIECAAAECBAgIOMwAAQIECBAgQIAAAQIECBAgEF5AwBG+hQogQIAAAQIECBAgQIAAAQIEBBxmgAABAgQIECBAgAABAgQIEAgvIOAI30IFECBAgAABAgQIECBAgAABAgIOM0CAAAECBAgQIECAAAECBAiEFxBwhG+hAggQIECAAAECBAgQIECAAAEBhxkgQIAAAQIECBAgQIAAAQIEwgsIOMK3UAEECBAgQIAAAQIECBAgQICAgMMMECBAgAABAgQIECBAgAABAuEFBBzhW6gAAgQIECBAgAABAgQIECBAQMBhBggQIECAAAECBAgQIECAAIHwAgKO8C1UAAECBAgQIECAAAECBAgQICDgMAMECBAgQIAAAQIECBAgQIBAeAEBR/gWKoAAAQIECBAgQIAAAQIECBAQcJgBAgQIECBAgAABAgQIECBAILyAgCN8CxVAgAABAgQIECBAgAABAgQICDjMAAECBAgQIECAAAECBAgQIBBeQMARvoUKIECAAAECBAgQIECAAAECBAQcZoAAAQIECBAgQIAAAQIECBAILyDgCN9CBRAgQIAAAQIECBAgQIAAAQICDjNAgAABAgQIECBAgAABAgQIhBcQcIRvoQIIECBAgAABAgQIECBAgAABAYcZIECAAAECBAgQIECAAAECBMILCDjCt1ABBAgQIECAAAECBAgQIECAgIDDDPRSoHwtjdLV9M5DFXc17eWzae9Qr/UiAgQ6K/C23/ubaSNN0sahF1zSG+mBtP/t12+mN/Ljb/n/D70jLyRAoEsCbzs3fDO9M9VpdOg1TtJ+ejC94dxwaDEvJECAwMoEBBwro3fgWQTKV9LW7e2+8z+rtJGadGKWfb5tm5yuppKuTv/7kl5POe19+z/30uvCkLko2wmBIwuUv5M2017aTDmdSGUaXNw6D/zu/3/kfR5pgyq9nppvh6FXvuMc4dxwJEovJjBPgZWdG+78fXCrmCvf/nthI13NH7r9d8Q8C7Uv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ucDhlX+D43K98CcFjvDytvjoAscWt2omAgQIECBAgAABAosKCByLcq/nywSO9ezCk/z7Bsc4+omKYyBAgAABAgQIECBA4DKBMU2HVx/8b2Iv02t/SuBo729zT39+g0Pg2NxaDUSAAAECBAgQIEBgMQGBYzHq1X2RwLG6lez7gc6BY/IGx76PwPQECBAgQIAAAQIE5gkcXnuDY55g9NMCR3RxW33sj29v76fhJypb3a+5CBAgQIAAAQIECDy7wJgEjmdHXucXCBzr3Mtun+ocOE7j9Hm3AAYnQIAAAQIECBAgQGCWwBjj8PrDH+9n/REfTgoIHMm1bfehBY7t7tZkBAgQIECAAAECBJYQEDiWUF7ndwgc69zLbp/ql7e391fe4Njt/g1OgAABAgQIECBAYLbAGIc33uCYzVj8AwJHcWsbfuYvgWPyE5UNr9hoBAgQIECAAAECBJ5ZYBze/OonKs+MvMo/L3Csci37fahz4BgCx34PwOQECBAgQIAAAQIEZgqcpnH4QeCYqdj8uMDR3Ntmn1rg2OxqDUaAAAECBAgQIEBgEQGBYxHmVX6JwLHKtez3oc6BYzr5icp+L8DkBAgQIECAAAECBGYKDG9wzBTMflzgyK7OgxMgQIAAAQIECBAgQIAAAQL/CQgcboEAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAW8XlIAAAeVElEQVQAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECDwN4UTT/xKUkEiAAAAAElFTkSuQmCC","e":1},{"id":"image_3","w":1080,"h":1080,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAQ4CAYAAADsEGyPAAAgAElEQVR4Xuzd4Ynlh3nF4WtUgNVB1EFIASM2HagEpYN0kHQQd2B1kKQDwRX67FQQq4O4gGGDQIFgYpRd71j+vecR7CfN/d97nvOCxGFm9lcP/xAgQIAAAQIECBAgQIAAAQIE4gK/in9+H58AAQIECBAgQIAAAQIECBAg8DBwOAICBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgElK5UcAACAASURBVAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIECBAgAABAgTyAgaOfIUCECBAgAABAgQIECBAgAABAgYON0CAAAECBAgQIECAAAECBAjkBQwc+QoFIECAAAECBAgQIECAAAECBAwcboAAAQIECBAgQIAAAQIECBDICxg48hUKQIAAAQIECBAgQIAAAQIECBg43AABAgQIECBAgAABAgQIECCQFzBw5CsUgAABAgQIECBAgAABAgQIEDBwuAECBAgQIECAAAECBAgQIEAgL2DgyFcoAAECBAgQIECAAAECBAgQIGDgcAMECBAgQIAAAQIECBAgQIBAXsDAka9QAAIECBAgQIAAAQIECBAgQMDA4QYIECBAgAABAgQIECBAgACBvICBI1+hAAQIECBAgAABAgQIECBAgICBww0QIECAAAECBAgQIECAAAECeQEDR75CAQgQIECAAAECBAgQIECAAAEDhxsgQIAAAQIECBAgQIAAAQIE8gIGjnyFAhAgQIAAAQIECBAgQIAAAQIGDjdAgAABAgQIECBAgAABAgQI5AUMHPkKBSBAgAABAgQIECBAgAABAgQMHG6AAAECBAgQIECAAAECBAgQyAsYOPIVCkCAAAECBAgQIECAAAECBAgYONwAAQIECBAgQIAAAQIECBAgkBcwcOQrFIAAAQIECBAgQIAAAQIECBAwcLgBAgQIECBAgAABAgQIECBAIC9g4MhXKAABAgQIECBAgAABAgQIECBg4HADBAgQIECAAAECBAgQIECAQF7AwJGvUAACBAgQIECAAAECBAgQIEDAwOEGCBAgQIAAAQIECBAgQIAAgbyAgSNfoQAECBAgQIAAAQIECBAgQICAgcMNECBAgAABAgQIECBAgAABAnkBA0e+QgEIECBAgAABAgQIECBAgAABA4cbIECAAAECBAgQIECAAAECBPICBo58hQIQIECAAAECBAgQIECAAAECBg43QIAAAQIECBAgQIAAAQIECOQFDBz5CgUgQIAAAQIECBAgQIAAAQIEDBxugAABAgQIECBAgAABAgQIEMgLGDjyFQpAgAABAgQIECBAgAABAgQIGDjcAAECBAgQIECAAAECBAgQIJAXMHDkKxSAAAECBAgQIECAAAECBAgQMHC4AQIECBAgQIAAAQIECBAgQCAvYODIVygAAQIECBAgQIAAAQIECBAgYOBwAwQIECBAgAABAgQIECBAgEBewMCRr1AAAgQIECBAgAABAgQIECBAwMDhBggQIECAAAECBAgQIECAAIG8gIEjX6EABAgQIECAAAECBAgQIECAgIHDDRAgQIAAAQIECBAgQIAAAQJ5AQNHvkIBCBAgQIAAAQIECBAgQIAAAQOHGyBAgAABAgQIvKHAu3fvPn99ff368Xj8+Odv/+it/uPxeHzz2WefffPtt9/+1xt+DI8mQIAAAQLnBQwc5ysWkAABAgQIEPilBF5eXn4cNX7zeDx+/TOf4Yf3799//d133337S31W70uAAAECBOoCBo56gz4/AQIECBAg8Fcp8OWXX/7z+/fv/+kDP9w/PJ/Pbz7wNb6cAAECBAgQeDweBg5nQIAAAQIECBD4xAI/fefGbz/mse/fv/9738nxMXJeQ4AAAQLrAgaO9QuQnwABAgQIEPikAj/9zo3f/z9+LOVPve8Pz+fzi0/6oTyMAAEC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gAABAuMCBo7xAxCfAAECBAgQIECAAAECBAhcEDBwXGhRBgIECBAgQIAAAQIECBAgMC5g4Bg/APEJECBAgAABAgQIECBAgMAFAQPHhRZlIECAAAECBAgQIECAAAEC4wIGjvEDEJ8AAQIECBAgQIAAAQIECFwQMHBcaFEGAgQIECBAgAABAgQIECAwLmDgGD8A8QkQIECAAAECBAgQIECAwAUBA8eFFmUgQIAAAQIECPx3O3ZQAAAAQECsf2s93CIYLwQIECBAgEBcwMERH4D4BAgQIECAAAECBAgQIEDgQcDB8dCiDAQIECBAgAABAgQIECBAIC7g4IgPQHwCBAgQIECAAAECBAgQIPAg4OB4aFEGAgQIECBAgAABAgQIECAQF3BwxAcgPgECBAgQIECAAAECBAgQeBBwcDy0KAMBAgQIECBAgAABAgQIEIgLODjiAxCfAAECBAgQIECAAAECBAg8CDg4HlqUgQABAgQIECBAgAABAgQIxAUcHPEBiE+AAAECBAgQIECAAAECBB4EHBwPLcpAgAABAgQIECBAgAABAgTiAg6O+ADEJ0CAAAECBAgQIECAAAECDwIOjocWZSBAgAABAgQIECBAgAABAnEBB0d8AOITIECAAAECBAgQIECAAIEHAQfHQ4syECBAgAABAgQIECBAgACBuICDIz4A8QkQIECAAAECBAgQIECAwIOAg+OhRRkIECBAgAABAgQIECBAgEBcwMERH4D4BAgQIECAAAECBAgQIEDgQcDB8dCiDAQIECBAgAABAgQIECBAIC7g4IgPQHwCBAgQIECAAAECBAgQIPAg4OB4aFEGAgQIECBAgAABAgQIECAQF3BwxAcgPgECBAgQIECAAAECBAgQeBBwcDy0KAMBAgQIECBAgAABAgQIEIgLODjiAxCfAAECBAgQIECAAAECBAg8CDg4HlqUgQABAgQIECBAgAABAgQIxAUcHPEBiE+AAAECBAgQIECAAAECBB4EHBwPLcpAgAABAgQIECBAgAABAgTiAg6O+ADEJ0CAAAECBAgQIECAAAECDwIOjocWZSBAgAABAgQIECBAgAABAnEBB0d8AOITIECAAAECBAgQIECAAIEHAQfHQ4syECBAgAABAgQIECBAgACBuICDIz4A8QkQIECAAAECBAgQIECAwIOAg+OhRRkIECBAgAABAgQIECBAgEBcwMERH4D4BAgQIECAAAECBAgQIEDgQcDB8dCiDAQIECBAgAABAgQIECBAIC7g4IgPQHwCBAgQIECAAAECBAgQIPAg4OB4aFEGAgQIECBAgAABAgQIECAQF3BwxAcgPgECBAgQIECAAAECBAgQeBBwcDy0KAMBAgQIECBAgAABAgQIEIgLODjiAxCfAAECBAgQIECAAAECBAg8CDg4HlqUgQABAgQIECBAgAABAgQIxAUcHPEBiE+AAAECBAgQIECAAAECBB4EHBwPLcpAgAABAgQIECBAgAABAgTiAg6O+ADEJ0CAAAECBAgQIECAAAECDwIOjocWZSBAgAABAgQIECBAgAABAnEBB0d8AOITIECAAAECBAgQIECAAIEHAQfHQ4syECBAgAABAgQIECBAgACBuICDIz4A8QkQIECAAAECBAgQIECAwIOAg+OhRRkIECBAgAABAgQIECBAgEBcwMERH4D4BAgQIECAAAECBAgQIEDgQcDB8dCiDAQIECBAgAABAgQIECBAIC7g4IgPQHwCBAgQIECAAAECBAgQIPAg4OB4aFEGAgQIECBAgAABAgQIECAQF3BwxAcgPgECBAgQIECAAAECBAgQeBBwcDy0KAMBAgQIECBAgAABAgQIEIgLODjiAxCfAAECBAgQIECAAAECBAg8CDg4HlqUgQABAgQIECBAgAABAgQIxAUcHPEBiE+AAAECBAgQIECAAAECBB4EHBwPLcpAgAABAgQIECBAgAABAgTiAg6O+ADEJ0CAAAECBAgQIECAAAECDwIOjocWZSBAgAABAgQIECBAgAABAnEBB0d8AOITIECAAAECBAgQIECAAIEHAQfHQ4syECBAgAABAgQIECBAgACBuICDIz4A8QkQIECAAAECBAgQIECAwIOAg+OhRRkIECBAgAABAgQIECBAgEBcwMERH4D4BAgQIECAAAECBAgQIEDgQcDB8dCiDAQIECBAgAABAgQIECBAIC7g4IgPQHwCBAgQIECAAAECBAgQIPAg4OB4aFEGAgQIECBAgAABAgQIECAQF3BwxAcgPgECBAgQIECAAAECBAgQeBBwcDy0KAMBAgQIECBAgAABAgQIEIgLODjiAxCfAAECBAgQIECAAAECBAg8CDg4HlqUgQABAgQIECBAgAABAgQIxAUcHPEBiE+AAAECBAgQIECAAAECBB4EHBwPLcpAgAABAgQIECBAgAABAgTiAg6O+ADEJ0CAAAECBAgQIECAAAECDwIOjocWZSBAgAABAgQIECBAgAABAnEBB0d8AOITIECAAAECBAgQIECAAIEHAQfHQ4syECBAgAABAgQIECBAgACBuICDIz4A8QkQIECAAAECBAgQIECAwIOAg+OhRRkIECBAgAABAgQIECBAgEBcwMERH4D4BAgQIECAAAECBAgQIEDgQcDB8dCiDAQIECBAgAABAgQIECBAIC7g4IgPQHwCBAgQIECAAAECBAgQIPAg4OB4aFEGAgQIECBAgAABAgQIECAQF3BwxAcgPgECBAgQIECAAAECBAgQeBBwcDy0KAMBAgQIECBAgAABAgQIEIgLODjiAxCfAAECBAgQIECAAAECBAg8CDg4HlqUgQABAgQIECBAgAABAgQIxAUcHPEBiE+AAAECBAgQIECAAAECBB4EHBwPLcpAgAABAgQIECBAgAABAgTiAg6O+ADEJ0CAAAECBAgQIECAAAECDwIOjocWZSBAgAABAgQIECBAgAABAnEBB0d8AOITIECAAAECBAgQIECAAIEHAQfHQ4syECBAgAABAgQIECBAgACBuICDIz4A8QkQIECAAAECBAgQIECAwIOAg+OhRRkIECBAgAABAgQIECBAgEBcwMERH4D4BAgQIECAAAECBAgQIEDgQcDB8dCiDAQIECBAgAABAgQIECBAIC7g4IgPQHwCBAgQIECAAAECBAgQIPAg4OB4aFEGAgQIECBAgAABAgQIECAQF3BwxAcgPgECBAgQIECAAAECBAgQeBBwcDy0KAMBAgQIECBAgAABAgQIEIgLODjiAxCfAAECBAgQIECAAAECBAg8CDg4HlqUgQABAgQIECBAgAABAgQIxAUcHPEBiE+AAAECBAgQIECAAAECBB4EHBwPLcpAgAABAgQIECBAgAABAgTiAg6O+ADEJ0CAAAECBAgQIECAAAECDwIOjocWZSBAgAABAgQIECBAgAABAnEBB0d8AOITIECAAAECBAgQIECAAIEHAQfHQ4syECBAgAABAgQIECBAgACBuICDIz4A8QkQIECAAAECBAgQIECAwIOAg+OhRRkIECBAgAABAgQIECBAgEBcwMERH4D4BAgQIECAAAECBAgQIEDgQcDB8dCiDAQIECBAgAABAgQIECBAIC7g4IgPQHwCBAgQIECAAAECBAgQIPAg4OB4aFEGAgQIECBAgAABAgQIECAQF3BwxAcgPgECBAgQIECAAAECBAgQeBBwcDy0KAMBAgQIECBAgAABAgQIEIgLODjiAxCfAAECBAgQIECAAAECBAg8CDg4HlqUgQABAgQIECBAgAABAgQIxAUcHPEBiE+AAAECBAgQIECAAAECBB4EHBwPLcpAgAABAgQIECBAgAABAgTiAg6O+ADEJ0CAAAECBAgQIECAAAECDwIOjocWZSBAgAABAgQIECBAgAABAnEBB0d8AOITIECAAAECBAgQIECAAIEHAQfHQ4syECBAgAABAgQIECBAgACBuICDIz4A8QkQIECAAAECBAgQIECAwIOAg+OhRRkIECBAgAABAgQIECBAgEBcwMERH4D4BAgQIECAAAECBAgQIEDgQWAbGIM51m4xaQAAAABJRU5ErkJggg==","e":1},{"id":"image_4","w":1080,"h":1080,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAQ4CAYAAADsEGyPAAAgAElEQVR4Xuzd0Ymu53WG4V9MAU4HTgfRsdCAOojdgV1COkkHUQnqIBt+nQd3YHdgFSAUNvZB8InJ7HkWc6NL4MN5v1nXWmBzs7f81cM/BAgQIECAAAECBAgQIECAAIG4wFfx39+vT4AAAQIECBAgQIAAAQIECBB4CByOgAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAtAImrMAACAASURBVAQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQK/UoHvvvvuX37++ec/PB6Pz//5t39g+NPj8fj+5eXl+0+fPv31V0pkbAIECBAICQgcoWX5VQkQIECAAAEC7yXw+vr6u88B4/F4/OafvPnT5wDyfD5/eK9ve4cAAQIECCwEBI6FqjcJECBAgAABAh9Y4PX19fOf2Piv/+ev+Mfn8/k5iPiHAAECBAh8SAGB40OuxS9FgAABAgQIENgIfPvtt9999dVX//3G13/vT3K8Uc6PESBAgMBcQOCYE/sAAQIECBAgQODjCLy+vv758Xj89o2/0U8vLy//6t/J8UY9P0aAAAECUwGBY8rrcQIECBAgQIDAxxF4419N+ccB/uP5fP7nx5nKb0KAAAECBP4mIHC4BAIECBAgQIDAr0Tg9fX1878o9N+/cNw/PZ/Pr7/wDT9OgAABAgTeXUDgeHdSDxIgQIAAAQIEPqbA6+vrL+/xmz2fT/8b8j0gvUGAAAEC7yrgv5zeldNjBAgQIECAAIGPKyBwfNzd+M0IECBA4MsFBI4vN/QCAQIECBAgQCAhIHAk1uSXJECAAIE3Cggcb4TzYwQIECBAgACBmsDr6+tfH4/Hb7709/ZXVL5U0M8TIECAwEJA4FioepMAAQIECBAg8AEF/EtGP+BS/EoECBAg8G4CAse7UXqIAAECBAgQIPCxBfzfxH7s/fjtCBAgQODLBASOL/Pz0wQIECBAgACBlMDr6+ufH4/Hb9/4S//l5eXl60+fPn3+qy7+IUCAAAECH0pA4PhQ6/DLECBAgAABAgS2At98883XLy8v//PGr/z++Xz+8Maf9WMECBAgQGAqIHBMeT1OgAABAgQIEPh4Am/8qyp/fD6f33+8afxGBAgQIEDgbwICh0sgQIAAAQIECPwKBf7+Jzk+/2mMf/bXVf7yyy+//OHHH3/89CtkMjIBAgQIhAQEjtCy/KoECBAgQIAAgfcW+Puf5vjd4/H47v/8X8j+9Hg8PgeNH/ypjfcW9x4BAgQIrAQEjpWsdwkQIECAAAECBAgQIECAAIEzAYHjjNqHCBAgQIAAAQIECBAgQIAAgZWAwLGS9S4BAgQIECBAgAABAgQIECBwJiBwnFH7EAECBAgQIECAAAECBAgQILASEDhWst4lQIAAAQIECBAgQIAAAQIEzgQEjjNqHyJAgAABAgQIECBAgAABAgRWAgLHSta7BAgQIECAAAECBAgQIECAwJmAwHFG7UMECBAgQIAAAQIECBAgQIDASkDgWMl6lwABAgQIECBAgAABAgQIEDgTEDjOqH2IAAECBAgQIECAAAECBAgQWAkIHCtZ7xIgQIAAAQIECBAgQIAAAQJnAgLHGbUPESBAgAABAgQIECBAgAABAisBgWMl610CBAgQIECAAAECBAgQIEDgTEDgOKP2IQIECBAgQIAAAQIECBAgQGAlIHCsZL1LgAABAgQIECBAgAABAgQInAkIHGfUPkSAAAECBAgQIECAAAECBAisBASOlax3CRAgQIAAAQIECBAgQIAAgTMBgeOM2ocIECBAgAABAgQIECBAgACBlYDAsZL1LgECBAgQIECAAAECBAgQIHAmIHCcUfsQAQIECBAgQIAAAQIECBAgsBIQOFay3iVAgAABAgQIECBAgAABAgTOBASOM2ofIkCAAAECBAgQIECAAAECBFYCAsdK1rsECBAgQIAAAQIECBAgQIDAmYDAcUbtQwQIECBAgAABAgQIECBAgMBKQOBYyXqXAAECBAgQIECAAAECBAgQOBMQOM6ofYgAAQIECBAgQIAAAQIECBBYCQgcK1nvEiBAgAABAgQIECBAgAABAmcCAscZtQ8RIECAAAECBAgQIECAAAECKwGBYyXrXQIECBAgQIAAAQIECBAgQOBMQOA4o/YhAgQIECBAgAABAgQIECBAYCUgcKxkvUuAAAECBAgQIECAAAECBAicCQgcZ9Q+RIAAAQIECBAgQIAAAQIECKwEBI6VrHcJECBAgAABAgQIECBAgACBMwGB44zahwgQIECAAAECBAgQIECAAIGVgMCxkvUuAQIECBAgQIAAAQIECBAgcCYgcJxR+xABAgQIECBAgAABAgQIECCwEhA4VrLeJUCAAAECBAgQIECAAAECBM4EBI4zah8iQIAAAQIECBAgQIAAAQIEVgICx0rWuwQIECBAgAABAgQIECBAgMCZgMBxRu1DBAgQIECAAAECBAgQIECAwEpA4FjJepcAAQIECBAgQIAAAQIECBA4ExA4zqh9iAABAgQIECBAgAABAgQIEFgJCBwrWe8SIECAAAECBAgQIECAAAECZwICxxm1DxEgQIAAAQIECBAgQIAAAQIrAYFjJetdAgQIECBAgAABAgQIECBA4ExA4Dij9iECBAgQIECAAAECBAgQIEBgJSBwrGS9S4AAAQIECBAgQIAAAQIECJwJCBxn1D5EgAABAgQIECBAgAABAgQIrAQEjpWsdwkQIECAAAECBAgQIECAAIEzAYHjjNqHCBAgQIAAAQIECBAgQIAAgZWAwLGS9S4BAgQIECBAgAABAgQIECBwJiBwnFH7EAECBAgQIECAAAECBAgQILASEDhWst4lQIAAAQIECBAgQIAAAQIEzgQEjjNqHyJAgAABAgQIECBAgAABAgRWAgLHSta7BAgQIECAAAECBAgQIECAwJmAwHFG7UMECBAgQIAAAQIECBAgQIDASkDgWMl6lwABAgQIECBAgAABAgQIEDgTEDjOqH2IAAECBAgQIECAAAECBAgQWAkIHCtZ7xIgQIAAAQIECBAgQIAAAQJnAgLHGbUPESBAgAABAgQIECBAgAABAisBgWMl610CBAgQIECAAAECBAgQIEDgTEDgOKP2IQIECBAgQIAAAQIECBAgQGAlIHCsZL1LgAABAgQIECBAgAABAgQInAkIHGfUPkSAAAECBAgQIECAAAECBAisBASOlax3CRAgQIAAAQIECBAgQIAAgTMBgeOM2ocIECBAgAABAgQIECBAgACBlYDAsZL1LgECBAgQIECAAAECBAgQU93xWAAAHR1JREFUIHAmIHCcUfsQAQIECBAgQIAAAQIECBAgsBIQOFay3iVAgAABAgQIECBAgAABAgTOBASOM2ofIkCAAAECBAgQIECAAAECBFYCAsdK1rsECBAgQIAAAQIECBAgQIDAmYDAcUbtQwQIECBAgAABAgQIECBAgMBKQOBYyXqXAAECBAgQIECAAAECBAgQOBMQOM6ofYgAAQIECBAgQIAAAQIECBBYCQgcK1nvEiBAgAABAgQIECBAgAABAmcCAscZtQ8RIECAAAECBAgQIECAAAECKwGBYyXrXQIECBAgQIAAAQIECBAgQOBMQOA4o/YhAgQIECBAgAABAgQIECBAYCUgcKxkvUuAAAECBAgQIECAAAECBAicCQgcZ9Q+RIAAAQIECBAgQIAAAQIECKwEBI6VrHcJECBAgAABAgQIECBAgACBMwGB44zahwgQIECAAAECBAgQIECAAIGVgMCxkvUuAQIECBAgQIAAAQIECBAgcCYgcJxR+xABAgQIECBAgAABAgQIECCwEhA4VrLeJUCAAAECBAgQIECAAAECBM4EBI4zah8iQIAAAQIECBAgQIAAAQIEVgICx0rWuwQIECBAgAABAgQIECBAgMCZgMBxRu1DBAgQIECAAAECBAgQIECAwEpA4FjJepcAAQIECBAgQIAAAQIECBA4ExA4zqh9iAABAgQIECBAgAABAgQIEFgJCBwrWe8SIECAAAECBAgQIECAAAECZwICxxm1DxEgQIAAAQIECBAgQIAAAQIrAYFjJetdAgQIECBAgAABAgQIECBA4ExA4Dij9iECBAgQIECAAAECBAgQIEBgJSBwrGS9S4AAAQIECBAgQIAAAQIECJwJCBxn1D5EgAABAgQIECBAgAABAgQIrAQEjpWsdwkQIECAAAECBAgQIECAAIEzAYHjjNqHCBAgQIAAAQIECBAgQIAAgZWAwLGS9S4BAgQIECBAgAABAgQIECBwJiBwnFH7EAECBAgQIECAAAECBAgQILASEDhWst4lQIAAAQIECBAgQIAAAQIEzgQEjjNqHyJAgAABAgQIECBAgAABAgRWAgLHSta7BAgQIECAAAECBAgQIECAwJmAwHFG7UMECBAgQIAAAQIECBAgQIDASkDgWMl6lwABAgQIECBAgAABAgQIEDgTEDjOqH2IAAECBAgQIECAAAECBAgQWAkIHCtZ7xIgQIAAAQIECBAgQIAAAQJnAgLHGbUPESBAgAABAgQIECBAgAABAisBgWMl610CBAgQIECAAAECBAgQIEDgTEDgOKP2IQIECBAgQIAAAQIECBAgQGAlIHCsZL1LgAABAgQIECBAgAABAgQInAkIHGfUPkSAAAECBAgQIECAAAECBAisBASOlax3CRAgQIAAAQIECBAgQIAAgTMBgeOM2ocIECBAgAABAgQIECBAgACBlYDAsZL1LgECBAgQIECAAAECBAgQIHAmIHCcUfsQAQIECBAgQIAAAQIECBAgsBIQOFay3iVAgAABAgQIECBAgAABAgTOBASOM2ofIkCAAAECBAgQIECAAAECBFYCAsdK1rsECBAgQIAAAQIECBAgQIDAmYDAcUbtQwQIECBAgAABAgQIECBAgMBKQOBYyXqXAAECBAgQIECAAAECBAgQOBMQOM6ofYgAAQIECBAgQIAAAQIECBBYCQgcK1nvEiBAgAABAgQIECBAgAABAmcCAscZtQ8RIECAAAECBAgQIECAAAECKwGBYyXrXQIECBAgQIAAAQIECBAgQOBMQOA4o/YhAgQIECBAgAABAgQIECBAYCUgcKxkvUuAAAECBAgQIECAAAECBAicCQgcZ9Q+RIAAAQIECBAgQIAAAQIECKwEBI6VrHcJECBAgAABAgQIECBAgACBMwGB44zahwgQIECAAAECBAgQIECAAIGVgMCxkvUuAQIECBAgQIAAAQIECBAgcCYgcJxR+xABAgQIECBAgAABAgQIECCwEhA4VrLeJUCAAAECBAgQIECAAAECBM4EBI4zah8iQIAAAQIECBAgQIAAAQIEVgICx0rWuwQIECBAgAABAgQIECBAgMCZgMBxRu1DBAgQIECAAAECBAgQIECAwEpA4FjJepcAAQIECBAgQIAAAQIECBA4ExA4zqh9iAABAgQIECBAgAABAgQIEFgJCBwrWe8SIECAAAECBAgQIECAAAECZwICxxm1DxEgQIAAAQIECBAgQIAAAQIrAYFjJetdAgQIECBAgAABAgQIECBA4ExA4Dij9iECBAgQIECAAAECBAgQIEBgJSBwrGS9S4AAAQIECBAgQIAAAQIECJwJCBxn1D5EgAABAgQIECBAgAABAgQIrAQEjpWsdwkQIECAAAECBAgQIECAAIEzAYHjjNqHCBAgQIAAAQIECBAgQIAAgZWAwLGS9S4BAgQIECBAgAABAgQIECBwJiBwnFH7EAECBAgQIECAAAECBAgQILASEDhWst4lQIAAAQIECBAgQIAAAQIEzgQEjjNqHyJAgAABAgQIECBAgAABAgRWAgLHSta7BAgQIECAAAECBAgQIECAwJmAwHFG7UMECBAgQIAAAQIECBAgQIDASkDgWMl6lwABAgQIECBAgAABAgQIEDgTEDjOqH2IAAECBAgQIECAAAECBAgQWAkIHCtZ7xIgQIAAAQIECBAgQIAAAQJnAgLHGbUPESBAgAABAgQIECBAgAABAisBgWMl610CBAgQIECAAAECBAgQIEDgTEDgOKP2IQIECBAgQIAAAQIECBAgQGAlIHCsZL1LgAABAgQIECBAgAABAgQInAkIHGfUPkSAAAECBAgQIECAAAECBAisBASOlax3CRAgQIAAAQIECBAgQIAAgTMBgeOM2ocIECBAgAABAgQIECBAgACBlYDAsZL1LgECBAgQIECAAAECBAgQIHAmIHCcUfsQAQIECBAgQIAAAQIECBAgsBIQOFay3iVAgAABAgQIECBAgAABAgTOBASOM2ofIkCAAAECBAgQIECAAAECBFYCAsdK1rsECBAgQIAAAQIECBAgQIDAmYDAcUbtQwQIECBAgAABAgQIECBAgMBKQOBYyXqXAAECBAgQIECAAAECBAgQOBMQOM6ofYgAAQIECBAgQIAAAQIECBBYCQgcK1nvEiBAgAABAgQIECBAgAABAmcCAscZtQ8RIECAAAECBAgQIECAAAECKwGBYyXrXQIECBAgQIAAAQIECBAgQOBMQOA4o/YhAgQIECBAgAABAgQIECBAYCUgcKxkvUuAAAECBAgQIECAAAECBAicCQgcZ9Q+RIAAAQIECBAgQIAAAQIECKwEBI6VrHcJECBAgAABAgQIECBAgACBMwGB44zahwgQIECAAAECBAgQIECAAIGVgMCxkvUuAQIECBAgQIAAAQIECBAgcCYgcJxR+xABAgQIECBAgAABAgQIECCwEhA4VrLeJUCAAAECBAgQIECAAAECBM4EBI4zah8iQIAAAQIECBAgQIAAAQIEVgICx0rWuwQIECBAgAABAgQIECBAgMCZgMBxRu1DBAgQIECAAAECBAgQIECAwEpA4FjJepcAAQIECBAgQIAAAQIECBA4ExA4zqh9iAABAgQIECBAgAABAgQIEFgJCBwrWe8SIECAAAECBAgQIECAAAECZwICxxm1DxEgQIAAAQIECBAgQIAAAQIrAYFjJetdAgQIECBAgAABAgQIECBA4ExA4Dij9iECBAgQIECAAAECBAgQIEBgJSBwrGS9S4AAAQIECBAgQIAAAQIECJwJCBxn1D5EgAABAgQIECBAgAABAgQIrAQEjpWsdwkQIECAAAECBAgQIECAAIEzAYHjjNqHCBAgQIAAAQIECBAgQIAAgZWAwLGS9S4BAgQIECBAgAABAgQIECBwJiBwnFH7EAECBAgQIECAAAECBAgQILASEDhWst4lQIAAAQIECBAgQIAAAQIEzgQEjjNqHyJAgAABAgQIECBAgAABAgRWAgLHSta7BAgQIECAAAECBAgQIECAwJmAwHFG7UMECBAgQIAAAQIECBAgQIDASkDgWMl6lwABAgQIECBAgAABAgQIEDgTEDjOqH2IAAECBAgQIECAAAECBAgQWAkIHCtZ7xIgQIAAAQIECBAgQIAAAQJnAgLHGbUPESBAgAABAgQIECBAgAABAisBgWMl610CBAgQIECAAAECBAgQIEDgTEDgOKP2IQIECBAgQIAAAQIECBAgQGAlIHCsZL1LgAABAgQIECBAgAABAgQInAkIHGfUPkSAAAECBAgQIECAAAECBAisBASOlax3CRAgQIAAAQIECBAgQIAAgTMBgeOM2ocIECBAgAABAgQIECBAgACBlYDAsZL1LgECBAgQIECAAAECBAgQIHAmIHCcUfsQAQIECBAgQIAAAQIECBAgsBIQOFay3iVAgAABAgQIECBAgAABAgTOBASOM2ofIkCAAAECBAgQIECAAAECBFYCAsdK1rsECBAgQIAAAQIECBAgQIDAmYDAcUbtQwQIECBAgAABAgQIECBAgMBKQOBYyXqXAAECBAgQIECAAAECBAgQOBMQOM6ofYgAAQIECBAgQIAAAQIECBBYCQgcK1nvEiBAgAABAgQIECBAgAABAmcCAscZtQ8RIECAAAECBAgQIECAAAECKwGBYyXrXQIECBAgQIAAAQIECBAgQOBMQOA4o/YhAgQIECBAgAABAgQIECBAYCUgcKxkvUuAAAECBAgQIECAAAECBAicCQgcZ9Q+RIAAAQIECBAgQIAAAQIECKwEBI6VrHcJECBAgAABAgQIECBAgACBMwGB44zahwgQIECAAAECBAgQIECAAIGVgMCxkvUuAQIECBAgQIAAAQIECBAgcCYgcJxR+xABAgQIECBAgAABAgQIECCwEhA4VrLeJUCAAAECBAgQIECAAAECBM4EBI4zah8iQIAAAQIECBAgQIAAAQIEVgICx0rWuwQIECBAgAABAgQIECBAgMCZgMBxRu1DBAgQIECAAAECBAgQIECAwEpA4FjJepcAAQIECBAgQIAAAQIECBA4ExA4zqh9iAABAgQIECBAgAABAgQIEFgJCBwrWe8SIECAAAECBAgQIECAAAECZwICxxm1DxEgQIAAAQIECBAgQIAAAQIrAYFjJetdAgQIECBAgAABAgQIECBA4ExA4Dij9iECBAgQIECAAAECBAgQIEBgJSBwrGS9S4AAAQIECBAgQIAAAQIECJwJCBxn1D5EgAABAgQIECBAgAABAgQIrAQEjpWsdwkQIECAAAECBAgQIECAAIEzAYHjjNqHCBAgQIAAAQIECBAgQIAAgZWAwLGS9S4BAgQIECBAgAABAgQIECBwJiBwnFH7EAECBAgQIECAAAECBAgQILASEDhWst4lQIAAAQIECBAgQIAAAQIEzgQEjjNqHyJAgAABAgQIECBAgAABAgRWAgLHSta7BAgQIECAAAECBAgQIECAwJmAwHFG7UMECBAgQIAAAQIECBAgQIDASkDgWMl6lwABAgQIECBAgAABAgQIEDgTEDjOqH2IAAECBAgQIECAAAECBAgQWAkIHCtZ7xIgQIAAAQIECBAgQIAAAQJnAgLHGbUPESBAgAABAgQIECBAgAABAisBgWMl610CBAgQIECAwP+2Y8c0AAAACMP8u0YFu2oAkp4jQIAAAQIECGQCAkdG7YgAAQIECBAgQIAAAQIECBB4CQgcL1m7BAgQIECAAAECBAgQIECAQCYgcGTUjggQIECAAAECBAgQIECAAIGXgMDxkrVLgAABAgQIECBAgAABAgQIZAICR0btiAABAgQIECBAgAABAgQIEHgJCBwvWbsECBAgQIAAAQIECBAgQIBAJiBwZNSOCBAgQIAAAQIECBAgQIAAgZeAwPGStUuAAAECBAgQIECAAAECBAhkAgJHRu2IAAECBAgQIECAAAECBAgQeAkIHC9ZuwQIECBAgAABAgQIECBAgEAmIHBk1I4IECBAgAABAgQIECBAgACBl4DA8ZK1S4AAAQIECBAgQIAAAQIECGQCAkdG7YgAAQIECBAgQIAAAQIECBB4CQgcL1m7BAgQIECAAAECBAgQIECAQCYgcGTUjggQIECAAAECBAgQIECAAIGXgMDxkrVLgAABAgQIECBAgAABAgQIZAICR0btiAABAgQIECBAgAABAgQIEHgJCBwvWbsECBAgQIAAAQIECBAgQIBAJiBwZNSOCBAgQIAAAQIECBAgQIAAgZeAwPGStUuAAAECBAgQIECAAAECBAhkAgJHRu2IAAECBAgQIECAAAECBAgQeAkIHC9ZuwQIECBAgAABAgQIECBAgEAmIHBk1I4IECBAgAABAgQIECBAgACBl4DA8ZK1S4AAAQIECBAgQIAAAQIECGQCAkdG7YgAAQIECBAgQIAAAQIECBB4CQgcL1m7BAgQIECAAAECBAgQIECAQCYgcGTUjggQIECAAAECBAgQIECAAIGXgMDxkrVLgAABAgQIECBAgAABAgQIZAICR0btiAABAgQIECBAgAABAgQIEHgJCBwvWbsECBAgQIAAAQIECBAgQIBAJiBwZNSOCBAgQIAAAQIECBAgQIAAgZeAwPGStUuAAAECBAgQIECAAAECBAhkAgJHRu2IAAECBAgQIECAAAECBAgQeAkIHC9ZuwQIECBAgAABAgQIECBAgEAmIHBk1I4IECBAgAABAgQIECBAgACBl4DA8ZK1S4AAAQIECBAgQIAAAQIECGQCAkdG7YgAAQIECBAgQIAAAQIECBB4CQgcL1m7BAgQIECAAAECBAgQIECAQCYgcGTUjggQIECAAAECBAgQIECAAIGXgMDxkrVLgAABAgQIECBAgAABAgQIZAICR0btiAABAgQIECBAgAABAgQIEHgJCBwvWbsECBAgQIAAAQIECBAgQIBAJiBwZNSOCBAgQIAAAQIECBAgQIAAgZeAwPGStUuAAAECBAgQIECAAAECBAhkAgJHRu2IAAECBAgQIECAAAECBAgQeAkIHC9ZuwQIECBAgAABAgQIECBAgEAmIHBk1I4IECBAgAABAgQIECBAgACBl4DA8ZK1S4AAAQIECBAgQIAAAQIECGQCAkdG7YgAAQIECBAgQIAAAQIECBB4CQgcL1m7BAgQIECAAAECBAgQIECAQCYgcGTUjggQIECAAAECBAgQIECAAIGXgMDxkrVLgAABAgQIECBAgAABAgQIZAICR0btiAABAgQIECBAgAABAgQIEHgJCBwvWbsECBAgQIAAAQIECBAgQIBAJiBwZNSOCBAgQIAAAQIECBAgQIAAgZeAwPGStUuAAAECBAgQIECAAAECBAhkAgJHRu2IAAECBAgQIECAAAECBAgQeAkIHC9ZuwQIECBAgAABAgQIECBAgEAmIHBk1I4IECBAgAABAgQIECBAgACBl4DA8ZK1S4AAAQIECBAgQIAAAQIECGQCAkdG7YgAAQIECBAgQIAAAQIECBB4CQgcL1m7BAgQIECAAAECBAgQIECAQCYgcGTUjggQIECAAAECBAgQIECAAIGXgMDxkrVLgAABAgQIECBAgAABAgQIZAICR0btiAABAgQIECBAgAABAgQIEHgJCBwvWbsECBAgQIAAAQIECBAgQIBAJiBwZNSOCBAgQIAAAQIECBAgQIAAgZeAwPGStUuAAAECBAgQIECAAAECBAhkAgJHRu2IAAECBAgQIECAAAECBAgQeAkIHC9ZuwQIECBAgAABAgQIECBAgEAmIHBk1I4IECBAgAABAgQIECBAgACBl4DA8ZK1S4AAAQIECBAgQIAAAQIECGQCAkdG7YgAAQIECBAgQIAAAQIECBB4CQgcL1m7BAgQIECAAAECBAgQIECAQCYgcGTUjggQIECAAAECBAgQIECAAIGXgMDxkrVLgAABAgQIECBAgAABAgQIZAICR0btiAABAgQIECBAgAABAgQIEHgJCBwvWbsECBAgQIAAAQIECBAgQIBAJiBwZNSOCBAgQIAAAQIECBAgQIAAgZeAwPGStUuAAAECBAgQIECAAAECBAhkAgJHRu2IAAECBAgQIECAAAECBAgQeAkIHC9ZuwQIECBAgAABAgQIECBAgEAmIHBk1I4IECBAgAABAgQIECBAgACBl4DA8ZK1S4AAAQIECBAgQIAAAQIECGQCAkdG7YgAAQIECBAgQIAAAQIECBB4CQgcL1m7BAgQIECAAAECBAgQIECAQCYgcGTUjggQIECAAAECBAgQIECAAIGXgMDxkrVLgAABAgQIECBAgAABAgQIZAICR0btiAABAgQIECBAgAABAgQIEHgJCBwvWbsECBAgQIAAAQIECBAgQIBAJiBwZNSOCBAgQIAAAQIECBAgQIAAgZeAwPGStUuAAAECBAgQIECAAAECBAhkAgJHRu2IAAECBAgQIECAAAECBAgQeAkIHC9ZuwQIECBAgAABAgQIECBAgEAmIHBk1I4IECBAgAABAgQIECBAgACBl4DA8ZK1S4AAAQIECBAgQIAAAQIECGQCAkdG7YgAAQIECBAgQIAAAQIECBB4CQgcL1m7BAgQIECAAAECBAgQIECAQCYgcGTUjggQIECAAAECBAgQIECAAIGXgMDxkrVLgAABAgQIECBAgAABAgQIZAICR0btiAABAgQIECBAgAABAgQIEHgJCBwvWbsECBAgQIAAAQIECBAgQIBAJiBwZNSOCBAgQIAAAQIECBAgQIAAgZeAwPGStUuAAAECBAgQIECAAAECBAhkAgJHRu2IAAECBAgQIECAAAECBAgQeAkIHC9ZuwQIECBAgAABAgQIECBAgEAmIHBk1I4IECBAgAABAgQIECBAgACBl4DA8ZK1S4AAAQIECBAgQIAAAQIECGQCAkdG7YgAAQIECBAgQIAAAQIECBB4CQgcL1m7BAgQIECAAAECBAgQIECAQCYgcGTUjggQIECAAAECBAgQIECAAIGXgMDxkrVLgAABAgQIECBAgAABAgQIZAICR0btiAABAgQIECBAgAABAgQIEHgJCBwvWbsECBAgQIAAAQIECBAgQIBAJiBwZNSOCBAgQIAAAQIECBAgQIAAgZeAwPGStUuAAAECBAgQIECAAAECBAhkAgJHRu2IAAECBAgQIECAAAECBAgQeAkIHC9ZuwQIECBAgAABAgQIECBAgEAmIHBk1I4IECBAgAABAgQIECBAgACBl4DA8ZK1S4AAAQIECBAgQIAAAQIECGQCAkdG7YgAAQIECBAgQIAAAQIECBB4CQgcL1m7BAgQIECAAAECBAgQIECAQCYgcGTUjggQIECAAAECBAgQIECAAIGXgMDxkrVLgAABAgQIECBAgAABAgQIZAICR0btiAABAgQIECBAgAABAgQIEHgJCBwvWbsECBAgQIAAAQIECBAgQIBAJiBwZNSOCBAgQIAAAQIECBAgQIAAgZeAwPGStUuAAAECBAgQIECAAAECBAhkAgJHRu2IAAECBAgQIECAAAECBAgQeAkIHC9ZuwQIECBAgAABAgQIECBAgEAmIHBk1I4IECBAgAABAgQIECBAgACBl4DA8ZK1S4AAAQIECBAgQIAAAQIECGQCAkdG7YgAAQIECBAgQIAAAQIECBB4CQgcL1m7BAgQIECAAAECBAgQIECAQCYgcGTUjggQIECAAAECBAgQIECAAIGXgMDxkrVLgAABAgQIECBAgAABAgQIZAICR0btiAABAgQIECBAgAABAgQIEHgJCBwvWbsECBAgQIAAAQIECBAgQIBAJiBwZNSOCBAgQIAAAQIECBAgQIAAgZeAwPGStUuAAAECBAgQIECAAAECBAhkAgJHRu2IAAECBAgQIECAAAECBAgQeAkIHC9ZuwQIECBAgAABAgQIECBAgEAmIHBk1I4IECBAgAABAgQIECBAgACBl8AADOloOb9ayHQAAAAASUVORK5CYII=","e":1},{"id":"image_5","w":1080,"h":1080,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAQ4CAYAAADsEGyPAAAgAElEQVR4Xuzd0Y3n91WH4f96CkBUQDqIr1ceJXSQDggdQAehg1ABpgLcAYvH90AH0AGuYNFKFhcRIocdYz7v+LHku9+Mv+c55+rV2n738BcBAgQIECBAgAABAgQIECBAIC7wLv5+zydAgAABAgQIECBAgAABAgQIPAQOR0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAPAKgAUAACAASURBVAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABAgQIECBAgEBeQODIr9AABAgQIECAAAECBAgQIECAgMDhBggQIECAAAECBAgQIECAAIG8gMCRX6EBCBAgQIAAAQIECBAgQIAAAYHDDRAgQIAAAQIECBAgQIAAAQJ5AYEjv0IDECBAgAABAgQIECBAgAABAgKHGyBAgAABAgQIECBAgAABAgTyAgJHfoUGIECAAAECBAgQIECAAAECBAQON0CAAAECBAgQIECAAAECBAjkBQSO/AoNQIAAAQIECBAgQIAAAQIECAgcboAAAQIECBAgQIAAAQIECBDICwgc+RUagAABAgQIECBAgAABAgQIEBA43AABAgQIECBAgAABAgQIECCQFxA48is0AAECBAgQIECAAAECBAgQICBwuAECBAgQIECAAAECBAgQIEAgLyBw5FdoAAIECBAgQIAAAQIECBAgQEDgcAMECBAgQIAAAQIECBAgQIBAXkDgyK/QAAQIECBAgAABAgQIECBAgIDA4QYIECBAgAABAgQIECBAgACBvIDAkV+hAQgQIECAAAECBAgQIECAAAGBww0QIECAAAECBAgQIECAAAECeQGBI79CAxAgQIAAAQIECBAgQIAAAQIChxsgQIAAAQIECBAgQIAAAQIE8gICR36FBiBAgAABAgQIECBAgAABAgQEDjdAgAABAgQIECBAgAABAgQI5AUEjvwKDUCAAAECBAgQIECAAAECBAgIHG6AAAECBAgQIECAAAECBAgQyAsIHPkVGoAAAQIECBAgQIAAAQIECBAQONwAAQIECBAgQIAAAQIECBAgkBcQOPIrNAABAgQIECBAgAABAgQIECAgcLgBAgQIECBAgAABAgQIECBAIC8gcORXaAACBAgQIECAAAECBAgQIEBA4HADBAgQIECAAAECBAgQIECAQF5A4Miv0AAECBAgQIAAAQIECBAgQICAwOEGCBAgQIAAAQIECBAgQIAAgbyAwJFfoQEIECBAgAABAgQIECBAgAABgcMNECBAgAABAgQIECBAgAABAnkBgSO/QgMQIECAAAECBAgQIECAAAECAocbIECAAAECBAgQIECAAAECBPICAkd+hQYgQIAAAQIECBAgQIAAAQIEBA43QIAAAQIECBAgQIAAAQIECOQFBI78Cg1AgAABAgQIECBAgAABAgQICBxugAABAgQIECBAgAABAgQIEMgLCBz5FRqAAAECBAgQIECAAAECBAgQEDjcAAECBAgQIECAAAECBAgQIJAXEDjyKzQAAQIECBAgQIAAAQIECBAgIHC4AQIECBAgQIAAAQIECBAgQCAvIHDkV2gAAgQIECBAgAABAgQIECBAQOBwAwQIECBAgAABApMCz8/Pv308Hp/+/tUfPPBf3717980XX3zx+w8fPvzH5OM9igABAgR+cgGB4ycn9w8kQIAAAQIECBD4nwTev3//5dPT0zePx+PP/ojU94/H43cvLy+/J0qAAAECBAQON0CAAAECBAgQIDAj8MOf2vi7/+WD/v7l5eXTn/TwFwECBAj8jAUEjp/x8o1OgAABAgQIEFgS+OFPbvzzZ77pr/1Jjs+U82MECBB4IwICxxtZpDEIECBAgAABAnWB5+fnf3k8Hr/8zDm+f3p6+vLDhw//9pk/78cIECBAIC4gcMQX6PkECBAgQIAAgbcg8Pz8/JvH4/EPr5nl48ePf/vdd9/91Wt+h58lQIAAga6AwNHdnZcTIECAAAECBN6MwPPz89ePx+MvXjnQv7+8vPzilb/DjxMgQIBAVEDgiC7OswkQIECAAAECb0nglf96yn9RPD09/an/dexbugyzECBA4C4gcNytfEmAAAECBAgQIPB/JPD8/Pzxx/jVHz9+/PPvvvvuw4/xu/wOAgQIEGgJCBytfXktAQIECBAgQOBNCggcb3KthiJAgMBPKiBw/KTc/mEECBAgQIAAAQL/nYB/RcVdECBAgMBrBQSO1wr6eQIECBAgQIAAgVcL+I+MvprQLyBAgMDPXkDg+NmfAAACBAgQIECAwP+/wFdfffXrd+/e/eNrXvLu3bu/+fbbb3/3mt/hZwkQIECgKyBwdHfn5QQIECBAgACBNyXw/Pz86T8O+qvPHOr7p6enX/g/qHymnh8jQIDAGxAQON7AEo1AgAABAgQIEHgLAu/fv//y6enpU+T4k8+Y5y9fXl6+/oyf8yMECBAg8EYEBI43skhjECBAgAABAgTegsBnRg5x4y0s3wwECBB4pYDA8UpAP06AAAECBAgQIPDjCvwQOT79aYxf/pHf/P3j8fjty8vLNz/uC/w2AgQIECgKCBzFrXkzAQIECBAgQOBnIPD8/PybTwHj8Xj8+g/+tZV/ejwe3zw9PX3tv7nxMzgEIxIgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIEB+TyDQAAHYZJREFUCBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAAQIECBAgQOAoIHAcoXxGgAABAgQIECBAgAABAgQI7AoIHLu78TICBAgQIECAAAECBAgQIEDgKCBwHKF8RoAAAQIECBAgQIAAAQIECOwKCBy7u/EyAgQIECBAgAABAgQIECBA4CggcByhfEaAAAECBAgQIECAAAECBAjsCggcu7vxMgIECBAgQIAAgf9sxw6JAAAAEIj1b02Fl4h5BDccBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBRwcEQoMQIECBAgQIAAAQIECBAgQOBXwMHxu41mBAgQIECAAAECBAgQIECAQBQYSPdvOfe969kAAAAASUVORK5CYII=","e":1},{"id":"image_6","w":1080,"h":1080,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAQ4CAYAAADsEGyPAAAgAElEQVR4Xuzd35Hb9rkGYHCx93YqiFJBlFvNwtlUEKWC2BVEp4KjVBCdCixXELmCrAWNbo9UQawKYt2LwpmfDtYjrYg/JLgi3+WzM74Sl8T3vJ+d4BUIrCo/BAgQIECAAAECBAgQIECAAIFwgVX48Tt8AgQIECBAgAABAgQIECBAgECl4LAEBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQILK6KqsAACAASURBVECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIAAAQIECBAgQIAAAQIKDjtAgAABAgQIECBAgAABAgQIxAsoOOIjNAABAgQIECBAgAABAgQIECCg4LADBAgQIECAAAECBAgQIECAQLyAgiM+QgMQIECAAAECBAgQIECAAAECCg47QIAAAQIECBAgQIAAAQIECMQLKDjiIzQAAQIECBAgQIAAAQIECBAgoOCwAwQIECBAgAABAgQIECBAgEC8gIIjPkIDECBAgAABAgQIECBAgAABAgoOO0CAAAECBAgQIECAAAECBAjECyg44iM0AAECBAgQIECAAAECBAgQIKDgsAMECBAgQIAAAQIECBAgQIBAvICCIz5CAxAgQIBAssA333zzuOu6yzLDarW6Ojs7e3J1dfVL8kyOnQABAgQIECBwCAEFxyHUfSYBAgQIEKiq6uLi4slqtfrbDYyf2rb9UHhM/TRN821VVeW19/py5OnV1dXPU7/nzwkQIECAAAECd1FAwXEXUzUTAQIECBy9wOXl5dfr9fo/mw50vV7/4eXLl6+Ghuh/96qqqt/feM3bqqq+bdv22dEDOEACBAgQIECAwJ4FFBx7BvV2BAgQIEBgjsDFxcXlarX616bXdl33pxcvXpQCY+NP0zRPq6r668Afv63r+p6vucxJwWsIECBAgACBuySg4LhLaZqFAAECBGIEFhYc3cSg37VtW0oQPwQIECBAgACBkxFQcJxM1AYlQCBZ4MGDB/fPz88flhnevXv3bOzrC8lzntKx32bBsVqt/v78+fPHp+RpVgIECBAgQICAgsMOECBA4MgF+htJfn/jMP0N/ZHnNnV4CwuO8pSVr0Y+w35MBeDPCRAgQIAAgTsnoOC4c5EaiACBuyQwciNK91kID3pJwTHw9JVrEbsRvhsOnwABAgQIENhNQMGxm5vfIkCAwBcRWHIS/EUO0IfsLLA024Ebjb5dr9eXvsK0cyx+kQABAgQIEAgWUHAEh+fQCRC4+wJLT4LvvlDuhPvItrzH2dnZZVHouu7nuq6feXpK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AAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQIECBAgAABAgQIECBAQMFhBwgQIECAAAECBAgQIECAAIF4AQVHfIQGIECAAAECBAgQIECAAAECBBQcdoAAAQIECBAgQIAAAQIECBCIF1BwxEdoAAIECBAgQIAAAQIECBAgQEDBYQcIECBAgAABAgQIECBAgACBeAEFR3yEBiBAgAABAgQIECBAgAABAgQUHHaAAAECBAgQIECAAAECBAgQiBdQcMRHaAACBAgQIECAAAECBAgQIEBAwWEHCBAgQIAAAQIECBAgQIAAgXgBBUd8hAYgQIAAAQIECBAgQIAAAQIEFBx2gAABAgQIECBAgAABAgQIEIgXUHDER2gAAgQI/F87dlADAAADIcy/a3SQVMK6e0GAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwjAafQZAAAH3klEQVQQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvYDAsX+hAwgQIECAAAECBAgQIECAAAGBwwYIECBAgAABAgQIECBAgACBvUCsBGHPV4DBmQAAAABJRU5ErkJggg==","e":1}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"Cup/loading animation.ai","cl":"ai","refId":"image_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":71.0000028918893,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":2,"nm":"cup shape/loading animation.ai","cl":"ai","td":1,"refId":"image_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[94.556,94.556,100],"ix":6}},"ao":0,"ip":0,"op":71.0000028918893,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":2,"nm":"wave/loading animation.ai","cl":"ai","tt":1,"refId":"image_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.607,"y":0.595},"o":{"x":0.529,"y":0},"t":0,"s":[660.805,540,0],"to":[-30.035,0,0],"ti":[68.54,0,0]},{"i":{"x":0.507,"y":1},"o":{"x":0.21,"y":0.779},"t":30,"s":[469.965,532,0],"to":[-30.703,0,0],"ti":[13.455,0,0]},{"t":70.0000028511585,"s":[399.866,540,0]}],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":71.0000028918893,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":2,"nm":"dot 1/loading animation.ai","cl":"ai","refId":"image_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[608,678,0],"ix":2},"a":{"a":0,"k":[608,678,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[131.78,131.78,100]},{"t":20.0000008146167,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":71.0000028918893,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":2,"nm":"dot 2/loading animation.ai","cl":"ai","refId":"image_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[646,676,0],"ix":2},"a":{"a":0,"k":[646,676,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":25,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":35,"s":[131.78,131.78,100]},{"t":45.0000018328876,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":71.0000028918893,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":2,"nm":"Dot 3/loading animation.ai","cl":"ai","refId":"image_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[684.966,675.302,0],"ix":2},"a":{"a":0,"k":[684.966,675.302,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":50,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":60,"s":[131.78,131.78,100]},{"t":70.0000028511585,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":71.0000028918893,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":2,"nm":"text/loading animation.ai","cl":"ai","refId":"image_6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":71.0000028918893,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index a942b60..ca76bf8 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -1,11 +1,18 @@ + - + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 449df2b..a0dbd7e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,7 +2,9 @@ #03a9f4 #0288d1 + #f6f6f6 #ffab40 + #2c2c2c #ffc77f #30000000 @android:color/white diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 0000000..2291a30 --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eafc192..7f7821a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ 易开发 + 该功能需要读写内部存储权限,点击前往设置 GuideActivity Settings 上一步 @@ -11,18 +12,19 @@ 无障碍功能已开启 该设备未root,无法开启 基本设置 - 高级功能设置 + 高级功能 开启无障碍辅助 开启悬浮窗权限 - 推荐开启,用于获取应用数据库等信息,不开启部分信息无法显示,Xposed无法使用 + 推荐开启,用于获取应用SharedPreference、数据库等数据,关闭后部分信息无法显示 开启root权限 启用Xposed特性 支持脱壳,更多功能持续开发中,点击进入管理 辅助功能用于界面布局分析等 关于 软件开发中,交流群:627962572 - 界面分析 - 点击分析 + 界面布局 + 功能需要root权限,请开启后使用! + 点击查看 首次安装时间 最后更新时间 守护进程 @@ -36,7 +38,7 @@ 恢复 保存失败,请按错误提示修改后再提交 检测到值有修改,是否保存? - 保存成功,由于缓存原因,新数据重启后才会生效 + 保存成功,由于缓存原因,新数据重启后才会生效,是否立即重启%1$s? 退出 是否退出应用? 确定 @@ -47,24 +49,23 @@ 基本信息 加固类型 正在分析 - 请稍后... + 请稍后... 正在获取应用信息 请开启无障碍功能 该表数据为空 获取数据库信息失败 - 获取中... + 获取中... 数据库列表 应用设置 - 备份Apk文件 - 备份data目录 + 备份应用 + 还原应用 重启应用 停止应用 清理应用数据 卸载应用 请开启root权限 - 部分功能需要root权限,请打开后使用! 是否清除数据和缓存? 清理完成 是否停止该应用? @@ -74,19 +75,40 @@ 备份失败 备份成功,文件已备份到内部存储下com.wrbug.developerHelper目录中,是否立即分享给好友? 分享失败 - 导出脱壳数据 正在打包文件 导出失败! - 无脱壳文件 - Xposed设置 - 管理、移除需要脱壳的应用 - 脱壳应用管理 移除该项 请先开启Xposed设置 检查更新 - 正在检查新版本... + 正在检查新版本... 暂无新版本 - 检查失败... + 检查失败... 发现新版本 下载 + 通知栏常驻 + 用于进程保活,当应用经常被杀死时,请打开该功能 + 请选择需要备份的内容,备份所有数据支持无损恢复 + 正在备份数据 + 备份失败 + 跳过该备份 + 备份正在进行中,请勿关闭该窗口 + 应用安装包 + Data目录 + Android/Data目录 + 备份已完成,您可以前往 %1$s 目录查看备份文件 + 等待中 + 打包备份文件 + 暂无数据 + %1$d个备份 + 上一次:%1$s + 其他功能 + 无需备份 + 备份管理 + 管理/恢复已备份的应用 + 已取消 + %1$s 的备份 + 备注 + 删除 + 是否删除该备份? + 删除备份失败,请重试 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ddc3fd5..906b089 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -7,6 +7,7 @@ @color/colorPrimaryDark @color/colorAccent @android:color/white + @color/colorWindowBackground @@ -36,6 +37,25 @@ @android:color/white + + + + + + + + + diff --git a/app/src/test/java/com/wrbug/developerhelper/ExampleUnitTest.kt b/app/src/test/java/com/wrbug/developerhelper/ExampleUnitTest.kt deleted file mode 100644 index 34b59a3..0000000 --- a/app/src/test/java/com/wrbug/developerhelper/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.wrbug.developerhelper - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/basecommon/.gitignore b/basecommon/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/basecommon/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/basecommon/build.gradle b/basecommon/build.gradle deleted file mode 100644 index 922788e..0000000 --- a/basecommon/build.gradle +++ /dev/null @@ -1,41 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt'//kapt 插件 - -android { - compileSdkVersion 28 - - - - defaultConfig { - minSdkVersion 21 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } -} -def lifecycle_version = "2.0.0" -dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.appcompat:appcompat:1.0.2' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.google.android.material:material:1.1.0-alpha01' - implementation 'android.arch.lifecycle:extensions:1.1.1' - annotationProcessor "android.arch.lifecycle:compiler:1.1.1" - implementation project(':commonutil') - implementation project(':commonwidget') -} -repositories { - mavenCentral() -} diff --git a/basecommon/proguard-rules.pro b/basecommon/proguard-rules.pro deleted file mode 100644 index f1b4245..0000000 --- a/basecommon/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/basecommon/src/androidTest/java/com/wrbug/developerhelper/basecommon/ExampleInstrumentedTest.java b/basecommon/src/androidTest/java/com/wrbug/developerhelper/basecommon/ExampleInstrumentedTest.java deleted file mode 100644 index 7e11c97..0000000 --- a/basecommon/src/androidTest/java/com/wrbug/developerhelper/basecommon/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.wrbug.developerhelper.basecommon; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.wrbug.developerhelper.basecommon.test", appContext.getPackageName()); - } -} diff --git a/basecommon/src/main/AndroidManifest.xml b/basecommon/src/main/AndroidManifest.xml deleted file mode 100644 index 443e74f..0000000 --- a/basecommon/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - diff --git a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/AppCompatActivityExt.kt b/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/AppCompatActivityExt.kt deleted file mode 100644 index bd807e8..0000000 --- a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/AppCompatActivityExt.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.wrbug.developerhelper.basecommon - -import android.content.Intent -import androidx.annotation.IdRes -import androidx.appcompat.app.ActionBar -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentTransaction -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProviders -import com.wrbug.developerhelper.basecommon.activityresultcallback.ActResultRequest -import com.wrbug.developerhelper.basecommon.activityresultcallback.ActivityResultCallback - -/** - * Various extension functions for AppCompatActivity. - */ - - -fun AppCompatActivity.setupActionBar(@IdRes toolbarId: Int, action: ActionBar.() -> Unit) { - setSupportActionBar(findViewById(toolbarId)) - supportActionBar?.run { - setDisplayHomeAsUpEnabled(false) - action() - } -} - -fun AppCompatActivity.obtainViewModel(viewModelClass: Class) = - ViewModelProviders.of(this).get(viewModelClass) - -/** - * Runs a FragmentTransaction, then calls commit(). - */ -private inline fun FragmentManager.transact(action: FragmentTransaction.() -> Unit) { - beginTransaction().apply { - action() - }.commit() -} - -fun AppCompatActivity.startActivityForResult(intent: Intent, callback: ActivityResultCallback) { - ActResultRequest(this).startForResult(intent, callback) -} - -fun AppCompatActivity.startActivityForResultOk( - intent: Intent, - action: Intent.() -> Unit -) { - ActResultRequest(this).startForResult(intent, object : ActivityResultCallback() { - override fun onActivityResultOk(data: Intent) { - action(data) - } - }) -} diff --git a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseActivity.kt b/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseActivity.kt deleted file mode 100644 index 3dd8673..0000000 --- a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/BaseActivity.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.wrbug.developerhelper.basecommon - -import android.annotation.TargetApi -import android.content.pm.PackageManager -import android.os.Build -import android.os.Bundle -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import com.google.android.material.snackbar.Snackbar -import java.util.ArrayList - -abstract class BaseActivity : AppCompatActivity() { - private lateinit var toastRootView: View - protected lateinit var context: BaseActivity - private var mPermissionCallback: PermissionCallback? = null - - companion object { - private const val PERMISSION_REQUEST_CODE = 0xAADF1 - } - - override fun onCreate(savedInstanceState: Bundle?) { - context = this - super.onCreate(savedInstanceState) - } - - override fun setContentView(layoutResID: Int) { - toastRootView = layoutInflater.inflate(layoutResID, null) - setContentView(toastRootView) - } - - fun showSnack(msg: String) { - Snackbar.make(toastRootView, msg, Snackbar.LENGTH_SHORT).show() - } - - fun showSnack(id: Int) { - Snackbar.make(toastRootView, id, Snackbar.LENGTH_SHORT).show() - } - - - @TargetApi(Build.VERSION_CODES.M) - fun requestPermission(permissions: Array, callback: PermissionCallback) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - callback.granted() - return - } - val list = ArrayList() - for (permission in permissions) { - val hasPermission = checkSelfPermission(permission) - if (hasPermission != PackageManager.PERMISSION_GRANTED) { - list.add(permission) - } - } - if (list.isEmpty()) { - callback.granted() - return - } - mPermissionCallback = callback - requestPermissions(list.toTypedArray(), PERMISSION_REQUEST_CODE) - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - if (requestCode == PERMISSION_REQUEST_CODE) { - val list = ArrayList() - for (i in grantResults.indices) { - if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { - list.add(permissions[i]) - } - } - if (list.isEmpty()) { - mPermissionCallback?.granted() - return - } - mPermissionCallback?.denied(list) - return - } - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - } - - abstract class PermissionCallback { - abstract fun granted() - - open fun denied(permissions: List) { - - } - } -} \ No newline at end of file diff --git a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/ToastExts.kt b/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/ToastExts.kt deleted file mode 100644 index bd5c601..0000000 --- a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/ToastExts.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.wrbug.developerhelper.basecommon - -import android.app.Application -import android.os.Handler -import android.os.Looper -import androidx.annotation.StringRes -import com.wrbug.developerhelper.commonwidget.flexibletoast.FlexibleToast - -//fun Activity.showToast(msg: CharSequence?) { -// Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() -// -//} - - -fun showToast(msg: CharSequence) { - BaseApp.instance.showToast(msg.toString()) -} - -fun showToast(@StringRes id: Int) { - BaseApp.instance.showToast(id) -} - -fun Application.showToast(msg: CharSequence) { - val appHandler = Handler() - val flexibleToast = FlexibleToast(this) - val builder = FlexibleToast.Builder(this).setGravity(FlexibleToast.GRAVITY_BOTTOM) - builder.setSecondText(msg.toString()) - if (Looper.myLooper() !== Looper.getMainLooper()) { - appHandler.post { flexibleToast.toastShow(builder) } - } else { - flexibleToast.toastShow(builder) - } -} diff --git a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/entry/HierarchyNode.kt b/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/entry/HierarchyNode.kt deleted file mode 100644 index 795b145..0000000 --- a/basecommon/src/main/java/com/wrbug/developerhelper/basecommon/entry/HierarchyNode.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.wrbug.developerhelper.basecommon.entry - -import android.graphics.Rect -import android.os.Parcel -import android.os.Parcelable -import com.google.gson.reflect.TypeToken -import com.wrbug.developerhelper.commonutil.JsonHelper -import java.io.Serializable - -@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") -class HierarchyNode() : Parcelable, Serializable { - var id: Long = -1L - var screenBounds: Rect? = null - var parentBounds: Rect? = null - var checkable: Boolean = false - var checked: Boolean = false - var widget: String = "" - var clickable: Boolean = false - var contentDesc: String = "" - var enabled: Boolean = false - var focusable: Boolean = false - var focused: Boolean = false - var index: String = "" - var longClickable: Boolean = false - var packageName: String = "" - var password: Boolean = false - var scrollable: Boolean = false - var selected: Boolean = false - var text: String = "" - var resourceId: String = "" - var idHex: String? = null - var parentId: Long = -1L - var childId: ArrayList = arrayListOf() - - constructor(parcel: Parcel) : this() { - id = parcel.readLong() - screenBounds = parcel.readParcelable(Rect::class.java.classLoader) - parentBounds = parcel.readParcelable(Rect::class.java.classLoader) - checkable = parcel.readByte() != 0.toByte() - checked = parcel.readByte() != 0.toByte() - widget = parcel.readString() - clickable = parcel.readByte() != 0.toByte() - contentDesc = parcel.readString() - enabled = parcel.readByte() != 0.toByte() - focusable = parcel.readByte() != 0.toByte() - focused = parcel.readByte() != 0.toByte() - index = parcel.readString() - longClickable = parcel.readByte() != 0.toByte() - packageName = parcel.readString() - password = parcel.readByte() != 0.toByte() - scrollable = parcel.readByte() != 0.toByte() - selected = parcel.readByte() != 0.toByte() - text = parcel.readString() - resourceId = parcel.readString() - idHex = parcel.readString() - parentId = parcel.readLong() - childId = JsonHelper.fromJson(parcel.readString(), object : TypeToken>() {}.type) ?: - arrayListOf() - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeLong(id) - parcel.writeParcelable(screenBounds, flags) - parcel.writeParcelable(parentBounds, flags) - parcel.writeByte(if (checkable) 1 else 0) - parcel.writeByte(if (checked) 1 else 0) - parcel.writeString(widget) - parcel.writeByte(if (clickable) 1 else 0) - parcel.writeString(contentDesc) - parcel.writeByte(if (enabled) 1 else 0) - parcel.writeByte(if (focusable) 1 else 0) - parcel.writeByte(if (focused) 1 else 0) - parcel.writeString(index) - parcel.writeByte(if (longClickable) 1 else 0) - parcel.writeString(packageName) - parcel.writeByte(if (password) 1 else 0) - parcel.writeByte(if (scrollable) 1 else 0) - parcel.writeByte(if (selected) 1 else 0) - parcel.writeString(text) - parcel.writeString(resourceId) - parcel.writeString(idHex) - parcel.writeLong(parentId) - parcel.writeString(JsonHelper.toJson(childId)) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): HierarchyNode { - return HierarchyNode(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - -} \ No newline at end of file diff --git a/basecommon/src/main/res/values/strings.xml b/basecommon/src/main/res/values/strings.xml deleted file mode 100644 index 717ea7a..0000000 --- a/basecommon/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - baseCommon - diff --git a/basecommon/src/test/java/com/wrbug/developerhelper/basecommon/ExampleUnitTest.java b/basecommon/src/test/java/com/wrbug/developerhelper/basecommon/ExampleUnitTest.java deleted file mode 100644 index 99a6b24..0000000 --- a/basecommon/src/test/java/com/wrbug/developerhelper/basecommon/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.wrbug.developerhelper.basecommon; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/basemoduleimport/.gitignore b/basemoduleimport/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/basemoduleimport/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/basemoduleimport/build.gradle b/basemoduleimport/build.gradle deleted file mode 100644 index 71a8f8a..0000000 --- a/basemoduleimport/build.gradle +++ /dev/null @@ -1,45 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion 28 - - - - defaultConfig { - minSdkVersion 21 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - -} - -dependencies { - api fileTree(include: ['*.jar'], dir: 'libs') - api 'androidx.appcompat:appcompat:1.0.2' - api 'androidx.constraintlayout:constraintlayout:1.1.3' - api 'androidx.legacy:legacy-support-v4:1.0.0' - api 'androidx.recyclerview:recyclerview:1.0.0' - api 'com.google.android.material:material:1.1.0-alpha02' - api project(':commonutil') - api project(':commonwidget') - api project(':mmkv') - api project(':netbare-core') - api project(':netbare-injector') - api project(':basecommon') - api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} -repositories { - mavenCentral() -} diff --git a/basemoduleimport/proguard-rules.pro b/basemoduleimport/proguard-rules.pro deleted file mode 100644 index f1b4245..0000000 --- a/basemoduleimport/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/basemoduleimport/src/androidTest/java/com/wrbug/developerhelper/basewidgetimport/ExampleInstrumentedTest.java b/basemoduleimport/src/androidTest/java/com/wrbug/developerhelper/basewidgetimport/ExampleInstrumentedTest.java deleted file mode 100644 index 38b9eb4..0000000 --- a/basemoduleimport/src/androidTest/java/com/wrbug/developerhelper/basewidgetimport/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.wrbug.developerhelper.basewidgetimport; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.wrbug.developerhelper.basewidgetimport.test", appContext.getPackageName()); - } -} diff --git a/basemoduleimport/src/main/AndroidManifest.xml b/basemoduleimport/src/main/AndroidManifest.xml deleted file mode 100644 index 27a1cb8..0000000 --- a/basemoduleimport/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - diff --git a/basemoduleimport/src/main/res/values/strings.xml b/basemoduleimport/src/main/res/values/strings.xml deleted file mode 100644 index 2e0519d..0000000 --- a/basemoduleimport/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - basewidgetimport - diff --git a/basemoduleimport/src/test/java/com/wrbug/developerhelper/basewidgetimport/ExampleUnitTest.java b/basemoduleimport/src/test/java/com/wrbug/developerhelper/basewidgetimport/ExampleUnitTest.java deleted file mode 100644 index 32c485c..0000000 --- a/basemoduleimport/src/test/java/com/wrbug/developerhelper/basewidgetimport/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.wrbug.developerhelper.basewidgetimport; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 46e05c0..183b457 100644 --- a/build.gradle +++ b/build.gradle @@ -1,35 +1,11 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - ext.kotlin_version = '1.3.21' - ext.kotlin_version = '1.3.11' - ext.gradle_version = '3.2.0' - repositories { - google() - jcenter() - maven { - url "https://jitpack.io" - } - } - dependencies { - classpath "com.android.tools.build:gradle:$gradle_version" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} -allprojects { - repositories { - google() - jcenter() - maven { - url "https://jitpack.io" - } - } +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '8.2.2' apply false + id 'com.android.library' version '8.2.2' apply false + id 'org.jetbrains.kotlin.android' version '1.9.22' apply false } task clean(type: Delete) { delete rootProject.buildDir -} +} \ No newline at end of file diff --git a/common.gradle b/common.gradle new file mode 100644 index 0000000..a84c948 --- /dev/null +++ b/common.gradle @@ -0,0 +1,19 @@ +apply plugin: 'kotlin-parcelize' + +android { + buildFeatures { + viewBinding true + } + kotlinOptions { + jvmTarget = '1.8' + } + lintOptions { + abortOnError true + disable 'UnusedResources' + ignoreWarnings true + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} \ No newline at end of file diff --git a/commonutil/build.gradle b/commonutil/build.gradle index 280a427..b39d840 100644 --- a/commonutil/build.gradle +++ b/commonutil/build.gradle @@ -1,14 +1,13 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' +apply from: "${rootDir}/common.gradle" android { - compileSdkVersion 28 - - - + namespace "com.wrbug.developerhelper.commonutil" + compileSdk 34 defaultConfig { - minSdkVersion 21 - targetSdkVersion 28 + minSdkVersion 23 + targetSdkVersion 34 versionCode 1 versionName "1.0" @@ -27,19 +26,16 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - api 'com.jaredrummler:android-shell:1.0.0' - implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.appcompat:appcompat:1.7.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.0-alpha3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha3' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation project(':mmkv') api "org.jetbrains.anko:anko-commons:0.10.8" api 'com.google.code.gson:gson:2.8.5' api 'com.elvishew:xlog:1.6.1' api 'com.squareup.okhttp3:okhttp:3.12.1' api group: 'org.jsoup', name: 'jsoup', version: '1.11.3' -} -repositories { - mavenCentral() + api "io.reactivex.rxjava3:rxjava:3.0.3" + api 'io.reactivex.rxjava3:rxandroid:3.0.0' } diff --git a/commonutil/src/androidTest/java/com/wrbug/developerhelper/commonutil/ExampleInstrumentedTest.java b/commonutil/src/androidTest/java/com/wrbug/developerhelper/commonutil/ExampleInstrumentedTest.java deleted file mode 100644 index 87b387b..0000000 --- a/commonutil/src/androidTest/java/com/wrbug/developerhelper/commonutil/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.wrbug.developerhelper.commonutil; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.wrbug.developerhelper.commonutil.test", appContext.getPackageName()); - } -} diff --git a/commonutil/src/main/AndroidManifest.xml b/commonutil/src/main/AndroidManifest.xml index 68282c7..cf5f98d 100644 --- a/commonutil/src/main/AndroidManifest.xml +++ b/commonutil/src/main/AndroidManifest.xml @@ -1,2 +1,8 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.wrbug.developerhelper.commonutil"> + + + diff --git a/commonutil/src/main/assets/busybox_arm64 b/commonutil/src/main/assets/busybox_arm64 new file mode 100644 index 0000000..cf9e5be Binary files /dev/null and b/commonutil/src/main/assets/busybox_arm64 differ diff --git a/commonutil/src/main/assets/busybox_x86_64 b/commonutil/src/main/assets/busybox_x86_64 new file mode 100755 index 0000000..77523e2 Binary files /dev/null and b/commonutil/src/main/assets/busybox_x86_64 differ diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/AppInfoManager.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/AppInfoManager.kt index ab0b1d1..7f6e08e 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/AppInfoManager.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/AppInfoManager.kt @@ -1,9 +1,12 @@ package com.wrbug.developerhelper.commonutil +import android.app.ActivityManager +import android.content.Context import com.wrbug.developerhelper.commonutil.entity.ApkInfo import com.wrbug.developerhelper.commonutil.shell.ShellManager import java.io.File + object AppInfoManager { private val appMap = HashMap() @@ -31,8 +34,27 @@ object AppInfoManager { } + fun getTopActivityClassName(context: Context): String? { + var topActivityClass: String? = null + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + try { + val runningTaskInfos = activityManager.getRunningTasks(1) + if (runningTaskInfos != null && runningTaskInfos.size> 0) { + val f = runningTaskInfos[0].topActivity + topActivityClass = f!!.className + } + } catch (e: Exception) { + } + return topActivityClass + } + + fun getSharedPreferencesFiles(packageName: String): Array { - val path = "/data/data/$packageName/shared_prefs" + return getSharedPreferencesFiles(Constant.dataDir, packageName) + } + + private fun getSharedPreferencesFiles(dir: String, packageName: String): Array { + val path = "$dir/$packageName/shared_prefs" val list = ShellManager.lsDir(path) val files = ArrayList() for (file in list) { @@ -46,5 +68,4 @@ object AppInfoManager { return files.toTypedArray() } - } \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/AppManagerUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/AppManagerUtils.kt index 1a50b09..5d7fb28 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/AppManagerUtils.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/AppManagerUtils.kt @@ -3,6 +3,7 @@ package com.wrbug.developerhelper.commonutil import android.content.Context import android.content.Intent import android.net.Uri +import android.widget.Toast import com.wrbug.developerhelper.commonutil.shell.ShellManager @@ -20,4 +21,14 @@ object AppManagerUtils { fun forceStopApp(packageName: String): Boolean { return ShellManager.forceStopApp(packageName) } + + fun restartApp(context: Context, packageName: String) { + if (!forceStopApp(packageName)) { + Toast.makeText(context, "重启失败", Toast.LENGTH_SHORT).show() + return + } + val intent = context.packageManager.getLaunchIntentForPackage(packageName) + intent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } } \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ClipboardUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ClipboardUtils.kt index 79e719d..4f54f22 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ClipboardUtils.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ClipboardUtils.kt @@ -31,7 +31,7 @@ object ClipboardUtils { try { val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager // 将文本内容放到系统剪贴板里。 - cm.primaryClip = ClipData.newPlainText(null, text) + cm.setPrimaryClip(ClipData.newPlainText(null, text)) } catch (t: Throwable) { } diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/CommonUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/CommonUtils.kt index f112bd2..d7430fa 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/CommonUtils.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/CommonUtils.kt @@ -2,10 +2,41 @@ package com.wrbug.developerhelper.commonutil import android.app.Application import android.content.Context +import com.wrbug.developerhelper.commonutil.entity.CpuABI +import com.wrbug.developerhelper.commonutil.shell.ShellUtils +import org.jetbrains.anko.doAsync + object CommonUtils { lateinit var application: Application fun register(ctx: Context) { application = ctx.applicationContext as Application + releaseBusyBox(ctx) + } + + private fun releaseBusyBox(ctx: Context) { + doAsync { + val name = when (getCPUABI()) { + CpuABI.ARM -> "busybox_arm64" + CpuABI.X86 -> "busybox_x86_64" + } + val data = ctx.resources.assets.open(name).readBytes() + val file = ShellUtils.busyBoxFile + file.writeBytes(data) + ShellUtils.run("chmod +x " + file.absolutePath) + } + } + + + private fun getCPUABI(): CpuABI { + val result = ShellUtils.run("getprop ro.product.cpu.abi") + if (result.isSuccessful.not()) { + return CpuABI.ARM + } + return if (result.getStdout().contains("x86")) { + CpuABI.X86 + } else { + CpuABI.ARM + } } } \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/Constant.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/Constant.kt new file mode 100644 index 0000000..373c805 --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/Constant.kt @@ -0,0 +1,20 @@ +package com.wrbug.developerhelper.commonutil + +import android.os.Build + +object Constant { + private const val DATA_MIRROR_DIR = "/data_mirror/data_ce/null/0" + private const val DATA_DIR = "/data/data" + + fun getDataDir(packageName: String): String { + return "$dataDir/$packageName" + } + + val dataDir by lazy { + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.R) { + DATA_MIRROR_DIR + } else { + DATA_DIR + } + } +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/FileExt.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/FileExt.kt new file mode 100644 index 0000000..ec660ce --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/FileExt.kt @@ -0,0 +1,9 @@ +package com.wrbug.developerhelper.commonutil + +import java.io.File + +fun File.safeRead(): String { + return runCatching { + readText() + }.getOrDefault("") +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/FileUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/FileUtils.kt deleted file mode 100644 index 33c6241..0000000 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/FileUtils.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.wrbug.developerhelper.commonutil - -import android.content.Context -import android.media.MediaMetadataRetriever -import android.net.Uri -import android.os.Build -import androidx.core.content.FileProvider -import java.io.* - - -/** - * Created by wrbug on 2017年8月23日. - */ -object FileUtils { - - - fun writeByteToFile(data: ByteArray, path: String) { - try { - val localFileOutputStream = FileOutputStream(path) - localFileOutputStream.write(data) - localFileOutputStream.close() - } catch (e: Exception) { - } - } - - - fun inputstreamtofile(ins: InputStream, file: File) { - try { - if (file.exists()) { - file.createNewFile() - } - val os = FileOutputStream(file) - os.write(ins.readBytes()) - os.close() - ins.close() - } catch (t: Throwable) { - - } - - } - - - fun inputStream2String(ins: InputStream): String { - val out = StringBuffer() - val b = ByteArray(4096) - try { - var n: Int = ins.read(b) - while (n != -1) { - out.append(String(b, 0, n)) - n = ins.read(b) - } - } catch (e: IOException) { - return "" - } - - return out.toString() - } - - - fun readFile(file: File): String { - var inputStream: FileInputStream? = null - try { - inputStream = FileInputStream(file) - val bytes = ByteArray(inputStream.available()) - inputStream.read(bytes) - return String(bytes) - } catch (e: IOException) { - } finally { - inputStream?.close() - } - return "" - } - - fun whiteFile(file: File, data: String) { - var outputStream: FileOutputStream? = null - try { - outputStream = FileOutputStream(file) - outputStream.write(data.toByteArray()) - } catch (e: IOException) { - }finally { - outputStream?.close() - } - - } -} - - -fun File.toUri(context: Context): Uri? { - return if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.N) { - FileProvider.getUriForFile(context, "com.wrbug.developerhelper.fileprovider", this) - } else { - Uri.fromFile(this) - } -} diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/JsonHelper.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/JsonHelper.kt index 62be985..0d743bf 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/JsonHelper.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/JsonHelper.kt @@ -3,6 +3,7 @@ package com.wrbug.developerhelper.commonutil import com.google.gson.Gson import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.reflect.TypeToken import java.lang.reflect.Type @@ -12,6 +13,19 @@ import java.lang.reflect.Type * @author wrbug * @since 2017年9月29日 */ + +fun Any.toJson(): String? { + return try { + JsonHelper.toJson(this) + } catch (t: Throwable) { + null + } +} + +inline fun String.fromJson(): T? { + return JsonHelper.fromJson(this, object : TypeToken() {}.type) +} + object JsonHelper { val gson = Gson() diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/LogUtilsExt.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/LogUtilsExt.kt index ef9a1a4..b9acf57 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/LogUtilsExt.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/LogUtilsExt.kt @@ -1,13 +1,26 @@ package com.wrbug.developerhelper.commonutil +import com.elvishew.xlog.LogConfiguration +import com.elvishew.xlog.LogLevel import com.elvishew.xlog.XLog +import com.elvishew.xlog.internal.DefaultsFactory import com.wrbug.developerhelper.commonutil.JsonHelper +private var init = false fun Any.print() { + if (!init) { + XLog.init( + LogConfiguration.Builder().logLevel(LogLevel.ALL).tag("developerHelper.print-->").build(), + DefaultsFactory.createPrinter() + ) + init = true + } + XLog.i("----------易开发Log----------") when (this) { is CharSequence, is Number, is Boolean -> XLog.i(this) + is Throwable -> XLog.e(this) else -> XLog.json(JsonHelper.toJson(this)) } XLog.i("----------结束----------") diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ProcessUtil.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ProcessUtil.kt new file mode 100644 index 0000000..c8164c7 --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ProcessUtil.kt @@ -0,0 +1,75 @@ +package com.wrbug.developerhelper.commonutil + +import android.app.ActivityManager +import android.content.Context +import android.os.Process +import android.text.TextUtils + +import java.io.FileInputStream +import java.io.IOException + +object ProcessUtil { + + fun isMainProc(context: Context): Boolean { + val myPid = Process.myPid() + var procName = readProcName(context, myPid) + if (TextUtils.isEmpty(procName)) { + procName = readProcName(myPid) + } + return context.packageName == procName + } + + + fun readProcName(context: Context, myPid: Int): String? { + var myProcess: ActivityManager.RunningAppProcessInfo? = null + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + try { + val list = activityManager.runningAppProcesses + if (list != null) { + for (info in list) { + if (info.pid == myPid) { + myProcess = info + break + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + + if (myProcess != null) { + return myProcess.processName + } + + return null + } + + fun readProcName(myPid: Int=Process.myPid()): String? { + var fileInputStream: FileInputStream? = null + try { + fileInputStream = FileInputStream("/proc/$myPid/cmdline") + val buffer = ByteArray(128) + val len = fileInputStream.read(buffer) + if (len <= 0) { + return null + } + var index = 0 + while (index < buffer.size) { + if (buffer[index]> 128 || buffer[index] <= 0) { + break + } + index++ + } + return String(buffer, 0, index) + } catch (ignore: Exception) { + ignore.printStackTrace() + } finally { + try { + fileInputStream?.close() + } catch (ignore: IOException) { + } + + } + return null + } +} diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ReflectExts.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ReflectExts.kt new file mode 100644 index 0000000..5066229 --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ReflectExts.kt @@ -0,0 +1,115 @@ +package com.wrbug.developerhelper.commonutil +import java.lang.reflect.Field +import java.lang.reflect.Method + +fun Class<*>.setValue(obj: Any?, fieldName: String, value: Any?) { + this.matchField(fieldName)?.set(obj, value) +} + +fun Class.getValue(obj: T?, fieldName: String): Any? { + return this.matchField(fieldName)?.get(obj) +} + +fun Any.getFieldValue(fieldName: String): Any? { + return javaClass.getValue(this, fieldName) +} + +fun Any.setFieldValue(fieldName: String, value: Any?) { + javaClass.setValue(this, fieldName, value) +} + +fun Any.callMethod(methodName: String, vararg args: Any?): Any? { + return this.javaClass.matchMethod(methodName, *args)?.invoke(this, *args) +} + +fun Class.callMethod(obj: T?, methodName: String, vararg args: Any?): Any? { + return this.matchMethod(methodName, *args)?.invoke(obj, *args) +} + +fun Class<*>.matchMethod(methodName: String, vararg args: Any?): Method? { + return findDeclaredMethod(methodName, *args) ?: findMethod(methodName, *args) +} + +fun Class<*>.matchField(fieldName: String): Field? { + return findDeclaredField(fieldName) ?: findField(fieldName) +} + +fun Class<*>.findDeclaredField(fieldName: String): Field? { + return runCatching { + this.getDeclaredField(fieldName).apply { + if (!isAccessible) { + isAccessible = true + } + } + }.getOrElse { + if (this == Any::class.java) { + return null + } + this.superclass.findDeclaredField(fieldName) + } +} + +fun Class<*>.findDeclaredMethod(methodName: String, vararg args: Any?): Method? { + val method = + this.declaredMethods.asSequence() + .filter { it.name == methodName && it.parameterCount == args.size } + .find { + val size = args.size + for (i in 0 until size) { + val arg = args[i] ?: continue + if (!it.parameterTypes[i].isAssignableFrom(arg.javaClass)) { + return@find false + } + } + return@find true + }?.apply { + if (!isAccessible) { + isAccessible = true + } + } + return method ?: if (this == Any::class.java) { + null + } else { + superclass.findDeclaredMethod(methodName, *args) + } +} + +fun Class<*>.findField(fieldName: String): Field? { + return runCatching { + this.getField(fieldName).apply { + if (!isAccessible) { + isAccessible = true + } + } + }.getOrElse { + if (this == Any::class.java) { + return null + } + this.superclass.findField(fieldName) + } +} + +fun Class<*>.findMethod(methodName: String, vararg args: Any?): Method? { + val method = + this.methods.asSequence() + .filter { it.name == methodName && it.parameterCount == args.size } + .find { + val size = args.size + for (i in 0..size) { + val arg = args[i] ?: continue + if (!it.parameterTypes[i].isAssignableFrom(arg.javaClass)) { + return@find false + } + } + return@find true + }?.apply { + if (!isAccessible) { + isAccessible = true + } + } + return method ?: if (this == Any::class.java) { + null + } else { + superclass.findDeclaredMethod(methodName, *args) + } +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/RootUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/RootUtils.kt new file mode 100644 index 0000000..6f7864a --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/RootUtils.kt @@ -0,0 +1,9 @@ +package com.wrbug.developerhelper.commonutil + +import com.wrbug.developerhelper.mmkv.ConfigKv +import com.wrbug.developerhelper.mmkv.manager.MMKVManager + +object RootUtils { + val configKv = MMKVManager.get(ConfigKv::class.java) + fun isRoot() = configKv.isOpenRoot() +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/RxJavaExt.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/RxJavaExt.kt new file mode 100644 index 0000000..4bfefc3 --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/RxJavaExt.kt @@ -0,0 +1,31 @@ +package com.wrbug.developerhelper.commonutil + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.core.SingleEmitter +import io.reactivex.rxjava3.core.SingleOnSubscribe +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.schedulers.Schedulers + +fun Single.runOnIO(): Single { + return this.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) +} + +fun Single.observeOnMain(): Single { + return this.observeOn(AndroidSchedulers.mainThread()) +} + +fun safeCreateSingle(source: (SingleEmitter) -> Unit): Single { + return Single.create { emit -> + runCatching { + source(emit) + }.getOrElse { + emit.onError(it) + } + } +} + +fun Disposable.addTo(compositeDisposable: CompositeDisposable) { + compositeDisposable.add(this) +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ShellUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ShellUtils.kt deleted file mode 100644 index 8c36c0f..0000000 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ShellUtils.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.wrbug.developerhelper.commonutil - -import com.jaredrummler.android.shell.CommandResult -import com.jaredrummler.android.shell.Shell -import com.wrbug.developerhelper.mmkv.ConfigKv -import com.wrbug.developerhelper.mmkv.manager.MMKVManager -import org.jetbrains.anko.doAsync - -object ShellUtils { - val configKv = MMKVManager.get(ConfigKv::class.java) - fun run(cmds: Array, callback: ShellResultCallback) { - doAsync { - val run = Shell.SH.run(*cmds) - callback.onComplete(run) - } - } - - fun run(vararg cmds: String): CommandResult { - return Shell.SH.run(*cmds) - } - - fun runWithSu(cmds: Array, callback: ShellResultCallback) { - if (!configKv.isOpenRoot()) { - callback.onError("未开启root权限") - return - } - doAsync { - val run = Shell.SU.run(*cmds) - callback.onComplete(run) - } - } - - fun runWithSuIgnoreSetting(vararg cmds: String): CommandResult { - return Shell.SU.run(*cmds) - } - - - fun runWithSu(vararg cmds: String): CommandResult { - if (configKv.isOpenRoot().not()) { - return CommandResult(arrayListOf("未开启root权限"), arrayListOf("未开启root权限"), 1) - } - return Shell.SU.run(*cmds) - } - - fun isRoot(): Boolean { - return Shell.SU.available() - } - - abstract class ShellResultCallback(vararg args: Any) { - protected var args = args - open fun onComplete(result: CommandResult) { - - } - - open fun onError(msg: String) { - - } - } - -} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/SpannableBuilder.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/SpannableBuilder.kt new file mode 100644 index 0000000..acd44ff --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/SpannableBuilder.kt @@ -0,0 +1,125 @@ +package com.wrbug.developerhelper.commonutil + +import android.content.Context +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextPaint +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.text.style.StrikethroughSpan +import android.text.style.StyleSpan +import android.text.style.TextAppearanceSpan +import android.view.View + +class SpannableBuilder private constructor(private val context: Context, private val text: String) { + + private val spanMap = hashMapOf>>() + + companion object { + fun with(context: Context, strRes: Int): SpannableBuilder { + return SpannableBuilder(context, strRes) + } + + fun with(context: Context, text: String): SpannableBuilder { + return SpannableBuilder(context, text) + } + } + + private var strRes: Int = -1 + + private constructor(context: Context, strRes: Int) : this(context, "") { + this.strRes = strRes + } + + fun addSpanWithTextAppearance( + value: String, + textAppearance: Int, + color: Int? = null, + index: Int = 0 + ): SpannableBuilder { + addSpan(value, TextAppearanceSpan(context, textAppearance), index) + if (color != null) { + addSpan(value, ForegroundColorSpan(color), index) + } + return this + } + + fun addSpanWithClickListener( + key: String, + linkColor: Int, + index: Int = 0, + listener: () -> Unit + ): SpannableBuilder { + addSpan(key, object : ClickableSpan() { + override fun onClick(widget: View) { + listener() + } + + override fun updateDrawState(ds: TextPaint) { + ds.isUnderlineText = true + ds.color = linkColor + } + }, index) + return this + } + + fun addSpanWithColor(value: String, color: Int, index: Int = 0): SpannableBuilder { + addSpan(value, ForegroundColorSpan(color), index) + return this + } + + + fun addSpanWithDeleteLine(value: String, index: Int = 0): SpannableBuilder { + addSpan(value, StrikethroughSpan(), index) + return this + } + + fun addSpanWithBold(value: String, index: Int = 0): SpannableBuilder { + addSpan(value, StyleSpan(Typeface.BOLD), index) + return this + } + + fun addCustomSpan(value: String, what: Any, index: Int = 0): SpannableBuilder { + addSpan(value, what, index) + return this + } + + private fun addSpan(key: String, what: Any, index: Int) { + spanMap[key] = (spanMap[key] ?: arrayListOf()).apply { add(index to what) } + } + + fun build(): Spannable { + val originStr = if (strRes == -1) { + text + } else { + context.getString(strRes) + } + val spannableBuilder = SpannableStringBuilder(originStr) + spanMap.forEach { (key, pair) -> + pair.forEach { (index, what) -> + val strIndex = index.let { + var i = it + var currentIndex = 0 + while (i != 0) { + i-- + currentIndex = originStr.indexOf(key, currentIndex) + key.length + } + originStr.indexOf(key, currentIndex) + } + if (strIndex == -1) { + return@forEach + } + spannableBuilder.setSpan( + what, + strIndex, + strIndex + key.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + + } + return spannableBuilder + } +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/StringConvertUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/StringConvertUtils.kt index 4aca828..a3cb476 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/StringConvertUtils.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/StringConvertUtils.kt @@ -4,7 +4,6 @@ import java.math.BigDecimal import java.math.BigInteger import java.util.regex.Pattern - fun String.isInt(): Boolean { if (isNumber()) { return BigInteger(this).toLong() < Integer.MAX_VALUE && BigInteger(this).toLong()> Integer.MIN_VALUE @@ -39,30 +38,32 @@ fun String.toBoolean(): Boolean { return false } -fun String.toInt(): Int { +fun String?.toInt(): Int { + this ?: return 0 if (isInt()) { return BigInteger(this).toInt() } return 0 } - -fun String.toLong(): Long { +fun String?.toLong(): Long { + this ?: return 0 if (isNumber()) { return BigInteger(this).toLong() } return 0L } - -fun String.toFloat(): Float { +fun String?.toFloat(): Float { + this ?: return 0F if (isDecimal()) { return BigDecimal(this).toFloat() } return 0F } -fun String.toDouble(): Double { +fun String?.toDouble(): Double { + this ?: return 0.0 if (isDecimal()) { return BigDecimal(this).toDouble() } diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/UiUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/UiUtils.kt index be487bf..fe73ddd 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/UiUtils.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/UiUtils.kt @@ -10,21 +10,17 @@ fun Context.dp2px(dpVal: Float): Int = UiUtils.dp2px(this, dpVal) fun Fragment.dp2px(dpVal: Float): Int = UiUtils.dp2px(activity!!, dpVal) fun Dialog.dp2px(dpVal: Float): Int = UiUtils.dp2px(context, dpVal) fun View.dp2px(dpVal: Float): Int = UiUtils.dp2px(context, dpVal) +fun Float.dpInt(context: Context) = UiUtils.dp2px(context, this) +fun Int.dpInt(context: Context = CommonUtils.application) = UiUtils.dp2px(context, toFloat()) object UiUtils { - + private var statusBarHeight: Int = -1 fun dp2px(context: Context = CommonUtils.application, dpVal: Float): Int { return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - dpVal, - context.resources?.displayMetrics + TypedValue.COMPLEX_UNIT_DIP, dpVal, context.resources?.displayMetrics ).toInt() } - fun sp2px(context: Context = CommonUtils.application, spVal: Float): Int { - return TypedValue.applyDimension(2, spVal, context.resources?.displayMetrics).toInt() - } - fun px2dp(context: Context = CommonUtils.application, pxVal: Float): Float { val scale = context.resources?.displayMetrics?.density!! return pxVal / scale @@ -54,12 +50,16 @@ object UiUtils { * 获取状态栏的高度 */ fun getStatusHeight(context: Context = CommonUtils.application): Int { + if (statusBarHeight != -1) { + return statusBarHeight + } var result: Int? = 10 val resourceId = context.resources?.getIdentifier("status_bar_height", "dimen", "android") ?: 0 if (resourceId> 0) { result = context.resources?.getDimensionPixelOffset(resourceId) } - return result ?: 0 + statusBarHeight = result ?: 0 + return statusBarHeight } } \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ZipUtil.java b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ZipUtil.java new file mode 100644 index 0000000..f61ac17 --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ZipUtil.java @@ -0,0 +1,229 @@ +package com.wrbug.developerhelper.commonutil; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.CRC32; +import java.util.zip.CheckedOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +public class ZipUtil { + + /** + * 缓冲器大小 + */ + private static final int BUFFER = 512; + + /** + * 压缩得到的文件的后缀名 + */ + private static final String SUFFIX = ".zip"; + + /** + * 得到源文件路径的所有文件 + * + * @param dirFile 压缩源文件路径 + */ + public static List getAllFile(File dirFile) { + + List fileList = new ArrayList(); + + File[] files = dirFile.listFiles(); + for (File file : files) {//文件 + if (file.isFile()) { + fileList.add(file); + System.out.println("add file:" + file.getName()); + } else {//目录 + if (file.listFiles().length != 0) {//非空目录 + fileList.addAll(getAllFile(file));//把递归文件加到fileList中 + } else {//空目录 + fileList.add(file); + System.out.println("add empty dir:" + file.getName()); + } + } + } + return fileList; + } + + /** + * 获取相对路径 + * + * @param dirPath 源文件路径 + * @param file 准备压缩的单个文件 + */ + public static String getRelativePath(String dirPath, File file) { + File dirFile = new File(dirPath); + String relativePath = file.getName(); + + while (true) { + file = file.getParentFile(); + if (file == null) break; + if (file.equals(dirFile)) { + break; + } else { + relativePath = file.getName() + "/" + relativePath; + } + } + return relativePath; + } + + + /** + * @param destPath 解压目标路径 + * @param fileName 解压文件的相对路径 + */ + public static File createFile(String destPath, String fileName) { + + String[] dirs = fileName.split("/");//将文件名的各级目录分解 + File file = new File(destPath); + + if (dirs.length> 1) {//文件有上级目录 + for (int i = 0; i < dirs.length - 1; i++) { + file = new File(file, dirs[i]);//依次创建文件对象知道文件的上一级目录 + } + + if (!file.exists()) { + file.mkdirs();//文件对应目录若不存在,则创建 + try { + System.out.println("mkdirs: " + file.getCanonicalPath()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + file = new File(file, dirs[dirs.length - 1]);//创建文件 + + return file; + } else { + if (!file.exists()) {//若目标路径的目录不存在,则创建 + file.mkdirs(); + try { + System.out.println("mkdirs: " + file.getCanonicalPath()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + file = new File(file, dirs[0]);//创建文件 + + return file; + } + + } + + /** + * 没有指定压缩目标路径进行压缩,用默认的路径进行压缩 + * + * @param dirPath 压缩源文件路径 + */ + public static void compress(String dirPath) { + + int firstIndex = dirPath.indexOf("/"); + int lastIndex = dirPath.lastIndexOf("/"); + String zipFileName = dirPath.substring(0, firstIndex + 1) + dirPath.substring(lastIndex + 1); + compress(dirPath, zipFileName); + } + + /** + * 压缩文件 + * + * @param dirPath 压缩源文件路径 + * @param zipFileName 压缩目标文件路径 + */ + public static void compress(String dirPath, String zipFileName) { + + + zipFileName = zipFileName + SUFFIX;//添加文件的后缀名 + + File dirFile = new File(dirPath); + List fileList = getAllFile(dirFile); + + byte[] buffer = new byte[BUFFER]; + ZipEntry zipEntry = null; + int readLength = 0; //每次读取出来的长度 + + try { + // 对输出文件做CRC32校验 + CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream( + zipFileName), new CRC32()); + ZipOutputStream zos = new ZipOutputStream(cos); + + for (File file : fileList) { + + if (file.isFile()) { //若是文件,则压缩文件 + + zipEntry = new ZipEntry(getRelativePath(dirPath, file)); // + zipEntry.setSize(file.length()); + zipEntry.setTime(file.lastModified()); + zos.putNextEntry(zipEntry); + + InputStream is = new BufferedInputStream(new FileInputStream(file)); + + while ((readLength = is.read(buffer, 0, BUFFER)) != -1) { + zos.write(buffer, 0, readLength); + } + is.close(); + System.out.println("file compress:" + file.getCanonicalPath()); + } else { //若是空目录,则写入zip条目中 + + zipEntry = new ZipEntry(getRelativePath(dirPath, file)); + zos.putNextEntry(zipEntry); + System.out.println("dir compress: " + file.getCanonicalPath() + "/"); + } + } + zos.close(); //最后得关闭流,不然压缩最后一个文件会出错 + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 解压 + */ + public static void decompress(String zipFileName, String destPath) { + + try { + + zipFileName = zipFileName + SUFFIX; + ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFileName)); + ZipEntry zipEntry = null; + byte[] buffer = new byte[BUFFER];//缓冲器 + int readLength = 0;//每次读出来的长度 + while ((zipEntry = zis.getNextEntry()) != null) { + if (zipEntry.isDirectory()) {//若是目录 + File file = new File(destPath + "/" + zipEntry.getName()); + if (!file.exists()) { + file.mkdirs(); + System.out.println("mkdirs:" + file.getCanonicalPath()); + continue; + } + }//若是文件 + File file = createFile(destPath, zipEntry.getName()); + System.out.println("file created: " + file.getCanonicalPath()); + OutputStream os = new FileOutputStream(file); + while ((readLength = zis.read(buffer, 0, BUFFER)) != -1) { + os.write(buffer, 0, readLength); + } + os.close(); + System.out.println("file uncompressed: " + file.getCanonicalPath()); + } + + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/ApkInfo.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/ApkInfo.kt index 5e8b861..f0ca8de 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/ApkInfo.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/ApkInfo.kt @@ -6,13 +6,14 @@ import android.graphics.drawable.Drawable import android.os.Parcel import android.os.Parcelable import com.wrbug.developerhelper.commonutil.CommonUtils +import kotlinx.parcelize.Parcelize -class ApkInfo(val packageInfo: PackageInfo, val applicationInfo: ApplicationInfo) : Parcelable { - - constructor(parcel: Parcel) : this( - parcel.readParcelable(PackageInfo::class.java.classLoader), - parcel.readParcelable(ApplicationInfo::class.java.classLoader) - ) +@Parcelize +class ApkInfo( + val packageInfo: PackageInfo, + val applicationInfo: ApplicationInfo, + var topActivity: String = "" +) : Parcelable { fun getIco(): Drawable { return applicationInfo.loadIcon(CommonUtils.application.packageManager) @@ -20,26 +21,11 @@ class ApkInfo(val packageInfo: PackageInfo, val applicationInfo: ApplicationInfo fun getAppName(): String { val label = CommonUtils.application.packageManager.getApplicationLabel(applicationInfo) - return if (label == null) "" else label.toString() - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeParcelable(packageInfo, flags) - parcel.writeParcelable(applicationInfo, flags) + return label.toString() } - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ApkInfo { - return ApkInfo(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } + fun generateBackupApkFileName(): String { + return packageInfo.versionName + ".apk" } } \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/CpuABI.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/CpuABI.kt new file mode 100644 index 0000000..952e1b8 --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/CpuABI.kt @@ -0,0 +1,5 @@ +package com.wrbug.developerhelper.commonutil.entity + +enum class CpuABI { + ARM,X86 +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/FragmentInfo.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/FragmentInfo.kt index ab23081..cea0527 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/FragmentInfo.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/FragmentInfo.kt @@ -1,71 +1,23 @@ package com.wrbug.developerhelper.commonutil.entity -import android.os.Parcel import android.os.Parcelable - -class FragmentInfo() : Parcelable { - var name = "" - var fragmentId = "" - var containerId = "" - var tag = "" - var state = 0 - var index = 0 - var who = "" - var backStackNesting = 0 - var added = false - var removing = false - var fromLayout = false - var inLayout = false - var hidden = true - var detached = false - - constructor(parcel: Parcel) : this() { - name = parcel.readString() - fragmentId = parcel.readString() - containerId = parcel.readString() - tag = parcel.readString() - state = parcel.readInt() - index = parcel.readInt() - who = parcel.readString() - backStackNesting = parcel.readInt() - added = parcel.readByte() != 0.toByte() - removing = parcel.readByte() != 0.toByte() - fromLayout = parcel.readByte() != 0.toByte() - inLayout = parcel.readByte() != 0.toByte() - hidden = parcel.readByte() != 0.toByte() - detached = parcel.readByte() != 0.toByte() - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(name) - parcel.writeString(fragmentId) - parcel.writeString(containerId) - parcel.writeString(tag) - parcel.writeInt(state) - parcel.writeInt(index) - parcel.writeString(who) - parcel.writeInt(backStackNesting) - parcel.writeByte(if (added) 1 else 0) - parcel.writeByte(if (removing) 1 else 0) - parcel.writeByte(if (fromLayout) 1 else 0) - parcel.writeByte(if (inLayout) 1 else 0) - parcel.writeByte(if (hidden) 1 else 0) - parcel.writeByte(if (detached) 1 else 0) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): FragmentInfo { - return FragmentInfo(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - +import kotlinx.parcelize.Parcelize + +@Parcelize +data class FragmentInfo( + var name: String = "", + var containerId: String = "", + var tag: String = "", + var state: Int = 0, + var index: Int = 0, + var who: String = "", + var backStackNesting: Int = 0, + var added: Boolean = false, + var removing: Boolean = false, + var fromLayout: Boolean = false, + var inLayout: Boolean = false, + var hidden: Boolean = true, + var detached: Boolean = false +): Parcelable { } \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/TopActivityInfo.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/TopActivityInfo.kt index d5e415a..2dd2b58 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/TopActivityInfo.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/TopActivityInfo.kt @@ -1,36 +1,22 @@ package com.wrbug.developerhelper.commonutil.entity -import android.os.Parcel import android.os.Parcelable +import kotlinx.parcelize.Parcelize -class TopActivityInfo() : Parcelable { - var activity = "" - var viewIdHex = HashMap() +@Parcelize +class TopActivityInfo( + var packageName: String = "", + var activity: String = "", var fragments: Array? = null +): Parcelable { - constructor(parcel: Parcel) : this() { - activity = parcel.readString() - fragments = parcel.createTypedArray(FragmentInfo) - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(activity) - parcel.writeTypedArray(fragments, flags) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): TopActivityInfo { - return TopActivityInfo(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) + fun setFullActivity(ac: String) { + kotlin.runCatching { + val arr = ac.split("/") + packageName = arr[0] + activity = arr[0] + arr[1] } } - + var viewIdHex = HashMap() } \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/Callback.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/Callback.kt deleted file mode 100644 index 4bc41c9..0000000 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/Callback.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.wrbug.developerhelper.commonutil.shell - -interface Callback { - fun onSuccess(data: T) - fun onFailed(msg: String = "") {} -} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/CommandResult.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/CommandResult.kt new file mode 100644 index 0000000..4894b56 --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/CommandResult.kt @@ -0,0 +1,28 @@ +package com.wrbug.developerhelper.commonutil.shell + +data class CommandResult( + val stdout: List, + val stderr: List, + val exitCode: Int, + val details: Shell.Command.Result.Details? +) { + val isSuccessful: Boolean + get() = exitCode == 0 + + fun getStdout(): String { + return toString(stdout) + } + + /** + * Get the standard error. + * + * @return The standard error as a string. + */ + fun getStderr(): String { + return toString(stderr) + } + + private fun toString(lines: List?): String { + return lines?.joinToString("\n").orEmpty() + } +} diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/Shell.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/Shell.kt new file mode 100644 index 0000000..4841d47 --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/Shell.kt @@ -0,0 +1,826 @@ +package com.wrbug.developerhelper.commonutil.shell + +import com.wrbug.developerhelper.commonutil.toInt +import java.io.BufferedReader +import java.io.DataOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.io.OutputStream +import java.util.Collections +import java.util.UUID +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock +import java.util.regex.Pattern + +/** Environment variable. */ +typealias Variable = String +/** Environment variable value. */ +typealias Value = String +/** A [Map] for the environment variables used in the shell. */ +typealias EnvironmentMap = Map + + +/** + * A shell starts a [Process] with the provided shell and additional/optional environment variables. + * The shell handles maintaining the [Process] and reads standard output and standard error streams, + * returning stdout, stderr, and the last exit code as a [Command.Result] when a command is complete. + * + * Example usage: + * + * val sh = Shell("sh") + * val result = sh.run("echo 'Hello, World!'") + * assert(result.isSuccess) + * assert(result.stdout() == "Hello, World") + * + * @property path The path to the shell to start. + * @property environment Map of all environment variables to include with the system environment. + * Default value is an empty map. + * @throws Shell.NotFoundException If the shell cannot be opened this runtime exception is thrown. + * @author Jared Rummler (jaredrummler@gmail.com) + * @since 05-05-2021 + */ +class Shell @Throws(NotFoundException::class) @JvmOverloads constructor( + val path: String, + val environment: EnvironmentMap = emptyMap() +) { + + + /** + * Construct a new [Shell] with optional environment variable arguments as a [Pair]. + * + * @param shell The path to the shell to start. + * @param environment varargs of all environment variables as a [Pair] which are included + * with the system environment. + */ + constructor(shell: String, vararg environment: Pair) : + this(shell, environment.toEnvironmentMap()) + + /** + * Construct a new [Shell] with optional environment variable arguments as an array. + * + * @param shell The path to the shell to start. + * @param environment varargs of all environment variables as a [Pair] which are included + * with the system environment. + */ + constructor(shell: String, environment: Array) : + this(shell, environment.toEnvironmentMap()) + + /** + * Get the current state of the shell + */ + var state: State = State.Idle + private set + + private val onResultListeners = mutableSetOf() + private val onStdOutListeners = mutableSetOf() + + private val onStdErrListeners = mutableSetOf() + private val stdin: StandardInputStream + private val stdoutReader: StreamReader + private val stderrReader: StreamReader + private var watchdog: Watchdog? = null + + private val process: Process + + init { + try { + process = runWithEnv(path, environment) + stdin = StandardInputStream(process.outputStream) + stdoutReader = StreamReader.createAndStart(THREAD_NAME_STDOUT, process.inputStream) + stderrReader = StreamReader.createAndStart(THREAD_NAME_STDERR, process.errorStream) + } catch (cause: Exception) { + throw NotFoundException(String.format(EXCEPTION_SHELL_CANNOT_OPEN, path), cause) + } + } + + /** + * Add a listener that will be invoked each time a command finishes. + * + * @param listener The listener to receive callbacks when commands finish executing. + * @return This shell instance for chaining calls. + */ + fun addOnCommandResultListener(listener: OnCommandResultListener) = apply { + onResultListeners.add(listener) + } + + /** + * Remove a listener previously added to stop receiving callbacks when commands finish. + * + * @param listener The listener registered via [addOnCommandResultListener]. + * @return This shell instance for chaining calls. + */ + fun removeOnCommandResultListener(listener: OnCommandResultListener) = apply { + onResultListeners.remove(listener) + } + + /** + * Add a listener that will be invoked each time the STDOUT stream reads a new line. + * + * @param listener The listener to receive callbacks when the STDOUT stream reads a line. + * @return This shell instance for chaining calls. + */ + fun addOnStdoutLineListener(listener: OnLineListener) = apply { + onStdOutListeners.add(listener) + } + + /** + * Remove a listener previously added to stop receiving callbacks for STDOUT read lines. + * + * @param listener The listener registered via [addOnStdoutLineListener]. + * @return This shell instance for chaining calls. + */ + fun removeOnStdoutLineListener(listener: OnLineListener) = apply { + onStdOutListeners.remove(listener) + } + + /** + * Add a listener that will be invoked each time the STDERR stream reads a new line. + * + * @param listener The listener to receive callbacks when the STDERR stream reads a line. + * @return This shell instance for chaining calls. + */ + fun addOnStderrLineListener(listener: OnLineListener) = apply { + onStdErrListeners.add(listener) + } + + /** + * Remove a listener previously added to stop receiving callbacks for STDERR read lines. + * + * @param listener The listener registered via [addOnStderrLineListener]. + * @return This shell instance for chaining calls. + */ + fun removeOnStderrLineListener(listener: OnLineListener) = apply { + onStdErrListeners.remove(listener) + } + + /** + * Run a command in the current shell and return its [result][Command.Result]. + * + * @param command The command to execute. + * @param config The [options][Command.Config] to set when running the command. + * @return The [result][Command.Result] containing stdout, stderr, status of running the command. + * @throws ClosedException if the shell was closed prior to running the command. + * @see shutdown + * @see run + */ + @Throws(ClosedException::class) + @Synchronized + fun run( + command: String, + config: Command.Config.Builder.() -> Unit, + ) = run(command, Command.Config.Builder().apply(config).create()) + + /** + * Run a command in the current shell and return its [result][Command.Result]. + * + * @param command The command to execute. + * @param config The [options][Command.Config] to set when running the command. + * @return The [result][Command.Result] containing stdout, stderr, status of running the command. + * @throws ClosedException if the shell was closed prior to running the command. + * @see shutdown + * @see run + */ + @Throws(ClosedException::class) + @Synchronized + @JvmOverloads + fun run( + command: String, + config: Command.Config = Command.Config.default(), + ): Command.Result { + // If the shell is shutdown, throw a ShellClosedException. + if (state == State.Shutdown) throw ClosedException(EXCEPTION_SHELL_SHUTDOWN) + + val stdout = Collections.synchronizedList(mutableListOf()) + val stderr = Collections.synchronizedList(mutableListOf()) + + val watchdog = Watchdog().also { watchdog = it } + var exitCode = Command.Status.INVALID + val uuid = config.uuid + + val onComplete = { marker: Command.Marker -> + when (marker.uuid) { + uuid -> + try { // Reached the end of reading the stream for the command. + if (marker.status != Command.Status.INVALID) { + exitCode = marker.status + } + } finally { + watchdog.signal() + } + } + } + + val lock = ReentrantLock() + val output = Collections.synchronizedList(mutableListOf()) + + // Function to process stderr and stdout streams. + fun onLine( + buffer: MutableList, + listeners: Set, + onLine: (line: String) -> Unit, + ) = { line: String -> + try { + lock.lock() + if (config.notify) { + listeners.forEach { listener -> listener.onLine(line) } + } + buffer.add(line) + output.add(line) + onLine(line) + } finally { + lock.unlock() + } + } + + stdoutReader.onComplete = onComplete + stderrReader.onComplete = onComplete + stdoutReader.onReadLine = onLine(stdout, onStdOutListeners, config.onStdOut) + stderrReader.onReadLine = when (config.redirectStdErr) { + true -> onLine(stdout, onStdOutListeners, config.onStdOut) + else -> onLine(stderr, onStdErrListeners, config.onStdErr) + } + + val startTime = System.currentTimeMillis() + try { + state = State.Running + // Write the command and command end marker to stdin. + write(command, "echo '$uuid' $?", "echo '$uuid'>&2") + // Wait for the result with a timeout, if provided. + if (!watchdog.await(config.timeout)) { + exitCode = Command.Status.TIMEOUT + config.onTimeout() + } + } catch (e: InterruptedException) { + exitCode = Command.Status.TERMINATED + config.onCancelled() + } finally { + this.watchdog = null + state = State.Idle + } + + if (exitCode != Command.Status.SUCCESS) { + // Exit with the error code in a subshell + // This is necessary because we send commands to signal a command was completed + write("$(exit $exitCode)") + } + + // Create the result from running the command. + val result = Command.Result.create( + uuid, + command, + stdout, + stderr, + output, + exitCode, + startTime + ) + + if (config.notify) { + onResultListeners.forEach { listener -> + listener.onResult(result) + } + } + + return result + } + + /** + * Check if the shell is idle. + * + * @return True if the shell is open but not running any commands. + */ + fun isIdle() = state is State.Idle + + /** + * Check if the shell is running a command. + * + * @return True if the shell is executing a command. + */ + fun isRunning() = state is State.Running + + /** + * Check if the shell is shutdown. + * + * @return True if the shell is closed. + * @see shutdown + */ + fun isShutdown() = state is State.Shutdown + + /** + * Check if the shell is alive and able to execute commands. + * + * @return True if the shell is running or idle. + */ + fun isAlive() = try { + process.exitValue(); false + } catch (e: IllegalThreadStateException) { + true + } + + /** + * Interrupt waiting for a command to complete. + */ + fun interrupt() { + watchdog?.abort() + } + + /** + * Shutdown the shell instance. After a shell is shutdown it can no longer execute commands + * and should be garbage collected. + */ + @Synchronized + fun shutdown() { + try { + write("exit") + process.waitFor() + stdin.closeQuietly() + onStdOutListeners.clear() + onStdErrListeners.clear() + stdoutReader.join() + stderrReader.join() + process.destroy() + } catch (ignored: IOException) { + } finally { + state = State.Shutdown + } + } + + private fun write(vararg commands: String) = try { + commands.forEach { command -> stdin.write(command) } + stdin.flush() + } catch (ignored: IOException) { + } + + private fun DataOutputStream.closeQuietly() = try { + close() + } catch (ignored: IOException) { + } + + /** + * Contains data classes used for running commands in a [Shell]. + * + * @see Command.Result + * @see Command.Config + * @see Command.Status + */ + object Command { + + /** + * The result of running a command in a shell. + * + * @property stdout A list of lines read from the standard input stream. + * @property stderr A list of lines read from the standard error stream. + * @property exitCode The status code of running the command. + * @property details Additional command result details. + */ + data class Result( + val stdout: List, + val stderr: List, + val output: List, + val exitCode: Int, + val details: Details? + ) { + + /** + * True when the exit code is equal to 0. + */ + val isSuccess: Boolean get() = exitCode == Status.SUCCESS + + /** + * Get [stdout] and [stderr] as a string, separated by new lines. + * + * @return The output of running the command in a shell. + */ + fun output(): String = output.joinToString("\n") + + /** + * Get [stdout] as a string, separated by new lines. + * + * @return The standard ouput string. + */ + fun stdout(): String = stdout.joinToString("\n") + + /** + * Get [stdout] as a string, separated by new lines. + * + * @return The standard ouput string. + */ + fun stderr(): String = stderr.joinToString("\n") + + /** + * Additional details pertaining to running a command in a shell. + * + * @property uuid The unique identifier associated with the command. + * @property command The command sent to the shell to execute. + * @property startTime The time—in milliseconds since January 1, 1970, 00:00:00 GMT—when + * the command started execution. + * @property endTime The time—in milliseconds since January 1, 1970, 00:00:00 GMT—when + * the command completed execution. + * @property elapsed The number of milliseconds it took to execute the command. + */ + data class Details internal constructor( + val uuid: UUID, + val command: String, + val startTime: Long, + val endTime: Long, + val elapsed: Long = endTime - startTime + ) + + companion object { + internal fun create( + uuid: UUID, + command: String, + stdout: List, + stderr: List, + output: List, + exitCode: Int, + startTime: Long, + endTime: Long = System.currentTimeMillis(), + ) = Result( + stdout, + stderr, + output, + exitCode, + Details(uuid, command, startTime, endTime) + ) + } + } + + /** + * Optional configuration settings when running a command in a [shell][Shell]. + * + * @property uuid The unique identifier associated with the command. + * @property redirectStdErr True to redirect STDERR to STDOUT. + * @property onStdOut Callback that is invoked when reading a line from stdout. + * @property onStdErr Callback that is invoked when reading a line from stderr. + * @property onCancelled Callback that is invoked when the command is interrupted. + * @property onTimeout Callback that is invoked when the command timed-out. + * @property timeout The time to wait before killing the command. + * @property notify True to notify any [OnLineListener] and [OnCommandResultListener] of the command. + */ + class Config private constructor( + val uuid: UUID = UUID.randomUUID(), + val redirectStdErr: Boolean = false, + val onStdOut: (line: String) -> Unit = {}, + val onStdErr: (line: String) -> Unit = {}, + val onCancelled: () -> Unit = {}, + val onTimeout: () -> Unit = {}, + val timeout: Timeout? = null, + val notify: Boolean = true + ) { + + /** + * Optional configuration settings when running a command in a [shell][Shell]. + * + * @property uuid The unique identifier associated with the command. + * @property redirectErrorStream True to redirect STDERR to STDOUT. + * @property onStdOut Callback that is invoked when reading a line from stdout. + * @property onStdErr Callback that is invoked when reading a line from stderr. + * @property onCancelled Callback that is invoked when the command is interrupted. + * @property onTimeout Callback that is invoked when the command timed-out. + * @property timeout The time to wait before killing the command. + * @property notify True to notify any [OnLineListener] and [OnCommandResultListener] of the command. + */ + class Builder { + var uuid: UUID = UUID.randomUUID() + var redirectErrorStream = false + var onStdOut: (line: String) -> Unit = {} + var onStdErr: (line: String) -> Unit = {} + var onCancelled: () -> Unit = {} + var onTimeout: () -> Unit = {} + var timeout: Timeout? = null + var notify = true + + /** + * Create the [Config] from this builder. + * + * @return A new [Config] for a command. + */ + fun create() = + Config( + uuid, + redirectErrorStream, + onStdOut, + onStdErr, + onCancelled, + onTimeout, + timeout, + notify + ) + } + + companion object { + + /** + * The default configuration for running a command in a shell. + * + * @return The default config. + */ + fun default(): Config = Builder().create() + + /** + * Config that doesn't invoke callbacks for line and command complete listeners. + */ + fun silent(): Config = Builder().apply { notify = false }.create() + } + } + + /** + * The command marker to process standard input/error streams. + * + * @property uuid The unique ID for a command. + * @property status the exit code for the last run command. + */ + internal data class Marker(val uuid: UUID, val status: Int) + + /** Exit codes */ + object Status { + /** OK exit code value */ + const val SUCCESS = 0 + + /** Command timeout exit status */ + const val TIMEOUT = 124 + + /** Command failed exit status */ + const val COMMAND_FAILED = 125 + + /** Command not executable exit status */ + const val NOT_EXECUTABLE = 126 + + /** Command not found exit status */ + const val NOT_FOUND = 127 + + /** Command terminated exit status. */ + const val TERMINATED = 128 + 30 + internal const val INVALID = 0x100 + } + } + + /** + * Interface to receive a callback when reading a line from standard output/error streams. + */ + interface OnLineListener { + + /** + * Called when a line was read from standard output/error streams + * + * @param line The string that was read. + */ + fun onLine(line: String) + } + + /** + * Interface to receive a callback when a command completes. + */ + interface OnCommandResultListener { + + /** + * Called when a command finishes running. + * + * @param result The result of running the command in a shell. + */ + fun onResult(result: Command.Result) + } + + /** + * A timeout used when running a command in a shell. + * + * @property value The value of the time based on the [unit]. + * @property unit The time unit for the [value]. + */ + data class Timeout(val value: Long, val unit: TimeUnit) + + /** + * The exception thrown when a command is passed to a closed shell. + */ + class ClosedException(message: String) : IOException(message) + + /** + * The exception thrown when the shell could not be opened. + */ + class NotFoundException(message: String, cause: Throwable) : RuntimeException(message, cause) + + /** + * Represents the possible states of the shell. + */ + sealed class State { + /** The shell is idle; no commands are in progress. */ + object Idle : State() + + /** The shell is currently running a command. */ + object Running : State() + + /** The shell has been shutdown. */ + object Shutdown : State() + } + + /** + * A class to cause the current thread to wait until a command completes or is aborted. + */ + private class Watchdog : CountDownLatch(STREAM_READER_COUNT) { + + private var aborted = false + + /** + * Releases the thread immediately instead of waiting for [signal] to be invoked twice. + */ + fun abort() { + if (count == 0L) return + aborted = true + while (count> 0) countDown() + } + + /** + * Signal that either standard output or standard input streams are finished processing. + */ + fun signal() = countDown() + + /** + * Causes the current thread to wait until [signal] is called twice. + * + * @param timeout The maximum time to wait before [AbortedException] is thrown. + * @throws AbortedException if the timeout completes before [signal] is called twice + * or if the thread is interrupted. + */ + @Throws(AbortedException::class) + fun await(timeout: Timeout?): Boolean { + return when (timeout) { + null -> { + await(); true + } + + else -> await(timeout.value, timeout.unit) + } + } + + override fun await() = super.await().also { + if (aborted) throw AbortedException() + } + + override fun await(timeout: Long, unit: TimeUnit) = super.await(timeout, unit).also { + if (aborted) throw AbortedException() + } + + companion object { + /** + * The number of times [signal] should be called to release the latch. + */ + private const val STREAM_READER_COUNT = 2 + + /** + * The exception thrown when [abort] is called and the [CountDownLatch] has not finished + */ + class AbortedException : InterruptedException() + } + } + + /** + * The [OutputStream] for writing commands to the shell. + */ + private class StandardInputStream(stream: OutputStream) : DataOutputStream(stream) { + + /** + * The helper function to write commands to the stream with an appended new line character. + * + * @param command The command to write. + */ + fun write(command: String) = write("$command\n".toByteArray(Charsets.UTF_8)) + } + + /** + * A thread that parses the standard/error streams for the shell. + * + * @param name The name of the stream. One of: [THREAD_NAME_STDOUT], [THREAD_NAME_STDERR] + * @param stream Either the [Process.getInputStream] or [Process.getErrorStream] + */ + private class StreamReader private constructor( + name: String, + private val stream: InputStream + ) : Thread(name) { + + /** + * The lambda that is invoked when a line is read from the stream. + */ + var onReadLine: (line: String) -> Unit = {} + + /** + * The lambda that is invoked when a command completes. + */ + var onComplete: (marker: Command.Marker) -> Unit = {} + + override fun run() = BufferedReader(InputStreamReader(stream)).forEachLine { line -> + pattern.matcher(line).let { matcher -> + if (matcher.matches()) { + val uuid = UUID.fromString(matcher.group(GROUP_UUID)) + onComplete( + when (val exitCode = matcher.group(GROUP_CODE)) { + null -> Command.Marker(uuid, Command.Status.INVALID) + else -> Command.Marker(uuid, exitCode.toInt()) + } + ) + } else { + onReadLine(line) + } + } + } + + companion object { + + private const val GROUP_UUID = 1 + + private const val GROUP_CODE = 2 + + // + private val pattern: Pattern = Pattern.compile( + "^([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\\s?([0-9]{1,3})?$", + Pattern.CASE_INSENSITIVE + ) + + internal fun createAndStart(name: String, stream: InputStream) = + StreamReader(name, stream).also { reader -> reader.start() } + } + } + + companion object { + + private const val THREAD_NAME_STDOUT = "STDOUT" + private const val THREAD_NAME_STDERR = "STDERR" + + private const val EXCEPTION_SHELL_CANNOT_OPEN = "Error opening shell: '%s'" + private const val EXCEPTION_SHELL_SHUTDOWN = "The shell is shutdown" + private val instances by lazy { mutableMapOf() } + + /** + * Returns a [Shell] instance using the [path] as the path to the shell/executable.\ + */ + operator fun get(path: String): Shell = instances[path]?.takeIf { shell -> + shell.isAlive() + } ?: Shell(path).also { shell -> + instances[path] = shell + } + + /** The Bourne shell (sh) */ + val SH: Shell get() = this["sh"] + + /** Switch to root, and run it as a shell */ + val SU: Shell get() = this["su"] + + /** + * Execute a command with the provided environment. + * + * @param command + * The name of the program to execute. E.g. "su" or "sh". + * @param environment + * Map of all environment variables to include with the system environment. + * @return The new [Process] instance. + * @throws IOException + * If the requested program could not be executed. + */ + @Throws(IOException::class) + private fun runWithEnv(command: String, environment: EnvironmentMap): Process = + Runtime.getRuntime().exec(command, (System.getenv() + environment).toArray()) + + /** + * Convert an array to an [EnvironmentMap] with each variable/value separated by '='. + * + * @return The array converted to an [EnvironmentMap]. + */ + private fun Array.toEnvironmentMap(): EnvironmentMap = + mutableMapOf().also { map -> + forEach { str -> + str.split("=").takeIf { arr -> + arr.size == 2 + }?.let { (variable, value) -> + map[variable] = value + } + } + }.toMap() + + /** + * Convert an array of [Pair] to an [EnvironmentMap]. + * + * @return The array of variable/value pairs as a new [EnvironmentMap]. + */ + private fun Array>.toEnvironmentMap(): EnvironmentMap = + mutableMapOf().also { map -> + forEach { (variable, value) -> + map[variable] = value + } + } + + /** + * Converts an [EnvironmentMap] to an array of strings with the variable/value + * separated by an '=' character. + * + * @return An array of environment variables. + */ + private fun EnvironmentMap.toArray(): Array = + mutableListOf().also { list -> + forEach { (variable, value) -> + list.add("$variable=$value") + } + }.toTypedArray() + } +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellException.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellException.kt new file mode 100644 index 0000000..7c6f82b --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellException.kt @@ -0,0 +1,4 @@ +package com.wrbug.developerhelper.commonutil.shell + +class ShellException(message: String) : Exception(message) { +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellManager.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellManager.kt index 801eb3c..61ddb69 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellManager.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellManager.kt @@ -1,18 +1,19 @@ package com.wrbug.developerhelper.commonutil.shell -import com.jaredrummler.android.shell.CommandResult -import com.wrbug.developerhelper.commonutil.CommonUtils -import com.wrbug.developerhelper.commonutil.ShellUtils -import com.wrbug.developerhelper.commonutil.entity.FragmentInfo +import com.wrbug.developerhelper.commonutil.Constant import com.wrbug.developerhelper.commonutil.entity.LsFileInfo import com.wrbug.developerhelper.commonutil.entity.TopActivityInfo +import com.wrbug.developerhelper.commonutil.runOnIO +import io.reactivex.rxjava3.core.Single import java.io.File import java.util.regex.Pattern - object ShellManager { + private const val SHELL_TOP_ACTIVITY = "dumpsys activity top" - private const val SHELL_PROCESS_PID_1 = "ps -ef | grep \"%1\$s\" | grep -v %1\$s:| grep -v grep | awk '{print \2ドル}'" + private const val SHELL_APP_ACTIVITY = "dumpsys activity %1\$s" + private const val SHELL_PROCESS_PID_1 = + "ps -ef | grep \"%1\$s\" | grep -v %1\$s:| grep -v grep | awk '{print \2ドル}'" private const val SHELL_PROCESS_PID_2 = "top -b -n 1 |grep %1\$s |grep -v grep|grep -v %1\$s:" private const val SHELL_PROCESS_PID_3 = "top -n 1 |grep %1\$s |grep -v grep|grep -v %1\$s:" private var SHELL_OPEN_ACCESSiBILITY_SERVICE = arrayOf( @@ -20,27 +21,24 @@ object ShellManager { "settings put secure accessibility_enabled 1" ) private const val SHELL_LS_FILE = "ls -l %1\$s" - private const val SHELL_GET_ZIP_FILE_LIST = - "app_process -Djava.class.path=/data/local/tmp/zip.dex /data/local/tmp Zip %s" private const val SHELL_CHECK_IS_SQLITE = "od -An -tx %1\$s |grep '694c5153'" private const val SHELL_UNINSTALL_APP = "pm uninstall %1\$s" private const val SHELL_CLEAR_APP_DATA = "pm clear %1\$s" private const val SHELL_FORCE_STOP_APP = "am force-stop %1\$s" - private val SHELL_OPEN_ADB_WIFI = arrayOf("setprop service.adb.tcp.port 5555", "stop adbd", "start adbd") - fun getTopActivity(callback: Callback) { - ShellUtils.runWithSu(arrayOf(SHELL_TOP_ACTIVITY), object : ShellUtils.ShellResultCallback() { - override fun onComplete(result: CommandResult) { - callback.onSuccess(getTopActivity(result)) - } + private val SHELL_OPEN_ADB_WIFI = + arrayOf("setprop service.adb.tcp.port 5555", "stop adbd", "start adbd") - override fun onError(msg: String) { - callback.onSuccess(null) - } - }) + fun getTopActivity(): Single { + return ShellUtils.runWithSuAsync(SHELL_TOP_ACTIVITY, useBusyBox = false).map { + getTopActivity(it) + }.runOnIO() + } + private fun tabCount(str: String): Int { + return (str.length - str.trimStart().length) / 2 } - fun getTopActivity(result: CommandResult): TopActivityInfo { + private fun getTopActivity(result: CommandResult): TopActivityInfo { val stdout = result.getStdout() val topActivityInfo = TopActivityInfo() val task_s = stdout.split("TASK ") @@ -50,10 +48,9 @@ object ShellManager { val pattern = Pattern.compile(regex) val matcher = pattern.matcher(task_) if (matcher.find()) { - topActivityInfo.activity = matcher.group().split(" ")[1] + topActivityInfo.setFullActivity(matcher.group().split(" ")[1]) } - val split = - task_.split("\n[ ]{4}[A-Z]".toRegex()).dropLastWhile { it.isEmpty() } + val split = task_.split("\n[ ]{4}[A-Z]".toRegex()).dropLastWhile { it.isEmpty() } for (s in split) { if (s.contains("iew Hierarchy")) { for (s1 in s.split("\n".toRegex()).dropLastWhile { it.isEmpty() }) { @@ -65,67 +62,9 @@ object ShellManager { continue } topActivityInfo.viewIdHex[split1[5].substring(split1[5].indexOf("id/"))] = - split1[4] - } - } - } else if (s.contains("ocal Activity")) { - } else if (s.contains("ctive Fragments")) { - val list = ArrayList() - val split2 = - s.split("\n {6}#[0-9]+:".toRegex()).filterNot { it.isBlank() } - for (s2 in split2) { - if (s2.contains("mFragmentId=")) { - val name = s2.trim { it <= ' ' }.substring(0, s2.indexOf("{") - 1) - val fragmentInfo = FragmentInfo() - list.add(fragmentInfo) - fragmentInfo.name = name - val split3 = - s2.replace("Child FragmentManager", "Child_FragmentManager").split(" ".toRegex()) - .dropLastWhile { it.isEmpty() } - for (s31 in split3) { - if (s31.contains("Child_FragmentManager")) { - break - } - val s3 = s31.replace("\n", "").replace(" ", "") - when { - s3.startsWith("mFragmentId=") -> fragmentInfo.fragmentId = - s3.replace("mFragmentId=", "") - s3.startsWith("mContainerId=") -> fragmentInfo.containerId = - s3.replace("mContainerId=", "") - s3.startsWith("mTag=") -> fragmentInfo.tag = s3.replace("mTag=", "") - s3.startsWith("mState=") -> fragmentInfo.state = - s3.replace("mState=", "").toInt() - s3.startsWith("mIndex=") -> fragmentInfo.index = - s3.replace("mIndex=", "").toInt() - s3.startsWith("mWho=") -> fragmentInfo.who = s3.replace("mWho=", "") - s3.startsWith("mBackStackNesting=") -> fragmentInfo.backStackNesting = - s3.replace("mBackStackNesting=", "").toInt() - s3.startsWith("mAdded=") -> fragmentInfo.added = s3.replace("mAdded=", "") == - "true" - s3.startsWith("mRemoving=") -> fragmentInfo.removing = s3.replace( - "mRemoving=", - "" - ) == "true" - s3.startsWith("mFromLayout=") -> fragmentInfo.fromLayout = s3.replace( - "mFromLayout=", - "" - ) == - "true" - s3.startsWith("mInLayout=") -> fragmentInfo.inLayout = s3.replace( - "mInLayout=", - "" - ) == "true" - s3.startsWith("mHidden=") -> fragmentInfo.hidden = s3.replace("mHidden=", "") == - "true" - s3.startsWith("mDetached=") -> fragmentInfo.detached = s3.replace( - "mDetached=", - "" - ) == "true" - } - } + split1[4] } } - topActivityInfo.fragments = list.toTypedArray() } } break @@ -151,13 +90,23 @@ object ShellManager { } fun getPid(packageName: String): String { - var result: CommandResult = ShellUtils.runWithSu(String.format(SHELL_PROCESS_PID_1, packageName)) + var result: CommandResult = + ShellUtils.runWithSu( + String.format(SHELL_PROCESS_PID_1, packageName), + useBusyBox = false + ) if (result.isSuccessful) { return result.getStdout() } - result = ShellUtils.runWithSu(String.format(SHELL_PROCESS_PID_2, packageName)) + result = ShellUtils.runWithSu( + String.format(SHELL_PROCESS_PID_2, packageName), + useBusyBox = false + ) if (result.isSuccessful.not()) { - result = ShellUtils.runWithSu(String.format(SHELL_PROCESS_PID_3, packageName)) + result = ShellUtils.runWithSu( + String.format(SHELL_PROCESS_PID_3, packageName), + useBusyBox = false + ) } if (result.isSuccessful.not()) { return "" @@ -166,7 +115,11 @@ object ShellManager { } fun getSqliteFiles(packageName: String): Array { - val dbPath = "/data/data/$packageName/databases" + return getSqliteFiles(Constant.dataDir, packageName) + } + + private fun getSqliteFiles(dir: String, packageName: String): Array { + val dbPath = "$dir/$packageName/databases" val list = lsDir(dbPath) val files = ArrayList() for (file in list) { @@ -182,91 +135,85 @@ object ShellManager { return files.toTypedArray() } - fun openAccessibilityService(callback: Callback? = null) { - ShellUtils.runWithSu(SHELL_OPEN_ACCESSiBILITY_SERVICE, object : ShellUtils.ShellResultCallback() { - override fun onComplete(result: CommandResult) { - callback?.onSuccess(result.isSuccessful && result.getStdout().isEmpty()) - } - - override fun onError(msg: String) { - callback?.onSuccess(false) - } - }) + fun openAccessibilityService(): Single { + return ShellUtils.runWithSuAsync(*SHELL_OPEN_ACCESSiBILITY_SERVICE, useBusyBox = false) + .map { + it.isSuccessful && it.getStdout().isEmpty() + }.onErrorReturn { false }.runOnIO() } fun catFile(filaPath: String): String { - val commandResult = ShellUtils.runWithSu("cat $filaPath") - return commandResult.getStdout() + val result = ShellUtils.runWithSu("cat $filaPath") + return result.getStdout() } fun rmFile(file: String): Boolean { - val commandResult = ShellUtils.runWithSu("rm -rf $file") - return commandResult.isSuccessful + val result = ShellUtils.runWithSu("rm -rf $file") + return result.isSuccessful } fun modifyFile(filaPath: String, content: String): Boolean { - val commandResult = ShellUtils.runWithSu("echo $content>> $filaPath") - return commandResult.isSuccessful + val result = ShellUtils.runWithSu("echo $content>> $filaPath") + return result.isSuccessful } fun cpFile(source: String, dst: String, mod: String = "666"): Boolean { val dir = dst.substring(0, dst.lastIndexOf("/")) - var commandResult = ShellUtils.runWithSu("mkdir -p $dir") - if (commandResult.isSuccessful.not()) { + var result = ShellUtils.runWithSu("mkdir -p $dir") + if (result.isSuccessful.not()) { return false } - commandResult = ShellUtils.runWithSu("cp -R $source $dst && chmod $mod $dst") - return commandResult.isSuccessful || commandResult.getStderr()?.contains("Operation not permitted") ?: false + result = ShellUtils.runWithSu("cp -R $source $dst && chmod $mod $dst") + return result.isSuccessful || result.getStderr() + .contains("Operation not permitted") } + fun tarCF(tarPath: String, srcPath: String): Boolean { + val dir = tarPath.substring(0, tarPath.lastIndexOf("/")) + var result = ShellUtils.runWithSu("mkdir -p $dir") + if (result.isSuccessful.not()) { + return false + } + result = ShellUtils.runWithSu("tar -pcf $tarPath -C $srcPath .") + return result.isSuccessful || result.getStderr() + .contains("Operation not permitted") + } + + fun catFile(source: String, dst: String, mod: String? = null): Boolean { val cmds = arrayListOf() cmds.add("cat $source> $dst") if (mod != null) { cmds.add("chmod $mod $dst") } - val commandResult = ShellUtils.runWithSu(*(cmds.toTypedArray())) - return commandResult.isSuccessful - } - - fun getZipFileList(path: String): List { - val file = File(CommonUtils.application.cacheDir, "zip.dex") - if (file.exists()) { - ShellUtils.runWithSu("cp ${file.absolutePath} /data/local/tmp", "rm -rf ${file.absolutePath}") - } - val commandResult = ShellUtils.runWithSu(String.format(SHELL_GET_ZIP_FILE_LIST, path)) - return commandResult.stdout + val result = ShellUtils.runWithSu(*(cmds.toTypedArray())) + return result.isSuccessful } fun lsDir(path: String): List { - val commandResult = ShellUtils.runWithSu("ls $path") - return commandResult.stdout - } - - fun findApkDir(packageName: String): String { - val cmd = "ls /data/app/|grep $packageName" - val dir = ShellUtils.runWithSu(cmd).getStdout() - return "/data/app/$dir/base.apk" - } - - fun uninstallApp(packageName: String): Boolean { - val commandResult = ShellUtils.runWithSu(String.format(SHELL_UNINSTALL_APP, packageName)) - return commandResult.isSuccessful + val result = ShellUtils.runWithSu("ls $path") + return result.stdout } fun clearAppData(packageName: String): Boolean { - val commandResult = ShellUtils.runWithSu(String.format(SHELL_CLEAR_APP_DATA, packageName)) - return commandResult.isSuccessful + val result = ShellUtils.runWithSu( + String.format(SHELL_CLEAR_APP_DATA, packageName), + useBusyBox = false + ) + return result.isSuccessful } fun forceStopApp(packageName: String): Boolean { - val commandResult = ShellUtils.runWithSu(String.format(SHELL_FORCE_STOP_APP, packageName)) - return commandResult.isSuccessful + val result = ShellUtils.runWithSu( + String.format(SHELL_FORCE_STOP_APP, packageName), + useBusyBox = false + ) + return result.isSuccessful } fun openAdbWifi(): Boolean { - val commandResult = ShellUtils.runWithSu(*SHELL_OPEN_ADB_WIFI) - return commandResult.isSuccessful +// val result = ShellUtils.runWithSu(*SHELL_OPEN_ADB_WIFI) + return false } } diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellUtils.kt new file mode 100644 index 0000000..10def5f --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellUtils.kt @@ -0,0 +1,84 @@ +package com.wrbug.developerhelper.commonutil.shell + +import android.util.Log +import com.wrbug.developerhelper.commonutil.CommonUtils +import com.wrbug.developerhelper.commonutil.RootUtils +import io.reactivex.rxjava3.core.Single +import java.io.File + +object ShellUtils { + private const val TAG = "ShellUtils" + private const val AVAILABLE_TEST_COMMANDS = "echo -BOC- \n id" + val busyBoxFile by lazy { + File(CommonUtils.application.cacheDir, "busybox") + } + + fun run(cmd: String, useBusyBox: Boolean = true): CommandResult { + return Shell.SH.run(appendBusyBox(useBusyBox, cmd)).convert() + } + + fun runWithSuAsync(vararg cmds: String, useBusyBox: Boolean = true): Single { + return Single.just(cmds).map { + if (!RootUtils.isRoot()) { + throw ShellException("未开启root权限") + } + Shell.SU.run(appendBusyBox(useBusyBox, *cmds)).convert() + } + } + + fun runWithSu(vararg cmd: String, useBusyBox: Boolean = true): CommandResult { + if (!RootUtils.isRoot()) { + return CommandResult( + emptyList(), + listOf("未开启root权限"), + -1, null + ) + } + return Shell.SU.run(appendBusyBox(useBusyBox, *cmd)).convert() + } + + private fun Shell.Command.Result.convert(): CommandResult { + return apply { + if (isSuccess.not()) { + Log.e(TAG, stderr.joinToString("\n")) + } + }.let { + CommandResult(it.stdout, it.stderr, exitCode, it.details) + } + } + + private fun appendBusyBox(useBusyBox: Boolean, vararg cmds: String): String { + if (cmds.isEmpty()) { + return "" + } + if (useBusyBox && busyBoxFile.exists() && busyBoxFile.canExecute()) { + return cmds.joinToString("\n") { busyBoxFile.absolutePath + " " + it } + } + return cmds.joinToString("\n") + } + + fun isRoot(): Boolean { + val result = Shell.SU.run(AVAILABLE_TEST_COMMANDS) + return parseAvailableResult(result.stdout, true) + } + + + private fun parseAvailableResult(stdout: List?, checkForRoot: Boolean): Boolean { + if (stdout == null) { + return false + } + // this is only one of many ways this can be done + var echoSeen = false + for (line in stdout) { + if (line.contains("uid=")) { + // id command is working, let's see if we are actually root + return !checkForRoot || line.contains("uid=0") + } else if (line.contains("-BOC-")) { + // if we end up here, at least the su command starts some kind of shell, let's hope it has root privileges - + // no way to know without additional native binaries + echoSeen = true + } + } + return echoSeen + } +} \ No newline at end of file diff --git a/commonutil/src/test/java/com/wrbug/developerhelper/commonutil/ExampleUnitTest.java b/commonutil/src/test/java/com/wrbug/developerhelper/commonutil/ExampleUnitTest.java deleted file mode 100644 index e4cabfb..0000000 --- a/commonutil/src/test/java/com/wrbug/developerhelper/commonutil/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.wrbug.developerhelper.commonutil; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/commonwidget/.gitignore b/commonwidget/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/commonwidget/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/commonwidget/build.gradle b/commonwidget/build.gradle deleted file mode 100644 index 9e215c4..0000000 --- a/commonwidget/build.gradle +++ /dev/null @@ -1,36 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion 28 - - - - defaultConfig { - minSdkVersion 21 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - -} - -dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - testImplementation 'junit:junit:4.12' - implementation project(':commonutil') - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} -repositories { - mavenCentral() -} diff --git a/commonwidget/proguard-rules.pro b/commonwidget/proguard-rules.pro deleted file mode 100644 index f1b4245..0000000 --- a/commonwidget/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/commonwidget/src/androidTest/java/com/wrbug/developerhelper/commonwidget/ExampleInstrumentedTest.java b/commonwidget/src/androidTest/java/com/wrbug/developerhelper/commonwidget/ExampleInstrumentedTest.java deleted file mode 100644 index 78375d2..0000000 --- a/commonwidget/src/androidTest/java/com/wrbug/developerhelper/commonwidget/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.wrbug.developerhelper.commonwidget; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.wrbug.developerhelper.commonwidget.test", appContext.getPackageName()); - } -} diff --git a/commonwidget/src/main/AndroidManifest.xml b/commonwidget/src/main/AndroidManifest.xml deleted file mode 100644 index 1099276..0000000 --- a/commonwidget/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - diff --git a/commonwidget/src/main/res/values/strings.xml b/commonwidget/src/main/res/values/strings.xml deleted file mode 100644 index f6c0897..0000000 --- a/commonwidget/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - commonwidget - diff --git a/commonwidget/src/test/java/com/wrbug/developerhelper/commonwidget/ExampleUnitTest.java b/commonwidget/src/test/java/com/wrbug/developerhelper/commonwidget/ExampleUnitTest.java deleted file mode 100644 index 0cf0c5a..0000000 --- a/commonwidget/src/test/java/com/wrbug/developerhelper/commonwidget/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.wrbug.developerhelper.commonwidget; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 4a5855f..02743a0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,4 +14,6 @@ org.gradle.jvmargs=-Xmx1536m # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true +kotlin.jvm.target.validation.mode = IGNORE +android.defaults.buildfeatures.buildconfig=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9a4163a..534885b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Wed Sep 11 19:29:17 CST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/ipc/build.gradle b/ipc/build.gradle index d8f586b..773da94 100644 --- a/ipc/build.gradle +++ b/ipc/build.gradle @@ -1,15 +1,16 @@ apply plugin: 'com.android.library' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' +apply from: "${rootDir}/common.gradle" android { - compileSdkVersion 28 + namespace "com.wrbug.developerhelper.ipc" + compileSdk 34 defaultConfig { minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 34 versionCode 1 versionName "1.0" @@ -28,14 +29,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - - implementation 'com.android.support:appcompat-v7:28.0.0' - testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation project(':commonutil') } -repositories { - mavenCentral() -} diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/AppXposedProcessData.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/AppXposedProcessData.kt new file mode 100644 index 0000000..59ede7f --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/AppXposedProcessData.kt @@ -0,0 +1,21 @@ +package com.wrbug.developerhelper.ipc.processshare + +import androidx.annotation.Keep +import com.wrbug.developerhelper.ipc.processshare.annotation.Url +import io.reactivex.rxjava3.core.Observable + +/** + * + * class: AppXposedProcessData.kt + * author: wrbug + * date: 2020年05月14日 + * description: + * + */ +@Keep +interface AppXposedProcessData : ProcessData { + fun setAppXposedStatus(map: Map) + + fun setAppXposedStatus(packageName: String, open: Boolean) + fun getAppXposedStatus(): Observable
    > +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/DataFinderListProcessData.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/DataFinderListProcessData.kt new file mode 100644 index 0000000..16d5a74 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/DataFinderListProcessData.kt @@ -0,0 +1,14 @@ +package com.wrbug.developerhelper.ipc.processshare + +import androidx.annotation.Keep +import com.wrbug.developerhelper.ipc.processshare.annotation.Url +import io.reactivex.rxjava3.core.Observable + +@Keep +interface DataFinderListProcessData : ProcessData { + + fun setData(map: Map) + + fun getData(): Observable
      > + +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/DefaultValue.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/DefaultValue.kt index 7ac1000..3430e1d 100644 --- a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/DefaultValue.kt +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/DefaultValue.kt @@ -1,8 +1,5 @@ package com.wrbug.developerhelper.ipc.processshare -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - @Target(AnnotationTarget.FIELD,AnnotationTarget.FUNCTION) -@Retention(RetentionPolicy.RUNTIME) +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) annotation class DefaultValue(val value: String) diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/DumpDexListProcessData.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/DumpDexListProcessData.kt index ca2b91d..96c4321 100644 --- a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/DumpDexListProcessData.kt +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/DumpDexListProcessData.kt @@ -1,8 +1,14 @@ package com.wrbug.developerhelper.ipc.processshare +import androidx.annotation.Keep +import com.wrbug.developerhelper.ipc.processshare.annotation.Url +import io.reactivex.rxjava3.core.Observable + +@Keep interface DumpDexListProcessData : ProcessData { - fun setData(list: List) + fun setData(map: Map) + + fun getData(): Observable
        > - fun getData(): ArrayList? } \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/FileProcessData.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/FileProcessData.kt new file mode 100644 index 0000000..1fbfbf4 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/FileProcessData.kt @@ -0,0 +1,24 @@ +package com.wrbug.developerhelper.ipc.processshare + +import androidx.annotation.Keep +import com.wrbug.developerhelper.ipc.processshare.annotation.Url +import io.reactivex.rxjava3.core.Observable + +/** + * + * class: FileProcessData.kt + * author: wrbug + * date: 2020年05月19日 + * description: + * + */ +@Keep +interface FileProcessData : ProcessData { + + @Url(TcpUrl.FileProcessDataUrl.GET_DATA_FINDER_ZIP_FILE) + fun getDataFinderZipFile(): Observable + + + @Url(TcpUrl.FileProcessDataUrl.GET_DUMP_SO_ZIP_FILE) + fun getDumpSoZipFile(): Observable +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/GlobalConfigProcessData.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/GlobalConfigProcessData.kt index fd3941d..6be6ee0 100644 --- a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/GlobalConfigProcessData.kt +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/GlobalConfigProcessData.kt @@ -1,8 +1,12 @@ package com.wrbug.developerhelper.ipc.processshare +import androidx.annotation.Keep +import com.wrbug.developerhelper.ipc.processshare.annotation.Url +import io.reactivex.rxjava3.core.Observable + +@Keep interface GlobalConfigProcessData : ProcessData { - @DefaultValue("true") - fun isXposedOpen(): Boolean + fun isXposedOpen(): Observable fun setXposedOpen(open: Boolean) } diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/MMapProcessDataInvocationHandler.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/MMapProcessDataInvocationHandler.kt new file mode 100644 index 0000000..ebf1804 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/MMapProcessDataInvocationHandler.kt @@ -0,0 +1,32 @@ +package com.wrbug.developerhelper.ipc.processshare + +import com.wrbug.developerhelper.commonutil.toJson +import com.wrbug.developerhelper.ipc.processshare.annotation.Url +import com.wrbug.developerhelper.ipc.processshare.tcp.TcpManager +import io.reactivex.rxjava3.core.Observable +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method + +class MMapProcessDataInvocationHandler : InvocationHandler { + override fun invoke(proxy: Any?, method: Method?, args: Array?): Any? { + method?.let { + val annotation = it.getAnnotation(Url::class.java) ?: throw Exception("@Url") + val url = annotation.value + val message: String = if (args.isNullOrEmpty()) { + "" + } else { + when (val data = args[0]) { + is Boolean, Int, Long, String, Float, Double -> data.toString() + else -> data.toJson() ?: "" + } + } + val result = TcpManager.sendMessage(url, message) + if (method.returnType == Observable::class.java) { + return result + } else { + result.subscribe({}, {}) + } + } + return null + } +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/ProcessDataManager.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/ProcessDataCreator.kt similarity index 63% rename from ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/ProcessDataManager.kt rename to ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/ProcessDataCreator.kt index 621dc88..5c2f834 100644 --- a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/ProcessDataManager.kt +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/ProcessDataCreator.kt @@ -2,11 +2,11 @@ package com.wrbug.developerhelper.ipc.processshare -import androidx.collection.ArrayMap +import com.wrbug.developerhelper.ipc.processshare.data.IpcFileDataManager import java.lang.reflect.Proxy -object ProcessDataManager { - private val map = ArrayMap, Any>() +object ProcessDataCreator { + private val map = hashMapOf, Any>() fun get(clazz: Class): T { if (map.containsKey(clazz)) { return map[clazz] as T @@ -17,9 +17,13 @@ object ProcessDataManager { } private fun obtainImpl(clazz: Class): T { + val service = IpcFileDataManager.getService(clazz) + if (service != null) { + return service + } val instance = Proxy.newProxyInstance( clazz.classLoader, arrayOf(clazz), - ProcessDataInvocationHandler(clazz) + ProcessDataInvocationHandler() ) return instance as T } diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/ProcessDataInvocationHandler.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/ProcessDataInvocationHandler.kt index 518f28d..bf9d62c 100644 --- a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/ProcessDataInvocationHandler.kt +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/ProcessDataInvocationHandler.kt @@ -1,92 +1,32 @@ package com.wrbug.developerhelper.ipc.processshare -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import com.jaredrummler.android.shell.Shell -import com.wrbug.developerhelper.commonutil.Base64 -import com.wrbug.developerhelper.commonutil.FileUtils -import java.io.File +import com.wrbug.developerhelper.commonutil.toJson +import com.wrbug.developerhelper.ipc.processshare.annotation.Url +import com.wrbug.developerhelper.ipc.processshare.tcp.TcpManager +import io.reactivex.rxjava3.core.Observable import java.lang.reflect.InvocationHandler import java.lang.reflect.Method -class ProcessDataInvocationHandler(val clazz: Class<*>) : InvocationHandler { - companion object { - private val gson = Gson() - } - +class ProcessDataInvocationHandler : InvocationHandler { override fun invoke(proxy: Any?, method: Method?, args: Array?): Any? { method?.let { - if (it.name.startsWith("set") && args != null && args.size == 1) { - setValue(it.name, args[0]) - } else if (it.name.startsWith("get")) { - return getValue(it, 3) - } else if (it.name.startsWith("is")) { - return getValue(it, 2) + val annotation = it.getAnnotation(Url::class.java) ?: throw Exception("@Url") + val url = annotation.value + val message: String = if (args.isNullOrEmpty()) { + "" + } else { + when (val data = args[0]) { + is Boolean, Int, Long, String, Float, Double -> data.toString() + else -> data.toJson() ?: "" + } + } + val result = TcpManager.sendMessage(url, message) + if (method.returnType == Observable::class.java) { + return result + } else { + result.subscribe({}, {}) } } return null } - - private fun getValue(method: Method, prefixLen: Int): Any? = with(method) { - val key = name.substring(prefixLen) - val data: Any = getDataFromFile()[key] ?: return defaultValue(method) - val value = when (method.returnType) { - Boolean::class.java, - Int::class.java, - Long::class.java, - String::class.java, - Float::class.java, - Double::class.java -> data - else -> gson.fromJson(data.toString(), method.returnType) - } - if (value != null) { - return value - } - return defaultValue(method) - } - - private fun defaultValue(method: Method): Any? { - val annotation = method.getAnnotation(DefaultValue::class.java) ?: return null - if (method.returnType == String::class.java) { - return annotation.value - } - return when (method.returnType) { - Boolean::class.java -> annotation.value.toBoolean() - Int::class.java -> annotation.value.toInt() - Long::class.java -> annotation.value.toLong() - Float::class.java -> annotation.value.toFloat() - Double::class.java -> annotation.value.toDouble() - else -> null - } - - } - - private fun setValue(name: String, data: Any) { - val key = name.substring(3) - val map = getDataFromFile() - when (data) { - is Boolean, Int, Long, String, Float, Double -> map[key] = data - else -> map[key] = gson.toJson(data) - } - saveData(map) - } - - private fun saveData(map: HashMap) { - val file = File("/data/local/tmp/developerHelper", "${clazz.name}.txt") - val json = Gson().toJson(map) - val data = Base64.encodeAsString(json.toByteArray()) - val commandResult = - Shell.SU.run("echo $data> ${file.absolutePath} && chmod 777 ${file.absolutePath}") - } - - private fun getDataFromFile(): HashMap { - try { - val file = File("/data/local/tmp/developerHelper", "${clazz.name}.txt") - val json = Base64.decode(FileUtils.readFile(file).replace("\n", "")) - return Gson().fromJson(json, object : TypeToken>() {}.type) - ?: return hashMapOf() - } catch (t: Throwable) { - } - return hashMapOf() - } } \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/TcpUrl.java b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/TcpUrl.java new file mode 100644 index 0000000..557830b --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/TcpUrl.java @@ -0,0 +1,9 @@ +package com.wrbug.developerhelper.ipc.processshare; + +public class TcpUrl { + public static class FileProcessDataUrl { + public static final String GET_DATA_FINDER_ZIP_FILE = "FileProcessDataUrl/getDataFinderZipFile"; + public static final String GET_DUMP_SO_ZIP_FILE = "FileProcessDataUrl/getDumpSoZipFile"; + + } +} diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/annotation/Url.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/annotation/Url.kt new file mode 100644 index 0000000..8ad1c61 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/annotation/Url.kt @@ -0,0 +1,6 @@ +package com.wrbug.developerhelper.ipc.processshare.annotation + + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Url(val value: String) diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/AppXposedProcessDataImpl.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/AppXposedProcessDataImpl.kt new file mode 100644 index 0000000..b9e57b9 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/AppXposedProcessDataImpl.kt @@ -0,0 +1,37 @@ +package com.wrbug.developerhelper.ipc.processshare.data + +import com.wrbug.developerhelper.commonutil.fromJson +import com.wrbug.developerhelper.commonutil.toJson +import com.wrbug.developerhelper.ipc.processshare.AppXposedProcessData +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.schedulers.Schedulers + +class AppXposedProcessDataImpl : AppXposedProcessData { + private val file = IpcFileDataManager.getFile("AppXposedProcessData") + private fun getMap(): Map { + return try { + file.getData().mapValues { it.value as Boolean } + } catch (t: Throwable) { + emptyMap() + } + } + + override fun setAppXposedStatus(map: Map) { + val m = HashMap(getMap()) + m.putAll(map) + file.save(m) + } + + override fun setAppXposedStatus(packageName: String, open: Boolean) { + setAppXposedStatus(mapOf(packageName to open)) + } + + override fun getAppXposedStatus(): Observable
          > { + return Observable.just(getMap()) + .subscribeOn(Schedulers.io()) + .onErrorReturn { + emptyMap() + } + } + +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/DataFinderListProcessDataImpl.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/DataFinderListProcessDataImpl.kt new file mode 100644 index 0000000..9586609 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/DataFinderListProcessDataImpl.kt @@ -0,0 +1,41 @@ +package com.wrbug.developerhelper.ipc.processshare.data + +import com.wrbug.developerhelper.commonutil.fromJson +import com.wrbug.developerhelper.commonutil.toJson +import com.wrbug.developerhelper.ipc.processshare.DataFinderListProcessData +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.schedulers.Schedulers + + +/** + * + * class: DataFinderListProcessDataImpl.kt + * author: wrbug + * date: 2020年05月19日 + * description: + * + */ +class DataFinderListProcessDataImpl : DataFinderListProcessData { + private val file = IpcFileDataManager.getFile("DataFinderListProcessData") + private fun getMap(): Map { + return try { + file.getData().mapValues { it.value as Boolean } + } catch (t: Throwable) { + emptyMap() + } + } + override fun setData(map: Map) { + val m = HashMap(getMap()) + m.putAll(map) + file.save(m) + } + + override fun getData(): Observable
            > { + return Observable.just(getMap()) + .subscribeOn(Schedulers.io()) + .onErrorReturn { + emptyMap() + } + } + +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/DumpDexListProcessDataImpl.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/DumpDexListProcessDataImpl.kt new file mode 100644 index 0000000..c81bfa7 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/DumpDexListProcessDataImpl.kt @@ -0,0 +1,43 @@ +package com.wrbug.developerhelper.ipc.processshare.data + +import com.wrbug.developerhelper.commonutil.fromJson +import com.wrbug.developerhelper.commonutil.toJson +import com.wrbug.developerhelper.ipc.processshare.DumpDexListProcessData +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.schedulers.Schedulers + + +/** + * + * class: DumpDexListProcessDataImpl.kt + * author: wrbug + * date: 2020年05月15日 + * description: + * + */ +class DumpDexListProcessDataImpl : DumpDexListProcessData { + private val file = IpcFileDataManager.getFile("DataFinderListProcessData") + private fun getMap(): Map { + return try { + file.getData().mapValues { it.value as Boolean } + } catch (t: Throwable) { + emptyMap() + } + } + + override fun setData(map: Map) { + val m = HashMap(getMap()) + m.putAll(map) + file.save(m) + } + + override fun getData(): Observable
              > { + return Observable.just(getMap()) + .subscribeOn(Schedulers.io()) + .onErrorReturn { + emptyMap() + } + } + + +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/GlobalConfigProcessDataImpl.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/GlobalConfigProcessDataImpl.kt new file mode 100644 index 0000000..f076831 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/GlobalConfigProcessDataImpl.kt @@ -0,0 +1,55 @@ +package com.wrbug.developerhelper.ipc.processshare.data + +import com.wrbug.developerhelper.commonutil.fromJson +import com.wrbug.developerhelper.commonutil.toJson +import com.wrbug.developerhelper.ipc.processshare.GlobalConfigProcessData +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.schedulers.Schedulers + + +/** + * + * class: GlobalConfigProcessDataImpl.kt + * author: wrbug + * date: 2020年05月15日 + * description: + * + */ +class GlobalConfigProcessDataImpl : GlobalConfigProcessData { + companion object { + private const val KEY_IS_XPOSED_OPEN = "isXposedOpen" + } + + private val file = IpcFileDataManager.getFile("DataFinderListProcessData") + private fun getMap(): Map { + return try { + file.getData() + } catch (t: Throwable) { + emptyMap() + } + } + + private fun setValue(key: String, value: Any) { + try { + val map = HashMap(getMap()) + map[key] = value + file.save(map) + } catch (t: Throwable) { + t.printStackTrace() + } + + } + + override fun isXposedOpen(): Observable { + return Observable.just(getMap()[KEY_IS_XPOSED_OPEN] as Boolean) + .subscribeOn(Schedulers.io()) + .onErrorReturn { + false + } + } + + override fun setXposedOpen(open: Boolean) { + setValue(KEY_IS_XPOSED_OPEN, open) + } + +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/IpcFileDataManager.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/IpcFileDataManager.kt new file mode 100644 index 0000000..84ba104 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/IpcFileDataManager.kt @@ -0,0 +1,23 @@ +package com.wrbug.developerhelper.ipc.processshare.data + +import com.wrbug.developerhelper.ipc.processshare.AppXposedProcessData +import com.wrbug.developerhelper.ipc.processshare.DataFinderListProcessData +import com.wrbug.developerhelper.ipc.processshare.DumpDexListProcessData +import com.wrbug.developerhelper.ipc.processshare.GlobalConfigProcessData +import java.io.File + +object IpcFileDataManager { + private const val PATH = "/data/local/tmp/developerhelper" + private val map = hashMapOf, Any>( + AppXposedProcessData::class.java to AppXposedProcessDataImpl(), + DataFinderListProcessData::class.java to DataFinderListProcessDataImpl(), + DumpDexListProcessData::class.java to DumpDexListProcessDataImpl(), + GlobalConfigProcessData::class.java to GlobalConfigProcessDataImpl() + ) + + fun getFile(name: String) = IpcFileInfo(File(PATH, "${name}.dat")) + fun getService(clazz: Class): T? { + return map[clazz] as? T + } +} + diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/IpcFileInfo.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/IpcFileInfo.kt new file mode 100644 index 0000000..ed6d627 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/IpcFileInfo.kt @@ -0,0 +1,41 @@ +package com.wrbug.developerhelper.ipc.processshare.data + +import com.wrbug.developerhelper.commonutil.Base64 +import com.wrbug.developerhelper.commonutil.fromJson +import com.wrbug.developerhelper.commonutil.shell.Shell +import com.wrbug.developerhelper.commonutil.toJson +import java.io.File + +class IpcFileInfo(private var file: File) { + + fun save(data: Map) { + save((data.toJson() ?: "{}").toByteArray()) + } + + fun getData(): Map { + try { + if (!file.exists()) { + return emptyMap() + } + val text = file.readText().replace("\n", "") + val data = Base64.decode(text) + return data.fromJson
                >() ?: emptyMap() + } catch (t: Throwable) { + return emptyMap() + } + } + + private fun save(data: ByteArray) { + val encode = Base64.encodeAsString(data) + if (file.exists() && file.canWrite()) { + saveFromJava(encode) + return + } + val commandResult = + Shell.SU.run("echo $encode> ${file.absolutePath} && chmod 777 ${file.absolutePath}") + } + + private fun saveFromJava(data: String) { + file.writeText(data) + } +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/AppXposedProcessDataManager.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/AppXposedProcessDataManager.kt new file mode 100644 index 0000000..17c23a0 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/AppXposedProcessDataManager.kt @@ -0,0 +1,45 @@ +package com.wrbug.developerhelper.ipc.processshare.manager + +import com.wrbug.developerhelper.ipc.processshare.AppXposedProcessData + +class AppXposedProcessDataManager private constructor() : + ProcessDataManager() { + fun setAppXposedStatusList(list: Map) { + processData.setAppXposedStatus(list) + } + + fun setAppXposedStatusList(vararg pairs: Pair) { + setAppXposedStatusList(mapOf(*pairs)) + } + + + fun getAppXposedStatusList(): Map { + return processData.getAppXposedStatus().blockingFirst() + } + + fun isAppXposedOpened(packageName: String): Boolean { + return getAppXposedStatusList()[packageName] == true + } + + fun getOpenedAppXposedList(): List { + val map = getAppXposedStatusList() + val list = ArrayList() + map.forEach { entry -> + if (entry.value) { + list.add(entry.key) + } + } + return list + } + + fun setAppXposedStatus(packageName: String, open: Boolean) { + processData.setAppXposedStatus(packageName, open) + + } + + companion object { + val instance = AppXposedProcessDataManager() + + + } +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/DataFinderListProcessDataManager.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/DataFinderListProcessDataManager.kt new file mode 100644 index 0000000..e58dee4 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/DataFinderListProcessDataManager.kt @@ -0,0 +1,38 @@ +package com.wrbug.developerhelper.ipc.processshare.manager + +import com.wrbug.developerhelper.commonutil.fromJson +import com.wrbug.developerhelper.ipc.processshare.DataFinderListProcessData +import com.wrbug.developerhelper.ipc.processshare.DumpDexListProcessData +import io.reactivex.rxjava3.core.Observable + + +/** + * + * class: DataFinderListProcessDataManager.kt + * author: wrbug + * date: 2020年05月19日 + * description: + * + */ +class DataFinderListProcessDataManager private constructor() : + ProcessDataManager() { + fun setData(map: Map) { + processData.setData(map) + } + + fun setData(vararg pairs: Pair) { + processData.setData(hashMapOf(*pairs)) + } + + fun getData(): HashMap { + return HashMap(processData.getData().blockingFirst()) + } + + fun containPackage(packageName: String): Boolean { + return getData()[packageName] ?: false + } + + companion object { + val instance = DataFinderListProcessDataManager() + } +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/DumpDexListProcessDataManager.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/DumpDexListProcessDataManager.kt new file mode 100644 index 0000000..ab10d88 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/DumpDexListProcessDataManager.kt @@ -0,0 +1,37 @@ +package com.wrbug.developerhelper.ipc.processshare.manager + +import com.wrbug.developerhelper.commonutil.fromJson +import com.wrbug.developerhelper.ipc.processshare.DumpDexListProcessData +import io.reactivex.rxjava3.core.Observable + + +/** + * + * class: DumpDexListProcessDataManager.kt + * author: wrbug + * date: 2020年05月15日 + * description: + * + */ +class DumpDexListProcessDataManager private constructor() : + ProcessDataManager() { + fun setData(map: Map) { + processData.setData(map) + } + + fun setData(vararg pairs: Pair) { + processData.setData(hashMapOf(*pairs)) + } + + fun getData(): HashMap { + return HashMap(processData.getData().blockingFirst()) + } + + fun containPackage(packageName: String): Boolean { + return getData()[packageName] ?: false + } + + companion object { + val instance = DumpDexListProcessDataManager() + } +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/FileProcessDataManager.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/FileProcessDataManager.kt new file mode 100644 index 0000000..020763f --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/FileProcessDataManager.kt @@ -0,0 +1,34 @@ +package com.wrbug.developerhelper.ipc.processshare.manager + +import com.wrbug.developerhelper.commonutil.Base64 +import com.wrbug.developerhelper.ipc.processshare.FileProcessData +import io.reactivex.rxjava3.core.Observable + + +/** + * + * class: FileProcessDataManager.kt + * author: wrbug + * date: 2020年05月19日 + * description: + * + */ +class FileProcessDataManager private constructor() : + ProcessDataManager() { + + fun getDataFinderZipFile(): Observable { + return processData.getDataFinderZipFile().map { + Base64.decode(it.toCharArray()) + } + } + + fun getDumpSoZipFile(): Observable { + return processData.getDumpSoZipFile().map { + Base64.decode(it.toCharArray()) + } + } + + companion object { + val instance = FileProcessDataManager() + } +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/GlobalConfigProcessDataManager.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/GlobalConfigProcessDataManager.kt new file mode 100644 index 0000000..c339b0e --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/GlobalConfigProcessDataManager.kt @@ -0,0 +1,29 @@ +package com.wrbug.developerhelper.ipc.processshare.manager + +import com.wrbug.developerhelper.commonutil.print +import com.wrbug.developerhelper.ipc.processshare.GlobalConfigProcessData +import io.reactivex.rxjava3.core.Observable + + +/** + * + * class: GlobalConfigProcessDataManager.kt + * author: wrbug + * date: 2020年05月14日 + * description: + * + */ +class GlobalConfigProcessDataManager private constructor() : + ProcessDataManager() { + fun isXposedOpen(): Boolean { + return processData.isXposedOpen().blockingFirst() + } + + fun setXposedOpen(open: Boolean) { + processData.setXposedOpen(open) + } + + companion object { + val instance = GlobalConfigProcessDataManager() + } +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/ProcessDataManager.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/ProcessDataManager.kt new file mode 100644 index 0000000..b387c3e --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/manager/ProcessDataManager.kt @@ -0,0 +1,23 @@ +package com.wrbug.developerhelper.ipc.processshare.manager + +import com.wrbug.developerhelper.ipc.processshare.ProcessData +import com.wrbug.developerhelper.ipc.processshare.ProcessDataCreator +import java.lang.reflect.ParameterizedType + + +/** + * + * class: ProcessDataCreator.kt + * author: wrbug + * date: 2020年05月14日 + * description: + * + */ +open class ProcessDataManager { + + + protected val processData: T by lazy { + val type = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] + ProcessDataCreator.get(type as Class) + } +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/MessageHandler.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/MessageHandler.kt new file mode 100644 index 0000000..c9f4fb3 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/MessageHandler.kt @@ -0,0 +1,5 @@ +package com.wrbug.developerhelper.ipc.processshare.tcp + +interface MessageHandler { + fun handle(action: String, message: String): String +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/MessageReceiver.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/MessageReceiver.kt new file mode 100644 index 0000000..3344e9d --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/MessageReceiver.kt @@ -0,0 +1,5 @@ +package com.wrbug.developerhelper.ipc.processshare.tcp + +interface MessageReceiver { + fun onReceived(message: String) +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/TCPClient.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/TCPClient.kt new file mode 100644 index 0000000..0b016b0 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/TCPClient.kt @@ -0,0 +1,21 @@ +import java.io.BufferedReader +import java.io.DataOutputStream +import java.io.InputStreamReader +import java.net.Socket + +object TCPClient { + private var clientSocket: Socket? = null + @Throws(Exception::class) + @JvmStatic + fun main(args: Array) { + println("请输入要转换的字符串:") + clientSocket = Socket("localhost", 23456) + val outToServer = DataOutputStream(clientSocket!!.getOutputStream()) + val inFromServer = BufferedReader(InputStreamReader(clientSocket!!.getInputStream())) + for (i in 5 downTo 1) { + outToServer.writeBytes("123456" + '\n') + println("FROM SERVER:" + inFromServer.readLine()) + } + clientSocket!!.close() + } +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/TCPServer.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/TCPServer.kt new file mode 100644 index 0000000..8342bec --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/TCPServer.kt @@ -0,0 +1,253 @@ +package com.wrbug.developerhelper.ipc.processshare.tcp + +import com.wrbug.developerhelper.commonutil.print +import org.jetbrains.anko.doAsync +import java.io.* +import java.net.InetAddress +import java.net.ServerSocket +import java.net.Socket +import java.net.SocketAddress +import java.util.* + +class TCPServer { + private var mMessageListener: OnMessageReceived? = null + private var mConnectListener: OnConnect? = null + private var mDisconnectListener: OnDisconnect? = null + private var mServerClosedListener: OnServerClose? = null + private var mServerStartListener: OnServerStart? = null + private var serverSocket: ServerSocket? = null + private var lastClientIndex: Short = 0 + private val clients: MutableMap = HashMap() + var isServerRunning = false + private set + + fun startServer(port: String?) { + startServer(Integer.valueOf(port)) + } + + fun startServer(port: Int) { + doAsync { + + isServerRunning = true + var socket: Socket? = null + try { + serverSocket = ServerSocket(port) + } catch (e: IOException) { + e.printStackTrace() + isServerRunning = false + } + ("startServer: " + (mServerStartListener != null)).print() + if (mServerStartListener != null) { + mServerStartListener!!.serverStarted(port) + } + while (isServerRunning) { + "Accepting client".print() + try { + socket = serverSocket!!.accept() + val client = Client(socket) + lastClientIndex++ + clients[lastClientIndex.toInt()] = client + Thread(client).start() + client.setIndex(lastClientIndex.toInt()) + if (mConnectListener != null) { + mConnectListener!!.connected( + socket, + socket.localAddress, + +socket.localPort, + socket.localSocketAddress, + lastClientIndex.toInt() + ) + } + } catch (e: IOException) { + isServerRunning = false + break + } + } + if (mServerClosedListener != null) { + mServerClosedListener!!.serverClosed(port) + } + + } + } + + fun closeServer() { + try { + "closeServer: ".print() + isServerRunning = false + serverSocket!!.close() + kickAll() + } catch (e: IOException) { + e.printStackTrace() + } + } + + fun kickAll() { + for (client in clients.values) { + client.kill() + } + } + + fun kick(clientIndex: Int) { + clients[clientIndex]!!.kill() + } + + fun sendln(clientIndex: Int, message: String?) { + clients[clientIndex]?.output?.run { + println(message) + flush() + } + } + + fun send(clientIndex: Int, message: String?) { + clients[clientIndex]?.output?.run { + print(message) + flush() + } + } + + fun broadcast(message: String?) { + for (client in clients.values) { + client.output?.run { + print(message) + flush() + } + } + } + + fun broadcastln(message: String?) { + for (client in clients.values) { + client.output?.run { + println(message) + flush() + } + } + } + + fun getClients(): Map { + return clients + } + + val clientsCount: Int + get() = clients.size + + //---------------------------------------------[Listeners]----------------------------------------------// + fun setOnMessageReceivedListener(listener: OnMessageReceived?) { + mMessageListener = listener + } + + fun setOnConnectListener(listener: OnConnect?) { + mConnectListener = listener + } + + fun setOnDisconnectListener(listener: OnDisconnect?) { + mDisconnectListener = listener + } + + fun setOnServerClosedListener(listener: OnServerClose?) { + mServerClosedListener = listener + } + + fun setOnServerStartListener(listener: OnServerStart?) { + mServerStartListener = listener + } + + //---------------------------------------------[Interfaces]---------------------------------------------// + interface OnMessageReceived { + fun messageReceived(message: String?, clientIndex: Int) + } + + interface OnConnect { + fun connected( + socket: Socket?, + localAddress: InetAddress?, + port: Int, + localSocketAddress: SocketAddress?, + clientIndex: Int + ) + } + + interface OnDisconnect { + fun disconnected( + socket: Socket?, + localAddress: InetAddress?, + port: Int, + localSocketAddress: SocketAddress?, + clientIndex: Int + ) + } + + interface OnServerClose { + fun serverClosed(port: Int) + } + + interface OnServerStart { + fun serverStarted(port: Int) + } + + //--------------------------------------------[Client class]--------------------------------------------// + inner class Client(private val socket: Socket) : Runnable { + var output: PrintWriter? = null + private var input: BufferedReader? = null + private var clientIndex = 0 + override fun run() { + while (isServerRunning) { + "Read line (Client: $clientIndex)".print() + try { + val line = input!!.readLine() + println(line) + if (mMessageListener != null) { + if (line == null) { + socket.close() + clients.remove(clientIndex) + if (mDisconnectListener != null) { + mDisconnectListener!!.disconnected( + socket, + socket.localAddress, + +socket.localPort, + socket.localSocketAddress, + clientIndex + ) + } + break + } else { + mMessageListener!!.messageReceived(line, clientIndex) + } + } + } catch (e: IOException) { + e.printStackTrace() + } + } + } + + fun kill() { + try { + socket.shutdownInput() + } catch (e: Exception) { + } + try { + socket.shutdownOutput() + } catch (e: Exception) { + } + try { + socket.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + + fun setIndex(index: Int) { + clientIndex = index + } + + init { + try { + input = BufferedReader(InputStreamReader(socket.getInputStream())) + output = + PrintWriter(BufferedWriter(OutputStreamWriter(socket.getOutputStream())), true) + } catch (e: IOException) { + e.printStackTrace() + } + } + } + +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/TcpManager.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/TcpManager.kt new file mode 100644 index 0000000..8355d97 --- /dev/null +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/tcp/TcpManager.kt @@ -0,0 +1,66 @@ +package com.wrbug.developerhelper.ipc.processshare.tcp + +import com.google.gson.annotations.SerializedName +import com.wrbug.developerhelper.commonutil.fromJson +import com.wrbug.developerhelper.commonutil.print +import com.wrbug.developerhelper.commonutil.toJson +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.schedulers.Schedulers +import java.io.BufferedReader +import java.io.DataOutputStream +import java.io.InputStreamReader +import java.net.Socket +import kotlin.concurrent.thread + +object TcpManager { + private const val PORT = 23412 + private val tcpServer = TCPServer() + var messageHandler: MessageHandler? = null + + + fun startServer() { + tcpServer.setOnMessageReceivedListener(object : TCPServer.OnMessageReceived { + override fun messageReceived(message: String?, clientIndex: Int) { + val data = message?.fromJson() + if (data == null || data.action.isNullOrEmpty()) { + tcpServer.sendln(clientIndex, "") + return + } + "action=${data.action} data=${data.data} $clientIndex".print() + tcpServer.sendln( + clientIndex, + messageHandler?.handle(data.action ?: "", data.data ?: "") ?: "" + ) + } + }) + tcpServer.startServer(PORT) + } + + + fun sendMessage( + action: String, + message: String + ): Observable { + return Observable.create { + val clientSocket = Socket("localhost", PORT) + val outToServer = DataOutputStream(clientSocket.getOutputStream()) + val inFromServer = BufferedReader(InputStreamReader(clientSocket.getInputStream())) + val data = TCPData(action, message) + outToServer.writeBytes(data.toJson() + '\n') + val result = inFromServer.readLine() + clientSocket.close() + "onReceived: $result".print() + it.onNext(result ?: "") + }.onErrorResumeNext { + it.printStackTrace() + Observable.just("") + }.subscribeOn(Schedulers.io()) + } + + data class TCPData( + @SerializedName("action") + var action: String?, + @SerializedName("data") + var data: String? + ) +} \ No newline at end of file diff --git a/ipc/src/test/java/com/wrbug/developerhelper/ipc/ExampleUnitTest.java b/ipc/src/test/java/com/wrbug/developerhelper/ipc/ExampleUnitTest.java deleted file mode 100644 index 753cad4..0000000 --- a/ipc/src/test/java/com/wrbug/developerhelper/ipc/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.wrbug.developerhelper.ipc; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/mmkv/build.gradle b/mmkv/build.gradle index 1f89e36..5c77bb5 100644 --- a/mmkv/build.gradle +++ b/mmkv/build.gradle @@ -1,21 +1,26 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' +apply from: "${rootDir}/common.gradle" android { - compileSdkVersion 28 - - + namespace "com.wrbug.developerhelper.mmkv" + compileSdk 34 defaultConfig { - minSdkVersion 21 - targetSdkVersion 28 + minSdkVersion 23 + targetSdkVersion 34 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { jvmTarget = "17" } buildTypes { release { minifyEnabled false @@ -27,14 +32,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'com.tencent:mmkv:1.0.14' - implementation 'androidx.appcompat:appcompat:1.0.2' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.0-alpha3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha3' - implementation 'com.google.code.gson:gson:2.8.5' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} -repositories { - mavenCentral() + implementation 'com.tencent:mmkv:1.3.9' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'com.google.code.gson:gson:2.10' } diff --git a/mmkv/src/androidTest/java/com/wrbug/developerhelper/mmkv/ExampleInstrumentedTest.java b/mmkv/src/androidTest/java/com/wrbug/developerhelper/mmkv/ExampleInstrumentedTest.java deleted file mode 100644 index 161e03c..0000000 --- a/mmkv/src/androidTest/java/com/wrbug/developerhelper/mmkv/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.wrbug.developerhelper.mmkv; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.wrbug.developerhelper.mmkv.test", appContext.getPackageName()); - } -} diff --git a/mmkv/src/test/java/com/wrbug/developerhelper/mmkv/ExampleUnitTest.java b/mmkv/src/test/java/com/wrbug/developerhelper/mmkv/ExampleUnitTest.java deleted file mode 100644 index 9b42f39..0000000 --- a/mmkv/src/test/java/com/wrbug/developerhelper/mmkv/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.wrbug.developerhelper.mmkv; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/netbare-core/.gitignore b/netbare-core/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/netbare-core/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/netbare-core/build.gradle b/netbare-core/build.gradle deleted file mode 100644 index 1784f6e..0000000 --- a/netbare-core/build.gradle +++ /dev/null @@ -1,30 +0,0 @@ -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 21 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - - implementation 'com.android.support:appcompat-v7:28.0.0' - - implementation 'org.bouncycastle:bcpkix-jdk15on:1.56' - implementation 'org.bouncycastle:bcprov-jdk15on:1.56' - implementation 'com.google.guava:guava:19.0' -} diff --git a/netbare-core/src/main/AndroidManifest.xml b/netbare-core/src/main/AndroidManifest.xml deleted file mode 100644 index b4a5f13..0000000 --- a/netbare-core/src/main/AndroidManifest.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBare.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBare.java deleted file mode 100644 index e1b51cd..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBare.java +++ /dev/null @@ -1,193 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare; - -import android.app.Activity; -import android.app.Application; -import android.content.Intent; -import android.net.VpnService; -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.NonNull; - -import androidx.core.content.ContextCompat; -import com.github.megatronking.netbare.gateway.DefaultVirtualGatewayFactory; -import com.github.megatronking.netbare.gateway.VirtualGatewayFactory; -import com.github.megatronking.netbare.ssl.JKS; - -import java.util.LinkedHashSet; -import java.util.Set; - -/** - * NetBare is a single instance, we can use this class to config and manage the NetBare service. - * The NetBare service is an implement class of {@link VpnService}, before starting this service, - * should call {@link #prepare()} to check the vpn state. - * - * Start and stop the NetBare service: - *
                - * 
                - * NetBare.get().start(config);
                - * NetBare.get().stop();
                - * 
                - * 
                - * - * @author Megatron King - * @since 2018年10月07日 09:28 - */ -public final class NetBare { - - private static class Holder { - - private static final NetBare INSTANCE = new NetBare(); - - } - - private final Set mListeners; - private final Handler mMainThreadHandler; - - private Application mApp; - private NetBareConfig mNetBareConfig; - - private boolean mAlive; - - public static NetBare get() { - return Holder.INSTANCE; - } - - private NetBare() { - mListeners = new LinkedHashSet(); - mMainThreadHandler = new Handler(Looper.getMainLooper()); - } - - /** - * Attach an application instance to NetBare. We recommend you to call this method in your - * {@link Application} class. - * - * @param application The application instance. - * @param debug Should print logs in console. - * @return The single instance of NetBare. - */ - public NetBare attachApplication(@NonNull Application application, boolean debug) { - JKS.init(application); - mApp = application; - NetBareLog.setDebug(debug); - return this; - } - - /** - * Prepare to establish a VPN connection. This method returns {@code null} if the VPN - * application is already prepared or if the user has previously consented to the VPN - * application. Otherwise, it returns an {@link Intent} to a system activity. The application - * should launch the activity using {@link Activity#startActivityForResult} to get itself - * prepared. - * - * @return The intent to call using {@link Activity#startActivityForResult}. - */ - public Intent prepare() { - return VpnService.prepare(mApp); - } - - /** - * Start the NetBare service with your specific configuration. If the service is started, - * {@link NetBareListener#onServiceStarted()} will be invoked. - * - * @param config The configuration for NetBare service. - */ - public void start(@NonNull NetBareConfig config) { - if (config.mtu <= 0) { - throw new RuntimeException("Must set mtu in NetBareConfig"); - } - if (config.address == null) { - throw new RuntimeException("Must set address in NetBareConfig"); - } - mNetBareConfig = config; - Intent intent = new Intent(NetBareService.ACTION_START); - intent.setPackage(mApp.getPackageName()); - ContextCompat.startForegroundService(mApp, intent); - } - - /** - * Stop the NetBare service. If the service is started, - * {@link NetBareListener#onServiceStopped()} will be invoked. - */ - public void stop() { - Intent intent = new Intent(NetBareService.ACTION_STOP); - intent.setPackage(mApp.getPackageName()); - mApp.startService(intent); - } - - /** - * Whether the NetBare service is alive or not. - * - * @return True if the service is alive, false otherwise. - */ - public boolean isActive() { - return mAlive; - } - - /** - * Register a callback to be invoked when the service state changes. - * - * @param listener The callback to register. - */ - public void registerNetBareListener(NetBareListener listener) { - mListeners.add(listener); - } - - /** - * Remove a previously installed service state callback. - * - * @param listener The callback to remove. - */ - public void unregisterNetBareListener(NetBareListener listener) { - mListeners.remove(listener); - } - - /* package */ NetBareConfig getConfig() { - return mNetBareConfig; - } - - /* package */ VirtualGatewayFactory getGatewayFactory() { - // Make sure the virtual gateway not be null. - return mNetBareConfig.gatewayFactory == null ? DefaultVirtualGatewayFactory.create() : - mNetBareConfig.gatewayFactory; - } - - /* package */ void notifyServiceStarted() { - mAlive = true; - mMainThreadHandler.post(new Runnable() { - @Override - public void run() { - for (NetBareListener listener : mListeners) { - listener.onServiceStarted(); - } - } - }); - } - - /* package */ void notifyServiceStopped() { - mAlive = false; - mMainThreadHandler.post(new Runnable() { - @Override - public void run() { - for (NetBareListener listener : mListeners) { - listener.onServiceStopped(); - } - } - }); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareConfig.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareConfig.java deleted file mode 100644 index c690a03..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareConfig.java +++ /dev/null @@ -1,307 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare; - -import android.app.PendingIntent; -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.gateway.VirtualGatewayFactory; -import com.github.megatronking.netbare.http.HttpInterceptorFactory; -import com.github.megatronking.netbare.http.HttpVirtualGatewayFactory; -import com.github.megatronking.netbare.ip.IpAddress; -import com.github.megatronking.netbare.net.Session; -import com.github.megatronking.netbare.net.UidProvider; -import com.github.megatronking.netbare.ssl.JKS; - -import java.net.InetAddress; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * The configuration class for NetBare. Use {@link Builder} to construct an instance. - * - * @author Megatron King - * @since 2018年10月07日 11:20 - */ -public final class NetBareConfig { - - String session; - PendingIntent configureIntent; - int mtu; - IpAddress address; - Set routes; - Set dnsServers; - Set allowedApplications; - Set disallowedApplications; - Set allowedHosts; - Set disallowedHosts; - VirtualGatewayFactory gatewayFactory; - UidProvider uidProvider; - boolean dumpUid; - boolean excludeSelf; - - private NetBareConfig() { - } - - /** - * Create a new builder based on the current. - * - * @return A new builder instance. - */ - public Builder newBuilder() { - Builder builder = new Builder(); - builder.mConfig = this; - return builder; - } - - /** - * Create a default config using {@link HttpVirtualGatewayFactory} for HTTP protocol. - * - * @param jks JSK instance, not null. - * @param interceptors A collection of {@link HttpInterceptorFactory}. - * @return A NetBare config instance. - */ - public static NetBareConfig defaultHttpConfig(@NonNull JKS jks, - List interceptors) { - return defaultConfig().newBuilder() - .setVirtualGatewayFactory(new HttpVirtualGatewayFactory(jks, interceptors)) - .build(); - } - - /** - * Create a default config. - * - * @return A NetBare config instance. - */ - public static NetBareConfig defaultConfig() { - return new Builder() - .dumpUid(false) - .setMtu(4096) - .setAddress(new IpAddress("10.1.10.1", 32)) - .setSession("NetBare") - .addRoute(new IpAddress("0.0.0.0", 0)) - .build(); - } - - /** - * Helper class to createServerEngine a VPN Service. - */ - public static class Builder { - - private NetBareConfig mConfig; - - public Builder() { - this.mConfig = new NetBareConfig(); - this.mConfig.routes = new HashSet(); - this.mConfig.dnsServers = new HashSet(); - this.mConfig.allowedApplications = new HashSet(); - this.mConfig.disallowedApplications = new HashSet(); - this.mConfig.allowedHosts = new HashSet(); - this.mConfig.disallowedHosts = new HashSet(); - } - - /** - * Set the name of this session. It will be displayed in system-managed dialogs and - * notifications. This is recommended not required. - * - * @param session Session name. - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder setSession(@NonNull String session) { - mConfig.session = session; - return this; - } - - /** - * Set the {@link PendingIntent} to an activity for users to configure the VPN connection. - * If it is not set, the button to configure will not be shown in system-managed dialogs. - * - * @param intent An Activity intent. - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder setConfigureIntent(@NonNull PendingIntent intent) { - mConfig.configureIntent = intent; - return this; - } - - /** - * Set the maximum transmission unit (MTU) of the VPN interface. If it is not set, the - * default value in the operating system will be used. - * - * @param mtu Maximum transmission unit (MTU). - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder setMtu(int mtu) { - mConfig.mtu = mtu; - return this; - } - - /** - * Convenience method to add a network address to the VPN interface using a numeric address - * string. See {@link InetAddress} for the definitions of numeric address formats. - * - * Adding an address implicitly allows traffic from that address family (i.e., IPv4 or IPv6) - * to be routed over the VPN. - * - * @param address IPv4 or IPv6 address. - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder setAddress(@NonNull IpAddress address) { - mConfig.address = address; - return this; - } - - /** - * Add a network route to the VPN interface. Both IPv4 and IPv6 routes are supported. - * - * Adding a route implicitly allows traffic from that address family (i.e., IPv4 or IPv6) - * to be routed over the VPN. - * - * @param address IPv4 or IPv6 address. - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder addRoute(@NonNull IpAddress address) { - mConfig.routes.add(address); - return this; - } - - /** - * Add a DNS server to the VPN connection. Both IPv4 and IPv6 addresses are supported. - * If none is set, the DNS servers of the default network will be used. - * - * Adding a server implicitly allows traffic from that address family (i.e., IPv4 or IPv6) - * to be routed over the VPN. - * - * @param address IPv4 or IPv6 address. - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder addDnsServer(@NonNull String address) { - mConfig.dnsServers.add(address); - return this; - } - - /** - * Adds an application that's allowed to access the VPN connection. - * - * If this method is called at least once, only applications added through this method (and - * no others) are allowed access. Else (if this method is never called), all applications - * are allowed by default. If some applications are added, other, un-added applications - * will use networking as if the VPN wasn't running. - * - * @param packageName The full name (e.g.: "com.google.apps.contacts") of an application. - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder addAllowedApplication(@NonNull String packageName) { - mConfig.allowedApplications.add(packageName); - return this; - } - - /** - * Adds an application that's denied access to the VPN connection. - * - * By default, all applications are allowed access, except for those denied through this - * method. Denied applications will use networking as if the VPN wasn't running. - * - * @param packageName The full name (e.g.: "com.google.apps.contacts") of an application. - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder addDisallowedApplication(@NonNull String packageName) { - mConfig.disallowedApplications.add(packageName); - return this; - } - - /** - * Adds an ip host or a domain host that's allowed to capture. - * - * @param host An ip host or a domain host, not support the domain host. - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder addAllowedHost(@NonNull String host) { - mConfig.allowedHosts.add(host); - return this; - } - - /** - * Adds an ip host or a domain host that's denied access to capture. - * - * @param host An ip host or a domain host, not support the domain host. - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder addDisallowedHost(@NonNull String host) { - mConfig.disallowedHosts.add(host); - return this; - } - - /** - * Set the factory of gateway, the gateway will handle some intercepted actions before the - * server and client received the final data. - * - * @param gatewayFactory A factory of gateway. - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder setVirtualGatewayFactory(VirtualGatewayFactory gatewayFactory) { - mConfig.gatewayFactory = gatewayFactory; - return this; - } - - /** - * Dump the uid of the session, you can get the value from {@link Session#uid}. This config - * will cost much battery. - * - * @param dumpUid Should dump session's uid from /proc/net/ - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder dumpUid(boolean dumpUid) { - mConfig.dumpUid = dumpUid; - return this; - } - - /** - * Exclude all net packets of the app self, this config is associated with {@link #dumpUid}. - * If the config of dumpUid is false, the excludeSelf will be forced to false too. - * - * @param excludeSelf Should exclude all net packets of the app self. - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder excludeSelf(boolean excludeSelf) { - mConfig.excludeSelf = excludeSelf; - return this; - } - - /** - * Sets an uid provider. - * - * @param provider This interface provides a known uid for a session. - * @return this {@link Builder} object to facilitate chaining method calls. - */ - public Builder setUidProvider(UidProvider provider) { - mConfig.uidProvider = provider; - return this; - } - - /** - * Create the instance of {@link NetBareConfig}. - * - * @return The instance of {@link NetBareConfig}. - */ - public NetBareConfig build() { - return mConfig; - } - - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareListener.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareListener.java deleted file mode 100644 index 0c547d0..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareListener.java +++ /dev/null @@ -1,38 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare; - -/** - * Interface definition for a callback to be invoked when the NetBare service state changes. - * - * @author Megatron King - * @since 2018年10月11日 19:44 - */ -public interface NetBareListener { - - /** - * Callback method to be invoked when the NetBare service is started. It usual is called after - * {@link NetBare#start(NetBareConfig)}. - */ - void onServiceStarted(); - - /** - * Callback method to be invoked when the NetBare service is stopped. It usual is called after - * {@link NetBare#stop()} or another VPN service is established. - */ - void onServiceStopped(); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareLog.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareLog.java deleted file mode 100644 index d9aff21..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareLog.java +++ /dev/null @@ -1,169 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare; - -import android.util.Log; - -/** - * A static log util using in NetBare, and the tag is 'NetBare'; - * - * @author Megatron King - * @since 2018年10月08日 23:12 - */ -public final class NetBareLog { - - private static final String TAG = "NetBare"; - - private static boolean sDebug; - - private NetBareLog() { - } - - /* package */ static void setDebug(boolean debug) { - sDebug = debug; - } - - /** - * Print a verbose level log in console. - * - * @param msg The message you would like logged. - */ - public static void v(String msg) { - if (!sDebug || msg == null) { - return; - } - Log.v(TAG, msg); - } - - /** - * Print a verbose level log in console. - * - * @param msg The message you would like logged. - * @param args Arguments referenced by the format specifiers in the format string. - */ - public static void v(String msg, Object... args) { - v(format(msg, args)); - } - - /** - * Print a debug level log in console. - * - * @param msg The message you would like logged. - */ - public static void d(String msg) { - if (!sDebug || msg == null) { - return; - } - Log.d(TAG, msg); - } - - /** - * Print a debug level log in console. - * - * @param msg The message you would like logged. - * @param args Arguments referenced by the format specifiers in the format string. - */ - public static void d(String msg, Object... args) { - d(format(msg, args)); - } - - /** - * Print a info level log in console. - * - * @param msg The message you would like logged. - */ - public static void i(String msg) { - if (!sDebug || msg == null) { - return; - } - Log.i(TAG, msg); - } - - /** - * Print a info level log in console. - * - * @param msg The message you would like logged. - * @param args Arguments referenced by the format specifiers in the format string. - */ - public static void i(String msg, Object... args) { - i(format(msg, args)); - } - - /** - * Print a error level log in console. - * - * @param msg The message you would like logged. - */ - public static void e(String msg) { - if (!sDebug || msg == null) { - return; - } - Log.e(TAG, msg); - } - - /** - * Print a error level log in console. - * - * @param msg The message you would like logged. - * @param args Arguments referenced by the format specifiers in the format string. - */ - public static void e(String msg, Object... args) { - e(format(msg, args)); - } - - /** - * Print a warning level log in console. - * - * @param msg The message you would like logged. - */ - public static void w(String msg) { - if (!sDebug || msg == null) { - return; - } - Log.w(TAG, msg); - } - - /** - * Print a warning level log in console. - * - * @param msg The message you would like logged. - * @param args Arguments referenced by the format specifiers in the format string. - */ - public static void w(String msg, Object... args) { - w(format(msg, args)); - } - - /** - * Print a fatal level log in console. - * - * @param throwable The error you would like logged. - */ - public static void wtf(Throwable throwable) { - if (!sDebug || throwable == null) { - return; - } - Log.wtf(TAG, throwable); - } - - private static String format(String format, Object... objs) { - if (objs == null || objs.length == 0) { - return format; - } else { - return String.format(format, objs); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareService.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareService.java deleted file mode 100644 index a4edb1a..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareService.java +++ /dev/null @@ -1,121 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare; - -import android.app.Notification; -import android.app.NotificationManager; -import android.content.Intent; -import android.net.VpnService; -import androidx.annotation.NonNull; - -/** - * Base class for NetBare services. - *

                - * NetBare service is an implement of {@link VpnService}, it establishes a vpn connection to - * route incoming and outgoing net packets. The NetBare service are forced to display a notification - * due to intercepting packets raises huge security concerns. - *

                - *

                - * The NetBare service is managed by {@link NetBare}, and you can use {@link NetBareListener} to - * observe the state. - *

                - * - * @author Megatron King - * @since 2018年10月08日 21:09 - */ -public abstract class NetBareService extends VpnService { - - /** - * Start capturing target app's net packets. - */ - public static final String ACTION_START = - "com.github.megatronking.netbare.action.Start"; - - /** - * Stop capturing target app's net packets. - */ - public static final String ACTION_STOP = - "com.github.megatronking.netbare.action.Stop"; - - /** - * The identifier for this notification as per - * {@link NotificationManager#notify(int, Notification)}; must not be 0. - * - * @return The identifier - */ - protected abstract int notificationId(); - - /** - * A {@link Notification} object describing what to show the user. Must not be null. - * - * @return The Notification to be displayed. - */ - @NonNull - protected abstract Notification createNotification(); - - private NetBareThread mNetBareThread; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent == null) { - return START_NOT_STICKY; - } - String action = intent.getAction(); - if (ACTION_START.equals(action)) { - startNetBare(); - startForeground(notificationId(), createNotification()); - } else if (ACTION_STOP.equals(action)) { - stopNetBare(); - stopForeground(true); - stopSelf(); - } else { - stopSelf(); - } - return super.onStartCommand(intent, flags, startId); - } - - @Override - public void onDestroy() { - super.onDestroy(); - stopNetBare(); - stopForeground(true); - } - - private void startNetBare() { - // Terminate previous service. - stopNetBare(); - - NetBareConfig config = NetBare.get().getConfig(); - if (config == null) { - throw new IllegalArgumentException("Must start NetBareService with a " + - "NetBareConfig"); - } - - NetBareLog.i("Start NetBare service!"); - mNetBareThread = new NetBareThread(this, config); - mNetBareThread.start(); - } - - private void stopNetBare() { - if (mNetBareThread == null) { - return; - } - NetBareLog.i("Stop NetBare service!"); - mNetBareThread.interrupt(); - mNetBareThread = null; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareThread.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareThread.java deleted file mode 100644 index cfb9a1a..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareThread.java +++ /dev/null @@ -1,235 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare; - -import android.content.pm.PackageManager; -import android.net.VpnService; -import android.os.ParcelFileDescriptor; -import android.os.SystemClock; - -import com.github.megatronking.netbare.ip.IpAddress; -import com.github.megatronking.netbare.ip.IpHeader; -import com.github.megatronking.netbare.ip.Protocol; -import com.github.megatronking.netbare.net.UidDumper; -import com.github.megatronking.netbare.proxy.IcmpProxyServerForwarder; -import com.github.megatronking.netbare.proxy.ProxyServerForwarder; -import com.github.megatronking.netbare.proxy.TcpProxyServerForwarder; -import com.github.megatronking.netbare.proxy.UdpProxyServerForwarder; - -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * A work thread running NetBare core logic. NetBase established the VPN connection is this thread - * and read packets from the VPN file descriptor and transfer them to local proxy servers. Every - * IP protocol runs an independent local proxy server to receive the packets. - * - * @author Megatron King - * @since 2018年10月08日 19:38 - */ -/* package */ final class NetBareThread extends Thread { - - private static final int TRANSPORT_WAIT_TIME = 5; - - private final NetBareConfig mConfig; - private final VpnService mVpnService; - - private boolean mRunning; - - /* package */ NetBareThread(VpnService vpnService, NetBareConfig config) { - super("NetBare"); - this.mVpnService = vpnService; - this.mConfig = config; - } - - @Override - public void start() { - mRunning = true; - super.start(); - } - - @Override - public void interrupt() { - mRunning = false; - super.interrupt(); - } - - @Override - public void run() { - super.run(); - if (!mRunning) { - return; - } - - // Notify NetBareListener that the service is started now. - NetBare.get().notifyServiceStarted(); - - PacketsTransfer packetsTransfer = null; - try { - packetsTransfer = new PacketsTransfer(mVpnService, mConfig); - } catch (IOException e) { - NetBareLog.wtf(e); - } - if (packetsTransfer != null) { - // Establish VPN, it runs a while loop unless failed. - establishVpn(packetsTransfer); - } - - // Notify NetBareListener that the service is stopped now. - NetBare.get().notifyServiceStopped(); - - } - - private void establishVpn(PacketsTransfer packetsTransfer) { - VpnService.Builder builder = mVpnService.new Builder(); - builder.setMtu(mConfig.mtu); - builder.addAddress(mConfig.address.address, mConfig.address.prefixLength); - if (mConfig.session != null) { - builder.setSession(mConfig.session); - } - if (mConfig.configureIntent != null) { - builder.setConfigureIntent(mConfig.configureIntent); - } - for (IpAddress ip : mConfig.routes) { - builder.addRoute(ip.address, ip.prefixLength); - } - for (String address : mConfig.dnsServers) { - builder.addDnsServer(address); - } - try { - for (String packageName : mConfig.allowedApplications) { - builder.addAllowedApplication(packageName); - } - for (String packageName : mConfig.disallowedApplications) { - builder.addDisallowedApplication(packageName); - } - // Add self to allowed list. - if (!mConfig.allowedApplications.isEmpty()) { - builder.addAllowedApplication(mVpnService.getPackageName()); - } - } catch (PackageManager.NameNotFoundException e) { - NetBareLog.wtf(e); - } - ParcelFileDescriptor vpnDescriptor = builder.establish(); - if (vpnDescriptor == null) { - return; - } - - // Open io with the VPN descriptor. - FileDescriptor descriptor = vpnDescriptor.getFileDescriptor(); - if (descriptor == null) { - return; - } - InputStream input = new FileInputStream(descriptor); - OutputStream output = new FileOutputStream(descriptor); - int mtu = mConfig.mtu; - - packetsTransfer.start(); - - try { - // Read packets from input io and forward them to proxy servers. - while (mRunning) { - packetsTransfer.transfer(input, output, mtu); - } - } catch (IOException e) { - NetBareLog.wtf(e); - } - - packetsTransfer.stop(); - - NetBareUtils.closeQuietly(vpnDescriptor); - NetBareUtils.closeQuietly(input); - NetBareUtils.closeQuietly(output); - } - - private static class PacketsTransfer { - - private final UidDumper mUidDumper; - private final Map mForwarderRegistry; - - private PacketsTransfer(VpnService service, NetBareConfig config) throws IOException { - int mtu = config.mtu; - String localIp = config.address.address; - this.mUidDumper = config.dumpUid ? new UidDumper(localIp, config.uidProvider) : null; - // Register all supported protocols here. - this.mForwarderRegistry = new LinkedHashMap(3); - // TCP - this.mForwarderRegistry.put(Protocol.TCP, new TcpProxyServerForwarder(service, localIp, mtu, - mUidDumper)); - // UDP - this.mForwarderRegistry.put(Protocol.UDP, new UdpProxyServerForwarder(service, mtu, - mUidDumper)); - // ICMP - this.mForwarderRegistry.put(Protocol.ICMP, new IcmpProxyServerForwarder()); - } - - private void start() { - if (mUidDumper != null) { - mUidDumper.startDump(); - } - for (ProxyServerForwarder forwarder : mForwarderRegistry.values()) { - forwarder.prepare(); - } - } - - private void stop() { - for (ProxyServerForwarder forwarder : mForwarderRegistry.values()) { - forwarder.release(); - } - mForwarderRegistry.clear(); - if (mUidDumper != null) { - mUidDumper.stopDump(); - } - } - - private void transfer(InputStream input, OutputStream output, int mtu) throws IOException { - // The thread would be blocked if there is no outgoing packets from input stream. - byte[] packet = new byte[mtu]; - int readLength = input.read(packet); - if (readLength < 0) { - throw new IOException("Read -1 from vpn FileDescriptor."); - } - if (readLength == 0) { - SystemClock.sleep(TRANSPORT_WAIT_TIME); - return; - } - transfer(packet, readLength, output); - } - - private void transfer(byte[] packet, int len, OutputStream output) { - if (len < IpHeader.MIN_HEADER_LENGTH) { - NetBareLog.w("Ip header length < " + IpHeader.MIN_HEADER_LENGTH); - return; - } - IpHeader ipHeader = new IpHeader(packet, 0); - Protocol protocol = Protocol.parse(ipHeader.getProtocol()); - ProxyServerForwarder forwarder = mForwarderRegistry.get(protocol); - if (forwarder != null) { - forwarder.forward(packet, len, output); - } else { - NetBareLog.w("Unknown ip protocol: " + ipHeader.getProtocol()); - } - } - - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareUtils.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareUtils.java deleted file mode 100644 index e322239..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareUtils.java +++ /dev/null @@ -1,153 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare; - -import android.text.TextUtils; - -import java.io.Closeable; -import java.io.IOException; - -/** - * A collection of assorted utility classes. - * - * @author Megatron King - * @since 2018年10月08日 22:52 - */ -public final class NetBareUtils { - - /** - * Http line end (CRLF) symbol. - */ - public static final String LINE_END = "\r\n"; - - /** - * Http line end (CRLF) regex. - */ - public static final String LINE_END_REGEX = "\\r\\n"; - - /** - * A byte array of http line end (CRLF). - */ - public static final byte[] LINE_END_BYTES = LINE_END.getBytes(); - - /** - * Http double line end (CRLF), it separate the headers and body. - */ - public static final String PART_END = "\r\n\r\n"; - - /** - * A byte array of double http line end (CRLF). - */ - public static final byte[] PART_END_BYTES = "\r\n\r\n".getBytes(); - - /** - * Convert a int ip value to ipv4 string. - * - * @param ip The ip address. - * @return A ipv4 string value, format is N.N.N.N - */ - public static String convertIp(int ip) { - return String.format("%s.%s.%s.%s", (ip>> 24) & 0x00FF, - (ip>> 16) & 0x00FF, (ip>> 8) & 0x00FF, (ip & 0x00FF)); - } - - /** - * Convert a string ip value to int. - * - * @param ip The ip address. - * @return A int ip value. - */ - public static int convertIp(String ip) { - String[] arrayStrings = ip.split("\\."); - return (Integer.parseInt(arrayStrings[0]) << 24) - | (Integer.parseInt(arrayStrings[1]) << 16) - | (Integer.parseInt(arrayStrings[2]) << 8) - | (Integer.parseInt(arrayStrings[3])); - } - - /** - * Convert a short ip value to int. - * - * @param port The port. - * @return A int port value. - */ - public static int convertPort(short port) { - return port & 0xFFFF; - } - - /** - * Closes a closeable object or release resource. - * - * @param closeable A closeable object like io stream. - */ - public static void closeQuietly(Closeable closeable) { - if (closeable != null) { - try { - closeable.close(); - } catch (IOException e) { - NetBareLog.wtf(e); - } - } - } - - /** - * Parse a string to a integer value. If the string is not a integer value, this will return the - * default value. - * - * @param string The string value. - * @param defaultValue The default integer value. - * @return The integer value. - */ - public static int parseInt(String string, int defaultValue) { - int result = defaultValue; - - if (TextUtils.isEmpty(string)) { - return result; - } - - try { - result = Integer.parseInt(string); - } catch (Exception e) { - // parse error - } - return result; - } - - /** - * Parse a string to a integer value with a radix. If the string is not a integer value, this - * will return the default value. - * - * @param string The string value. - * @param radix The radix to be used. - * @param defaultValue The default integer value. - * @return The integer value. - */ - public static int parseInt(String string, int radix, int defaultValue) { - int result = defaultValue; - - if (TextUtils.isEmpty(string)) { - return result; - } - - try { - result = Integer.parseInt(string, radix); - } catch (Exception e) { - // parse error - } - return result; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareVirtualGateway.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareVirtualGateway.java deleted file mode 100644 index 50e613f..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareVirtualGateway.java +++ /dev/null @@ -1,334 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare; - -import android.os.Process; - -import com.github.megatronking.netbare.gateway.Request; -import com.github.megatronking.netbare.gateway.Response; -import com.github.megatronking.netbare.gateway.VirtualGateway; -import com.github.megatronking.netbare.ip.Protocol; -import com.github.megatronking.netbare.net.Session; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.HashSet; -import java.util.Set; - -/** - * The main virtual gateway used in proxy servers, it wraps the actual virtual gateway. We use this - * class to do some internal verifications. - * - * @author Megatron King - * @since 2018年11月17日 23:10 - */ -public final class NetBareVirtualGateway extends VirtualGateway { - - /** - * Policy is indeterminate, we should resolve the policy before process data. - */ - private static final int POLICY_INDETERMINATE = 0; - - /** - * This policy allows data flow to configured virtual gateway. - */ - private static final int POLICY_ALLOWED = 1; - - /** - * This policy doesn't allow data flow to configured virtual gateway. - */ - private static final int POLICY_DISALLOWED = 2; - - private final VirtualGateway mGateway; - private final Session mSession; - private final NetBareXLog mLog; - - private int mPolicy; - - private boolean mRequestFinished; - private boolean mResponseFinished; - - public NetBareVirtualGateway(Session session, Request request, Response response) { - super(session, request, response); - mGateway = NetBare.get().getGatewayFactory().create(session, request, response); - mSession = session; - mLog = new NetBareXLog(session); - - NetBareConfig config = NetBare.get().getConfig(); - if (config == null || (config.excludeSelf && session.uid == Process.myUid())) { - // Exclude the app itself. - mLog.w("Exclude an app-self connection!"); - mPolicy = POLICY_DISALLOWED; - } else { - mPolicy = POLICY_INDETERMINATE; - } - } - - @Override - public void sendRequest(ByteBuffer buffer) throws IOException { - if (mRequestFinished) { - mLog.w("Drop a buffer due to request has finished."); - return; - } - resolvePolicyIfNecessary(buffer); - if (mPolicy == POLICY_ALLOWED) { - mGateway.sendRequest(buffer); - } else if (mPolicy == POLICY_DISALLOWED) { - super.sendRequest(buffer); - } - } - - @Override - public void sendResponse(ByteBuffer buffer) throws IOException { - if (mResponseFinished) { - mLog.w("Drop a buffer due to response has finished."); - return; - } - resolvePolicyIfNecessary(buffer); - if (mPolicy == POLICY_ALLOWED) { - mGateway.sendResponse(buffer); - } else if (mPolicy == POLICY_DISALLOWED) { - super.sendResponse(buffer); - } - } - - @Override - public void sendRequestFinished() { - if (mRequestFinished) { - return; - } - mLog.i("Gateway request finished!"); - mRequestFinished = true; - if (mPolicy == POLICY_ALLOWED) { - mGateway.sendRequestFinished(); - } else if (mPolicy == POLICY_DISALLOWED) { - super.sendRequestFinished(); - } - } - - @Override - public void sendResponseFinished() { - if (mResponseFinished) { - return; - } - mLog.i("Gateway response finished!"); - mResponseFinished = true; - if (mPolicy == POLICY_ALLOWED) { - mGateway.sendResponseFinished(); - } else if (mPolicy == POLICY_DISALLOWED) { - super.sendResponseFinished(); - } - } - - private void resolvePolicyIfNecessary(ByteBuffer buffer) { - if (mPolicy != POLICY_INDETERMINATE) { - // Resolved. - return; - } - if (!buffer.hasRemaining()) { - // Invalid buffer remaining, do nothing. - return; - } - if (mSession.protocol != Protocol.TCP) { - mPolicy = POLICY_ALLOWED; - return; - } - - // Now we verify the TCP protocol host - String domain; - if (isHttp(buffer)) { - domain = parseHttpHost(buffer.array(), buffer.position(), buffer.remaining()); - } else { - domain = parseHttpsHost(buffer.array(), buffer.position(), buffer.remaining()); - } - if (domain == null) { - // Maybe not http protocol. - mPolicy = POLICY_ALLOWED; - return; - } else { - mSession.host = domain; - } - NetBareConfig config = NetBare.get().getConfig(); - Set allowedHost = new HashSet(config.allowedHosts); - Set disallowedHost = new HashSet(config.disallowedHosts); - - boolean isAllowedHostEmpty = allowedHost.isEmpty(); - boolean isDisallowedHostEmpty = disallowedHost.isEmpty(); - - if (isAllowedHostEmpty && isDisallowedHostEmpty) { - // No white and black list, it means allow everything. - mPolicy = POLICY_ALLOWED; - return; - } - - if (!isDisallowedHostEmpty) { - // Check domain hosts. - for (String host : disallowedHost) { - if (host.equals(domain)) { - // Denied host. - mPolicy = POLICY_DISALLOWED; - return; - } - } - // Check ip hosts. - for (String host : disallowedHost) { - if (host.equals(NetBareUtils.convertIp(mSession.remoteIp))) { - // Denied host. - mPolicy = POLICY_DISALLOWED; - return; - } - } - } - - if (!isAllowedHostEmpty) { - for (String host : allowedHost) { - if (host.equals(domain)) { - mPolicy = POLICY_ALLOWED; - return; - } - } - for (String host : allowedHost) { - if (host.equals(NetBareUtils.convertIp(mSession.remoteIp))) { - mPolicy = POLICY_ALLOWED; - return; - } - } - mPolicy = POLICY_DISALLOWED; - } else { - mPolicy = POLICY_ALLOWED; - } - } - - private boolean isHttp(ByteBuffer buffer) { - switch (buffer.get(buffer.position())) { - // HTTP methods. - case 'G': - case 'H': - case 'P': - case 'D': - case 'O': - case 'T': - case 'C': - return true; - default: - // Unknown first byte data. - break; - } - return false; - } - - private String parseHttpHost(byte[] buffer, int offset, int size) { - String header = new String(buffer, offset, size); - String[] headers = header.split(NetBareUtils.LINE_END_REGEX); - if (headers.length <= 1) { - return null; - } - for (int i = 1; i < headers.length; i++) { - String requestHeader = headers[i]; - // Reach the header end - if (requestHeader.isEmpty()) { - return null; - } - String[] nameValue = requestHeader.split(":"); - if (nameValue.length < 2) { - return null; - } - String name = nameValue[0].trim(); - String value = requestHeader.replaceFirst(nameValue[0] + ": ", "").trim(); - if (name.toLowerCase().equals("host")) { - return value; - } - } - return null; - } - - private String parseHttpsHost(byte[] buffer, int offset, int size) { - int limit = offset + size; - // Client Hello - if (size <= 43 || buffer[offset] != 0x16) { - mLog.w("Failed to get host from SNI: Bad ssl packet."); - return null; - } - // Skip 43 byte header - offset += 43; - - // Read sessionID - if (offset + 1> limit) { - mLog.w("Failed to get host from SNI: No session id."); - return null; - } - int sessionIDLength = buffer[offset++] & 0xFF; - offset += sessionIDLength; - - // Read cipher suites - if (offset + 2> limit) { - mLog.w("Failed to get host from SNI: No cipher suites."); - return null; - } - - int cipherSuitesLength = readShort(buffer, offset) & 0xFFFF; - offset += 2; - offset += cipherSuitesLength; - - // Read Compression method. - if (offset + 1> limit) { - mLog.w("Failed to get host from SNI: No compression method."); - return null; - } - int compressionMethodLength = buffer[offset++] & 0xFF; - offset += compressionMethodLength; - - // Read Extensions - if (offset + 2> limit) { - mLog.w("Failed to get host from SNI: no extensions."); - return null; - } - int extensionsLength = readShort(buffer, offset) & 0xFFFF; - offset += 2; - - if (offset + extensionsLength> limit) { - mLog.w("Failed to get host from SNI: no sni."); - return null; - } - - while (offset + 4 <= limit) { - int type0 = buffer[offset++] & 0xFF; - int type1 = buffer[offset++] & 0xFF; - int length = readShort(buffer, offset) & 0xFFFF; - offset += 2; - // Got the SNI info - if (type0 == 0x00 && type1 == 0x00 && length> 5) { - offset += 5; - length -= 5; - if (offset + length> limit) { - return null; - } - return new String(buffer, offset, length); - } else { - offset += length; - } - - } - mLog.w("Failed to get host from SNI: no host."); - return null; - } - - private short readShort(byte[] data, int offset) { - int r = ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF); - return (short) r; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareXLog.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareXLog.java deleted file mode 100644 index 882ac79..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareXLog.java +++ /dev/null @@ -1,174 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare; - -import com.github.megatronking.netbare.net.Session; -import com.github.megatronking.netbare.ip.Protocol; - -/** - * A log util using in NetBare, it uses the protocol, ip and port as the prefix. - * - * @author Megatron King - * @since 2018年10月14日 10:25 - */ -public final class NetBareXLog { - - private final String mPrefix; - - /** - * Constructs a NetBareXLog instance with the net information. - * - * @param protocol The IP protocol. - * @param ip The ip address, a string value. - * @param port The port, a short value. - */ - public NetBareXLog(Protocol protocol, String ip, short port) { - this(protocol, ip, NetBareUtils.convertPort(port)); - } - - /** - * Constructs a NetBareXLog instance with the net information. - * - * @param protocol The IP protocol. - * @param ip The ip address, a int value. - * @param port The port, a short value. - */ - public NetBareXLog(Protocol protocol, int ip, short port) { - this(protocol, NetBareUtils.convertIp(ip), port); - } - - /** - * Constructs a NetBareXLog instance with the net information. - * - * @param protocol The IP protocol. - * @param ip The ip address, a int value. - * @param port The port, a int value. - */ - public NetBareXLog(Protocol protocol, int ip, int port) { - this(protocol, NetBareUtils.convertIp(ip), port); - } - - /** - * Constructs a NetBareXLog instance with the net information. - * - * @param session The session contains net information. - */ - public NetBareXLog(Session session) { - this(session.protocol, session.remoteIp, session.remotePort); - } - - /** - * Constructs a NetBareXLog instance with the net information. - * - * @param protocol The IP protocol. - * @param ip The ip address, a string value. - * @param port The port, a int value. - */ - public NetBareXLog(Protocol protocol, String ip, int port) { - this.mPrefix = "[" + protocol.name() + "][" + ip + ":" + port + "]"; - } - - /** - * Print a verbose level log in console, format is '[protocol][ip:port]message'. - * - * @param msg The message you would like logged. - */ - public void v(String msg) { - NetBareLog.v(mPrefix + msg); - } - - - public void v(String msg, Object... args) { - NetBareLog.v(mPrefix + msg, args); - } - - /** - * Print a debug level log in console, format is '[protocol][ip:port]message'. - * - * @param msg The message you would like logged. - */ - public void d(String msg) { - NetBareLog.d(mPrefix + msg); - } - - /** - * Print a verbose level log in console, format is '[protocol][ip:port]message'. - * - * @param msg The message you would like logged. - * @param args Arguments referenced by the format specifiers in the format string. - */ - public void d(String msg, Object... args) { - NetBareLog.d(mPrefix + msg, args); - } - - /** - * Print a info level log in console, format is '[protocol][ip:port]message'. - * - * @param msg The message you would like logged. - */ - public void i(String msg) { - NetBareLog.i(mPrefix + msg); - } - - /** - * Print a info level log in console, format is '[protocol][ip:port]message'. - * - * @param msg The message you would like logged. - * @param args Arguments referenced by the format specifiers in the format string. - */ - public void i(String msg, Object... args) { - NetBareLog.i(mPrefix + msg, args); - } - - /** - * Print a error level log in console, format is '[protocol][ip:port]message'. - * - * @param msg The message you would like logged. - */ - public void e(String msg) { - NetBareLog.e(mPrefix + msg); - } - - /** - * Print a error level log in console, format is '[protocol][ip:port]message'. - * - * @param msg The message you would like logged. - * @param args Arguments referenced by the format specifiers in the format string. - */ - public void e(String msg, Object... args) { - NetBareLog.e(mPrefix + msg, args); - } - - /** - * Print a warning level log in console, format is '[protocol][ip:port]message'. - * - * @param msg The message you would like logged. - */ - public void w(String msg) { - NetBareLog.w(mPrefix + msg); - } - - /** - * Print a warning level log in console, format is '[protocol][ip:port]message'. - * - * @param msg The message you would like logged. - * @param args Arguments referenced by the format specifiers in the format string. - */ - public void w(String msg, Object... args) { - NetBareLog.w(mPrefix + msg, args); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGateway.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGateway.java deleted file mode 100644 index 5cab425..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGateway.java +++ /dev/null @@ -1,70 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import com.github.megatronking.netbare.net.Session; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -/** - * A {@link VirtualGateway} provides the interception service. Interceptors are organized as a list - * in chain, can observe and modify packets. Use {@link DefaultVirtualGatewayFactory} to create an - * instance. - * - * @author Megatron King - * @since 2018年11月01日 23:35 - */ -/* package */ class DefaultVirtualGateway extends VirtualGateway { - - private final List mInterceptors; - - /* package */ DefaultVirtualGateway(Session session, Request request, Response response, - List factories) { - super(session, request, response); - this.mInterceptors = new ArrayList(factories.size()); - for (InterceptorFactory factory : factories) { - mInterceptors.add(factory.create()); - } - } - - @Override - public void sendRequest(ByteBuffer buffer) throws IOException { - new RequestChain(mRequest, mInterceptors).process(buffer); - } - - @Override - public void sendResponse(ByteBuffer buffer) throws IOException { - new ResponseChain(mResponse, mInterceptors).process(buffer); - } - - @Override - public void sendRequestFinished() { - for (Interceptor interceptor: mInterceptors) { - interceptor.onRequestFinished(mRequest); - } - } - - @Override - public void sendResponseFinished() { - for (Interceptor interceptor: mInterceptors) { - interceptor.onResponseFinished(mResponse); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGatewayFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGatewayFactory.java deleted file mode 100644 index d1c70eb..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGatewayFactory.java +++ /dev/null @@ -1,64 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.net.Session; - -import java.util.ArrayList; -import java.util.List; - -/** - * A {@link VirtualGatewayFactory} that produces the {@link DefaultVirtualGateway}. - * - * @author Megatron King - * @since 2018年11月01日 23:29 - */ -public final class DefaultVirtualGatewayFactory implements VirtualGatewayFactory { - - private List mFactories; - - private DefaultVirtualGatewayFactory(@NonNull List factories) { - this.mFactories = factories; - } - - @Override - public VirtualGateway create(Session session, Request request, Response response) { - return new DefaultVirtualGateway(session, request, response, new ArrayList(mFactories)); - } - - /** - * Create a {@link VirtualGatewayFactory} instance with a collection of - * {@link InterceptorFactory}. - * - * @param factories a collection of {@link InterceptorFactory}. - * @return A instance of {@link DefaultVirtualGatewayFactory}. - */ - public static VirtualGatewayFactory create(@NonNull List factories) { - return new DefaultVirtualGatewayFactory(factories); - } - - /** - * Create a {@link VirtualGatewayFactory} instance that not contains {@link Interceptor}. - * - * @return A instance of {@link VirtualGatewayFactory}. - */ - public static VirtualGatewayFactory create() { - return create(new ArrayList()); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Interceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Interceptor.java deleted file mode 100644 index b340bab..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Interceptor.java +++ /dev/null @@ -1,80 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import androidx.annotation.NonNull; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * A virtual gateway interceptor, observes and modifies requests/responses. Interceptors are - * organized by a virtual gateway, and process net packets one by one. - *

                - * Methods are thread-safety due to interceptors are running in the local proxy server threads. - *

                - *

                - * Use {@link InterceptorFactory} to create an interceptor instance. - *

                - * @author Megatron King - * @since 2018年11月13日 23:46 - */ -public interface Interceptor { - - /** - * Intercept request packet, and delivery it to next interceptor or the terminal. - *

                - * Remember do not block this method for a long time, because all the connections share the - * same thread. - *

                - * - * @param chain The request chain, call {@linkplain RequestChain#process(ByteBuffer)} to - * delivery the packet. - * @param buffer A nio buffer contains the packet data. - * @throws IOException If an I/O error has occurred. - */ - void intercept(@NonNull RequestChain chain, @NonNull ByteBuffer buffer) throws IOException; - - /** - * Intercept request packet, and delivery it to next interceptor or the terminal. - *

                - * Remember do not block this method for a long time, because all the connections share the - * same thread. - * - * @param chain The response chain, call {@linkplain ResponseChain#process(ByteBuffer)} to - * delivery the packet. - * @param buffer A nio buffer contains the packet data. - * @throws IOException If an I/O error has occurred. - */ - void intercept(@NonNull ResponseChain chain, @NonNull ByteBuffer buffer) throws IOException; - - /** - * Invoked when a session's request has finished. It means the client has no more data sent to - * server in this session, and it might invoked multi times if a connection is keep-alive. - * - * @param request The request. - */ - void onRequestFinished(@NonNull Request request); - - /** - * Invoked when a session's response has finished. It means the server has no more data sent to - * client in this session, and it might invoked multi times if a connection is keep-alive. - * - * @param response The response. - */ - void onResponseFinished(@NonNull Response response); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorChain.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorChain.java deleted file mode 100644 index 6f51b47..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorChain.java +++ /dev/null @@ -1,99 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; - -/** - * A chain with multiple {@link Interceptor} in series. The interceptors process net packets one by - * one, and send the modified packets to tunnel in the end. - * - * @param An implementation of {@link TunnelFlow}, responsible for sending data to tunnel. - * @param An implementation of {@link Interceptor}. - * - * @author Megatron King - * @since 2018年11月13日 23:00 - */ -public abstract class InterceptorChain { - - private T mFlow; - private List mInterceptors; - private int mIndex; - - /** - * Hand the net packets to the next {@link Interceptor}. - * - * @param buffer A buffer contains net packet data. - * @param flow A {@link TunnelFlow} implementation. - * @param interceptors A collection of all interceptors in chain. - * @param index The next interceptor index. - * @throws IOException If an I/O error has occurred. - */ - protected abstract void processNext(ByteBuffer buffer, T flow, List interceptors, int index) - throws IOException; - - /** - * Constructs an intercept chain with a tunnel flow instance and a collection of interceptors. - * - * @param flow A {@link TunnelFlow} implementation. - * @param interceptors A collection of interceptors. - */ - public InterceptorChain(T flow, List interceptors) { - this(flow, interceptors, 0); - } - - /** - * Constructs a new intercept chain with the tunnel flow instance and a collection of - * interceptors. The chain will start from the given index. - * - * @param flow A {@link TunnelFlow} implementation. - * @param interceptors A collection of interceptors. - * @param index The head index. - */ - public InterceptorChain(T flow, List interceptors, int index) { - this.mFlow = flow; - this.mInterceptors = interceptors; - this.mIndex = index; - } - - /** - * Finish the interception and send the packet to tunnel. - * - * @param buffer A buffer contains net packet data. - * @throws IOException If an I/O error has occurred. - */ - public void processFinal(ByteBuffer buffer) throws IOException { - mFlow.process(buffer); - } - - /** - * Hand the net packets to the next. If all interceptors have been processed, the packets will - * be sent to tunnel, otherwise hand it to the next interceptor. - * - * @param buffer A buffer contains net packet data. - * @throws IOException If an I/O error has occurred. - */ - public void process(ByteBuffer buffer) throws IOException { - if (mIndex>= mInterceptors.size()) { - processFinal(buffer); - } else { - processNext(buffer, mFlow, mInterceptors, mIndex); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorFactory.java deleted file mode 100644 index 48173d0..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import androidx.annotation.NonNull; - -/** - * Factory used by developer to create their own interceptor for virtual gateway. - * - * @author Megatron King - * @since 2018年11月02日 23:46 - */ -public interface InterceptorFactory { - - /** - * Creates an interceptor instance and immediately returns it, it must not be null. - * - * @return A newly created interceptor. - */ - @NonNull - Interceptor create(); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Request.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Request.java deleted file mode 100644 index c99e3b1..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Request.java +++ /dev/null @@ -1,48 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import com.github.megatronking.netbare.tunnel.Tunnel; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * A client requester, it connects to the remote server tunnel directly. We can send packet to the - * remote server using {@link #process(ByteBuffer)}. - * - * @author Megatron King - * @since 2018年11月05日 22:18 - */ -public class Request extends SessionTunnelFlow { - - private Tunnel mTunnel; - - public Request() { - } - - public Request(Tunnel tunnel) { - this.mTunnel = tunnel; - } - - @Override - public void process(ByteBuffer buffer) throws IOException { - if (mTunnel != null) { - mTunnel.write(buffer); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/RequestChain.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/RequestChain.java deleted file mode 100644 index 8ac3813..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/RequestChain.java +++ /dev/null @@ -1,58 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import androidx.annotation.NonNull; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; - -/** - * A request chain, responsible for intercepting request packets. - * - * @author Megatron King - * @since 2018年11月14日 23:18 - */ -public class RequestChain extends InterceptorChain { - - private Request mRequest; - - /* package */ RequestChain(Request request, List interceptors) { - super(request, interceptors); - mRequest = request; - } - - private RequestChain(Request request, List interceptors, int index) { - super(request, interceptors, index); - mRequest = request; - } - - @Override - protected void processNext(ByteBuffer buffer, Request request, List interceptors, - int index) throws IOException { - Interceptor interceptor = interceptors.get(index); - if (interceptor != null) { - interceptor.intercept(new RequestChain(request, interceptors, ++index), buffer); - } - } - - @NonNull - public Request request() { - return mRequest; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Response.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Response.java deleted file mode 100644 index 5fc46fb..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Response.java +++ /dev/null @@ -1,48 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import com.github.megatronking.netbare.tunnel.Tunnel; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * A server response, it connects to VPN file descriptor. We can send packet to the client using - * {@link #process(ByteBuffer)}. - * - * @author Megatron King - * @since 2018年11月05日 22:24 - */ -public class Response extends SessionTunnelFlow { - - private Tunnel mTunnel; - - public Response() { - } - - public Response(Tunnel tunnel) { - this.mTunnel = tunnel; - } - - @Override - public void process(ByteBuffer buffer) throws IOException { - if (mTunnel != null) { - mTunnel.write(buffer); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/ResponseChain.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/ResponseChain.java deleted file mode 100644 index 0b65f5c..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/ResponseChain.java +++ /dev/null @@ -1,58 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import androidx.annotation.NonNull; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; - -/** - * A response chain, responsible for intercepting response packets. - * - * @author Megatron King - * @since 2018年11月14日 23:19 - */ -public class ResponseChain extends InterceptorChain { - - private Response mResponse; - - /* package */ ResponseChain(Response response, List interceptors) { - super(response, interceptors); - mResponse = response; - } - - private ResponseChain(Response response, List interceptors, int index) { - super(response, interceptors, index); - mResponse = response; - } - - @Override - protected void processNext(ByteBuffer buffer, Response response, List interceptors, - int index) throws IOException { - Interceptor interceptor = interceptors.get(index); - if (interceptor != null) { - interceptor.intercept(new ResponseChain(response, interceptors, ++index), buffer); - } - } - - @NonNull - public Response response() { - return mResponse; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SessionTunnelFlow.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SessionTunnelFlow.java deleted file mode 100644 index 9d4bfa8..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SessionTunnelFlow.java +++ /dev/null @@ -1,101 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import com.github.megatronking.netbare.NetBareConfig; -import com.github.megatronking.netbare.NetBareUtils; -import com.github.megatronking.netbare.ip.Protocol; -import com.github.megatronking.netbare.net.Session; - -/** - * A tunnel flow contains the session information. - * - * @author Megatron King - * @since 2018年11月05日 21:43 - */ -public abstract class SessionTunnelFlow implements TunnelFlow { - - private Session mSession; - - /* package */ void setSession(Session session) { - mSession = session; - } - - /** - * Returns the session's unique id. - * - * @return The session id. - */ - public String id() { - return mSession.id; - } - - /** - * Returns the session created time, you can think of it as the start time of the request. - * - * @return Session created time. - */ - public long time() { - return mSession.time; - } - - /** - * Returns the identifier of this session's process uid. This value is not guaranteed, it is up - * to {@link NetBareConfig#dumpUid}. And if dumps the uid failed, it will return 0. - * - * @return The session's process uid. - */ - public int uid() { - return mSession.uid; - } - - /** - * Returns the remote server's IPV4 address. - * - * @return The remote server's IPV4 address. - */ - public String ip() { - return NetBareUtils.convertIp(mSession.remoteIp); - } - - /** - * Returns the remote server's host name. - * - * @return The remote server's host name. - */ - public String host() { - return mSession.host; - } - - /** - * Returns the remote server's port. - * - * @return The remote server's port. - */ - public int port() { - return NetBareUtils.convertPort(mSession.remotePort); - } - - /** - * Returns the IP protocol. - * - * @return IP protocol. - */ - public Protocol protocol() { - return mSession.protocol; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SpecVirtualGateway.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SpecVirtualGateway.java deleted file mode 100644 index bc8ed2a..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SpecVirtualGateway.java +++ /dev/null @@ -1,102 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import com.github.megatronking.netbare.ip.Protocol; -import com.github.megatronking.netbare.net.Session; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * The spec VirtualGateway filter the net packets by {@link Protocol}. - * - * @author Megatron King - * @since 2018年11月03日 10:34 - */ -public abstract class SpecVirtualGateway extends VirtualGateway { - - private final boolean mIsSpec; - - public SpecVirtualGateway(Protocol protocol, Session session, Request request, - Response response) { - super(session, request, response); - this.mIsSpec = protocol == session.protocol; - } - - /** - * The specific protocol packets sent to server will flow through this method. - * - * @param buffer A byte buffer contains the net packet data. - * @throws IOException If an I/O error has occurred. - */ - protected abstract void onSpecRequest(ByteBuffer buffer) throws IOException; - - /** - * The specific protocol packets sent to client will flow through this method. - * - * @param buffer A byte buffer contains the net packet data. - * @throws IOException If an I/O error has occurred. - */ - protected abstract void onSpecResponse(ByteBuffer buffer) throws IOException; - - /** - * Notify virtual gateway that no longer has data sent to the server. - */ - protected abstract void onSpecRequestFinished(); - - /** - * Notify virtual gateway that no longer has data sent to the client. - */ - protected abstract void onSpecResponseFinished(); - - @Override - public final void sendRequest(ByteBuffer buffer) throws IOException { - if (mIsSpec) { - onSpecRequest(buffer); - } else { - super.sendRequest(buffer); - } - } - - @Override - public final void sendResponse(ByteBuffer buffer) throws IOException { - if (mIsSpec) { - onSpecResponse(buffer); - } else { - super.sendResponse(buffer); - } - } - - @Override - public final void sendRequestFinished() { - if (mIsSpec) { - onSpecRequestFinished(); - } else { - super.sendRequestFinished(); - } - } - - @Override - public final void sendResponseFinished() { - if (mIsSpec) { - onSpecResponseFinished(); - } else { - super.sendResponseFinished(); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/TunnelFlow.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/TunnelFlow.java deleted file mode 100644 index 3427dee..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/TunnelFlow.java +++ /dev/null @@ -1,37 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * A tunnel flow interface, the implement class should bind a tunnel. - * - * @author Megatron King - * @since 2018年11月05日 20:20 - */ -/* package */ interface TunnelFlow { - - /** - * Send a packet to remote tunnel, and the tunnel will send it to the terminal. - * - * @param buffer A net packet buffer. - * @throws IOException If an I/O error has occurred. - */ - void process(ByteBuffer buffer) throws IOException; - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGateway.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGateway.java deleted file mode 100644 index a89b368..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGateway.java +++ /dev/null @@ -1,92 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import com.github.megatronking.netbare.net.Session; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * Virtual Gateway is a virtual net packets interception distributor, all packets will flow through - * it. We can define our own virtual gateway to decode and encode the packets. The Virtual - * Gateway wraps a request tunnel {@link Request} and a response tunnel {@link Response}, these - * tunnels are responsible for communicating with the terminal(client and server). - * - * @author Megatron King - * @since 2018年11月01日 23:48 - */ -public class VirtualGateway { - - /** - * The request tunnel connects to the server terminal. We can call - * {@link Request#process(ByteBuffer)} to send data. - */ - protected Request mRequest; - - /** - * The response tunnel connects to the client terminal. We can call - * {@link Response#process(ByteBuffer)} to send data. - */ - protected Response mResponse; - - /** - * Constructs a VirtualGateway object with the net session, request tunnel and response tunnel. - * - * @param session The net session contains basic net information such as IPs and ports. - * @param request The request tunnel connects to the server terminal. - * @param response The response tunnel connects to the client terminal. - */ - public VirtualGateway(Session session, Request request, Response response) { - request.setSession(session); - response.setSession(session); - this.mRequest = request; - this.mResponse = response; - } - - /** - * Send a packet to server terminal through the request tunnel. - * - * @param buffer A byte buffer contains the net packet data. - * @throws IOException If an I/O error has occurred. - */ - public void sendRequest(ByteBuffer buffer) throws IOException { - mRequest.process(buffer); - } - - /** - * Send a packet to client terminal through the response tunnel. - * - * @param buffer A byte buffer contains the net packet data. - * @throws IOException If an I/O error has occurred. - */ - public void sendResponse(ByteBuffer buffer) throws IOException { - mResponse.process(buffer); - } - - /** - * Notify virtual gateway that no longer has data sent to the server. - */ - public void sendRequestFinished() { - } - - /** - * Notify virtual gateway that no longer has data sent to the client. - */ - public void sendResponseFinished() { - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGatewayFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGatewayFactory.java deleted file mode 100644 index bdfb9cc..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGatewayFactory.java +++ /dev/null @@ -1,37 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.gateway; - -import com.github.megatronking.netbare.net.Session; - -/** - * A factory that produces the {@link VirtualGateway}. - * - * @author Megatron King - * @since 2018年11月01日 23:23 - */ -public interface VirtualGatewayFactory { - - /** - * Returns a new {@link VirtualGateway} for the given arguments. - * - * @param session A network session. - * @param request A request connects to the remote server tunnel. - * @param response A response connects to VPN file descriptor - */ - VirtualGateway create(Session session, Request request, Response response); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/ContainerHttpInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/ContainerHttpInterceptor.java deleted file mode 100644 index 0c8e07f..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/ContainerHttpInterceptor.java +++ /dev/null @@ -1,196 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * One http virtual gateway may have multi http sessions, but we don't want to share interceptors - * between them. Use a container to manage different sessions, every session has independent - * interceptor instances. - * - * @author Megatron King - * @since 2019年1月6日 16:13 - */ -/* package */ class ContainerHttpInterceptor extends HttpInterceptor { - - private final Map mSessions; - private final HttpInterceptorsFactory mSubInterceptorsFactory; - - /* package */ ContainerHttpInterceptor(HttpInterceptorsFactory factory) { - this.mSubInterceptorsFactory = factory; - this.mSessions = new ConcurrentHashMap(); - } - - @Override - protected void intercept(@NonNull final HttpRequestChain chain, @NonNull ByteBuffer buffer) - throws IOException { - HttpRequest request = chain.request(); - Session session = findSessionById(request.id()); - session.request = request; - if (session.interceptors == null) { - session.interceptors = mSubInterceptorsFactory.create(); - } - new HttpContainerRequestChain(chain, session.interceptors).process(buffer); - } - - @Override - protected void intercept(@NonNull final HttpResponseChain chain, @NonNull ByteBuffer buffer) - throws IOException { - HttpResponse response = chain.response(); - Session session = findSessionById(response.id()); - session.response = response; - if (session.interceptors == null) { - session.interceptors = mSubInterceptorsFactory.create(); - } - new HttpContainerResponseChain(chain, session.interceptors).process(buffer); - } - - @Override - protected void onRequestFinished(@NonNull HttpRequest request) { - if (request instanceof HttpZygoteRequest) { - // This means the connection is down, finish all. - for (Session session : mSessions.values()) { - if (session.request != null && session.interceptors != null) { - for (HttpInterceptor interceptor : session.interceptors) { - interceptor.onRequestFinished(session.request); - } - } - } - mSessions.clear(); - } else { - Session session = mSessions.remove(request.id()); - if (session != null && session.interceptors != null) { - for (HttpInterceptor interceptor : session.interceptors) { - interceptor.onRequestFinished(session.request); - } - } - } - } - - @Override - protected void onResponseFinished(@NonNull HttpResponse response) { - if (response instanceof HttpZygoteResponse) { - // This means the connection is down, finish all. - for (Session session : mSessions.values()) { - if (session != null && session.response != null && session.interceptors != null) { - for (HttpInterceptor interceptor : session.interceptors) { - interceptor.onResponseFinished(session.response); - } - } - } - } else { - Session session = mSessions.remove(response.id()); - if (session != null && session.interceptors != null) { - for (HttpInterceptor interceptor : session.interceptors) { - interceptor.onResponseFinished(session.response); - } - } - } - } - - private Session findSessionById(String id) { - Session session; - if (mSessions.containsKey(id)) { - session = mSessions.get(id); - } else { - session = new Session(); - mSessions.put(id, session); - } - return session; - } - - private static final class Session { - - private HttpRequest request; - private HttpResponse response; - private List interceptors; - - } - - private static final class HttpContainerRequestChain extends HttpRequestChain { - - private final HttpRequestChain mChain; - private final List mInterceptors; - private final int mIndex; - - private HttpContainerRequestChain(HttpRequestChain chain, List interceptors) { - this(chain, interceptors, 0); - } - - private HttpContainerRequestChain(HttpRequestChain chain, List interceptors, - int index) { - super(chain.zygoteRequest(), interceptors, index); - this.mChain = chain; - this.mInterceptors = interceptors; - this.mIndex = index; - } - - @Override - public void process(ByteBuffer buffer) throws IOException { - if (mIndex>= mInterceptors.size()) { - mChain.process(buffer); - } else { - HttpInterceptor interceptor = mInterceptors.get(mIndex); - if (interceptor != null) { - interceptor.intercept(new HttpContainerRequestChain(mChain, mInterceptors, - mIndex + 1), buffer); - } - } - } - - } - - private static final class HttpContainerResponseChain extends HttpResponseChain { - - private final HttpResponseChain mChain; - private final List mInterceptors; - private final int mIndex; - - private HttpContainerResponseChain(HttpResponseChain chain, List interceptors) { - this(chain, interceptors, 0); - } - - private HttpContainerResponseChain(HttpResponseChain chain, List interceptors, - int index) { - super(chain.zygoteResponse(), interceptors, index); - this.mChain = chain; - this.mInterceptors = interceptors; - this.mIndex = index; - } - - @Override - public void process(ByteBuffer buffer) throws IOException { - if (mIndex>= mInterceptors.size()) { - mChain.process(buffer); - } else { - HttpInterceptor interceptor = mInterceptors.get(mIndex); - if (interceptor != null) { - interceptor.intercept(new HttpContainerResponseChain(mChain, mInterceptors, - mIndex + 1), buffer); - } - } - } - - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/Http2SniffInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/Http2SniffInterceptor.java deleted file mode 100644 index 5395640..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/Http2SniffInterceptor.java +++ /dev/null @@ -1,110 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareXLog; -import com.github.megatronking.netbare.http2.Http2; -import com.google.common.primitives.Bytes; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * Verifies the HTTP packet and determines whether it is a HTTP2 protocol packets. - * - * @author Megatron King - * @since 2019年1月5日 14:02 - */ -/* package */ class Http2SniffInterceptor extends HttpIndexInterceptor { - - private SSLRefluxCallback mCallback; - private NetBareXLog mLog; - - /* package */ Http2SniffInterceptor(SSLRefluxCallback callback) { - this.mCallback = callback; - } - - @Override - protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - if (index == 0) { - HttpRequest request = chain.request(); - if (mLog == null) { - mLog = new NetBareXLog(request.protocol(), request.ip(), request.port()); - } - // HTTP2 is forces to use SSL connection. - if (request.isHttps()) { - if (buffer.hasRemaining() && Bytes.indexOf(buffer.array(), - Http2.CONNECTION_PREFACE) == buffer.position()) { - mLog.i("Send a connection preface to remote server."); - request.session().protocol = HttpProtocol.HTTP_2; - if (buffer.remaining() == Http2.CONNECTION_PREFACE.length) { - // Skip preface frame data. - mCallback.onRequest(request, buffer); - return; - } else { - ByteBuffer prefaceBuffer = ByteBuffer.allocate(Http2.CONNECTION_PREFACE.length); - prefaceBuffer.put(Http2.CONNECTION_PREFACE); - prefaceBuffer.flip(); - mCallback.onRequest(request, prefaceBuffer); - // The remaining data continues. - buffer.position(buffer.position() + Http2.CONNECTION_PREFACE.length); - } - } - } - } - if (buffer.hasRemaining()) { - chain.process(buffer); - } - } - - @Override - protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - if (index == 0) { - HttpResponse response = chain.response(); - if (mLog == null) { - mLog = new NetBareXLog(response.protocol(), response.ip(), response.port()); - } - // HTTP2 is forces to use SSL connection. - if (response.isHttps()) { - if (buffer.hasRemaining() && Bytes.indexOf(buffer.array(), - Http2.CONNECTION_PREFACE) == buffer.position()) { - mLog.i("Receive a connection preface from remote server."); - response.session().protocol = HttpProtocol.HTTP_2; - if (buffer.remaining() == Http2.CONNECTION_PREFACE.length) { - // Skip preface frame data. - mCallback.onResponse(response, buffer); - return; - } else { - ByteBuffer prefaceBuffer = ByteBuffer.allocate(Http2.CONNECTION_PREFACE.length); - prefaceBuffer.put(Http2.CONNECTION_PREFACE); - prefaceBuffer.flip(); - mCallback.onResponse(response, prefaceBuffer); - // The remaining data continues. - buffer.position(buffer.position() + Http2.CONNECTION_PREFACE.length); - } - } - } - } - if (buffer.hasRemaining()) { - chain.process(buffer); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderParseInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderParseInterceptor.java deleted file mode 100644 index 05ec77e..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderParseInterceptor.java +++ /dev/null @@ -1,186 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareUtils; -import com.github.megatronking.netbare.NetBareXLog; -import com.github.megatronking.netbare.ip.Protocol; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -/** - * Parse HTTP request header part and response header part from HTTP packets. The parse result will - * be set to {@link HttpSession}. - * - * @author Megatron King - * @since 2018年12月09日 12:19 - */ -/* package */ final class HttpHeaderParseInterceptor extends HttpIndexInterceptor { - - private NetBareXLog mLog; - - @Override - protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - if (index> 0) { - chain.process(buffer); - return; - } - if (mLog == null) { - mLog = new NetBareXLog(Protocol.TCP, chain.request().ip(), chain.request().port()); - } - parseRequestHeader(chain.request().session(), buffer); - chain.process(buffer); - } - - @Override - protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - if (index> 0) { - chain.process(buffer); - return; - } - if (mLog == null) { - mLog = new NetBareXLog(Protocol.TCP, chain.response().ip(), chain.response().port()); - } - parseResponseHeader(chain.response().session(), buffer); - chain.process(buffer); - } - - private void parseRequestHeader(HttpSession session, ByteBuffer buffer) { - session.reqBodyOffset = buffer.remaining(); - String headerString = new String(buffer.array(), buffer.position(), buffer.remaining()); - String[] headers = headerString.split(NetBareUtils.LINE_END_REGEX); - String[] requestLine = headers[0].split(" "); - if (requestLine.length != 3) { - mLog.w("Unexpected http request line: " + headers[0]); - return; - } - // Method - HttpMethod method = HttpMethod.parse(requestLine[0]); - if (method == HttpMethod.UNKNOWN) { - mLog.w("Unknown http request method: " + requestLine[0]); - return; - } - session.method = method; - // Path - session.path = requestLine[1]; - // Http Protocol - HttpProtocol protocol = HttpProtocol.parse(requestLine[2]); - if (protocol == HttpProtocol.UNKNOWN) { - mLog.w("Unknown http request protocol: " + requestLine[0]); - return; - } - session.protocol = protocol; - - // Http request headers - if (headers.length <= 1) { - mLog.w("Unexpected http request headers."); - return; - } - for (int i = 1; i < headers.length; i++) { - String requestHeader = headers[i]; - // Reach the header end - if (requestHeader.isEmpty()) { - continue; - } - String[] nameValue = requestHeader.split(":"); - if (nameValue.length < 2) { - mLog.w("Unexpected http request header: " + requestHeader); - continue; - } - String name = nameValue[0].trim(); - String value = requestHeader.replaceFirst(nameValue[0] + ": ", "").trim(); - List header = session.requestHeaders.get(name); - if (header == null) { - header = new ArrayList(1); - session.requestHeaders.put(name, header); - } - header.add(value); - } - } - - private void parseResponseHeader(HttpSession session, ByteBuffer buffer) { - session.resBodyOffset = buffer.remaining(); - String headerString = new String(buffer.array(), buffer.position(), buffer.remaining()); - // In some condition, no request but has response, we set the method to unknown. - if (session.method == null) { - session.method = HttpMethod.UNKNOWN; - } - String[] headers = headerString.split(NetBareUtils.LINE_END_REGEX); - String[] responseLine = headers[0].split(" "); - if (responseLine.length < 2) { - mLog.w("Unexpected http response line: " + headers[0]); - return; - } - // Http Protocol - HttpProtocol protocol = HttpProtocol.parse(responseLine[0]); - if (protocol == HttpProtocol.UNKNOWN) { - mLog.w("Unknown http response protocol: " + responseLine[0]); - return; - } - if (session.protocol != protocol) { - // Protocol downgrade - if (session.protocol != null) { - mLog.w("Unmatched http protocol, request is " + session.protocol + - " but response is " + responseLine[0]); - } - session.protocol = protocol; - } - // Code - int code = NetBareUtils.parseInt(responseLine[1], -1); - if (code == -1) { - mLog.w("Unexpected http response code: " + responseLine[1]); - return; - } - session.code = code; - // Message - session.message = headers[0].replaceFirst(responseLine[0], "") - .replaceFirst(responseLine[1], "").trim(); - - // Http response headers - if (headers.length <= 1) { - mLog.w("Unexpected http response headers."); - return; - } - for (int i = 1; i < headers.length; i++) { - String responseHeader = headers[i]; - // Reach the header end - if (responseHeader.isEmpty()) { - continue; - } - String[] nameValue = responseHeader.split(":"); - if (nameValue.length < 2) { - mLog.w("Unexpected http response header: " + responseHeader); - continue; - } - String name = nameValue[0].trim(); - String value = responseHeader.replaceFirst(nameValue[0] + ": ", "").trim(); - List header = session.responseHeaders.get(name); - if (header == null) { - header = new ArrayList(1); - session.responseHeaders.put(name, header); - } - header.add(value); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSeparateInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSeparateInterceptor.java deleted file mode 100644 index fbe83ee..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSeparateInterceptor.java +++ /dev/null @@ -1,137 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareXLog; -import com.github.megatronking.netbare.NetBareUtils; -import com.github.megatronking.netbare.ip.Protocol; -import com.google.common.primitives.Bytes; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * Separate HTTP header part and body part into different packets. - * - * @author Megatron King - * @since 2018年12月08日 15:36 - */ -/* package */ final class HttpHeaderSeparateInterceptor extends HttpPendingInterceptor { - - private boolean mRequestHeaderHandled; - private boolean mResponseHeaderHandled; - - private NetBareXLog mLog; - - @Override - protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, int index) - throws IOException { - if (mLog == null) { - mLog = new NetBareXLog(Protocol.TCP, chain.request().ip(), chain.request().port()); - } - if (mRequestHeaderHandled) { - chain.process(buffer); - return; - } - if (!buffer.hasRemaining()) { - chain.process(buffer); - return; - } - buffer = mergeRequestBuffer(buffer); - // Check the part end line. - int headerEndIndex = Bytes.indexOf(buffer.array(), NetBareUtils.PART_END_BYTES); - if (headerEndIndex < 0) { - mLog.w("Http request header data is not enough."); - // Not found the part end line, maybe the data is not enough, wait next buffer coming. - pendRequestBuffer(buffer); - } else { - mRequestHeaderHandled = true; - // Check whether the header and the body are in the same buffer. - boolean hasMultiPart = headerEndIndex < buffer.limit() - NetBareUtils.PART_END_BYTES.length; - if (hasMultiPart) { - mLog.w("Multiple http request parts are founded."); - // Separate the header and body data to two buffers. - int offset = headerEndIndex + NetBareUtils.PART_END_BYTES.length; - ByteBuffer headerBuffer = ByteBuffer.wrap(buffer.array(), buffer.position(), offset); - // Allocate a new buffer, do not use wrap, different buffers will share the same array. - ByteBuffer bodyBuffer = ByteBuffer.allocate(buffer.limit() - offset); - bodyBuffer.put(buffer.array(), offset, buffer.limit() - offset); - bodyBuffer.flip(); - chain.process(headerBuffer); - chain.process(bodyBuffer); - } else { - chain.process(buffer); - } - } - } - - @Override - protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, int index) - throws IOException { - if (mLog == null) { - mLog = new NetBareXLog(Protocol.TCP, chain.response().ip(), chain.response().port()); - } - if (mResponseHeaderHandled) { - chain.process(buffer); - return; - } - if (!buffer.hasRemaining()) { - chain.process(buffer); - return; - } - buffer = mergeResponseBuffer(buffer); - // Check the part end line. - int headerEndIndex = Bytes.indexOf(buffer.array(), NetBareUtils.PART_END_BYTES); - if (headerEndIndex < 0) { - mLog.w("Http response header data is not enough."); - // Not found the part end line, maybe the data is not enough, wait next buffer coming. - pendResponseBuffer(buffer); - } else { - mResponseHeaderHandled = true; - // Check whether the header and the body are in the same buffer. - boolean hasMultiPart = headerEndIndex < buffer.limit() - NetBareUtils.PART_END_BYTES.length; - if (hasMultiPart) { - mLog.w("Multiple http response parts are founded."); - // Separate the header and body data to two buffers. - int offset = headerEndIndex + NetBareUtils.PART_END_BYTES.length; - ByteBuffer headerBuffer = ByteBuffer.wrap(buffer.array(), buffer.position(), offset); - // Allocate a new buffer, do not use wrap, different buffers will share the same array. - ByteBuffer bodyBuffer = ByteBuffer.allocate(buffer.limit() - offset); - bodyBuffer.put(buffer.array(), offset, buffer.limit() - offset); - bodyBuffer.flip(); - chain.process(headerBuffer); - chain.process(bodyBuffer); - } else { - chain.process(buffer); - } - } - } - - @Override - protected void onRequestFinished(@NonNull HttpRequest request) { - super.onRequestFinished(request); - mRequestHeaderHandled = false; - } - - @Override - protected void onResponseFinished(@NonNull HttpResponse response) { - super.onResponseFinished(response); - mResponseHeaderHandled = false; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSniffInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSniffInterceptor.java deleted file mode 100644 index d52d945..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSniffInterceptor.java +++ /dev/null @@ -1,174 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareLog; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * Detect the plaintext packet header to determine is it the HTTP protocol. - * - * @author Megatron King - * @since 2019年1月31日 16:13 - */ -/* package */ class HttpHeaderSniffInterceptor extends HttpIndexInterceptor { - - private final SSLRefluxCallback mCallback; - - private boolean mRealHttpProtocol; - - /* package */ HttpHeaderSniffInterceptor(SSLRefluxCallback callback) { - this.mCallback = callback; - } - - @Override - protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - if (!buffer.hasRemaining()) { - return; - } - if (chain.request().httpProtocol() != null) { - chain.process(buffer); - return; - } - if (index == 0) { - if (requestHeaderFirstByteNotPassed(buffer.get(buffer.position()))) { - mCallback.onRequest(chain.request(), buffer); - return; - } - // Sniff request header method - if (buffer.remaining()>= 7 && requestHeaderMethodNotPassed(buffer)) { - mCallback.onRequest(chain.request(), buffer); - return; - } - mRealHttpProtocol = true; - chain.process(buffer); - } else { - if (mRealHttpProtocol) { - chain.process(buffer); - } else { - mCallback.onRequest(chain.request(), buffer); - } - } - } - - @Override - protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - if (!buffer.hasRemaining()) { - return; - } - if (chain.response().httpProtocol() != null) { - chain.process(buffer); - return; - } - if (index == 0) { - if (responseHeaderFirstByteNotPassed(buffer.get(buffer.position()))) { - mCallback.onResponse(chain.response(), buffer); - return; - } - // Sniff response header protocol - if (buffer.remaining()>= 8 && responseHeaderProtocolNotPassed(buffer)) { - mCallback.onResponse(chain.response(), buffer); - return; - } - mRealHttpProtocol = true; - chain.process(buffer); - } else { - if (mRealHttpProtocol) { - chain.process(buffer); - } else { - mCallback.onResponse(chain.response(), buffer); - } - } - } - - private boolean requestHeaderFirstByteNotPassed(byte first) { - switch (first) { - // GET - case 'G': - // HEAD - case 'H': - // POST, PUT, PATCH - case 'P': - // DELETE - case 'D': - // OPTIONS - case 'O': - // TRACE - case 'T': - // CONNECT - case 'C': - return false; - default: - // Unknown first byte data. - NetBareLog.w("Unknown first request header byte : " + first); - break; - } - return true; - } - - private boolean requestHeaderMethodNotPassed(ByteBuffer buffer) { - String headerMethod = new String(buffer.array(), buffer.position(), - buffer.position() + 7); - for (HttpMethod method : HttpMethod.values()) { - if (method == HttpMethod.UNKNOWN) { - continue; - } - if (headerMethod.startsWith(method.name())) { - return false; - } - } - NetBareLog.w("Unknown request header method : " + headerMethod); - return true; - } - - private boolean responseHeaderFirstByteNotPassed(byte first) { - switch (first) { - // h2 - case 'h': - // HTTP1.x - case 'H': - return false; - default: - // Unknown first byte data. - NetBareLog.w("Unknown first response header byte : " + first); - break; - } - return true; - } - - private boolean responseHeaderProtocolNotPassed(ByteBuffer buffer) { - String headerProtocol = new String(buffer.array(), buffer.position(), - buffer.position() + 8); - for (HttpProtocol protocol : HttpProtocol.values()) { - if (protocol == HttpProtocol.UNKNOWN || protocol == HttpProtocol.H2_PRIOR_KNOWLEDGE - || protocol == HttpProtocol.SPDY_3 || protocol == HttpProtocol.QUIC) { - continue; - } - if (headerProtocol.startsWith(protocol.toString())) { - return false; - } - } - NetBareLog.w("Unknown response header protocol : " + headerProtocol); - return true; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpId.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpId.java deleted file mode 100644 index aa168ae..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpId.java +++ /dev/null @@ -1,42 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import java.util.UUID; - -/** - * Regenerated http unique id for multi-sessions in one connection. - * - * @author Megatron King - * @since 2018年12月19日 16:35 - */ -public class HttpId { - - public String id; - public long time; - public int streamId; - - public HttpId() { - this(-1); - } - - public HttpId(int streamId) { - this.id = UUID.randomUUID().toString(); - this.time = System.currentTimeMillis(); - this.streamId = streamId; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpIndexInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpIndexInterceptor.java deleted file mode 100644 index 662b6e3..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpIndexInterceptor.java +++ /dev/null @@ -1,86 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * Add the index parameter in the {@link #intercept(HttpRequestChain, ByteBuffer)} and - * {@link #intercept(HttpResponseChain, ByteBuffer)}, it indicates the packet index in the session. - *

                - * The index will be reset when the session finished. - *

                - * - * @author Megatron King - * @since 2018年12月03日 21:00 - */ -public abstract class HttpIndexInterceptor extends HttpInterceptor { - - private int mRequestIndex; - private int mResponseIndex; - - /** - * The same like {@link #intercept(HttpRequestChain, ByteBuffer)}. - * - * @param chain The request chain, call {@linkplain HttpRequestChain#process(ByteBuffer)} to - * delivery the packet. - * @param buffer A nio buffer contains the packet data. - * @param index The packet index, started from 0. - * @throws IOException If an I/O error has occurred. - */ - protected abstract void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException; - - /** - * The same like {@link #intercept(HttpResponseChain, ByteBuffer)}. - * - * @param chain The response chain, call {@linkplain HttpResponseChain#process(ByteBuffer)} to - * delivery the packet. - * @param buffer A nio buffer contains the packet data. - * @param index The packet index, started from 0. - * @throws IOException If an I/O error has occurred. - */ - protected abstract void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException; - - @Override - protected final void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer) - throws IOException { - intercept(chain, buffer, mRequestIndex++); - } - - @Override - protected final void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer) - throws IOException { - intercept(chain, buffer, mResponseIndex++); - } - - @Override - protected void onRequestFinished(@NonNull HttpRequest request) { - super.onRequestFinished(request); - mRequestIndex = 0; - } - - @Override - protected void onResponseFinished(@NonNull HttpResponse response) { - super.onResponseFinished(response); - mResponseIndex = 0; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptor.java deleted file mode 100644 index 4ea6f26..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptor.java +++ /dev/null @@ -1,111 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.gateway.Interceptor; -import com.github.megatronking.netbare.gateway.Request; -import com.github.megatronking.netbare.gateway.RequestChain; -import com.github.megatronking.netbare.gateway.Response; -import com.github.megatronking.netbare.gateway.ResponseChain; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * A specific interceptor designed for {@link HttpVirtualGateway}, it focuses on the http protocol - * packets. The interceptor is an implement of {@link Interceptor}, methods are thread-safety and - * runs in local proxy server threads. - * - *

                - * Use {@link HttpInterceptorFactory} to create an http interceptor instance. - *

                - * - * @author Megatron King - * @since 2018年11月15日 19:40 - */ -public abstract class HttpInterceptor implements Interceptor { - - /** - * Intercept http request packet, and delivery it to next interceptor or the terminal. - *

                - * Remember do not block this method for a long time, because all the connections share the - * same thread. - *

                - * - * @param chain The request chain, call {@linkplain HttpRequestChain#process(ByteBuffer)} to - * delivery the packet. - * @param buffer A nio buffer contains the packet data. - * @throws IOException If an I/O error has occurred. - */ - protected abstract void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer) - throws IOException; - - /** - * Intercept http response packet, and delivery it to next interceptor or the terminal. - *

                - * Remember do not block this method for a long time, because all the connections share the - * same thread. - *

                - * - * @param chain The response chain, call {@linkplain HttpResponseChain#process(ByteBuffer)} to - * delivery the packet. - * @param buffer A nio buffer contains the packet data. - * @throws IOException If an I/O error has occurred. - */ - protected abstract void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer) - throws IOException; - - /** - * Invoked when a session's request has finished. It means the client has no more data sent to - * server in this session, and it might invoked multi times if a connection is keep-alive. - * - * @param request The request. - */ - protected void onRequestFinished(@NonNull HttpRequest request) { - } - - /** - * Invoked when a session's response has finished. It means the server has no more data sent to - * client in this session, and it might invoked multi times if a connection is keep-alive. - * - * @param response The response. - */ - protected void onResponseFinished(@NonNull HttpResponse response) { - } - - @Override - public final void intercept(@NonNull RequestChain chain, @NonNull ByteBuffer buffer) { - // Override the abstract method instead. - } - - @Override - public final void intercept(@NonNull ResponseChain chain, @NonNull ByteBuffer buffer) { - // Override the abstract method instead. - } - - @Override - public final void onRequestFinished(@NonNull Request request) { - // Override the abstract method instead. - } - - @Override - public final void onResponseFinished(@NonNull Response response) { - // Override the abstract method instead. - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorFactory.java deleted file mode 100644 index 3816f23..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.gateway.InterceptorFactory; - -/** - * Factory used by developer to create their own interceptor for {@link HttpVirtualGateway}. - * - * @author Megatron King - * @since 2018年11月15日 21:58 - */ -public interface HttpInterceptorFactory extends InterceptorFactory { - - /** - * Creates a http interceptor instance and immediately returns it, it must not be null. - * - * @return A newly created http interceptor. - */ - @NonNull - @Override - HttpInterceptor create(); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorsFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorsFactory.java deleted file mode 100644 index 62a778e..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorsFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import java.util.List; - -/** - * Factory creates a collection {@link HttpInterceptor}s. - * - * @author Megatron King - * @since 2018年11月15日 21:58 - */ -/* package */ interface HttpInterceptorsFactory { - - /** - * Creates a collection of http interceptor instances and immediately returns it, - * it must not be null. - * - * @return A http interceptor list. - */ - @NonNull - List create(); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMethod.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMethod.java deleted file mode 100644 index 4d2b937..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMethod.java +++ /dev/null @@ -1,101 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -/** - * HTTP defines a set of request methods to indicate the desired action to be performed for a given - * resource. - * - * See https://tools.ietf.org/html/rfc7231#section-4 - * - * @author Megatron King - * @since 2018年10月15日 19:59 - */ -public enum HttpMethod { - - /** - * It means NetBare does not know the method. - */ - UNKNOWN, - - /** - * The GET method requests a representation of the specified resource. Requests using GET - * should only retrieve data. - */ - GET, - - /** - * The HEAD method asks for a response identical to that of a GET request, but without the - * response body. - */ - HEAD, - - /** - * The POST method is used to submit an entity to the specified resource, often causing - * a change in state or side effects on the server. - */ - POST, - - /** - * The PUT method replaces all current representations of the target resource with the request - * payload. - */ - PUT, - - /** - * The DELETE method deletes the specified resource. - */ - DELETE, - - /** - * The CONNECT method establishes a tunnel to the server identified by the target resource. - */ - CONNECT, - - /** - * The OPTIONS method is used to describe the communication options for the target resource. - */ - OPTIONS, - - /** - * The TRACE method performs a message loop-back test along the path to the target resource. - */ - TRACE, - - /** - * The PATCH method is used to apply partial modifications to a resource. - */ - PATCH; - - /** - * Returns the request method enum. - * - * @param methodValue A string method presents in request line. - * @return A HttpMethod enum. - */ - @NonNull - public static HttpMethod parse(@NonNull String methodValue) { - for (HttpMethod method : values()) { - if (method.name().equals(methodValue)) { - return method; - } - } - return UNKNOWN; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMultiplexInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMultiplexInterceptor.java deleted file mode 100644 index e3d730d..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMultiplexInterceptor.java +++ /dev/null @@ -1,89 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareXLog; -import com.github.megatronking.netbare.ip.Protocol; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * If a HTTP connection is keep-alive, there will be multiple sessions go through the same virtual - * gateway. Those sessions are saw as one and not distinguished, this will increase the difficulty - * of interception. We use this interceptor to separate them into independent sessions and - * intercept them one by one. - * - * @author Megatron King - * @since 2018年12月15日 15:17 - */ -/* package */ class HttpMultiplexInterceptor extends HttpIndexInterceptor { - - private final HttpZygoteRequest mZygoteRequest; - private final HttpZygoteResponse mZygoteResponse; - - private int mResponseIndex; - private NetBareXLog mLog; - - private boolean mWebSocket; - - /* package */ HttpMultiplexInterceptor(HttpZygoteRequest zygoteRequest, - HttpZygoteResponse zygoteResponse) { - this.mZygoteRequest = zygoteRequest; - this.mZygoteResponse = zygoteResponse; - } - - @Override - protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - if (chain.request().httpProtocol() != HttpProtocol.HTTP_1_1) { - chain.process(buffer); - return; - } - // Check the protocol is web socket - if (!mWebSocket) { - mWebSocket = mZygoteResponse.isWebSocket(); - } - if (mResponseIndex> 0 && !mWebSocket) { - if (mLog == null) { - mLog = new NetBareXLog(Protocol.TCP, chain.request().ip(), chain.request().port()); - } - mResponseIndex = 0; - mLog.w("Multiplex is found in one connection."); - // Multiplex sessions. - HttpId newId = new HttpId(); - mZygoteRequest.zygote(newId); - mZygoteResponse.zygote(newId); - } - chain.process(buffer); - } - - @Override - protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - mResponseIndex++; - chain.process(buffer); - } - - @Override - protected void onResponseFinished(@NonNull HttpResponse response) { - mResponseIndex = 0; - super.onResponseFinished(response); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpPendingInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpPendingInterceptor.java deleted file mode 100644 index e8b7c6f..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpPendingInterceptor.java +++ /dev/null @@ -1,123 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -/** - * An abstract interceptor provides multi-apis for packet pending. The packet will be stored in a - * queue, and you can merge them with another packet. - * - * @author Megatron King - * @since 2018年12月09日 12:07 - */ -public abstract class HttpPendingInterceptor extends HttpIndexInterceptor { - - private List mRequestPendingBuffers; - private List mResponsePendingBuffers; - - /** - * Constructs a {@link HttpPendingInterceptor} instance. - */ - public HttpPendingInterceptor() { - mRequestPendingBuffers = new ArrayList(); - mResponsePendingBuffers = new ArrayList(); - } - - @Override - protected void onRequestFinished(@NonNull HttpRequest request) { - super.onRequestFinished(request); - mRequestPendingBuffers.clear(); - } - - @Override - protected void onResponseFinished(@NonNull HttpResponse response) { - super.onResponseFinished(response); - mResponsePendingBuffers.clear(); - } - - /** - * Pend a request packet buffer to waiting queue. - * - * @param buffer A request packet. - */ - protected void pendRequestBuffer(ByteBuffer buffer) { - mRequestPendingBuffers.add(buffer); - } - - /** - * Pend a response packet buffer to waiting queue. - * - * @param buffer A response packet. - */ - protected void pendResponseBuffer(ByteBuffer buffer) { - mResponsePendingBuffers.add(buffer); - } - - /** - * Merge all the request pending buffers and a given buffer, and output a new buffer which - * contains all data. The pending buffers will be clear after the merge action. - * - * @param buffer A fresh packet buffer. - * @return A new buffer. - */ - protected ByteBuffer mergeRequestBuffer(ByteBuffer buffer) { - return merge(mRequestPendingBuffers, buffer); - } - - /** - * Merge all the response pending buffers and a given buffer, and output a new buffer which - * contains all data. The pending buffers will be clear after the merge action. - * - * @param buffer A fresh packet buffer. - * @return A new buffer. - */ - protected ByteBuffer mergeResponseBuffer(ByteBuffer buffer) { - return merge(mResponsePendingBuffers, buffer); - } - - private ByteBuffer merge(List pendingBuffers, ByteBuffer buffer) { - if (!pendingBuffers.isEmpty()) { - int total = 0; - for (ByteBuffer pendingBuffer : pendingBuffers) { - total += pendingBuffer.remaining(); - } - total += buffer.remaining(); - - // Merge elder buffer first. - int offset = 0; - byte[] array = new byte[total]; - for (ByteBuffer pendingBuffer : pendingBuffers) { - System.arraycopy(pendingBuffer.array(), pendingBuffer.position(), array, offset, - pendingBuffer.remaining()); - offset += pendingBuffer.remaining(); - } - - // Merge the incoming buffer - System.arraycopy(buffer.array(), buffer.position(), array, offset, buffer.remaining()); - - buffer = ByteBuffer.wrap(array); - // Clear all data. - pendingBuffers.clear(); - } - return buffer; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpProtocol.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpProtocol.java deleted file mode 100644 index dca37ea..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpProtocol.java +++ /dev/null @@ -1,115 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -/** - * Http protocols that NetBare defined. - * - * @author Megatron King - * @since 2018年10月15日 19:50 - */ -public enum HttpProtocol { - - /** - * It means NetBare does not know the protocol. - */ - UNKNOWN("unknown"), - - /** - * An obsolete plaintext framing that does not use persistent sockets by default. - */ - HTTP_1_0("HTTP/1.0"), - - /** - * A plaintext framing that includes persistent connections. - * - *

                This version of OkHttp implements RFC - * 7230, and tracks revisions to that spec. - */ - HTTP_1_1("HTTP/1.1"), - - /** - * Chromium's binary-framed protocol that includes header compression, multiplexing multiple - * requests on the same socket, and server-push. HTTP/1.1 semantics are layered on SPDY/3. - */ - SPDY_3("spdy/3.1"), - - /** - * The IETF's binary-framed protocol that includes header compression, multiplexing multiple - * requests on the same socket, and server-push. HTTP/1.1 semantics are layered on HTTP/2. - */ - HTTP_2("h2"), - - /** - * Cleartext HTTP/2 with no "upgrade" round trip. This option requires the client to have prior - * knowledge that the server supports cleartext HTTP/2. - * - * @see Starting HTTP/2 with Prior - * Knowledge - */ - H2_PRIOR_KNOWLEDGE("h2_prior_knowledge"), - - /** - * QUIC (Quick UDP Internet Connection) is a new multiplexed and secure transport atop UDP, - * designed from the ground up and optimized for HTTP/2 semantics. - * HTTP/1.1 semantics are layered on HTTP/2. - */ - QUIC("quic"); - - private final String protocol; - - HttpProtocol(String protocol) { - this.protocol = protocol; - } - - /** - * Returns the protocol identified by {@code protocol}. - * - * @param protocol A string protocol presents in request line and status line. - * @return A HttpProtocol enum. - */ - @NonNull - public static HttpProtocol parse(@NonNull String protocol) { - if (protocol.equalsIgnoreCase(HTTP_1_0.protocol)) { - return HTTP_1_0; - } else if (protocol.equalsIgnoreCase(HTTP_1_1.protocol)) { - return HTTP_1_1; - } else if (protocol.equalsIgnoreCase(H2_PRIOR_KNOWLEDGE.protocol)) { - return H2_PRIOR_KNOWLEDGE; - } else if (protocol.equalsIgnoreCase(HTTP_2.protocol)) { - return HTTP_2; - } else if (protocol.equalsIgnoreCase(SPDY_3.protocol)) { - return SPDY_3; - } else if (protocol.equalsIgnoreCase(QUIC.protocol)) { - return QUIC; - } else { - return UNKNOWN; - } - } - - /** - * Returns the protocol string value rather than it's name. - * - * @return Protocol value. - */ - @Override - public String toString() { - return this.protocol; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequest.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequest.java deleted file mode 100644 index e0107c6..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequest.java +++ /dev/null @@ -1,206 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import com.github.megatronking.netbare.gateway.Request; -import com.github.megatronking.netbare.http2.Http2Settings; -import com.github.megatronking.netbare.ip.Protocol; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Map; - -/** - * It is an implementation of {@link Request} represents the HTTP protocol. Instances of this - * class are not immutable. - * - * @author Megatron King - * @since 2018年11月11日 23:37 - */ -public class HttpRequest extends Request { - - private Request mRequest; - - private HttpId mHttpId; - private HttpSession mSession; - - /* package */ HttpRequest(Request request, HttpSession session) { - this(request, null, session); - } - - /* package */ HttpRequest(Request request, HttpId httpId, HttpSession session) { - this.mRequest = request; - this.mHttpId = httpId; - this.mSession = session; - } - - /* package */ HttpSession session() { - return mSession; - } - - @Override - public void process(ByteBuffer buffer) throws IOException { - mRequest.process(buffer); - } - - @Override - public String id() { - return mHttpId != null ? mHttpId.id : mRequest.id(); - } - - @Override - public long time() { - return mHttpId != null ? mHttpId.time : mRequest.time(); - } - - @Override - public int uid() { - return mRequest.uid(); - } - - @Override - public String ip() { - return mRequest.ip(); - } - - @Override - public int port() { - return mRequest.port(); - } - - @Override - public Protocol protocol() { - return mRequest.protocol(); - } - - @Override - public String host() { - return mRequest.host(); - } - - /** - * Returns the request method for this request. - * - * @return The request method. - */ - public HttpMethod method() { - return mSession.method; - } - - /** - * Returns this request's http protocol, such as {@link HttpProtocol#HTTP_1_1} or - * {@link HttpProtocol#HTTP_1_0}. - * - * @return The request protocol. - */ - public HttpProtocol httpProtocol() { - return mSession.protocol; - } - - /** - * Returns this request's path. - * - * @return The request path. - */ - public String path() { - return mSession.path; - } - - /** - * Whether the request is a HTTPS request. - * - * @return HTTPS returns true. - */ - public boolean isHttps() { - return mSession.isHttps; - } - - /** - * Returns this request's URL. - * - * @return The request URL. - */ - public String url() { - String path = path() == null ? "" : path(); - return (isHttps() ? "https://" : "http://") + host() + path; - } - - /** - * Returns this request's headers. - * - * @return A map of headers. - */ - public Map> requestHeaders() { - return mSession.requestHeaders; - } - - /** - * Returns this request's header values by name. - * - * @param name A header name. - * @return A collection of header values. - */ - public List requestHeader(String name) { - return requestHeaders().get(name); - } - - /** - * Returns the offset of request body's starting index in request data. - * - * @return Offset of request body. - */ - public int requestBodyOffset() { - return mSession.reqBodyOffset; - } - - /** - * Returns the HTTP/2 stream id. - * - * @return A stream id. - */ - public int streamId() { - return mHttpId != null ? mHttpId.streamId : -1; - } - - /** - * Returns the HTTP/2 client settings. - * - * @return Client settings. - */ - public Http2Settings clientHttp2Settings() { - return mSession.clientHttp2Settings; - } - - /** - * Returns the HTTP/2 peer settings. - * - * @return Client settings. - */ - public Http2Settings peerHttp2Settings() { - return mSession.peerHttp2Settings; - } - - /** - * Whether the current HTTP2 request stream is end. - * - * @return End is true. - */ - public boolean requestStreamEnd() { - return mSession.requestStreamEnd; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequestChain.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequestChain.java deleted file mode 100644 index df42e13..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequestChain.java +++ /dev/null @@ -1,65 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.gateway.InterceptorChain; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; - -/** - * Http request chain, responsible for intercepting http request packets. - * - * @author Megatron King - * @since 2018年11月16日 23:21 - */ -public class HttpRequestChain extends InterceptorChain { - - private HttpZygoteRequest mZygoteRequest; - - /* package */ HttpRequestChain(HttpZygoteRequest request, List interceptors) { - this(request, interceptors, 0); - } - - /* package */ HttpRequestChain(HttpZygoteRequest request, List interceptors, - int index) { - super(request, interceptors, index); - this.mZygoteRequest = request; - } - - HttpZygoteRequest zygoteRequest() { - return mZygoteRequest; - } - - @Override - protected void processNext(ByteBuffer buffer, HttpRequest request, - List interceptors, int index) throws IOException { - HttpInterceptor interceptor = interceptors.get(index); - if (interceptor != null) { - interceptor.intercept(new HttpRequestChain(mZygoteRequest, interceptors, ++index), buffer); - } - } - - @NonNull - public HttpRequest request() { - HttpRequest active = mZygoteRequest.getActive(); - return active != null ? active : mZygoteRequest; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponse.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponse.java deleted file mode 100644 index 94658ea..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponse.java +++ /dev/null @@ -1,278 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import com.github.megatronking.netbare.gateway.Response; -import com.github.megatronking.netbare.http2.Http2Settings; -import com.github.megatronking.netbare.ip.Protocol; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Map; - -/** - * It is an implementation of {@link Response} represents the HTTP protocol. Instances of this - * class are not immutable. - * - * @author Megatron King - * @since 2018年11月11日 23:37 - */ -public class HttpResponse extends Response { - - private Response mResponse; - - private HttpId mHttpId; - private HttpSession mSession; - - /* package */ HttpResponse(Response response, HttpSession session) { - this(response, null, session); - } - - /* package */ HttpResponse(Response response, HttpId httpId, HttpSession session) { - this.mResponse = response; - this.mHttpId = httpId; - this.mSession = session; - } - - /* package */ HttpSession session() { - return mSession; - } - - @Override - public void process(ByteBuffer buffer) throws IOException { - mResponse.process(buffer); - } - - @Override - public String id() { - return mHttpId != null ? mHttpId.id : mResponse.id(); - } - - @Override - public long time() { - return mHttpId != null ? mHttpId.time : mResponse.time(); - } - - @Override - public int uid() { - return mResponse.uid(); - } - - @Override - public String ip() { - return mResponse.ip(); - } - - @Override - public int port() { - return mResponse.port(); - } - - @Override - public Protocol protocol() { - return mResponse.protocol(); - } - - @Override - public String host() { - return mResponse.host(); - } - - /** - * Returns the request method for this request. - * - * @return The request method. - */ - public HttpMethod method() { - return mSession.method; - } - - /** - * Returns this response's http protocol, such as {@link HttpProtocol#HTTP_1_1} or - * {@link HttpProtocol#HTTP_1_0}. - * - * @return The response protocol. - */ - public HttpProtocol httpProtocol() { - return mSession.protocol; - } - - /** - * Returns this request's path. - * - * @return The request path. - */ - public String path() { - return mSession.path; - } - - /** - * Whether the request is a HTTPS request. - * - * @return HTTPS returns true. - */ - public boolean isHttps() { - return mSession.isHttps; - } - - /** - * Whether the request is a web socket protocol. - * - * @return Web socket protocol returns true. - */ - public boolean isWebSocket() { - if (mSession.code != 101) { - return false; - } - List upgradeHeaderValues = null; - for (Map.Entry> entry : mSession.responseHeaders.entrySet()) { - if ("upgrade".equalsIgnoreCase(entry.getKey())) { - upgradeHeaderValues = entry.getValue(); - } - } - if (upgradeHeaderValues == null || upgradeHeaderValues.isEmpty()) { - return false; - } - for (String value : upgradeHeaderValues) { - if ("websocket".equalsIgnoreCase(value)) { - return true; - } - } - return false; - } - - /** - * Returns this request's URL. - * - * @return The request URL. - */ - public String url() { - String path = path() == null ? "" : path(); - return (isHttps() ? "https://" : "http://") + host() + path; - } - - /** - * Returns this request's headers. - * - * @return A map of headers. - */ - public Map> requestHeaders() { - return mSession.requestHeaders; - } - - /** - * Returns this response's headers. - * - * @return A map of headers. - */ - public Map> responseHeaders() { - return mSession.responseHeaders; - } - - /** - * Returns this request's header values by name. - * - * @param name A header name. - * @return A collection of header values. - */ - public List requestHeader(String name) { - return requestHeaders().get(name); - } - - /** - * Returns this response's header values by name. - * - * @param name A header name. - * @return A collection of header values. - */ - public List responseHeader(String name) { - return responseHeaders().get(name); - } - - /** - * Returns the HTTP status code. - * - * @return Status code. - */ - public int code() { - return mSession.code; - } - - /** - * Returns the HTTP status message. - * - * @return Status message. - */ - public String message() { - return mSession.message; - } - - /** - * Returns the offset of request body's starting index in request data. - * - * @return Offset of request body. - */ - public int requestBodyOffset() { - return mSession.reqBodyOffset; - } - - /** - * Returns the offset of response body's starting index in request data. - * - * @return Offset of request body. - */ - public int responseBodyOffset() { - return mSession.resBodyOffset; - } - - /** - * Returns the HTTP2 stream id. - * - * @return A stream id. - */ - public int streamId() { - return mHttpId != null ? mHttpId.streamId : -1; - } - - /** - * Returns the HTTP/2 client settings. - * - * @return Client settings. - */ - public Http2Settings clientHttp2Settings() { - return mSession.clientHttp2Settings; - } - - /** - * Returns the HTTP/2 peer settings. - * - * @return Client settings. - */ - public Http2Settings peerHttp2Settings() { - return mSession.peerHttp2Settings; - } - - /** - * Whether the current HTTP2 response stream is end. - * - * @return End is true. - */ - public boolean responseStreamEnd() { - return mSession.responseStreamEnd; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponseChain.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponseChain.java deleted file mode 100644 index ca813d9..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponseChain.java +++ /dev/null @@ -1,65 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.gateway.InterceptorChain; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; - -/** - * Http response chain, responsible for intercepting http response packets. - * - * @author Megatron King - * @since 2018年11月16日 23:21 - */ -public class HttpResponseChain extends InterceptorChain { - - private HttpZygoteResponse mZygoteResponse; - - /* package */ HttpResponseChain(HttpZygoteResponse response, List interceptors) { - this(response, interceptors, 0); - } - - /* package */ HttpResponseChain(HttpZygoteResponse response, List interceptors, - int index) { - super(response, interceptors, index); - this.mZygoteResponse = response; - } - - HttpZygoteResponse zygoteResponse() { - return mZygoteResponse; - } - - @Override - protected void processNext(ByteBuffer buffer, HttpResponse response, - List interceptors, int index) throws IOException { - HttpInterceptor interceptor = interceptors.get(index); - if (interceptor != null) { - interceptor.intercept(new HttpResponseChain(mZygoteResponse, interceptors, ++index), buffer); - } - } - - @NonNull - public HttpResponse response() { - HttpResponse active = mZygoteResponse.getActive(); - return active != null ? active : mZygoteResponse; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSession.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSession.java deleted file mode 100644 index 1dc17ba..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSession.java +++ /dev/null @@ -1,48 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import com.github.megatronking.netbare.http2.Http2Settings; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Provides HTTP protocol session information. - * - * @author Megatron King - * @since 2018年11月10日 11:56 - */ -/* package */ class HttpSession { - - boolean isHttps; - HttpProtocol protocol; - HttpMethod method; - String path; - Map> requestHeaders = new LinkedHashMap(); - Map> responseHeaders = new LinkedHashMap(); - int code; - String message; - int reqBodyOffset; - int resBodyOffset; - // Belows is for HTTP2 - Http2Settings clientHttp2Settings; - Http2Settings peerHttp2Settings; - boolean requestStreamEnd; - boolean responseStreamEnd; - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSessionFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSessionFactory.java deleted file mode 100644 index 5a58f36..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSessionFactory.java +++ /dev/null @@ -1,46 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import java.util.HashMap; -import java.util.Map; - -/** - * A factory creates {@link HttpSession} instance by id. - * - * @author Megatron King - * @since 2019年1月6日 19:42 - */ -/* package */ class HttpSessionFactory { - - private final Map mHttpSession; - - /* package */ HttpSessionFactory() { - mHttpSession = new HashMap(1); - } - - HttpSession create(String id) { - HttpSession httpSession; - if (mHttpSession.containsKey(id)) { - httpSession = mHttpSession.get(id); - } else { - httpSession = new HttpSession(); - mHttpSession.put(id, httpSession); - } - return httpSession; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSniffInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSniffInterceptor.java deleted file mode 100644 index b0953ca..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSniffInterceptor.java +++ /dev/null @@ -1,111 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.ssl.SSLCodec; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * A fronted interceptor verifies the first net packet in order to determine whether it is a HTTP - * protocol packet. If the packet is not a valid HTTP packet, it will be sent to tunnel directly, - * otherwise sent to the next interceptor. - * - * @author Megatron King - * @since 2018年12月04日 11:58 - */ -/* package */ final class HttpSniffInterceptor extends HttpIndexInterceptor { - - private static final int TYPE_HTTP = 1; - private static final int TYPE_HTTPS = 2; - private static final int TYPE_INVALID = 3; - - private final HttpSession mSession; - - private int mType; - - /* package */ HttpSniffInterceptor(HttpSession session) { - this.mSession = session; - } - - @Override - protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - if (index == 0) { - mType = chain.request().host() == null ? TYPE_INVALID : verifyHttpType(buffer); - } - if (mType == TYPE_HTTPS) { - mSession.isHttps = true; - } - if (mType == TYPE_INVALID) { - chain.processFinal(buffer); - return; - } - chain.process(buffer); - } - - @Override - protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - if (mType == TYPE_INVALID) { - chain.processFinal(buffer); - return; - } - chain.process(buffer); - } - - private int verifyHttpType(ByteBuffer buffer) { - if (!buffer.hasRemaining()) { - return TYPE_INVALID; - } - byte first = buffer.get(buffer.position()); - switch (first) { - // GET - case 'G': - // HEAD - case 'H': - // POST, PUT, PATCH - case 'P': - // DELETE - case 'D': - // OPTIONS - case 'O': - // TRACE - case 'T': - // CONNECT - case 'C': - return TYPE_HTTP; - // HTTPS - case SSLCodec.SSL_CONTENT_TYPE_ALERT: - case SSLCodec.SSL_CONTENT_TYPE_APPLICATION_DATA: - case SSLCodec.SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC: - case SSLCodec.SSL_CONTENT_TYPE_EXTENSION_HEARTBEAT: - case SSLCodec.SSL_CONTENT_TYPE_HANDSHAKE: - return TYPE_HTTPS; - default: - // Unknown first byte data. - NetBareLog.e("Unknown first request byte : " + first); - break; - } - return TYPE_INVALID; - } - - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGateway.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGateway.java deleted file mode 100644 index ce85481..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGateway.java +++ /dev/null @@ -1,135 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.gateway.Request; -import com.github.megatronking.netbare.gateway.Response; -import com.github.megatronking.netbare.gateway.SpecVirtualGateway; -import com.github.megatronking.netbare.gateway.VirtualGateway; -import com.github.megatronking.netbare.http2.Http2DecodeInterceptor; -import com.github.megatronking.netbare.http2.Http2EncodeInterceptor; -import com.github.megatronking.netbare.ip.Protocol; -import com.github.megatronking.netbare.net.Session; -import com.github.megatronking.netbare.ssl.JKS; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -/** - * A {@link VirtualGateway} that is responsible for HTTP(S) packets interception. It integrates - * several internal {@link HttpInterceptor}s to decode and parse HTTP(S) packets. And also it - * supports extensional {@link HttpInterceptor}s. Use {@link HttpVirtualGatewayFactory} to - * create an instance. - * - * @author Megatron King - * @since 2018年11月20日 23:43 - */ -/* package */ class HttpVirtualGateway extends SpecVirtualGateway { - - private HttpZygoteRequest mHttpZygoteRequest; - private HttpZygoteResponse mHttpZygoteResponse; - - private List mInterceptors; - - /* package */ HttpVirtualGateway(Session session, Request request, Response response, JKS jks, - final List factories) { - super(Protocol.TCP, session, request, response); - - HttpSessionFactory sessionFactory = new HttpSessionFactory(); - this.mHttpZygoteRequest = new HttpZygoteRequest(request, sessionFactory); - this.mHttpZygoteResponse = new HttpZygoteResponse(response, sessionFactory); - - // Add default interceptors. - SSLCodecInterceptor codecInterceptor = new SSLCodecInterceptor(jks, request, response); - this.mInterceptors = new ArrayList(8); - - mInterceptors.add(new HttpSniffInterceptor(sessionFactory.create(session.id))); - mInterceptors.add(codecInterceptor); - mInterceptors.add(new Http2SniffInterceptor(codecInterceptor)); - mInterceptors.add(new Http2DecodeInterceptor(codecInterceptor, mHttpZygoteRequest, mHttpZygoteResponse)); - mInterceptors.add(new HttpMultiplexInterceptor(mHttpZygoteRequest, mHttpZygoteResponse)); - mInterceptors.add(new HttpHeaderSniffInterceptor(codecInterceptor)); - mInterceptors.add(new ContainerHttpInterceptor(new HttpInterceptorsFactory() { - @NonNull - @Override - public List create() { - List subs = new ArrayList(factories.size() + 2); - subs.add(new HttpHeaderSeparateInterceptor()); - subs.add(new HttpHeaderParseInterceptor()); - // Add extension interceptors. - for (HttpInterceptorFactory factory : factories) { - subs.add(factory.create()); - } - return subs; - } - })); - // Goalkeepers. - mInterceptors.add(mInterceptors.size(), new Http2EncodeInterceptor()); - mInterceptors.add(mInterceptors.size(), new SSLRefluxInterceptor(codecInterceptor)); - - // - // SSL Flow Model: - // - // Request Response - // - // out in in out - // ⇈ ⇊ ⇊ ⇈ - // Encrypted Encrypted - // ⇈ ⇊ ⇊ ⇈ - // ----------------------------------------------------------- - // | Codec Interceptor | - // ----------------------------------------------------------- - // ⇈ | ⇊ ... ⇊ | ⇈ - // | ⇊ ... ⇊ | - // ⇈ | Decrypted | interceptors | Decrypted | ⇈ - // | ⇊ ... ⇊ | - // ⇈ | ⇊ ... ⇊ | ⇈ - // ----------------------------------------------------------- - // | Reflux Interceptor | - // ----------------------------------------------------------- - // ⇈ ⇇ ⇇ ⇇ ⇊ ⇊ ⇉ ⇉ ⇉ ⇈ - // - } - - @Override - public void onSpecRequest(ByteBuffer buffer) throws IOException { - new HttpRequestChain(mHttpZygoteRequest, mInterceptors).process(buffer); - } - - @Override - public void onSpecResponse(ByteBuffer buffer) throws IOException { - new HttpResponseChain(mHttpZygoteResponse, mInterceptors).process(buffer); - } - - @Override - public void onSpecRequestFinished() { - for (HttpInterceptor interceptor: mInterceptors) { - interceptor.onRequestFinished(mHttpZygoteRequest); - } - } - - @Override - public void onSpecResponseFinished() { - for (HttpInterceptor interceptor: mInterceptors) { - interceptor.onResponseFinished(mHttpZygoteResponse); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGatewayFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGatewayFactory.java deleted file mode 100644 index 9c4a5f4..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGatewayFactory.java +++ /dev/null @@ -1,71 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.gateway.VirtualGateway; -import com.github.megatronking.netbare.gateway.VirtualGatewayFactory; -import com.github.megatronking.netbare.gateway.Request; -import com.github.megatronking.netbare.gateway.Response; -import com.github.megatronking.netbare.net.Session; -import com.github.megatronking.netbare.ssl.JKS; - -import java.util.ArrayList; -import java.util.List; - -/** - * A {@link VirtualGatewayFactory} that produces the {@link HttpVirtualGateway}. - * - * @author Megatron King - * @since 2018年11月20日 23:50 - */ -public class HttpVirtualGatewayFactory implements VirtualGatewayFactory { - - private List mFactories; - private JKS mJKS; - - /** - * Constructs a {@link HttpVirtualGatewayFactory} instance with {@link JKS} and a collection of - * {@link HttpInterceptorFactory}. - * - * @param factories a collection of {@link HttpInterceptorFactory}. - * @return A instance of {@link HttpVirtualGatewayFactory}. - */ - public HttpVirtualGatewayFactory(@NonNull JKS jks, - @NonNull List factories) { - this.mJKS = jks; - this.mFactories = factories; - } - - @Override - public VirtualGateway create(Session session, Request request, Response response) { - return new HttpVirtualGateway(session, request, response, mJKS, new ArrayList(mFactories)); - } - - /** - * Create a {@link HttpVirtualGatewayFactory} instance with {@link JKS} and a collection of - * {@link HttpInterceptorFactory}. - * - * @param factories a collection of {@link HttpInterceptorFactory}. - * @return A instance of {@link HttpVirtualGatewayFactory}. - */ - public static VirtualGatewayFactory create(@NonNull JKS authority, - @NonNull List factories) { - return new HttpVirtualGatewayFactory(authority, factories); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteRequest.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteRequest.java deleted file mode 100644 index cfd29df..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteRequest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import com.github.megatronking.netbare.gateway.Request; -import com.github.megatronking.netbare.http2.Http2Settings; -import com.github.megatronking.netbare.http2.Http2Updater; - -import java.util.HashMap; -import java.util.Map; - -/** - * A zygote http request class, it creates the real http request instance. - * - * @author Megatron King - * @since 2019年1月6日 17:00 - */ -public class HttpZygoteRequest extends HttpRequest implements Http2Updater { - - private final Request mRequest; - private final HttpSessionFactory mSessionFactory; - private final Map mCachedRequests; - - private HttpRequest mActiveRequest; - - /* package */ HttpZygoteRequest(Request request, HttpSessionFactory factory) { - super(request, factory.create(request.id())); - this.mRequest = request; - this.mSessionFactory = factory; - this.mCachedRequests = new HashMap(); - } - - public void zygote(HttpId id) { - if (mCachedRequests.containsKey(id.id)) { - mActiveRequest = mCachedRequests.get(id.id); - } else { - HttpSession originSession = session(); - HttpSession session = mSessionFactory.create(id.id); - session.isHttps = originSession.isHttps; - session.protocol = originSession.protocol; - session.clientHttp2Settings = originSession.clientHttp2Settings; - session.peerHttp2Settings = originSession.peerHttp2Settings; - HttpRequest request = new HttpRequest(mRequest, id, session); - mCachedRequests.put(id.id, request); - mActiveRequest = request; - } - } - - @Override - public void onSettingsUpdate(Http2Settings http2Settings) { - session().clientHttp2Settings = http2Settings; - } - - @Override - public void onStreamFinished() { - HttpRequest request = getActive(); - if (request != null) { - request.session().requestStreamEnd = true; - } - } - - HttpRequest getActive() { - return mActiveRequest; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteResponse.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteResponse.java deleted file mode 100644 index 714fdea..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteResponse.java +++ /dev/null @@ -1,80 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import com.github.megatronking.netbare.gateway.Response; -import com.github.megatronking.netbare.http2.Http2Settings; -import com.github.megatronking.netbare.http2.Http2Updater; - -import java.util.HashMap; -import java.util.Map; - -/** - * A zygote http response class, it creates the real http response instance. - * - * @author Megatron King - * @since 2019年1月6日 17:09 - */ -public class HttpZygoteResponse extends HttpResponse implements Http2Updater { - - private final Response mResponse; - private final HttpSessionFactory mSessionFactory; - private final Map mCachedResponses; - - private HttpResponse mActiveResponse; - - /* package */ HttpZygoteResponse(Response response, HttpSessionFactory factory) { - super(response, factory.create(response.id())); - this.mResponse = response; - this.mSessionFactory = factory; - this.mCachedResponses = new HashMap(); - } - - public void zygote(HttpId id) { - if (mCachedResponses.containsKey(id.id)) { - mActiveResponse = mCachedResponses.get(id.id); - } else { - HttpSession originSession = super.session(); - HttpSession session = mSessionFactory.create(id.id); - session.isHttps = originSession.isHttps; - session.protocol = originSession.protocol; - session.clientHttp2Settings = originSession.clientHttp2Settings; - session.peerHttp2Settings = originSession.peerHttp2Settings; - HttpResponse response = new HttpResponse(mResponse, id, session); - mCachedResponses.put(id.id, response); - mActiveResponse = response; - } - } - - @Override - public void onSettingsUpdate(Http2Settings http2Settings) { - session().peerHttp2Settings = http2Settings; - } - - @Override - public void onStreamFinished() { - HttpResponse response = getActive(); - if (response != null) { - response.session().responseStreamEnd = true; - } - } - - HttpResponse getActive() { - return mActiveResponse; - } - -} - diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLCodecInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLCodecInterceptor.java deleted file mode 100644 index 128ffac..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLCodecInterceptor.java +++ /dev/null @@ -1,256 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.NetBareXLog; -import com.github.megatronking.netbare.gateway.Request; -import com.github.megatronking.netbare.gateway.Response; -import com.github.megatronking.netbare.ip.Protocol; -import com.github.megatronking.netbare.ssl.JKS; -import com.github.megatronking.netbare.ssl.SSLCodec; -import com.github.megatronking.netbare.ssl.SSLEngineFactory; -import com.github.megatronking.netbare.ssl.SSLUtils; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.security.GeneralSecurityException; - -/** - * An interceptor decodes SSL encrypt packets to plaintext packets. - * - * @author Megatron King - * @since 2018年11月15日 15:39 - */ -/* package */ class SSLCodecInterceptor extends HttpPendingInterceptor implements SSLRefluxCallback { - - private static SSLEngineFactory sEngineFactory; - - private Request mRequest; - private Response mResponse; - - private JKS mJKS; - - private SSLHttpRequestCodec mRequestCodec; - private SSLHttpResponseCodec mResponseCodec; - - private NetBareXLog mLog; - - private boolean mClientAlpnResolved; - - /* package */ SSLCodecInterceptor(JKS jks, Request request, Response response) { - this.mJKS = jks; - this.mRequest = request; - this.mResponse = response; - - if (sEngineFactory == null) { - try { - sEngineFactory = new SSLEngineFactory(jks); - } catch (GeneralSecurityException | IOException e) { - NetBareLog.e("Create SSLEngineFactory failed: " + e.getMessage()); - } - } - - mRequestCodec = new SSLHttpRequestCodec(sEngineFactory); - mResponseCodec = new SSLHttpResponseCodec(sEngineFactory); - - mLog = new NetBareXLog(Protocol.TCP, request.ip(), request.port()); - } - - @Override - protected void intercept(@NonNull final HttpRequestChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - if (!chain.request().isHttps()) { - chain.process(buffer); - } else if (!mJKS.isInstalled()) { - // Skip all interceptors - chain.processFinal(buffer); - mLog.w("JSK not installed, skip all interceptors!"); - } else { - if (!mClientAlpnResolved) { - buffer = mergeRequestBuffer(buffer); - int verifyResult = SSLUtils.verifyPacket(buffer); - if (verifyResult == SSLUtils.PACKET_NOT_ENCRYPTED) { - throw new IOException("SSL packet is not encrypt."); - } - if (verifyResult == SSLUtils.PACKET_NOT_ENOUGH) { - pendRequestBuffer(buffer); - return; - } - - mRequestCodec.setRequest(chain.request()); - // Start handshake with remote server - mResponseCodec.setRequest(chain.request()); - - // Parse the ALPN protocol of client. - HttpProtocol[] protocols = SSLUtils.parseClientHelloAlpn(buffer); - mClientAlpnResolved = true; - - if (protocols == null || protocols.length == 0) { - mRequestCodec.setSelectedAlpnResolved(); - mResponseCodec.setSelectedAlpnResolved(); - mResponseCodec.prepareHandshake(); - } else { - // Detect remote server's ALPN and then continue request. - mResponseCodec.prepareHandshake(protocols, new SSLHttpResponseCodec.AlpnResolvedCallback() { - @Override - public void onResult(String selectedAlpnProtocol) throws IOException { - if (selectedAlpnProtocol != null) { - HttpProtocol protocol = HttpProtocol.parse(selectedAlpnProtocol); - // Only accept Http1.1 and Http2.0 - if (protocol == HttpProtocol.HTTP_1_1 || protocol == HttpProtocol.HTTP_2) { - mRequestCodec.setSelectedAlpnProtocol(protocol); - chain.request().session().protocol = protocol; - mLog.i("Server selected ALPN protocol: " + protocol.toString()); - } else { - mLog.w("Unexpected server ALPN protocol: " + protocol.toString()); - } - } - mRequestCodec.setSelectedAlpnResolved(); - // Continue request. - decodeRequest(chain, ByteBuffer.allocate(0)); - } - }); - } - } - // Hold the request buffer until the server ALPN configuration resolved. - if (!mRequestCodec.selectedAlpnResolved()) { - pendRequestBuffer(buffer); - return; - } - - decodeRequest(chain, buffer); - } - } - - @Override - protected void intercept(@NonNull final HttpResponseChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - if (!chain.response().isHttps()) { - chain.process(buffer); - } else if (!mJKS.isInstalled()) { - // Skip all interceptors - chain.processFinal(buffer); - mLog.w("JSK not installed, skip all interceptors!"); - } else { - // Merge buffers - decodeResponse(chain, buffer); - } - } - - @Override - public void onRequest(HttpRequest request, ByteBuffer buffer) throws IOException { - mResponseCodec.encode(buffer, new SSLCodec.CodecCallback() { - @Override - public void onPending(ByteBuffer buffer) { - } - - @Override - public void onProcess(ByteBuffer buffer) { - } - - @Override - public void onEncrypt(ByteBuffer buffer) throws IOException { - // The encrypt request data is sent to remote server - mRequest.process(buffer); - } - - @Override - public void onDecrypt(ByteBuffer buffer) { - } - }); - } - - @Override - public void onResponse(HttpResponse response, ByteBuffer buffer) throws IOException { - mRequestCodec.encode(buffer, new SSLCodec.CodecCallback() { - @Override - public void onPending(ByteBuffer buffer) { - } - - @Override - public void onProcess(ByteBuffer buffer) { - } - - @Override - public void onEncrypt(ByteBuffer buffer) throws IOException { - // The encrypt response data is sent to proxy server - mResponse.process(buffer); - } - - @Override - public void onDecrypt(ByteBuffer buffer) { - } - }); - } - - private void decodeRequest(final HttpRequestChain chain, ByteBuffer buffer) throws IOException { - // Merge buffers - mRequestCodec.decode(mergeRequestBuffer(buffer), - new SSLCodec.CodecCallback() { - @Override - public void onPending(ByteBuffer buffer) { - pendRequestBuffer(buffer); - } - - @Override - public void onProcess(ByteBuffer buffer) throws IOException { - chain.processFinal(buffer); - } - - @Override - public void onEncrypt(ByteBuffer buffer) throws IOException { - mResponse.process(buffer); - } - - @Override - public void onDecrypt(ByteBuffer buffer) throws IOException { - chain.process(buffer); - } - }); - } - - - private void decodeResponse(final HttpResponseChain chain, ByteBuffer buffer) throws IOException { - // Merge buffers - mResponseCodec.decode(mergeResponseBuffer(buffer), - new SSLCodec.CodecCallback() { - @Override - public void onPending(ByteBuffer buffer) { - pendResponseBuffer(buffer); - } - - @Override - public void onProcess(ByteBuffer buffer) throws IOException { - chain.processFinal(buffer); - } - - @Override - public void onEncrypt(ByteBuffer buffer) throws IOException { - mRequest.process(buffer); - } - - @Override - public void onDecrypt(ByteBuffer buffer) throws IOException { - chain.process(buffer); - } - - }); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpRequestCodec.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpRequestCodec.java deleted file mode 100644 index d49fa03..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpRequestCodec.java +++ /dev/null @@ -1,134 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.ssl.SSLEngineFactory; -import com.github.megatronking.netbare.ssl.SSLRequestCodec; - -import java.io.ByteArrayOutputStream; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.charset.Charset; - -import javax.net.ssl.SSLEngine; - -/** - * Http request ssl codec enables Application-Layer Protocol Negotiation(ALPN). - * - * See http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4 - * - * @author Megatron King - * @since 2019年1月3日 23:01 - */ -/* package */ class SSLHttpRequestCodec extends SSLRequestCodec { - - private HttpProtocol mSelectedAlpnProtocol; - private boolean mAlpnEnabled; - private boolean mSelectedAlpnResolved; - - /* package */ SSLHttpRequestCodec(SSLEngineFactory factory) { - super(factory); - } - - @Override - protected SSLEngine createEngine(SSLEngineFactory factory) { - return enableALPN(super.createEngine(factory)); - } - - public void setSelectedAlpnProtocol(HttpProtocol protocol) { - mSelectedAlpnProtocol = protocol; - } - - public void setSelectedAlpnResolved() { - mSelectedAlpnResolved = true; - } - - public boolean selectedAlpnResolved() { - return mSelectedAlpnResolved; - } - - private SSLEngine enableALPN(SSLEngine sslEngine) { - if (sslEngine == null) { - return null; - } - if (mAlpnEnabled) { - return sslEngine; - } - mAlpnEnabled = true; - // Nothing to enable. - if (mSelectedAlpnProtocol == null) { - return sslEngine; - } - try { - String sslEngineName = sslEngine.getClass().getSimpleName(); - if (sslEngineName.equals("Java8EngineWrapper")) { - enableJava8EngineWrapperAlpn(sslEngine); - } else if (sslEngineName.equals("ConscryptEngine")) { - enableConscryptEngineAlpn(sslEngine); - } else { - enableOpenSSLEngineImplAlpn(sslEngine); - } - } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | - InvocationTargetException e) { - NetBareLog.wtf(e); - } - return sslEngine; - } - - private void enableJava8EngineWrapperAlpn(SSLEngine sslEngine) throws NoSuchMethodException, - IllegalAccessException, - InvocationTargetException { - Method setApplicationProtocolsMethod = sslEngine.getClass().getDeclaredMethod( - "setApplicationProtocols", String[].class); - setApplicationProtocolsMethod.setAccessible(true); - String[] protocols = {mSelectedAlpnProtocol.toString().toLowerCase()}; - setApplicationProtocolsMethod.invoke(sslEngine, new Object[]{protocols}); - } - - private void enableConscryptEngineAlpn(SSLEngine sslEngine) throws NoSuchMethodException, IllegalAccessException, - InvocationTargetException { - Method setAlpnProtocolsMethod = sslEngine.getClass().getDeclaredMethod( - "setAlpnProtocols", String[].class); - setAlpnProtocolsMethod.setAccessible(true); - String[] protocols = {mSelectedAlpnProtocol.toString().toLowerCase()}; - setAlpnProtocolsMethod.invoke(sslEngine, new Object[]{protocols}); - } - - private void enableOpenSSLEngineImplAlpn(SSLEngine sslEngine) throws NoSuchFieldException, - IllegalAccessException { - Field sslParametersField = sslEngine.getClass().getDeclaredField("sslParameters"); - sslParametersField.setAccessible(true); - Object sslParameters = sslParametersField.get(sslEngine); - if (sslParameters != null) { - Field alpnProtocolsField = sslParameters.getClass().getDeclaredField("alpnProtocols"); - alpnProtocolsField.setAccessible(true); - alpnProtocolsField.set(sslParameters, concatLengthPrefixed()); - } - } - - - private byte[] concatLengthPrefixed() { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - String protocolStr = mSelectedAlpnProtocol.toString().toLowerCase(); - os.write(protocolStr.length()); - os.write(protocolStr.getBytes(Charset.forName("UTF-8")), 0, protocolStr.length()); - return os.toByteArray(); - } - - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpResponseCodec.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpResponseCodec.java deleted file mode 100644 index 27881c1..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpResponseCodec.java +++ /dev/null @@ -1,230 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import android.annotation.SuppressLint; -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.ssl.SSLEngineFactory; -import com.github.megatronking.netbare.ssl.SSLResponseCodec; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; - -import javax.net.ssl.SSLEngine; - -/** - * Http SSL codec enables Application-Layer Protocol Negotiation(ALPN). - * - * See http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4 - * - * @author Megatron King - * @since 2019年1月3日 23:31 - */ -/* package */ class SSLHttpResponseCodec extends SSLResponseCodec { - - private SSLEngine mSSLEngine; - - private boolean mAlpnEnabled; - private boolean mSelectedAlpnResolved; - - private HttpProtocol[] mClientAlpns; - private AlpnResolvedCallback mAlpnCallback; - - /* package */ SSLHttpResponseCodec(SSLEngineFactory factory) { - super(factory); - } - - @Override - protected SSLEngine createEngine(SSLEngineFactory factory) { - if (mSSLEngine == null) { - mSSLEngine = super.createEngine(factory); - if (mSSLEngine != null && mClientAlpns != null) { - enableAlpn(); - } - } - return mSSLEngine; - } - - @Override - public void decode(ByteBuffer buffer, @NonNull CodecCallback callback) throws IOException { - super.decode(buffer, callback); - // ALPN is put in ServerHello, once we receive the remote server packet, the ALPN must be - // resolved. - if (!mSelectedAlpnResolved) { - mAlpnCallback.onResult(getAlpnSelectedProtocol()); - } - mSelectedAlpnResolved = true; - } - - public void setSelectedAlpnResolved() { - mSelectedAlpnResolved = true; - } - - public void prepareHandshake(HttpProtocol[] protocols, AlpnResolvedCallback callback) - throws IOException { - this.mClientAlpns = protocols; - this.mAlpnCallback = callback; - super.prepareHandshake(); - } - - private void enableAlpn() { - try { - String sslEngineName = mSSLEngine.getClass().getSimpleName(); - if (sslEngineName.equals("Java8EngineWrapper")) { - enableJava8EngineWrapperAlpn(); - } else if (sslEngineName.equals("ConscryptEngine")) { - enableConscryptEngineAlpn(); - } else { - enableOpenSSLEngineImplAlpn(); - } - mAlpnEnabled = true; - } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | - InvocationTargetException e) { - NetBareLog.wtf(e); - } - } - - private void enableJava8EngineWrapperAlpn() throws NoSuchMethodException, IllegalAccessException, - InvocationTargetException { - Method setApplicationProtocolsMethod = mSSLEngine.getClass().getDeclaredMethod( - "setApplicationProtocols", String[].class); - setApplicationProtocolsMethod.setAccessible(true); - String[] protocols = new String[mClientAlpns.length]; - for (int i = 0; i < protocols.length; i++) { - protocols[i] = mClientAlpns[i].toString().toLowerCase(); - } - setApplicationProtocolsMethod.invoke(mSSLEngine, new Object[]{protocols}); - - Method setUseSessionTicketsMethod = mSSLEngine.getClass().getDeclaredMethod( - "setUseSessionTickets", boolean.class); - setUseSessionTicketsMethod.setAccessible(true); - setUseSessionTicketsMethod.invoke(mSSLEngine, true); - } - - private void enableConscryptEngineAlpn() throws NoSuchMethodException, IllegalAccessException, - InvocationTargetException { - Method setAlpnProtocolsMethod = mSSLEngine.getClass().getDeclaredMethod( - "setAlpnProtocols", String[].class); - setAlpnProtocolsMethod.setAccessible(true); - String[] protocols = new String[mClientAlpns.length]; - for (int i = 0; i < protocols.length; i++) { - protocols[i] = mClientAlpns[i].toString().toLowerCase(); - } - setAlpnProtocolsMethod.invoke(mSSLEngine, new Object[]{protocols}); - - Method setUseSessionTicketsMethod = mSSLEngine.getClass().getDeclaredMethod( - "setUseSessionTickets", boolean.class); - setUseSessionTicketsMethod.setAccessible(true); - setUseSessionTicketsMethod.invoke(mSSLEngine, true); - } - - private void enableOpenSSLEngineImplAlpn() throws NoSuchFieldException, IllegalAccessException { - Field sslParametersField = mSSLEngine.getClass().getDeclaredField("sslParameters"); - sslParametersField.setAccessible(true); - Object sslParameters = sslParametersField.get(mSSLEngine); - if (sslParameters == null) { - throw new IllegalAccessException("sslParameters value is null"); - } - Field useSessionTicketsField = sslParameters.getClass().getDeclaredField("useSessionTickets"); - useSessionTicketsField.setAccessible(true); - useSessionTicketsField.set(sslParameters, true); - Field useSniField = sslParameters.getClass().getDeclaredField("useSni"); - useSniField.setAccessible(true); - useSniField.set(sslParameters, true); - Field alpnProtocolsField = sslParameters.getClass().getDeclaredField("alpnProtocols"); - alpnProtocolsField.setAccessible(true); - alpnProtocolsField.set(sslParameters, concatLengthPrefixed(mClientAlpns)); - } - - private byte[] concatLengthPrefixed(HttpProtocol ... protocols) { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - for (HttpProtocol protocol : protocols) { - String protocolStr = protocol.toString().toLowerCase(); - os.write(protocolStr.length()); - os.write(protocolStr.getBytes(Charset.forName("UTF-8")), 0, protocolStr.length()); - } - return os.toByteArray(); - } - - - @SuppressLint("PrivateApi") - private String getAlpnSelectedProtocol() { - if (!mAlpnEnabled) { - return null; - } - String alpnResult = null; - try { - String sslEngineName = mSSLEngine.getClass().getSimpleName(); - if (sslEngineName.equals("Java8EngineWrapper")) { - alpnResult = getJava8EngineWrapperAlpn(); - } else if (sslEngineName.equals("ConscryptEngine")){ - alpnResult = getConscryptEngineAlpn(); - } else { - alpnResult = getOpenSSLEngineImplAlpn(); - } - } catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException - | IllegalAccessException | InvocationTargetException e) { - NetBareLog.e(e.getMessage()); - } - return alpnResult; - } - - private String getJava8EngineWrapperAlpn() throws NoSuchMethodException, IllegalAccessException, - InvocationTargetException { - Method getApplicationProtocolMethod = mSSLEngine.getClass().getDeclaredMethod( - "getApplicationProtocol"); - getApplicationProtocolMethod.setAccessible(true); - return (String) getApplicationProtocolMethod.invoke(mSSLEngine); - } - - private String getConscryptEngineAlpn() throws NoSuchMethodException, IllegalAccessException, - InvocationTargetException { - Method getAlpnSelectedProtocolMethod = mSSLEngine.getClass().getDeclaredMethod( - "getAlpnSelectedProtocol"); - getAlpnSelectedProtocolMethod.setAccessible(true); - byte[] selectedProtocol = (byte[]) getAlpnSelectedProtocolMethod.invoke(mSSLEngine); - return selectedProtocol != null ? new String(selectedProtocol, Charset.forName("UTF-8")) : null; - } - - @SuppressLint("PrivateApi") - private String getOpenSSLEngineImplAlpn() throws ClassNotFoundException, NoSuchMethodException, - NoSuchFieldException, IllegalAccessException, InvocationTargetException { - Class nativeCryptoClass = Class.forName("com.android.org.conscrypt.NativeCrypto"); - Method SSL_get0_alpn_selectedMethod = nativeCryptoClass.getDeclaredMethod( - "SSL_get0_alpn_selected", long.class); - SSL_get0_alpn_selectedMethod.setAccessible(true); - - Field sslNativePointerField = mSSLEngine.getClass().getDeclaredField("sslNativePointer"); - sslNativePointerField.setAccessible(true); - long sslNativePointer = (long) sslNativePointerField.get(mSSLEngine); - byte[] selectedProtocol = (byte[]) SSL_get0_alpn_selectedMethod.invoke(null, sslNativePointer); - return selectedProtocol != null ? new String(selectedProtocol, Charset.forName("UTF-8")) : null; - } - - interface AlpnResolvedCallback { - - void onResult(String selectedAlpnProtocol) throws IOException; - - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxCallback.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxCallback.java deleted file mode 100644 index c6139ba..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxCallback.java +++ /dev/null @@ -1,33 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * A callback to receive SSL plaintext packets and input them to {@link javax.net.ssl.SSLEngine}. - * - * @author Megatron King - * @since 2018年11月15日 14:35 - */ -public interface SSLRefluxCallback { - - void onRequest(HttpRequest request, ByteBuffer buffer) throws IOException; - - void onResponse(HttpResponse response, ByteBuffer buffer) throws IOException; - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxInterceptor.java deleted file mode 100644 index 611898a..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxInterceptor.java +++ /dev/null @@ -1,58 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * An interceptor locates at the last layer of the interceptors. It is responsible for send - * plaintext packets to {@link SSLCodecInterceptor}. - * - * @author Megatron King - * @since 2018年11月15日 15:39 - */ -/* package */ class SSLRefluxInterceptor extends HttpInterceptor { - - private SSLRefluxCallback mRefluxCallback; - - /* package */ SSLRefluxInterceptor(SSLRefluxCallback refluxCallback) { - this.mRefluxCallback = refluxCallback; - } - - @Override - protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer) - throws IOException { - if (chain.request().isHttps()) { - mRefluxCallback.onRequest(chain.request(), buffer); - } else { - chain.process(buffer); - } - } - - @Override - protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer) - throws IOException { - if (chain.response().isHttps()) { - mRefluxCallback.onResponse(chain.response(), buffer); - } else { - chain.process(buffer); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/DecodeCallback.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/DecodeCallback.java deleted file mode 100644 index 13d6525..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/DecodeCallback.java +++ /dev/null @@ -1,35 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http2; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * A callback for HTTP2 header and data decodes. - * - * @author Megatron King - * @since 2019年1月5日 20:36 - */ -/* package */ interface DecodeCallback { - - void onPending(ByteBuffer buffer); - - void onResult(ByteBuffer buffer, boolean isFinished) throws IOException; - - void onSkip(ByteBuffer buffer) throws IOException; - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/EncodeCallback.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/EncodeCallback.java deleted file mode 100644 index 02e6285..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/EncodeCallback.java +++ /dev/null @@ -1,31 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http2; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * A callback for HTTP2 header and data encodes. - * - * @author Megatron King - * @since 2019年1月6日 21:54 - */ -/* package */ interface EncodeCallback { - - void onResult(ByteBuffer buffer) throws IOException; - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/ErrorCode.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/ErrorCode.java deleted file mode 100644 index d144c93..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/ErrorCode.java +++ /dev/null @@ -1,87 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -/* - * Copyright (C) 2013 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.github.megatronking.netbare.http2; - -/** - * http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-7 - * - * @author Megatron King - * @since 2019年1月11日 23:10 - */ -/* package */ enum ErrorCode { - - /** - * Not an error! - */ - NO_ERROR(0), - - PROTOCOL_ERROR(1), - - INTERNAL_ERROR(2), - - FLOW_CONTROL_ERROR(3), - - SETTINGS_TIMEOUT(4), - - STREAM_CLOSED(5), - - FRAME_SIZE_ERROR(6), - - REFUSED_STREAM(7), - - CANCEL(8), - - COMPRESSION_ERROR(9), - - CONNECT_ERROR(0xa), - - ENHANCE_YOUR_CALM(0xb), - - INADEQUATE_SECURITY(0xc), - - HTTP_1_1_REQUIRED(0xd); - - public final int httpCode; - - ErrorCode(int httpCode) { - this.httpCode = httpCode; - } - - /* package */ static ErrorCode fromHttp2(int code) { - for (ErrorCode errorCode : ErrorCode.values()) { - if (errorCode.httpCode == code) { - return errorCode; - } - } - return null; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/FrameType.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/FrameType.java deleted file mode 100644 index e6780f6..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/FrameType.java +++ /dev/null @@ -1,120 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http2; - -import androidx.annotation.Nullable; - -/** - * This specification defines a number of frame types, each identified by a unique 8-bit type code. - * - * See https://httpwg.org/specs/rfc7540.html#FrameTypes - * - * @author Megatron King - * @since 2019年1月5日 15:30 - */ -/* package */ enum FrameType { - - /** - * DATA frames (type=0x0) convey arbitrary, variable-length sequences of octets associated with - * a stream. One or more DATA frames are used, for instance, to carry HTTP request or response - * payloads. - */ - DATA((byte)0x0), - - /** - * The HEADERS frame (type=0x1) is used to open a stream (Section 5.1), and additionally - * carries a header block fragment. HEADERS frames can be sent on a stream in the "idle", - * "reserved (local)", "open", or "half-closed (remote)" state. - */ - HEADERS((byte)0x1), - - /** - * The PRIORITY frame (type=0x2) specifies the sender-advised priority of a stream (Section 5.3). - * It can be sent in any stream state, including idle or closed streams. - */ - PRIORITY((byte)0x2), - - /** - * The RST_STREAM frame (type=0x3) allows for immediate termination of a stream. RST_STREAM is - * sent to request cancellation of a stream or to indicate that an error condition has occurred. - */ - RST_STREAM((byte)0x3), - - /** - * The SETTINGS frame (type=0x4) conveys configuration parameters that affect how endpoints - * communicate, such as preferences and constraints on peer behavior. The SETTINGS frame is - * also used to acknowledge the receipt of those parameters. Individually, a SETTINGS parameter - * can also be referred to as a "setting". - */ - SETTINGS((byte)0x4), - - /** - * The PUSH_PROMISE frame (type=0x5) is used to notify the peer endpoint in advance of streams - * the sender intends to initiate. The PUSH_PROMISE frame includes the unsigned 31-bit - * identifier of the stream the endpoint plans to create along with a set of headers that - * provide additional context for the stream. - */ - PUSH_PROMISE((byte)0x5), - - /** - * The PING frame (type=0x6) is a mechanism for measuring a minimal round-trip time from the - * sender, as well as determining whether an idle connection is still functional. PING frames - * can be sent from any endpoint. - */ - PING((byte)0x6), - - /** - * The GOAWAY frame (type=0x7) is used to initiate shutdown of a connection or to signal - * serious error conditions. GOAWAY allows an endpoint to gracefully stop accepting new streams - * while still finishing processing of previously established streams. This enables - * administrative actions, like server maintenance. - */ - GOAWAY((byte)0x7), - - /** - * The WINDOW_UPDATE frame (type=0x8) is used to implement flow control. - */ - WINDOW_UPDATE((byte)0x8), - - /** - * The CONTINUATION frame (type=0x9) is used to continue a sequence of header block fragments - * (Section 4.3). Any number of CONTINUATION frames can be sent, as long as the preceding frame - * is on the same stream and is a HEADERS, PUSH_PROMISE, or CONTINUATION frame without the - * END_HEADERS flag set. - */ - CONTINUATION((byte)0x9); - - private final byte value; - - FrameType(byte value) { - this.value = value; - } - - /* package */ byte get() { - return value; - } - - @Nullable - /* package */ static FrameType parse(byte value) { - for (FrameType type : values()) { - if (type.value == value) { - return type; - } - } - return null; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Hpack.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Hpack.java deleted file mode 100644 index e28eaa0..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Hpack.java +++ /dev/null @@ -1,753 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -/* - * Copyright (C) 2013 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.github.megatronking.netbare.http2; - -import android.text.TextUtils; - -import com.github.megatronking.netbare.NetBareUtils; -import com.github.megatronking.netbare.http.HttpMethod; -import com.github.megatronking.netbare.http.HttpProtocol; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Read and write HPACK v10. - * - * https://httpwg.org/specs/rfc7540.html#HeaderBlock - * - * This implementation uses an array for the dynamic table and a list for indexed entries. Dynamic - * entries are added to the array, starting in the last position moving forward. When the array - * fills, it is doubled. - * - * @author Megatron King - * @since 2019年1月5日 20:13 - */ -/* package */ final class Hpack { - - private static final int PREFIX_4_BITS = 0x0f; - private static final int PREFIX_5_BITS = 0x1f; - private static final int PREFIX_6_BITS = 0x3f; - private static final int PREFIX_7_BITS = 0x7f; - - private static final Header[] STATIC_HEADER_TABLE = new Header[] { - new Header(Header.TARGET_AUTHORITY, ""), - new Header(Header.TARGET_METHOD, "GET"), - new Header(Header.TARGET_METHOD, "POST"), - new Header(Header.TARGET_PATH, "/"), - new Header(Header.TARGET_PATH, "/index.html"), - new Header(Header.TARGET_SCHEME, "http"), - new Header(Header.TARGET_SCHEME, "https"), - new Header(Header.RESPONSE_STATUS, "200"), - new Header(Header.RESPONSE_STATUS, "204"), - new Header(Header.RESPONSE_STATUS, "206"), - new Header(Header.RESPONSE_STATUS, "304"), - new Header(Header.RESPONSE_STATUS, "400"), - new Header(Header.RESPONSE_STATUS, "404"), - new Header(Header.RESPONSE_STATUS, "500"), - new Header("accept-charset", ""), - new Header("accept-encoding", "gzip, deflate"), - new Header("accept-language", ""), - new Header("accept-ranges", ""), - new Header("accept", ""), - new Header("access-control-allow-origin", ""), - new Header("age", ""), - new Header("allow", ""), - new Header("authorization", ""), - new Header("cache-control", ""), - new Header("content-disposition", ""), - new Header("content-encoding", ""), - new Header("content-language", ""), - new Header("content-length", ""), - new Header("content-location", ""), - new Header("content-range", ""), - new Header("content-type", ""), - new Header("cookie", ""), - new Header("date", ""), - new Header("etag", ""), - new Header("expect", ""), - new Header("expires", ""), - new Header("from", ""), - new Header("host", ""), - new Header("if-match", ""), - new Header("if-modified-since", ""), - new Header("if-none-match", ""), - new Header("if-range", ""), - new Header("if-unmodified-since", ""), - new Header("last-modified", ""), - new Header("link", ""), - new Header("location", ""), - new Header("max-forwards", ""), - new Header("proxy-authenticate", ""), - new Header("proxy-authorization", ""), - new Header("range", ""), - new Header("referer", ""), - new Header("refresh", ""), - new Header("retry-after", ""), - new Header("server", ""), - new Header("set-cookie", ""), - new Header("strict-transport-security", ""), - new Header("transfer-encoding", ""), - new Header("user-agent", ""), - new Header("vary", ""), - new Header("via", ""), - new Header("www-authenticate", "") - }; - - private static final List HTTP_2_SKIPPED_REQUEST_HEADERS = Arrays.asList( - "connection", - "host", - "keep_alive", - "proxy_connection", - "te", - "transfer_encoding", - "encoding", - "upgrade"); - - private static final List HTTP_2_SKIPPED_RESPONSE_HEADERS = Arrays.asList( - "connection", - "host", - "keep_alive", - "proxy_connection", - "te", - "transfer_encoding", - "encoding", - "upgrade"); - - private static final Map NAME_TO_FIRST_INDEX = nameToFirstIndex(); - - private static Map nameToFirstIndex() { - Map result = new LinkedHashMap(STATIC_HEADER_TABLE.length); - for (int i = 0; i < STATIC_HEADER_TABLE.length; i++) { - if (!result.containsKey(STATIC_HEADER_TABLE[i].name)) { - result.put(STATIC_HEADER_TABLE[i].name, i); - } - } - return Collections.unmodifiableMap(result); - } - - private static final int DEFAULT_HEADER_TABLE_SIZE_SETTING = 4096; - private static final int SETTINGS_HEADER_TABLE_SIZE_LIMIT = 16384; - - static final class Reader { - - private final List mHeaders; - - private int mHeaderTableSizeSetting; - - private Header[] mDynamicTable; - private int mNextHeaderIndex; - private int mHeaderCount; - private int mDynamicTableByteCount; - - private int mMaxDynamicTableByteCount; - - /* package */ Reader() { - this.mHeaders = new ArrayList(); - this.mDynamicTable = new Header[8]; - this.mNextHeaderIndex = mDynamicTable.length - 1; - this.mMaxDynamicTableByteCount = DEFAULT_HEADER_TABLE_SIZE_SETTING; - this.mHeaderTableSizeSetting = DEFAULT_HEADER_TABLE_SIZE_SETTING; - } - - void setHeaderTableSizeSetting(int headerTableSizeSetting) { - if (mHeaderTableSizeSetting == headerTableSizeSetting) { - return; - } - this.mHeaderTableSizeSetting = headerTableSizeSetting; - int effectiveHeaderTableSize = Math.min(headerTableSizeSetting, - SETTINGS_HEADER_TABLE_SIZE_LIMIT); - - if (mMaxDynamicTableByteCount == effectiveHeaderTableSize) { - return; // No change. - } - mMaxDynamicTableByteCount = effectiveHeaderTableSize; - adjustDynamicTableByteCount(); - } - - void readHeaders(ByteBuffer buffer, byte flags, DecodeCallback callback) throws IOException, - IndexOutOfBoundsException { - mHeaders.clear(); - while (buffer.hasRemaining()) { - int b = buffer.get() & 0xff; - if (b == 0x80) { // 10000000 - throw new IOException("Hpack read headers failed: index == 0"); - } else if ((b & 0x80) == 0x80) { // 1NNNNNNN - int index = readInt(buffer, b, PREFIX_7_BITS); - readIndexedHeader(index - 1); - } else if (b == 0x40) { // 01000000 - readLiteralHeaderWithIncrementalIndexingNewName(buffer); - } else if ((b & 0x40) == 0x40) { // 01NNNNNN - int index = readInt(buffer, b, PREFIX_6_BITS); - readLiteralHeaderWithIncrementalIndexingIndexedName(buffer, index - 1); - } else if ((b & 0x20) == 0x20) { // 001NNNNN - mMaxDynamicTableByteCount = readInt(buffer, b, PREFIX_5_BITS); - if (mMaxDynamicTableByteCount < 0 - || mMaxDynamicTableByteCount> mHeaderTableSizeSetting) { - throw new IOException("Hpack read headers failed: Invalid dynamic table " + - "size update " + mMaxDynamicTableByteCount); - } - adjustDynamicTableByteCount(); - } else if (b == 0x10 || b == 0) { // 000?0000 - Ignore never indexed bit. - readLiteralHeaderWithoutIndexingNewName(buffer); - } else { // 000?NNNN - Ignore never indexed bit. - int index = readInt(buffer, b, PREFIX_4_BITS); - readLiteralHeaderWithoutIndexingIndexedName(buffer, index - 1); - } - } - // Build normal http header part - String method = null; - String path = null; - String host = null; - String status = null; - List

                headers = new ArrayList(); - for (Header header : mHeaders) { - if (header.name.equals(Header.TARGET_METHOD)) { - method = header.value; - } else if (header.name.equals(Header.TARGET_PATH)) { - path = header.value; - } else if (header.name.equals(Header.TARGET_AUTHORITY)) { - host = header.value; - } else if (header.name.equalsIgnoreCase("host")) { - host = header.value; - } else if (header.name.equalsIgnoreCase(Header.RESPONSE_STATUS)) { - status = header.value; - } else { - headers.add(header); - } - } - StringBuilder sb = new StringBuilder(); - if (method != null && path != null) { - sb.append(method).append(" ").append(path).append(" ").append(HttpProtocol.HTTP_2.toString()); - sb.append(NetBareUtils.LINE_END); - } - if (status != null) { - sb.append(HttpProtocol.HTTP_2.toString()).append(" ").append(status); - sb.append(NetBareUtils.LINE_END); - } - if (host != null) { - headers.add(0, new Header("Host", host)); - } - for (Header header : headers) { - if (header.name.equals(Header.TARGET_SCHEME)) { - continue; - } - sb.append(header.name).append(": "); - if (header.value != null) { - sb.append(header.value); - } - sb.append(NetBareUtils.LINE_END); - } - if ((flags & Http2.FLAG_END_HEADERS) != 0) { - sb.append(NetBareUtils.LINE_END); - } - callback.onResult(ByteBuffer.wrap(sb.toString().getBytes()), - (flags & Http2.FLAG_END_STREAM) != 0); - } - - private int readInt(ByteBuffer buffer, int firstByte, int prefixMask) { - int prefix = firstByte & prefixMask; - if (prefix < prefixMask) { - return prefix; // This was a single byte value. - } - - // This is a multibyte value. Read 7 bits at a time. - int result = prefixMask; - int shift = 0; - while (true) { - int b = buffer.get(); - if ((b & 0x80) != 0) { // Equivalent to (b>= 128) since b is in [0..255]. - result += (b & 0x7f) << shift; - shift += 7; - } else { - result += b << shift; // Last byte. - break; - } - } - return result; - } - - private int readByte(ByteBuffer buffer) { - return buffer.get() & 0xff; - } - - private void readIndexedHeader(int index) throws IOException { - if (isStaticHeader(index)) { - Header staticEntry = STATIC_HEADER_TABLE[index]; - mHeaders.add(staticEntry); - } else { - int dynamicTableIndex = dynamicTableIndex(index - STATIC_HEADER_TABLE.length); - if (dynamicTableIndex < 0 || dynamicTableIndex>= mDynamicTable.length) { - throw new IOException("Hpack read headers failed: Header index too large " + - (index + 1)); - } - Header header = mDynamicTable[dynamicTableIndex]; - if (header == null) { - throw new IOException("Hpack read headers failed: read dynamic table failed!"); - } - mHeaders.add(header); - } - } - - private int dynamicTableIndex(int index) { - return mNextHeaderIndex + 1 + index; - } - - private boolean isStaticHeader(int index) { - return index>= 0 && index <= STATIC_HEADER_TABLE.length - 1; - } - - private void readLiteralHeaderWithoutIndexingIndexedName(ByteBuffer buffer, int index) - throws IOException { - String name = getName(index); - String value = readString(buffer); - mHeaders.add(new Header(name, value)); - } - - private String getName(int index) throws IOException { - if (isStaticHeader(index)) { - return STATIC_HEADER_TABLE[index].name; - } else { - int dynamicTableIndex = dynamicTableIndex(index - STATIC_HEADER_TABLE.length); - if (dynamicTableIndex < 0 || dynamicTableIndex>= mDynamicTable.length) { - throw new IOException("Hpack read headers failed: Header index too large " + - (index + 1)); - } - return mDynamicTable[dynamicTableIndex].name; - } - } - - private String readString(ByteBuffer buffer) throws IOException { - int firstByte = readByte(buffer); - boolean huffmanDecode = (firstByte & 0x80) == 0x80; // 1NNNNNNN - int length = readInt(buffer, firstByte, PREFIX_7_BITS); - if (buffer.remaining() < length) { - throw new IOException("Hpack read headers failed: data not enough, expect: " + - length + " actual: " + buffer.remaining()); - } - byte[] data = new byte[length]; - buffer.get(data); - return new String(huffmanDecode ? Huffman.get().decode(data) : data); - } - - private void readLiteralHeaderWithIncrementalIndexingNewName(ByteBuffer buffer) - throws IOException { - String name = checkLowercase(readString(buffer)); - String value = readString(buffer); - insertIntoDynamicTable(-1, new Header(name, value)); - } - - private void readLiteralHeaderWithIncrementalIndexingIndexedName(ByteBuffer buffer, - int nameIndex) - throws IOException { - String name = getName(nameIndex); - String value = readString(buffer); - insertIntoDynamicTable(-1, new Header(name, value)); - } - - private void readLiteralHeaderWithoutIndexingNewName(ByteBuffer buffer) throws IOException { - String name = checkLowercase(readString(buffer)); - String value = readString(buffer); - mHeaders.add(new Header(name, value)); - } - - private void insertIntoDynamicTable(int index, Header entry) throws IOException { - mHeaders.add(entry); - - int delta = entry.hpackSize(); - if (index != -1) { // Index -1 == new header. - Header header = mDynamicTable[dynamicTableIndex(index)]; - if (header == null) { - throw new IOException("Hpack read headers failed: insert dynamic table failed!"); - } - delta -= header.hpackSize(); - } - - // if the new or replacement header is too big, drop all entries. - if (delta> mMaxDynamicTableByteCount) { - clearDynamicTable(); - return; - } - - // Evict headers to the required length. - int bytesToRecover = (mDynamicTableByteCount + delta) - mMaxDynamicTableByteCount; - int entriesEvicted = evictToRecoverBytes(bytesToRecover); - - if (index == -1) { // Adding a value to the dynamic table. - if (mHeaderCount + 1> mDynamicTable.length) { // Need to grow the dynamic table. - Header[] doubled = new Header[mDynamicTable.length * 2]; - System.arraycopy(mDynamicTable, 0, doubled, mDynamicTable.length, - mDynamicTable.length); - mNextHeaderIndex = mDynamicTable.length - 1; - mDynamicTable = doubled; - } - index = mNextHeaderIndex--; - mDynamicTable[index] = entry; - mHeaderCount++; - } else { // Replace value at same position. - index += dynamicTableIndex(index) + entriesEvicted; - mDynamicTable[index] = entry; - } - mDynamicTableByteCount += delta; - } - - private void clearDynamicTable() { - Arrays.fill(mDynamicTable, null); - mNextHeaderIndex = mDynamicTable.length - 1; - mHeaderCount = 0; - mDynamicTableByteCount = 0; - } - - private int evictToRecoverBytes(int bytesToRecover) { - int entriesToEvict = 0; - if (bytesToRecover> 0) { - // determine how many headers need to be evicted. - for (int j = mDynamicTable.length - 1; j>= mNextHeaderIndex && bytesToRecover> 0; j--) { - bytesToRecover -= mDynamicTable[j].hpackSize(); - mDynamicTableByteCount -= mDynamicTable[j].hpackSize(); - mHeaderCount--; - entriesToEvict++; - } - System.arraycopy(mDynamicTable, mNextHeaderIndex + 1, mDynamicTable, - mNextHeaderIndex + 1 + entriesToEvict, mHeaderCount); - mNextHeaderIndex += entriesToEvict; - } - return entriesToEvict; - } - - private void adjustDynamicTableByteCount() { - if (mMaxDynamicTableByteCount < mDynamicTableByteCount) { - if (mMaxDynamicTableByteCount == 0) { - clearDynamicTable(); - } else { - evictToRecoverBytes(mDynamicTableByteCount - mMaxDynamicTableByteCount); - } - } - } - - private String checkLowercase(String name) throws IOException { - for (int i = 0; i < name.length(); i++) { - char c = name.charAt(i); - if (c>= 'A' && c <= 'Z') { - throw new IOException("Hpack read headers failed: mixed case name: " + name); - } - } - return name; - } - - } - - static final class Writer { - - private int mSmallestHeaderTableSizeSetting; - private boolean mEmitDynamicTableSizeUpdate; - - private int mHeaderTableSizeSetting; - private int mMaxDynamicTableByteCount; - - // Visible for testing. - private Header[] mDynamicTable; - // Array is populated back to front, so new entries always have lowest index. - private int mNextHeaderIndex; - private int mHeaderCount = 0; - private int mDynamicTableByteCount = 0; - - private ByteArrayOutputStream mOut; - - Writer() { - this.mDynamicTable = new Header[8]; - this.mNextHeaderIndex = mDynamicTable.length - 1; - this.mSmallestHeaderTableSizeSetting = Integer.MAX_VALUE; - this.mHeaderTableSizeSetting = DEFAULT_HEADER_TABLE_SIZE_SETTING; - this.mMaxDynamicTableByteCount = DEFAULT_HEADER_TABLE_SIZE_SETTING; - } - - byte[] writeRequestHeaders(HttpMethod method, String path, String host, - Map> headers) throws IOException { - mOut = new ByteArrayOutputStream(); - List
                hpackHeaders = new ArrayList(); - hpackHeaders.add(new Header(Header.TARGET_METHOD, method.name())); - hpackHeaders.add(new Header(Header.TARGET_PATH, path)); - hpackHeaders.add(new Header(Header.TARGET_AUTHORITY, host)); - hpackHeaders.add(new Header(Header.TARGET_SCHEME, "https")); - for (Map.Entry> entry : headers.entrySet()) { - if (HTTP_2_SKIPPED_REQUEST_HEADERS.contains(entry.getKey().toLowerCase())) { - continue; - } - for (String value : entry.getValue()) { - hpackHeaders.add(new Header(entry.getKey(), value)); - } - } - return writeHeaders(hpackHeaders); - } - - byte[] writeResponseHeaders(int code, String message, Map> headers) - throws IOException { - mOut = new ByteArrayOutputStream(); - List
                hpackHeaders = new ArrayList(); - hpackHeaders.add(new Header(Header.RESPONSE_STATUS, TextUtils.isEmpty(message) ? String.valueOf(code) : - code + " " + message)); - for (Map.Entry> entry : headers.entrySet()) { - if (HTTP_2_SKIPPED_RESPONSE_HEADERS.contains(entry.getKey().toLowerCase())) { - continue; - } - for (String value : entry.getValue()) { - hpackHeaders.add(new Header(entry.getKey(), value)); - } - } - return writeHeaders(hpackHeaders); - } - - private void clearDynamicTable() { - Arrays.fill(mDynamicTable, null); - mNextHeaderIndex = mDynamicTable.length - 1; - mHeaderCount = 0; - mDynamicTableByteCount = 0; - } - - private int evictToRecoverBytes(int bytesToRecover) { - int entriesToEvict = 0; - if (bytesToRecover> 0) { - // determine how many headers need to be evicted. - for (int j = mDynamicTable.length - 1; j>= mNextHeaderIndex && bytesToRecover> 0; j--) { - bytesToRecover -= mDynamicTable[j].hpackSize(); - mDynamicTableByteCount -= mDynamicTable[j].hpackSize(); - mHeaderCount--; - entriesToEvict++; - } - System.arraycopy(mDynamicTable, mNextHeaderIndex + 1, mDynamicTable, - mNextHeaderIndex + 1 + entriesToEvict, mHeaderCount); - Arrays.fill(mDynamicTable, mNextHeaderIndex + 1, mNextHeaderIndex + 1 + entriesToEvict, null); - mNextHeaderIndex += entriesToEvict; - } - return entriesToEvict; - } - - private void insertIntoDynamicTable(Header entry) { - int delta = entry.hpackSize(); - - // if the new or replacement header is too big, drop all entries. - if (delta> mMaxDynamicTableByteCount) { - clearDynamicTable(); - return; - } - - // Evict headers to the required length. - int bytesToRecover = (mDynamicTableByteCount + delta) - mMaxDynamicTableByteCount; - evictToRecoverBytes(bytesToRecover); - - if (mHeaderCount + 1> mDynamicTable.length) { // Need to grow the dynamic table. - Header[] doubled = new Header[mDynamicTable.length * 2]; - System.arraycopy(mDynamicTable, 0, doubled, mDynamicTable.length, mDynamicTable.length); - mNextHeaderIndex = mDynamicTable.length - 1; - mDynamicTable = doubled; - } - int index = mNextHeaderIndex--; - mDynamicTable[index] = entry; - mHeaderCount++; - mDynamicTableByteCount += delta; - } - - private byte[] writeHeaders(List
                headerBlock) throws IOException { - if (mEmitDynamicTableSizeUpdate) { - if (mSmallestHeaderTableSizeSetting < mMaxDynamicTableByteCount) { - // Multiple dynamic table size updates! - writeInt(mSmallestHeaderTableSizeSetting, PREFIX_5_BITS, 0x20); - } - mEmitDynamicTableSizeUpdate = false; - mSmallestHeaderTableSizeSetting = Integer.MAX_VALUE; - writeInt(mMaxDynamicTableByteCount, PREFIX_5_BITS, 0x20); - } - - for (int i = 0, size = headerBlock.size(); i < size; i++) { - Header header = headerBlock.get(i); - String name = header.name.toLowerCase(); - String value = header.value; - int headerIndex = -1; - int headerNameIndex = -1; - - Integer staticIndex = NAME_TO_FIRST_INDEX.get(name); - if (staticIndex != null) { - headerNameIndex = staticIndex + 1; - if (headerNameIndex> 1 && headerNameIndex < 8) { - // Only search a subset of the static header table. Most entries have an empty value, so - // it's unnecessary to waste cycles looking at them. This check is built on the - // observation that the header entries we care about are in adjacent pairs, and we - // always know the first index of the pair. - if (TextUtils.equals(STATIC_HEADER_TABLE[headerNameIndex - 1].value, value)) { - headerIndex = headerNameIndex; - } else if (TextUtils.equals(STATIC_HEADER_TABLE[headerNameIndex].value, value)) { - headerIndex = headerNameIndex + 1; - } - } - } - - if (headerIndex == -1) { - for (int j = mNextHeaderIndex + 1, length = mDynamicTable.length; j < length; j++) { - if (TextUtils.equals(mDynamicTable[j].name, name)) { - if (TextUtils.equals(mDynamicTable[j].value, value)) { - headerIndex = j - mNextHeaderIndex + STATIC_HEADER_TABLE.length; - break; - } else if (headerNameIndex == -1) { - headerNameIndex = j - mNextHeaderIndex + STATIC_HEADER_TABLE.length; - } - } - } - } - - if (headerIndex != -1) { - // Indexed Header Field. - writeInt(headerIndex, PREFIX_7_BITS, 0x80); - } else if (headerNameIndex == -1) { - // Literal Header Field with Incremental Indexing - New Name. - mOut.write(0x40); - writeString(name); - writeString(value); - insertIntoDynamicTable(header); - } else if (name.startsWith(Header.PSEUDO_PREFIX) && !Header.TARGET_AUTHORITY.equals(name)) { - // Follow Chromes lead - only include the :authority pseudo header, but exclude all other - // pseudo headers. Literal Header Field without Indexing - Indexed Name. - writeInt(headerNameIndex, PREFIX_4_BITS, 0); - writeString(value); - } else { - // Literal Header Field with Incremental Indexing - Indexed Name. - writeInt(headerNameIndex, PREFIX_6_BITS, 0x40); - writeString(value); - insertIntoDynamicTable(header); - } - } - return mOut.toByteArray(); - } - - private void writeInt(int value, int prefixMask, int bits) { - // Write the raw value for a single byte value. - if (value < prefixMask) { - mOut.write(bits | value); - return; - } - - // Write the mask to start a multibyte value. - mOut.write(bits | prefixMask); - value -= prefixMask; - - // Write 7 bits at a time 'til we're done. - while (value>= 0x80) { - int b = value & 0x7f; - mOut.write(b | 0x80); - value>>>= 7; - } - mOut.write(value); - } - - private void writeString(String data) throws IOException { - byte[] stringBytes = data.getBytes(); - if (Huffman.get().encodedLength(data) < stringBytes.length) { - ByteBuffer buffer = ByteBuffer.allocate(stringBytes.length); - Huffman.get().encode(data, buffer); - buffer.flip(); - writeInt(buffer.remaining(), PREFIX_7_BITS, 0x80); - mOut.write(buffer.array(), buffer.position(), buffer.remaining()); - } else { - writeInt(stringBytes.length, PREFIX_7_BITS, 0); - mOut.write(data.getBytes()); - } - } - - void setHeaderTableSizeSetting(int headerTableSizeSetting) { - if (mHeaderTableSizeSetting == headerTableSizeSetting) { - return; - } - this.mHeaderTableSizeSetting = headerTableSizeSetting; - int effectiveHeaderTableSize = Math.min(headerTableSizeSetting, - SETTINGS_HEADER_TABLE_SIZE_LIMIT); - - if (mMaxDynamicTableByteCount == effectiveHeaderTableSize) { - return; // No change. - } - - if (effectiveHeaderTableSize < mMaxDynamicTableByteCount) { - mSmallestHeaderTableSizeSetting = Math.min(mSmallestHeaderTableSizeSetting, - effectiveHeaderTableSize); - } - mEmitDynamicTableSizeUpdate = true; - mMaxDynamicTableByteCount = effectiveHeaderTableSize; - adjustDynamicTableByteCount(); - } - - private void adjustDynamicTableByteCount() { - if (mMaxDynamicTableByteCount < mDynamicTableByteCount) { - if (mMaxDynamicTableByteCount == 0) { - clearDynamicTable(); - } else { - evictToRecoverBytes(mDynamicTableByteCount - mMaxDynamicTableByteCount); - } - } - } - - } - - private static final class Header { - - // Special header names defined in HTTP/2 spec. - private static final String PSEUDO_PREFIX = ":"; - private static final String RESPONSE_STATUS = ":status"; - private static final String TARGET_METHOD = ":method"; - private static final String TARGET_PATH = ":path"; - private static final String TARGET_SCHEME = ":scheme"; - private static final String TARGET_AUTHORITY = ":authority"; - - private String name; - private String value; - - private Header(String name, String value) { - this.name = name; - this.value = value; - } - - private int hpackSize() { - return 32 + name.getBytes().length + value.getBytes().length; - } - - @Override - public String toString() { - return name + ": " + value; - } - - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2.java deleted file mode 100644 index 2fd0af8..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2.java +++ /dev/null @@ -1,77 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http2; - -/** - * HTTP2 protocol constants and common methods. - * - * See https://httpwg.org/specs/rfc7540.html - * - * @author Megatron King - * @since 2019年1月5日 14:14 - */ -public final class Http2 { - - /** - * In HTTP/2, each endpoint is required to send a connection preface as a final confirmation of - * the protocol in use and to establish the initial settings for the HTTP/2 connection. - */ - public static final byte[] CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(); - - static final int FRAME_HEADER_LENGTH = 9; - - /** - * The initial max frame size, applied independently writing to, or reading from the peer. - * - * 0x4000 = 2^14 = 16384 - */ - static final int INITIAL_MAX_FRAME_SIZE = 0x4000; - - - static final byte FLAG_NONE = 0x0; - - /** - * Used for settings and ping. - */ - static final byte FLAG_ACK = 0x1; - - /** - * Used for headers and data. - */ - static final byte FLAG_END_STREAM = 0x1; - - /** - * Used for headers and continuation. - */ - static final byte FLAG_END_HEADERS = 0x4; - static final byte FLAG_END_PUSH_PROMISE = 0x4; - - /** - * Used for headers and data. - */ - static final byte FLAG_PADDED = 0x8; - - /** - * Used for headers. - */ - static final byte FLAG_PRIORITY = 0x20; - - /** - * Used for data. - */ - static final byte FLAG_COMPRESSED = 0x20; - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2DecodeInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2DecodeInterceptor.java deleted file mode 100644 index b533ecd..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2DecodeInterceptor.java +++ /dev/null @@ -1,481 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http2; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareXLog; -import com.github.megatronking.netbare.http.HttpId; -import com.github.megatronking.netbare.http.HttpPendingInterceptor; -import com.github.megatronking.netbare.http.HttpProtocol; -import com.github.megatronking.netbare.http.HttpRequest; -import com.github.megatronking.netbare.http.HttpRequestChain; -import com.github.megatronking.netbare.http.HttpResponse; -import com.github.megatronking.netbare.http.HttpResponseChain; -import com.github.megatronking.netbare.http.HttpZygoteRequest; -import com.github.megatronking.netbare.http.HttpZygoteResponse; -import com.github.megatronking.netbare.http.SSLRefluxCallback; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Decodes HTTP2 request and response packets. - * - * @author Megatron King - * @since 2019年1月5日 14:19 - */ -public final class Http2DecodeInterceptor extends HttpPendingInterceptor { - - private final SSLRefluxCallback mRefluxCallback; - - private final HttpZygoteRequest mZygoteRequest; - private final HttpZygoteResponse mZygoteResponse; - - private final Map mHttpIds; - - private final Http2Stream mRequestStream; - private final Http2Stream mResponseStream; - - private Hpack.Reader mHpackRequestReader; - private Hpack.Reader mHpackResponseReader; - - private NetBareXLog mLog; - - public Http2DecodeInterceptor(SSLRefluxCallback refluxCallback, HttpZygoteRequest zygoteRequest, - HttpZygoteResponse zygoteResponse) { - this.mRefluxCallback = refluxCallback; - - this.mZygoteRequest = zygoteRequest; - this.mZygoteResponse = zygoteResponse; - - this.mHttpIds = new ConcurrentHashMap(); - - this.mRequestStream = new Http2Stream(); - this.mResponseStream = new Http2Stream(); - } - - @Override - protected void intercept(@NonNull final HttpRequestChain chain, @NonNull ByteBuffer buffer, - int index) throws IOException { - if (chain.request().httpProtocol() == HttpProtocol.HTTP_2) { - if (!buffer.hasRemaining()) { - return; - } - if (mLog == null) { - HttpRequest request = chain.request(); - mLog = new NetBareXLog(request.protocol(), request.ip(), request.port()); - } - if (mHpackRequestReader == null) { - mHpackRequestReader = new Hpack.Reader(); - } - decode(mergeRequestBuffer(buffer), mHpackRequestReader, new DecodeCallback() { - - @Override - public void onPending(ByteBuffer buffer) { - pendRequestBuffer(buffer); - } - - @Override - public void onResult(ByteBuffer buffer, boolean isFinished) throws IOException { - int streamId = mRequestStream.id; - if (streamId < 0) { - throw new IOException("Http2 stream id is < 0"); - } - HttpId id = mHttpIds.get(streamId); - if (id == null) { - id = new HttpId(streamId); - mHttpIds.put(streamId, id); - } - mZygoteRequest.zygote(id); - if (isFinished) { - mZygoteRequest.onStreamFinished(); - } - if (!buffer.hasRemaining()) { - return; - } - chain.process(buffer); - } - - @Override - public void onSkip(ByteBuffer buffer) throws IOException { - mRefluxCallback.onRequest(chain.request(), buffer); - } - - }, mRequestStream, new Http2Updater() { - @Override - public void onSettingsUpdate(Http2Settings http2Settings) { - mZygoteRequest.onSettingsUpdate(http2Settings); - if (http2Settings.getHeaderTableSize()> 0) { - if (mHpackResponseReader == null) { - mHpackResponseReader = new Hpack.Reader(); - } - mHpackResponseReader.setHeaderTableSizeSetting(http2Settings.getHeaderTableSize()); - } - } - - @Override - public void onStreamFinished() { - mZygoteRequest.onStreamFinished(); - } - }); - } else { - chain.process(buffer); - } - } - - @Override - protected void intercept(@NonNull final HttpResponseChain chain, @NonNull ByteBuffer buffer, - int index) - throws IOException { - if (chain.response().httpProtocol() == HttpProtocol.HTTP_2) { - if (!buffer.hasRemaining()) { - return; - } - if (mLog == null) { - HttpResponse response = chain.response(); - mLog = new NetBareXLog(response.protocol(), response.ip(), response.port()); - } - if (mHpackResponseReader == null) { - mHpackResponseReader = new Hpack.Reader(); - } - decode(mergeResponseBuffer(buffer), mHpackResponseReader, new DecodeCallback() { - - @Override - public void onPending(ByteBuffer buffer) { - pendResponseBuffer(buffer); - } - - @Override - public void onResult(ByteBuffer buffer, boolean isFinished) throws IOException { - int streamId = mResponseStream.id; - if (streamId < 0) { - throw new IOException("Http2 stream id is < 0"); - } - HttpId id = mHttpIds.get(streamId); - if (id == null) { - id = new HttpId(streamId); - mHttpIds.put(streamId, id); - } - mZygoteResponse.zygote(id); - if (isFinished) { - mZygoteResponse.onStreamFinished(); - } - if (!buffer.hasRemaining()) { - return; - } - chain.process(buffer); - } - - @Override - public void onSkip(ByteBuffer buffer) throws IOException { - mRefluxCallback.onResponse(chain.response(), buffer); - } - - }, mResponseStream, new Http2Updater() { - @Override - public void onSettingsUpdate(Http2Settings http2Settings) { - mZygoteResponse.onSettingsUpdate(http2Settings); - if (http2Settings.getHeaderTableSize()> 0) { - if (mHpackRequestReader == null) { - mHpackRequestReader = new Hpack.Reader(); - } - mHpackRequestReader.setHeaderTableSizeSetting(http2Settings.getHeaderTableSize()); - } - } - - @Override - public void onStreamFinished() { - mZygoteResponse.onStreamFinished(); - } - }); - } else { - chain.process(buffer); - } - } - - private void decode(ByteBuffer buffer, Hpack.Reader reader, DecodeCallback callback, - Http2Stream stream, Http2Updater updater) - throws IOException { - // HTTP2 frame structure - // 0 1 2 3 - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | Length (24) | - // +---------------+---------------+---------------+ - // | Type (8) | Flags (8) | - // +-+-+-----------+---------------+-------------------------------+ - // |R| Stream Identifier (31) | - // +=+=============================================================+ - // | Frame Payload (0...) ... - // +---------------------------------------------------------------+ - if (buffer.remaining() < Http2.FRAME_HEADER_LENGTH) { - callback.onPending(buffer); - return; - } - int length = readMedium(buffer); - if (length < 0 || length> Http2.INITIAL_MAX_FRAME_SIZE) { - // Values greater than 214 (16,384) MUST NOT be sent unless the receiver has set a - // larger value for SETTINGS_MAX_FRAME_SIZE. - throw new IOException("Http2 frame size error: " + length); - } - // Check payload length - if (length + 6> buffer.remaining()) { - mLog.w("No enough http2 frame length, expect: %d actual: %d", length, - buffer.remaining() - 6); - // Packet not enough for one frame, wait next packet. - // Revert position. - buffer.position(buffer.position() - 3); - callback.onPending(buffer); - return; - } else if (length + 6 < buffer.remaining()) { - mLog.w("Multi http2 frames in one buffer, first frame length : %d, buffer length: %d", - length + 9, buffer.remaining() + 3); - // Separate multi-frames - ByteBuffer newBuffer = ByteBuffer.allocate(length + 9); - newBuffer.put(buffer.array(), buffer.position() - 3, newBuffer.capacity()); - newBuffer.flip(); - decode(newBuffer, reader, callback, stream, updater); - // Process the left data - buffer.position(buffer.position() + length + 6); - decode(buffer, reader, callback, stream, updater); - return; - } - - byte type = (byte) (buffer.get() & 0xff); - byte flags = (byte) (buffer.get() & 0xff); - int streamId = buffer.getInt() & 0x7fffffff; - FrameType frameType = FrameType.parse(type); - if (frameType == null) { - mLog.e("Unexpected http2 frame type: " + type); - // Discard frames that have unknown or unsupported types. - return; - } - if (stream.id != -1) { - if (streamId != stream.id && frameType == FrameType.CONTINUATION) { - throw new IOException("Http2 TYPE_CONTINUATION streamId changed!"); - } - } - mLog.i("Decode a http2 frame: " + frameType + " stream(" + streamId + - ") length(" + length + ")"); - stream.id = streamId; - switch (frameType) { - case DATA: - decodeData(buffer, length, flags, streamId, callback); - return; - case HEADERS: - case CONTINUATION: - decodeHeaders(buffer, reader, length, flags, streamId, callback); - return; - case SETTINGS: - decodeSettings(buffer, length, flags, streamId, updater); - // No return - break; - case GOAWAY: - decodeGoAway(buffer, length, flags, streamId); - // No return - break; - default: - break; - } - // Encrypt and send it to remote server directly. - buffer.position(buffer.position() - Http2.FRAME_HEADER_LENGTH); - callback.onSkip(buffer); - } - - private int readMedium(ByteBuffer buffer) { - return (buffer.get() & 0xff) << 16 - | (buffer.get() & 0xff) << 8 - | (buffer.get() & 0xff); - } - - private void decodeSettings(ByteBuffer buffer, int length, byte flags, int streamId, - Http2Updater receiver) - throws IOException { - if (streamId != 0) { - throw new IOException("Http2 TYPE_SETTINGS streamId != 0"); - } - if ((flags & Http2.FLAG_ACK) != 0) { - if (length != 0) { - throw new IOException("Http2 FRAME_SIZE_ERROR ack frame should be empty!"); - } - mLog.i("Http2 ack the settings"); - return; - } - if (length % 6 != 0) { - throw new IOException("Http2 TYPE_SETTINGS length %% 6 != 0: " + length); - } - int initPosition = buffer.position(); - Http2Settings settings = new Http2Settings(); - for (int i = 0; i < length; i += 6) { - int id = buffer.getShort() & 0xFFFF; - int value = buffer.getInt(); - switch (id) { - case 1: // SETTINGS_HEADER_TABLE_SIZE - mLog.i("Http2 SETTINGS_HEADER_TABLE_SIZE: " + value); - break; - case 2: // SETTINGS_ENABLE_PUSH - if (value != 0 && value != 1) { - throw new IOException("Http2 PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1"); - } - break; - case 3: // SETTINGS_MAX_CONCURRENT_STREAMS - id = 4; // Renumbered in draft 10. - mLog.i("Http2 SETTINGS_MAX_CONCURRENT_STREAMS: " + value); - break; - case 4: // SETTINGS_INITIAL_WINDOW_SIZE - id = 7; // Renumbered in draft 10. - if (value < 0) { - throw new IOException("Http2 PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE> 2^31 - 1"); - } - mLog.i("Http2 SETTINGS_INITIAL_WINDOW_SIZE: " + value); - break; - case 5: // SETTINGS_MAX_FRAME_SIZE - if (value < Http2.INITIAL_MAX_FRAME_SIZE || value> 16777215) { - throw new IOException("Http2 PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: " + value); - } - mLog.i("Http2 INITIAL_MAX_FRAME_SIZE: " + value); - break; - case 6: // SETTINGS_MAX_HEADER_LIST_SIZE - break; // Advisory only, so ignored. - default: - break; // Must ignore setting with unknown id. - } - settings.set(id, value); - } - // Reverse the position and sent to terminal. - buffer.position(initPosition); - receiver.onSettingsUpdate(settings); - } - - private void decodeHeaders(ByteBuffer buffer, Hpack.Reader reader, int length, byte flags, - int streamId, DecodeCallback callback) throws IOException { - // +---------------+ - // |Pad Length? (8)| - // +-+-------------+-----------------------------------------------+ - // |E| Stream Dependency? (31) | - // +-+-------------+-----------------------------------------------+ - // | Weight? (8) | - // +-+-------------+-----------------------------------------------+ - // | Header Block Fragment (*) ... - // +---------------------------------------------------------------+ - // | Padding (*) ... - // +---------------------------------------------------------------+ - if (streamId == 0) { - throw new IOException("Http2 PROTOCOL_ERROR: TYPE_HEADERS streamId == 0"); - } - short padding = (flags & Http2.FLAG_PADDED) != 0 ? (short) (buffer.get() & 0xff) : 0; - if ((flags & Http2.FLAG_PRIORITY) != 0) { - // Skip priority. - buffer.position(buffer.position() + 5); - } - length = lengthWithoutPadding(length, flags, padding); - boolean endStream = (flags & Http2.FLAG_END_STREAM) != 0; - if (length> 0) { - decodeHeaderBlock(buffer, reader, flags, callback); - } else { - // Notify stream is end - callback.onResult(ByteBuffer.allocate(0), endStream); - if (endStream) { - callback.onSkip(endStream(FrameType.HEADERS, streamId)); - } - } - } - - private void decodeHeaderBlock(ByteBuffer buffer, Hpack.Reader reader, byte flags, - DecodeCallback callback) throws IOException { - try { - reader.readHeaders(buffer, flags, callback); - } catch (IndexOutOfBoundsException e) { - throw new IOException("Http2 decode header block failed."); - } - } - - private void decodeData(ByteBuffer buffer, int length, byte flags, int streamId, - DecodeCallback callback) throws IOException { - if (streamId == 0) { - throw new IOException("Http2 PROTOCOL_ERROR: TYPE_DATA streamId == 0"); - } - boolean gzipped = (flags & Http2.FLAG_COMPRESSED) != 0; - if (gzipped) { - throw new IOException("Http2 PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA"); - } - short padding = (flags & Http2.FLAG_PADDED) != 0 ? (short) (buffer.get() & 0xff) : 0; - length = lengthWithoutPadding(length, flags, padding); - boolean endStream = (flags & Http2.FLAG_END_STREAM) != 0; - if (length> 0) { - callback.onResult(ByteBuffer.wrap(Arrays.copyOfRange(buffer.array(), buffer.position(), - buffer.position() + length)), endStream); - } else { - // Notify stream is end - callback.onResult(ByteBuffer.allocate(0), endStream); - if (endStream) { - callback.onSkip(endStream(FrameType.DATA, streamId)); - } - } - } - - private void decodeGoAway(ByteBuffer buffer, int length, byte flags, int streamId) - throws IOException { - if (length < 8) { - throw new IOException("Http2 TYPE_GOAWAY length < 8: " + length); - } - if (streamId != 0) { - throw new IOException("Http2 TYPE_GOAWAY streamId != 0"); - } - int initPosition = buffer.position(); - int lastStreamId = buffer.getInt(); - int errorCodeInt = buffer.getInt(); - int opaqueDataLength = length - 8; - ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt); - if (errorCode == null) { - throw new IOException("Http2 TYPE_GOAWAY unexpected error code: " + errorCodeInt); - } - mLog.e("Http2 TYPE_GOAWAY error code: " + errorCode + " last stream: " + lastStreamId); - if (opaqueDataLength> 0) { // Must read debug data in order to not corrupt the connection. - byte[] debugData = new byte[opaqueDataLength]; - buffer.get(debugData); - mLog.e("Http2 TYPE_GOAWAY debug data: " + new String(debugData)); - } - buffer.position(initPosition); - } - - private int lengthWithoutPadding(int length, byte flags, short padding) throws IOException { - if ((flags & Http2.FLAG_PADDED) != 0) { - length--; // Account for reading the padding length. - } - if (padding> length) { - throw new IOException("Http2 PROTOCOL_ERROR padding " + padding + "> remaining length " + length); - } - return (short) (length - padding); - } - - private ByteBuffer endStream(FrameType frameType, int streamId) { - ByteBuffer endBuffer = ByteBuffer.allocate(Http2.FRAME_HEADER_LENGTH); - endBuffer.put((byte) 0); - endBuffer.put((byte) 0); - endBuffer.put((byte) 0); - endBuffer.put((byte) (frameType.get() & 0xff)); - endBuffer.put((byte) (Http2.FLAG_END_STREAM & 0xff)); - endBuffer.putInt(streamId & 0x7fffffff); - endBuffer.flip(); - return endBuffer; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2EncodeInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2EncodeInterceptor.java deleted file mode 100644 index 26df14b..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2EncodeInterceptor.java +++ /dev/null @@ -1,245 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http2; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareXLog; -import com.github.megatronking.netbare.gateway.InterceptorChain; -import com.github.megatronking.netbare.http.HttpInterceptor; -import com.github.megatronking.netbare.http.HttpProtocol; -import com.github.megatronking.netbare.http.HttpRequest; -import com.github.megatronking.netbare.http.HttpRequestChain; -import com.github.megatronking.netbare.http.HttpResponse; -import com.github.megatronking.netbare.http.HttpResponseChain; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Encodes HTTP2 request and response packets. - * - * @author Megatron King - * @since 2019年1月5日 14:24 - */ -public final class Http2EncodeInterceptor extends HttpInterceptor { - - private final Map mStreamRequestIndexes; - private final Map mStreamResponseIndexes; - - private NetBareXLog mLog; - - private Hpack.Writer mHpackRequestWriter; - private Hpack.Writer mHpackResponseWriter; - - public Http2EncodeInterceptor() { - mStreamRequestIndexes = new ConcurrentHashMap(); - mStreamResponseIndexes = new ConcurrentHashMap(); - } - - @Override - protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer) - throws IOException { - if (chain.request().httpProtocol() == HttpProtocol.HTTP_2) { - if (mLog == null) { - HttpRequest request = chain.request(); - mLog = new NetBareXLog(request.protocol(), request.ip(), request.port()); - } - if (mHpackRequestWriter == null) { - mHpackRequestWriter = new Hpack.Writer(); - } - int index; - int streamId = chain.request().streamId(); - Integer requestIndex = mStreamRequestIndexes.get(streamId); - if (requestIndex != null) { - index = requestIndex + 1; - } else { - index = 0; - } - mStreamRequestIndexes.put(streamId, index); - if (index == 0) { - encodeRequestHeader(chain); - } else { - encodeRequestData(chain, buffer); - } - } else { - chain.process(buffer); - } - - } - - @Override - protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer) - throws IOException { - if (chain.response().httpProtocol() == HttpProtocol.HTTP_2) { - if (mLog == null) { - HttpResponse response = chain.response(); - mLog = new NetBareXLog(response.protocol(), response.ip(), response.port()); - } - if (mHpackResponseWriter == null) { - mHpackResponseWriter = new Hpack.Writer(); - } - int index; - int streamId = chain.response().streamId(); - Integer responseIndex = mStreamResponseIndexes.get(streamId); - if (responseIndex != null) { - index = responseIndex + 1; - } else { - index = 0; - } - mStreamResponseIndexes.put(streamId, index); - if (index == 0) { - encodeResponseHeader(chain); - } else { - encodeResponseData(chain, buffer); - } - } else { - chain.process(buffer); - } - } - - private void encodeRequestHeader(HttpRequestChain chain) throws IOException { - HttpRequest request = chain.request(); - Http2Settings peerHttp2Settings = request.peerHttp2Settings(); - if (peerHttp2Settings != null) { - int headerTableSize = peerHttp2Settings.getHeaderTableSize(); - if (headerTableSize != -1) { - mHpackRequestWriter.setHeaderTableSizeSetting(headerTableSize); - } - } - byte[] headerBlock = mHpackRequestWriter.writeRequestHeaders(request.method(), - request.path(), request.host(), request.requestHeaders()); - sendHeaderBlockFrame(chain, headerBlock, peerHttp2Settings, request.streamId(), - request.requestStreamEnd()); - } - - private void encodeResponseHeader(HttpResponseChain chain) throws IOException { - HttpResponse response = chain.response(); - Http2Settings clientHttp2Settings = response.clientHttp2Settings(); - if (clientHttp2Settings != null) { - int headerTableSize = clientHttp2Settings.getHeaderTableSize(); - if (headerTableSize != -1) { - mHpackResponseWriter.setHeaderTableSizeSetting(headerTableSize); - } - } - byte[] headerBlock = mHpackResponseWriter.writeResponseHeaders(response.code(), - response.message(), response.responseHeaders()); - sendHeaderBlockFrame(chain, headerBlock, clientHttp2Settings, response.streamId(), - response.responseStreamEnd()); - } - - private void encodeRequestData(HttpRequestChain chain, ByteBuffer buffer) throws IOException { - byte[] data = Arrays.copyOfRange(buffer.array(), buffer.position(), buffer.limit()); - HttpRequest request = chain.request(); - sendDataFrame(chain, data, request.peerHttp2Settings(), request.streamId(), - request.requestStreamEnd()); - } - - private void encodeResponseData(HttpResponseChain chain, ByteBuffer buffer) throws IOException { - byte[] data = Arrays.copyOfRange(buffer.array(), buffer.position(), buffer.limit()); - HttpResponse response = chain.response(); - sendDataFrame(chain, data, response.clientHttp2Settings(), response.streamId(), - response.responseStreamEnd()); - } - - private void sendHeaderBlockFrame(InterceptorChain chain, byte[] headerBlock, Http2Settings http2Settings, - int streamId, boolean endStream) throws IOException { - int maxFrameSize = http2Settings == null ? Http2.INITIAL_MAX_FRAME_SIZE : - http2Settings.getMaxFrameSize(Http2.INITIAL_MAX_FRAME_SIZE); - int byteCount = headerBlock.length; - int length = Math.min(maxFrameSize, byteCount); - byte type = FrameType.HEADERS.get(); - byte flags = 0; - if (byteCount == length) { - flags |= Http2.FLAG_END_HEADERS; - if (endStream) { - flags |= Http2.FLAG_END_STREAM; - } - } - ByteArrayOutputStream os = new ByteArrayOutputStream(); - os.write(frameHeader(streamId, length, type, flags)); - os.write(headerBlock, 0, length); - chain.process(ByteBuffer.wrap(os.toByteArray())); - if (byteCount> length) { - byte[] left = Arrays.copyOfRange(headerBlock, length, byteCount); - sendContinuationFrame(chain, left, streamId, maxFrameSize, byteCount - length, - endStream); - } - } - - private void sendContinuationFrame(InterceptorChain chain, byte[] headerBlock, int streamId, - int maxFrameSize, long byteCount, boolean endStream) throws IOException { - int offset = 0; - while (byteCount> 0) { - int length = (int) Math.min(maxFrameSize, byteCount); - byteCount -= length; - ByteArrayOutputStream os = new ByteArrayOutputStream(); - byte flags = 0; - if (byteCount == 0) { - flags |= Http2.FLAG_END_HEADERS; - if (endStream) { - mLog.i("Http2 stream end: " + streamId); - flags |= Http2.FLAG_END_STREAM; - } - } - os.write(frameHeader(streamId, length, FrameType.CONTINUATION.get(), flags)); - os.write(headerBlock, offset, length); - offset += length; - chain.process(ByteBuffer.wrap(os.toByteArray())); - } - } - - private void sendDataFrame(InterceptorChain chain, byte[] data, Http2Settings http2Settings, - int streamId, boolean endStream) throws IOException { - int maxFrameSize = http2Settings == null ? Http2.INITIAL_MAX_FRAME_SIZE : - http2Settings.getMaxFrameSize(Http2.INITIAL_MAX_FRAME_SIZE); - int byteCount = data.length; - byte type = FrameType.DATA.get(); - int offset = 0; - while (byteCount> 0) { - int length = Math.min(maxFrameSize, byteCount); - byteCount -= length; - ByteArrayOutputStream os = new ByteArrayOutputStream(); - byte flags = 0; - if (byteCount == 0 && endStream) { - mLog.i("Http2 stream end: " + streamId); - flags |= Http2.FLAG_END_STREAM; - } - os.write(frameHeader(streamId, length, type, flags)); - os.write(data, offset, length); - offset += length; - chain.process(ByteBuffer.wrap(os.toByteArray())); - } - } - - private byte[] frameHeader(int streamId, int length, byte type, byte flags) { - mLog.i("Encode a http2 frame: " + FrameType.parse(type) + " stream(" + streamId + - ") length(" + length + ")"); - ByteBuffer header = ByteBuffer.allocate(Http2.FRAME_HEADER_LENGTH); - header.put((byte) ((length>>> 16) & 0xff)); - header.put((byte) ((length>>> 8) & 0xff)); - header.put((byte) (length & 0xff)); - header.put((byte) (type & 0xff)); - header.put((byte) (flags & 0xff)); - header.putInt(streamId & 0x7fffffff); - return header.array(); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Settings.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Settings.java deleted file mode 100644 index a0f4991..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Settings.java +++ /dev/null @@ -1,165 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -/* - * Copyright (C) 2013 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.github.megatronking.netbare.http2; - -import java.util.Arrays; - -/** - * Http/2 peer settings. - * - * @author Megatron King - * @since 2019年1月6日 23:14 - */ -public final class Http2Settings { - - /** - * From the HTTP/2 specs, the default initial window size for all streams is 64 KiB. (Chrome 25 - * uses 10 MiB). - */ - private static final int DEFAULT_INITIAL_WINDOW_SIZE = 65535; - - /** - * HTTP/2: Size in bytes of the table used to decode the sender's header blocks. - */ - private static final int HEADER_TABLE_SIZE = 1; - /** - * HTTP/2: The peer must not send a PUSH_PROMISE frame when this is 0. - */ - private static final int ENABLE_PUSH = 2; - - /** - * Sender's maximum number of concurrent streams. - */ - private static final int MAX_CONCURRENT_STREAMS = 4; - - /** - * HTTP/2: Size in bytes of the largest frame payload the sender will accept. - */ - private static final int MAX_FRAME_SIZE = 5; - - /** - * HTTP/2: Advisory only. Size in bytes of the largest header list the sender will accept. - */ - private static final int MAX_HEADER_LIST_SIZE = 6; - - /** - * Window size in bytes. - */ - private static final int INITIAL_WINDOW_SIZE = 7; - - /** - * Total number of settings. - */ - private static final int COUNT = 10; - - /** - * Bitfield of which flags that values. - */ - private int set; - - /** - * Flag values. - */ - private final int[] values = new int[COUNT]; - - void clear() { - set = 0; - Arrays.fill(values, 0); - } - - Http2Settings set(int id, int value) { - if (id < 0 || id>= values.length) { - return this; // Discard unknown settings. - } - - int bit = 1 << id; - set |= bit; - values[id] = value; - return this; - } - - boolean isSet(int id) { - int bit = 1 << id; - return (set & bit) != 0; - } - - int get(int id) { - return values[id]; - } - - int size() { - return Integer.bitCount(set); - } - - int getHeaderTableSize() { - int bit = 1 << HEADER_TABLE_SIZE; - return (bit & set) != 0 ? values[HEADER_TABLE_SIZE] : -1; - } - - boolean getEnablePush(boolean defaultValue) { - int bit = 1 << ENABLE_PUSH; - return ((bit & set) != 0 ? values[ENABLE_PUSH] : defaultValue ? 1 : 0) == 1; - } - - int getMaxConcurrentStreams(int defaultValue) { - int bit = 1 << MAX_CONCURRENT_STREAMS; - return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue; - } - - int getMaxFrameSize(int defaultValue) { - int bit = 1 << MAX_FRAME_SIZE; - return (bit & set) != 0 ? values[MAX_FRAME_SIZE] : defaultValue; - } - - int getMaxHeaderListSize(int defaultValue) { - int bit = 1 << MAX_HEADER_LIST_SIZE; - return (bit & set) != 0 ? values[MAX_HEADER_LIST_SIZE] : defaultValue; - } - - int getInitialWindowSize() { - int bit = 1 << INITIAL_WINDOW_SIZE; - return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : DEFAULT_INITIAL_WINDOW_SIZE; - } - - /** - * Writes {@code other} into this. If any setting is populated by this and {@code other}, the - * value and flags from {@code other} will be kept. - */ - public void merge(Http2Settings other) { - for (int i = 0; i < COUNT; i++) { - if (!other.isSet(i)) { - continue; - } - set(i, other.get(i)); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Stream.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Stream.java deleted file mode 100644 index 03cfca2..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Stream.java +++ /dev/null @@ -1,33 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http2; - -/** - * A "stream" is an independent, bidirectional sequence of frames exchanged between the client and - * server within an HTTP/2 connection. - * - * @author Megatron King - * @since 2019年1月5日 23:16 - */ -/* package */ class Http2Stream { - - public int id; - - /* package */ Http2Stream() { - this.id = -1; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Updater.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Updater.java deleted file mode 100644 index cd461c8..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Updater.java +++ /dev/null @@ -1,30 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http2; - -/** - * A updater observes changes of HTTP2 session. - * - * @author Megatron King - * @since 2019年1月6日 23:23 - */ -public interface Http2Updater { - - void onSettingsUpdate(Http2Settings http2Settings); - - void onStreamFinished(); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Huffman.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Huffman.java deleted file mode 100644 index c5b4747..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Huffman.java +++ /dev/null @@ -1,236 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -/* - * Copyright (C) 2013 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.github.megatronking.netbare.http2; - -import java.io.ByteArrayOutputStream; -import java.nio.ByteBuffer; - -/** - * This class was originally composed from the following classes in Twitter Hpack. - *

                - *

                  - *
                • {@code com.twitter.hpack.HuffmanEncoder}
                • - *
                • {@code com.twitter.hpack.HuffmanDecoder}
                • - *
                • {@code com.twitter.hpack.HpackUtil}
                • - *
                - */ -/* package */ final class Huffman { - - // Appendix C: Huffman Codes - // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-B - private static final int[] CODES = { - 0x1ff8, 0x7fffd8, 0xfffffe2, 0xfffffe3, 0xfffffe4, 0xfffffe5, 0xfffffe6, 0xfffffe7, 0xfffffe8, - 0xffffea, 0x3ffffffc, 0xfffffe9, 0xfffffea, 0x3ffffffd, 0xfffffeb, 0xfffffec, 0xfffffed, - 0xfffffee, 0xfffffef, 0xffffff0, 0xffffff1, 0xffffff2, 0x3ffffffe, 0xffffff3, 0xffffff4, - 0xffffff5, 0xffffff6, 0xffffff7, 0xffffff8, 0xffffff9, 0xffffffa, 0xffffffb, 0x14, 0x3f8, - 0x3f9, 0xffa, 0x1ff9, 0x15, 0xf8, 0x7fa, 0x3fa, 0x3fb, 0xf9, 0x7fb, 0xfa, 0x16, 0x17, 0x18, - 0x0, 0x1, 0x2, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x5c, 0xfb, 0x7ffc, 0x20, 0xffb, - 0x3fc, 0x1ffa, 0x21, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, - 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0xfc, 0x73, 0xfd, 0x1ffb, 0x7fff0, - 0x1ffc, 0x3ffc, 0x22, 0x7ffd, 0x3, 0x23, 0x4, 0x24, 0x5, 0x25, 0x26, 0x27, 0x6, 0x74, 0x75, - 0x28, 0x29, 0x2a, 0x7, 0x2b, 0x76, 0x2c, 0x8, 0x9, 0x2d, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7ffe, - 0x7fc, 0x3ffd, 0x1ffd, 0xffffffc, 0xfffe6, 0x3fffd2, 0xfffe7, 0xfffe8, 0x3fffd3, 0x3fffd4, - 0x3fffd5, 0x7fffd9, 0x3fffd6, 0x7fffda, 0x7fffdb, 0x7fffdc, 0x7fffdd, 0x7fffde, 0xffffeb, - 0x7fffdf, 0xffffec, 0xffffed, 0x3fffd7, 0x7fffe0, 0xffffee, 0x7fffe1, 0x7fffe2, 0x7fffe3, - 0x7fffe4, 0x1fffdc, 0x3fffd8, 0x7fffe5, 0x3fffd9, 0x7fffe6, 0x7fffe7, 0xffffef, 0x3fffda, - 0x1fffdd, 0xfffe9, 0x3fffdb, 0x3fffdc, 0x7fffe8, 0x7fffe9, 0x1fffde, 0x7fffea, 0x3fffdd, - 0x3fffde, 0xfffff0, 0x1fffdf, 0x3fffdf, 0x7fffeb, 0x7fffec, 0x1fffe0, 0x1fffe1, 0x3fffe0, - 0x1fffe2, 0x7fffed, 0x3fffe1, 0x7fffee, 0x7fffef, 0xfffea, 0x3fffe2, 0x3fffe3, 0x3fffe4, - 0x7ffff0, 0x3fffe5, 0x3fffe6, 0x7ffff1, 0x3ffffe0, 0x3ffffe1, 0xfffeb, 0x7fff1, 0x3fffe7, - 0x7ffff2, 0x3fffe8, 0x1ffffec, 0x3ffffe2, 0x3ffffe3, 0x3ffffe4, 0x7ffffde, 0x7ffffdf, - 0x3ffffe5, 0xfffff1, 0x1ffffed, 0x7fff2, 0x1fffe3, 0x3ffffe6, 0x7ffffe0, 0x7ffffe1, 0x3ffffe7, - 0x7ffffe2, 0xfffff2, 0x1fffe4, 0x1fffe5, 0x3ffffe8, 0x3ffffe9, 0xffffffd, 0x7ffffe3, - 0x7ffffe4, 0x7ffffe5, 0xfffec, 0xfffff3, 0xfffed, 0x1fffe6, 0x3fffe9, 0x1fffe7, 0x1fffe8, - 0x7ffff3, 0x3fffea, 0x3fffeb, 0x1ffffee, 0x1ffffef, 0xfffff4, 0xfffff5, 0x3ffffea, 0x7ffff4, - 0x3ffffeb, 0x7ffffe6, 0x3ffffec, 0x3ffffed, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea, - 0x7ffffeb, 0xffffffe, 0x7ffffec, 0x7ffffed, 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x3ffffee - }; - - private static final byte[] CODE_LENGTHS = { - 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 30, - 28, 28, 28, 28, 28, 28, 28, 28, 28, 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6, 5, - 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10, 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6, 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, - 6, 6, 5, 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28, 20, 22, 20, 20, 22, 22, 22, 23, - 22, 23, 23, 23, 23, 23, 24, 23, 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, - 24, 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23, 21, 21, 22, 21, 23, 22, - 23, 23, 20, 22, 22, 22, 23, 22, 22, 23, 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, - 26, 24, 25, 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27, 20, 24, 20, 21, - 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23, 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, - 27, 27, 27, 27, 26 - }; - - private static final Huffman INSTANCE = new Huffman(); - - public static Huffman get() { - return INSTANCE; - } - - private final Node root = new Node(); - - private Huffman() { - buildTree(); - } - - byte[] decode(byte[] buf) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Node node = root; - int current = 0; - int nbits = 0; - for (byte bb : buf) { - int b = bb & 0xFF; - current = (current << 8) | b; - nbits += 8; - while (nbits>= 8) { - int c = (current>>> (nbits - 8)) & 0xFF; - node = node.children[c]; - if (node.children == null) { - // terminal node - baos.write(node.symbol); - nbits -= node.terminalBits; - node = root; - } else { - // non-terminal node - nbits -= 8; - } - } - } - - while (nbits> 0) { - int c = (current << (8 - nbits)) & 0xFF; - node = node.children[c]; - if (node.children != null || node.terminalBits> nbits) { - break; - } - baos.write(node.symbol); - nbits -= node.terminalBits; - node = root; - } - - return baos.toByteArray(); - } - - void encode(String data, ByteBuffer out) { - long current = 0; - int n = 0; - for (byte bb : data.getBytes()) { - int b = bb & 0xFF; - int code = CODES[b]; - int nbits = CODE_LENGTHS[b]; - - current <<= nbits; - current |= code; - n += nbits; - - while (n>= 8) { - n -= 8; - out.put((byte) (current>> n)); - } - } - - if (n> 0) { - current <<= (8 - n); - current |= (0xFF>>> n); - out.put((byte) current); - } - } - - int encodedLength(String data) { - long len = 0; - for (byte bb : data.getBytes()) { - int b = bb & 0xFF; - len += CODE_LENGTHS[b]; - } - return (int) ((len + 7)>> 3); - } - - private void buildTree() { - for (int i = 0; i < CODE_LENGTHS.length; i++) { - addCode(i, CODES[i], CODE_LENGTHS[i]); - } - } - - private void addCode(int sym, int code, byte len) { - Node terminal = new Node(sym, len); - - Node current = root; - while (len> 8) { - len -= 8; - int i = ((code>>> len) & 0xFF); - if (current.children == null) { - throw new IllegalStateException("invalid dictionary: prefix not unique"); - } - if (current.children[i] == null) { - current.children[i] = new Node(); - } - current = current.children[i]; - } - - int shift = 8 - len; - int start = (code << shift) & 0xFF; - int end = 1 << shift; - for (int i = start; i < start + end; i++) { - current.children[i] = terminal; - } - } - - private static final class Node { - - /** - * Null if terminal. - */ - private final Node[] children; - - /** - * Terminal nodes have a symbol. - */ - private final int symbol; - - /** - * Number of bits represented in the terminal node. - */ - private final int terminalBits; - - private Node() { - this.children = new Node[256]; - this.symbol = 0; // Not read. - this.terminalBits = 0; // Not read. - } - - private Node(int symbol, int bits) { - this.children = null; - this.symbol = symbol; - int b = bits & 0x07; - this.terminalBits = b == 0 ? 8 : b; - } - } -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/Header.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/Header.java deleted file mode 100644 index 803856f..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/Header.java +++ /dev/null @@ -1,80 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ip; - -/** - * An abstract header object for ip protocol packets, provides some common apis. - * - * @author Megatron King - * @since 2018年10月09日 16:28 - */ -/* package */ abstract class Header { - - byte[] packet; - int offset; - - /* package */ Header(byte[] packet, int offset) { - this.packet = packet; - this.offset = offset; - } - - byte readByte(int offset) { - return packet[offset]; - } - - void writeByte(byte value, int offset) { - packet[offset] = value; - } - - short readShort(int offset) { - int r = ((packet[offset] & 0xFF) << 8) | (packet[offset + 1] & 0xFF); - return (short) r; - } - - void writeShort(short value, int offset) { - packet[offset] = (byte) (value>> 8); - packet[offset + 1] = (byte) (value); - } - - int readInt(int offset) { - return ((packet[offset] & 0xFF) << 24) - | ((packet[offset + 1] & 0xFF) << 16) - | ((packet[offset + 2] & 0xFF) << 8) - | (packet[offset + 3] & 0xFF); - } - - void writeInt(int value, int offset) { - packet[offset] = (byte) (value>> 24); - packet[offset + 1] = (byte) (value>> 16); - packet[offset + 2] = (byte) (value>> 8); - packet[offset + 3] = (byte) value; - } - - long getSum(int offset, int len) { - long sum = 0; - while (len> 1) { - sum += readShort(offset) & 0xFFFF; - offset += 2; - len -= 2; - } - - if (len> 0) { - sum += (packet[offset] & 0xFF) << 8; - } - return sum; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IcmpHeader.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IcmpHeader.java deleted file mode 100644 index 160546e..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IcmpHeader.java +++ /dev/null @@ -1,91 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ip; - -/** - * ICMP messages are sent using the basic IP header. The first octet of the data portion of the - * datagram is a ICMP type field; the value of this field determines the format of the remaining - * data. Any field labeled "unused" is reserved for later extensions and must be zero when sent, - * but receivers should not use these fields (except to include them in the checksum). - * Unless otherwise noted under the individual format descriptions, the values of the internet - * header fields are as follows: - * - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Type | Code | Checksum | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | TBD | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Optional | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * - * See https://tools.ietf.org/html/rfc792 - * - * @author Megatron King - * @since 2018年10月10日 23:04 - */ -public class IcmpHeader extends Header { - - private static final short OFFSET_TYPE = 0; - private static final short OFFSET_CODE = 1; - private static final short OFFSET_CRC = 2; - - private IpHeader mIpHeader; - - public IcmpHeader(IpHeader header, byte[] packet, int offset) { - super(packet, offset); - mIpHeader = header; - } - - public IpHeader getIpHeader() { - return mIpHeader; - } - - public byte getType() { - return readByte(offset + OFFSET_TYPE); - } - - public byte getCode() { - return readByte(offset + OFFSET_CODE); - } - - public short getCrc() { - return readShort(offset + OFFSET_CRC); - } - - public void setCrc(short crc) { - writeShort(crc, offset + OFFSET_CRC); - } - - public void updateChecksum() { - setCrc((short) 0); - setCrc(computeChecksum()); - } - - private short computeChecksum() { - int dataLength = mIpHeader.getDataLength(); - long sum = mIpHeader.getIpSum(); - sum += mIpHeader.getProtocol() & 0xFF; - sum += dataLength; - sum += getSum(offset, dataLength); - while ((sum>> 16)> 0) { - sum = (sum & 0xFFFF) + (sum>> 16); - } - return (short) ~sum; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpAddress.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpAddress.java deleted file mode 100644 index cb73518..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpAddress.java +++ /dev/null @@ -1,83 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ip; - -import android.os.Parcel; -import android.os.Parcelable; - -import java.util.Objects; - -public class IpAddress implements Parcelable { - - public String address; - public int prefixLength; - - public IpAddress(String address, int prefixLength) { - this.address = address; - this.prefixLength = prefixLength; - } - - @Override - public String toString() { - return address + "/" + prefixLength; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof IpAddress)) { - return false; - } - // Compare string value. - return toString().equals(o.toString()); - } - - @Override - public int hashCode() { - return Objects.hash(address, prefixLength); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(this.address); - dest.writeInt(this.prefixLength); - } - - private IpAddress(Parcel in) { - this.address = in.readString(); - this.prefixLength = in.readInt(); - } - - public static final Creator CREATOR = new Creator() { - @Override - public IpAddress createFromParcel(Parcel source) { - return new IpAddress(source); - } - - @Override - public IpAddress[] newArray(int size) { - return new IpAddress[size]; - } - }; - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpHeader.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpHeader.java deleted file mode 100644 index de4aa46..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpHeader.java +++ /dev/null @@ -1,130 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ip; - -/** - * A summary of the contents of the internet header follows: - * - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * |Version| IHL |Type of Service| Total Length | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Identification |Flags| Fragment Offset | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Time to Live | Protocol | Header Checksum | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Source Address | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Destination Address | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Options | Padding | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * - * See https://tools.ietf.org/html/rfc791#section-3.1 - * - * @author Megatron King - * @since 2018年10月10日 21:02 - */ -public final class IpHeader extends Header { - - public static final int MIN_HEADER_LENGTH = 20; - - private static final int OFFSET_PROTOCOL = 9; - private static final int OFFSET_VER_IHL = 0; - private static final int OFFSET_SRC_IP = 12; - private static final int OFFSET_DEST_IP = 16; - private static final int OFFSET_TLEN = 2; - private static final int OFFSET_CRC = 10; - - public IpHeader(byte[] packet, int offset) { - super(packet, offset); - } - - public byte getProtocol() { - return packet[offset + OFFSET_PROTOCOL]; - } - - public void setProtocol(byte value) { - packet[offset + OFFSET_PROTOCOL] = value; - } - - public int getHeaderLength() { - return (packet[offset + OFFSET_VER_IHL] & 0x0F) * 4; - } - - public void setHeaderLength(int value) { - packet[offset + OFFSET_VER_IHL] = (byte) ((4 << 4) | (value / 4)); - } - - public int getSourceIp() { - return readInt(offset + OFFSET_SRC_IP); - } - - public void setSourceIp(int ip) { - writeInt(ip, offset + OFFSET_SRC_IP); - } - - public int getDestinationIp() { - return readInt(offset + OFFSET_DEST_IP); - } - - public void setDestinationIp(int ip) { - writeInt(ip, offset + OFFSET_DEST_IP); - } - - public int getDataLength() { - return this.getTotalLength() - this.getHeaderLength(); - } - - public int getTotalLength() { - return readShort(offset + OFFSET_TLEN) & 0xFFFF; - } - - public void setTotalLength(short len) { - writeShort(len, offset + OFFSET_TLEN); - } - - public short getCrc() { - return readShort(offset + OFFSET_CRC); - } - - public void setCrc(short crc) { - writeShort(crc, offset + OFFSET_CRC); - } - - public void updateChecksum() { - setCrc((short) 0); - setCrc(computeChecksum()); - } - - public long getIpSum() { - // length 8 = src ip(4) + dest ip(4) - return getSum(offset + OFFSET_SRC_IP, 8); - } - - private short computeChecksum() { - // The checksum field is the 16 bit one's complement of the one's complement sum of all - // 16 bit words in the header. - int len = getHeaderLength(); - long sum = getSum(offset, len); - while ((sum>> 16)> 0) { - sum = (sum & 0xFFFF) + (sum>> 16); - } - return (short) ~sum; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/Protocol.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/Protocol.java deleted file mode 100644 index 3f54a1c..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/Protocol.java +++ /dev/null @@ -1,68 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ip; - -import androidx.annotation.Nullable; - -/** - * The enum defines all supported IP protocols. - * - * Internet Protocol numbers see: - * https://en.wikipedia.org/wiki/List_of_IP_protocol_numbers - * - * @author Megatron King - * @since 2018年10月11日 00:13 - */ -public enum Protocol { - - /** - * Internet Control Message Protocol. - */ - ICMP((byte)1), - - /** - * Transmission Control Protocol. - */ - TCP((byte)6), - - /** - * User Datagram Protocol. - */ - UDP((byte)17); - - final byte number; - - Protocol(byte number) { - this.number = number; - } - - /** - * Parse the protocol by number. - * - * @param number Protocol number. - * @return The supported protocol number or null. - */ - @Nullable - public static Protocol parse(int number) { - for (Protocol protocol : values()) { - if (protocol.number == number) { - return protocol; - } - } - return null; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/TcpHeader.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/TcpHeader.java deleted file mode 100644 index 121488f..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/TcpHeader.java +++ /dev/null @@ -1,157 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ip; - -import java.util.Locale; - -/** - * TCP segments are sent as internet datagrams. The Internet Protocol header carries several - * information fields, including the source and destination host addresses. A TCP header follows - * the internet header, supplying information specific to the TCP protocol. This division allows - * for the existence of host level protocols other than TCP. - * - * TCP Header Format: - * - * 0 1 2 3 - * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Source Port | Destination Port | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Sequence Number | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Acknowledgment Number | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Data | |U|A|P|R|S|F| | - * | Offset| Reserved |R|C|S|S|Y|I| Window | - * | | |G|K|H|T|N|N| | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Checksum | Urgent Pointer | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Options | Padding | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | data | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * - * See https://tools.ietf.org/html/rfc793#section-3.1 - * - * @author Megatron King - * @since 2018年10月10日 22:19 - */ -public class TcpHeader extends Header { - - private static final int OFFSET_SRC_PORT = 0; - private static final int OFFSET_DEST_PORT = 2; - private static final int OFFSET_LENRES = 12; - private static final int OFFSET_CRC = 16; - private static final int OFFSET_FLAG = 13; - private static final int OFFSET_SEQ = 4; - private static final int OFFSET_ACK = 8; - - private static final int FIN = 1; - private static final int SYN = 2; - private static final int RST = 4; - private static final int PSH = 8; - private static final int ACK = 16; - private static final int URG = 32; - - private IpHeader mIpHeader; - - public TcpHeader(IpHeader header, byte[] packet, int offset) { - super(packet, offset); - mIpHeader = header; - } - - public void updateOffset(int offset) { - this.offset = offset; - } - - public short getSourcePort() { - return readShort(offset + OFFSET_SRC_PORT); - } - - public void setSourcePort(short port) { - writeShort(port, offset + OFFSET_SRC_PORT); - } - - public short getDestinationPort() { - return readShort(offset + OFFSET_DEST_PORT); - } - - public void setDestinationPort(short port) { - writeShort(port, offset + OFFSET_DEST_PORT); - } - - public int getHeaderLength() { - int lenres = packet[offset + OFFSET_LENRES] & 0xFF; - return (lenres>> 4) * 4; - } - - public short getCrc() { - return readShort(offset + OFFSET_CRC); - } - - public void setCrc(short crc) { - writeShort(crc, offset + OFFSET_CRC); - } - - public byte getFlag() { - return packet[offset + OFFSET_FLAG]; - } - - public int getSeqID() { - return readInt(offset + OFFSET_SEQ); - } - - public int getAckID() { - return readInt(offset + OFFSET_ACK); - } - - public void updateChecksum() { - setCrc((short) 0); - setCrc(computeChecksum()); - } - - private short computeChecksum() { - // Sum = Ip Sum(Source Address + Destination Address) + Protocol + TCP Length - // The checksum field is the 16 bit one's complement of the one's complement sum of all 16 - // bit words in the header and text. - int dataLength = mIpHeader.getDataLength(); - long sum = mIpHeader.getIpSum(); - sum += mIpHeader.getProtocol() & 0xFF; - sum += dataLength; - sum += getSum(offset, dataLength); - while ((sum>> 16)> 0) { - sum = (sum & 0xFFFF) + (sum>> 16); - } - return (short) ~sum; - } - - @Override - public String toString() { - return String.format(Locale.getDefault(), "%s%s%s%s%s%s %d -> %d %s:%s", - (getFlag() & SYN) == SYN ? "SYN" : "", - (getFlag() & ACK) == ACK ? "ACK" : "", - (getFlag() & PSH) == PSH ? "PSH" : "", - (getFlag() & RST) == RST ? "RST" : "", - (getFlag() & FIN) == FIN ? "FIN" : "", - (getFlag() & URG) == URG ? "URG" : "", - getSourcePort() & 0xFFFF, - getDestinationPort() & 0xFFFF, - getSeqID(), - getAckID()); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/UdpHeader.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/UdpHeader.java deleted file mode 100644 index 1e813f7..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/UdpHeader.java +++ /dev/null @@ -1,145 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ip; - -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Locale; - -/** - * The UDP module must be able to determine the source and destination internet addresses and - * the protocol field from the internet header. - * - * UDP Header Format: - * - * 0 7 8 15 16 23 24 31 - * +--------+--------+--------+--------+ - * | Source | Destination | - * | Port | Port | - * +--------+--------+--------+--------+ - * | | | - * | Length | Checksum | - * +--------+--------+--------+--------+ - * | - * | data octets ... - * +---------------- ... - * - * See https://tools.ietf.org/html/rfc768 - * - * @author Megatron King - * @since 2018年10月10日 23:04 - */ -public class UdpHeader extends Header { - - private static final short OFFSET_SRC_PORT = 0; - private static final short OFFSET_DEST_PORT = 2; - private static final short OFFSET_TLEN = 4; - private static final short OFFSET_CRC = 6; - - private IpHeader mIpHeader; - - public UdpHeader(IpHeader header, byte[] packet, int offset) { - super(packet, offset); - mIpHeader = header; - } - - public IpHeader getIpHeader() { - return mIpHeader; - } - - public short getSourcePort() { - return readShort(offset + OFFSET_SRC_PORT); - } - - public void setSourcePort(short port) { - writeShort(port, offset + OFFSET_SRC_PORT); - } - - public short getDestinationPort() { - return readShort(offset + OFFSET_DEST_PORT); - } - - public void setDestinationPort(short port) { - writeShort(port, offset + OFFSET_DEST_PORT); - } - - public short getCrc() { - return readShort(offset + OFFSET_CRC); - } - - public void setCrc(short crc) { - writeShort(crc, offset + OFFSET_CRC); - } - - public int getHeaderLength() { - return 8; - } - - public int getTotalLength() { - return readShort(offset + OFFSET_TLEN) & 0xFFFF; - } - - public void setTotalLength(short len) { - writeShort(len, offset + OFFSET_TLEN); - } - - public void updateChecksum() { - setCrc((short) 0); - setCrc(computeChecksum()); - } - - private short computeChecksum() { - // Sum = Ip Sum(Source Address + Destination Address) + Protocol + UDP Length - // Checksum is the 16-bit one's complement of the one's complement sum of a - // pseudo header of information from the IP header, the UDP header, and the - // data, padded with zero octets at the end (if necessary) to make a - // multiple of two octets. - int dataLength = mIpHeader.getDataLength(); - long sum = mIpHeader.getIpSum(); - sum += mIpHeader.getProtocol() & 0xFF; - sum += dataLength; - sum += getSum(offset, dataLength); - while ((sum>> 16)> 0) { - sum = (sum & 0xFFFF) + (sum>> 16); - } - return (short) ~sum; - } - - @Override - public String toString() { - return String.format(Locale.getDefault(), "%d -> %d", getSourcePort() & 0xFFFF, - getDestinationPort() & 0xFFFF); - } - - public UdpHeader copy() { - byte[] copyArray = Arrays.copyOf(packet, packet.length); - IpHeader ipHeader = new IpHeader(copyArray, 0); - return new UdpHeader(ipHeader, copyArray, offset); - } - - public ByteBuffer data() { - int size = mIpHeader.getDataLength() - getHeaderLength(); - int dataOffset = mIpHeader.getHeaderLength() + getHeaderLength(); - byte[] data = new byte[size]; - System.arraycopy(packet, dataOffset, data, 0, size); - return ByteBuffer.wrap(data); - } - - public ByteBuffer buffer() { - return ByteBuffer.wrap(packet, 0, mIpHeader.getTotalLength()); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/DumpCallback.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/DumpCallback.java deleted file mode 100644 index 140c991..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/net/DumpCallback.java +++ /dev/null @@ -1,33 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.net; - -/** - * A callback for dumping net info from /proc/net. - * - * @author Megatron King - * @since 2018年12月01日 16:30 - */ -/* package*/ interface DumpCallback { - - /** - * Invoked when a net info has dumped. - * - * @param net A dumped net object. - */ - void onDump(Net net); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/Net.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/Net.java deleted file mode 100644 index 64fef23..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/net/Net.java +++ /dev/null @@ -1,64 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.net; - -/** - * A dumped net info class contains IPs, ports and uid. - * - * @author Megatron King - * @since 2018年12月01日 22:33 - */ -public class Net { - - /** - * The identifier of a process's uid. - */ - public int uid; - - /** - * The local IP. - */ - public String localIp; - - /** - * The local port. - */ - public int localPort; - - /** - * The remote server IP. - */ - public String remoteIp; - - /** - * The remote server port. - */ - public int remotePort; - - /* package */ Net(int uid, String localIp, int localPort, String remoteIp, int remotePort) { - this.uid = uid; - this.localIp = localIp; - this.localPort = localPort; - this.remoteIp = remoteIp; - this.remotePort = remotePort; - } - - @Override - public String toString() { - return uid + " " + localIp + ":" + localPort + " -> " + remoteIp + ":" + remotePort; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/NetDumper.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/NetDumper.java deleted file mode 100644 index da1bf7e..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/net/NetDumper.java +++ /dev/null @@ -1,149 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.net; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.NetBareUtils; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; - -/** - * A Dumper uses for dumping net info from /proc/net/. - * - * @author Megatron King - * @since 2018年12月01日 22:36 - */ -/* package */ class NetDumper implements Runnable { - - private final String mArg; - private final String mLocalIp; - private final DumpCallback mCallback; - - private boolean mIsRunning; - - NetDumper(String arg, String localIp, DumpCallback callback) { - this.mArg = arg; - this.mLocalIp = localIp; - this.mCallback = callback; - } - - void startDump() { - mIsRunning = true; - UidDumper.THREAD_POOL_EXECUTOR.execute(this); - } - - void stopDump() { - mIsRunning = false; - } - - void pauseDump() { - synchronized (mArg) { - try { - mArg.wait(); - } catch (InterruptedException e) { - // do nothing - } - } - } - - void resumeDump() { - synchronized (mArg) { - mArg.notify(); - } - } - - @Override - public void run() { - while (mIsRunning) { - ProcessBuilder builder = new ProcessBuilder("cat", mArg); - InputStream is = null; - BufferedReader reader = null; - try { - Process process = builder.start(); - is = process.getInputStream(); - reader = new BufferedReader(new InputStreamReader(is)); - String line; - int index = 0; - while ((line = reader.readLine()) != null) { - index++; - if (index == 1) { - // Skip the table title - continue; - } - String[] columns = line.trim().split(" "); - if (columns.length < 8) { - // Uid is in the 8th of columns. - continue; - } - int uid = NetBareUtils.parseInt(columns[7], -1); - if (uid == -1 || uid == 0) { - continue; - } - String[] local = columns[1].split(":"); - if (local.length != 2 || local[0].length() < 8) { - continue; - } - String[] remote = columns[2].split(":"); - if (remote.length != 2 || remote[0].length() < 8) { - continue; - } - String localIp = parseIp(local[0]); - if (localIp == null || !localIp.equals(mLocalIp)) { - continue; - } - String remoteIp = parseIp(remote[0]); - if (remoteIp == null || remoteIp.equals("0.0.0.0") - || remoteIp.equals("255.255.255.255")) { - continue; - } - int localPort = parsePort(local[1]); - if (localPort == -1) { - continue; - } - int remotePort = parsePort(remote[1]); - if (remotePort == -1) { - continue; - } - mCallback.onDump(new Net(uid, localIp, localPort, remoteIp, remotePort)); - } - } catch (IOException e) { - NetBareLog.wtf(e); - } - NetBareUtils.closeQuietly(is); - NetBareUtils.closeQuietly(reader); - } - } - - private String parseIp(String ip) { - ip = ip.substring(ip.length() - 8); - int ip1 = NetBareUtils.parseInt(ip.substring(6, 8), 16, -1); - int ip2 = NetBareUtils.parseInt(ip.substring(4, 6), 16, -1); - int ip3 = NetBareUtils.parseInt(ip.substring(2, 4), 16, -1); - int ip4 = NetBareUtils.parseInt(ip.substring(0, 2), 16, -1); - if (ip1 < 0 || ip2 < 0 || ip3 < 0 || ip4 < 0) { - return null; - } - return ip1 + "." + ip2 + "." + ip3 + "." + ip4; - } - - private int parsePort(String port) { - return NetBareUtils.parseInt(port, 16, -1); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/Session.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/Session.java deleted file mode 100644 index 9c8f6ed..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/net/Session.java +++ /dev/null @@ -1,94 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.net; - -import com.github.megatronking.netbare.ip.Protocol; - -import java.util.UUID; - -/** - * This object represents a network session, it contains IPs, ports and IP packet details. - * - * @author Megatron King - * @since 2018年10月14日 23:39 - */ -public final class Session { - - /** - * IP protocol. - */ - public final Protocol protocol; - - /** - * Local vpn port. - */ - public final short localPort; - - /** - * Remote server port. - */ - public final short remotePort; - - /** - * Remote server IP. - */ - public final int remoteIp; - - /** - * An unique id uses to identify this session. - */ - public String id; - - /** - * Session started time. - */ - public long time; - - /** - * Remote server host. - */ - public String host; - - /** - * The process id that the session belongs to. - */ - public int uid; - - /** - * Packet counts. - */ - public int packetIndex; - - /** - * The total size of the packets that sends to remote server. - */ - public int sendDataSize; - - /** - * The total size of the packets that received from remote server. - */ - public int receiveDataSize; - - /* package */ Session(Protocol protocol, short localPort, short remotePort, int remoteIp) { - this.protocol = protocol; - this.localPort = localPort; - this.remotePort = remotePort; - this.remoteIp = remoteIp; - this.id = UUID.randomUUID().toString(); - this.time = System.currentTimeMillis(); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/SessionProvider.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/SessionProvider.java deleted file mode 100644 index 52f8019..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/net/SessionProvider.java +++ /dev/null @@ -1,94 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.net; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.github.megatronking.netbare.ip.Protocol; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * A session provider that provides the session instance query services. - * - * @author Megatron King - * @since 2018年10月15日 21:46 - */ -public final class SessionProvider { - - private static final int MAX_SESSION = 100; - - private final Map mSessions; - private final UidDumper mDumper; - - /** - * Constructs a session provider with a {@link UidDumper}. - * - * @param dumper Use to dump uid, can be null. - */ - public SessionProvider(UidDumper dumper) { - this.mSessions = new ConcurrentHashMap(MAX_SESSION); - this.mDumper = dumper; - } - - /** - * Query a session by local VPN port. - * - * @param localPort The local VPN port. - * @return The instance of {@link Session} if it exists, or null. - */ - @Nullable - public Session query(short localPort) { - Session session = mSessions.get(localPort); - if (mDumper != null && session != null && session.uid == 0) { - // Query uid again. - mDumper.request(session); - } - return session; - } - - /** - * Query or create a session by protocol, ports and remote server IP. - * - * @param protocol IP protocol. - * @param localPort Local VPN port. - * @param remotePort Remote server port. - * @param remoteIp Remote server IP. - * @return An instance of {@link Session}, if the instance not exists, will create a new one. - */ - @NonNull - public Session ensureQuery(Protocol protocol, short localPort, short remotePort, int remoteIp) { - Session session = mSessions.get(localPort); - if (session != null) { - if (session.protocol != protocol || session.localPort != localPort || - session.remotePort != remotePort || session.remoteIp != remoteIp) { - session = null; - } - } - if (session == null) { - session = new Session(protocol, localPort, remotePort, remoteIp); - mSessions.put(localPort, session); - // Dump uid from /proc/net/ - if (mDumper != null) { - mDumper.request(session); - } - } - return session; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/UidDumper.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/UidDumper.java deleted file mode 100644 index 82b2091..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/net/UidDumper.java +++ /dev/null @@ -1,184 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.net; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareConfig; -import com.github.megatronking.netbare.NetBareUtils; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; - -import java.util.Map; -import java.util.concurrent.Executor; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * A dumper analyzes /proc/net/ files to dump uid of the network session. This class may be a - * battery-killer, but can set {@link NetBareConfig.Builder#dumpUid} to false to close the dumper. - * - * @author Megatron King - * @since 2018年12月03日 16:54 - */ -public final class UidDumper implements DumpCallback { - - private static final int NET_ALIVE_SECONDS = 60; - private static final int NET_CONCURRENCY_LEVEL = 6; - private static final int NET_MAX_SIZE = 100; - - private static final int SESSION_ALIVE_SECONDS = 30; - private static final int SESSION_CONCURRENCY_LEVEL = 8; - private static final int SESSION_MAX_SIZE = 100; - - private static final int CORE_POOL_SIZE = 4; - private static final int MAXIMUM_POOL_SIZE = 4; - private static final int KEEP_ALIVE_SECONDS = 3 * 60; - private static final int QUEUE_SIZE = 32; - - private static final ThreadFactory sThreadFactory = new ThreadFactory() { - private final AtomicInteger mCount = new AtomicInteger(1); - - public Thread newThread(@NonNull Runnable r) { - return new Thread(r, "UidDumper #" + mCount.getAndIncrement()); - } - }; - - /** - * An {@link Executor} that can be used to execute tasks in parallel. - */ - /* package */ static final Executor THREAD_POOL_EXECUTOR; - - private final Cache mNetCaches; - private final Cache mWaitingSessions; - - private final UidProvider mUidProvider; - - private final NetDumper dumper1; - private final NetDumper dumper2; - private final NetDumper dumper3; - private final NetDumper dumper4; - - static { - ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( - CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, - new LinkedBlockingDeque(QUEUE_SIZE), sThreadFactory); - threadPoolExecutor.allowCoreThreadTimeOut(true); - THREAD_POOL_EXECUTOR = threadPoolExecutor; - } - - public UidDumper(String localIp, UidProvider provider) { - this.mUidProvider = provider; - this.mNetCaches = CacheBuilder.newBuilder() - .expireAfterAccess(NET_ALIVE_SECONDS, TimeUnit.SECONDS) - .concurrencyLevel(NET_CONCURRENCY_LEVEL) - .maximumSize(NET_MAX_SIZE) - .build(); - this.mWaitingSessions = CacheBuilder.newBuilder() - .expireAfterAccess(SESSION_ALIVE_SECONDS, TimeUnit.SECONDS) - .concurrencyLevel(SESSION_CONCURRENCY_LEVEL) - .maximumSize(SESSION_MAX_SIZE) - .build(); - - this.dumper1 = new NetDumper("/proc/net/tcp", localIp, this); - this.dumper2 = new NetDumper("/proc/net/tcp6", localIp, this); - this.dumper3 = new NetDumper("/proc/net/udp", localIp, this); - this.dumper4 = new NetDumper("/proc/net/udp6", localIp, this); - } - - public void startDump() { - dumper1.startDump(); - dumper2.startDump(); - dumper3.startDump(); - dumper4.startDump(); - } - - public void stopDump() { - dumper1.stopDump(); - dumper2.stopDump(); - dumper3.stopDump(); - dumper4.stopDump(); - } - - private void pauseDump() { - dumper1.pauseDump(); - dumper2.pauseDump(); - dumper3.pauseDump(); - dumper4.pauseDump(); - } - - private void resumeDump() { - dumper1.resumeDump(); - dumper2.resumeDump(); - dumper3.resumeDump(); - dumper4.resumeDump(); - } - - public void request(Session session) { - if (mUidProvider != null) { - int uid = mUidProvider.uid(session); - if (uid != UidProvider.UID_UNKNOWN) { - session.uid = uid; - return; - } - } - int port = NetBareUtils.convertPort(session.localPort); - Map caches = mNetCaches.asMap(); - if (caches.containsKey(port)) { - Net net = caches.get(port); - if (net != null) { - session.uid = net.uid; - } - } else { - // Find net by remote ip from cache - for (Net net : caches.values()) { - if (NetBareUtils.convertIp(net.remoteIp) == session.remoteIp) { - session.uid = net.uid; - return; - } - } - mWaitingSessions.put(port, session); - // resumeDump(); - } - } - - @Override - public void onDump(Net net) { - mNetCaches.put(net.localPort, net); - - if (mWaitingSessions.size() == 0) { - // If sleep the threads, some uid would be missed. But if keep the threads running, too - // much battery would be cost. - // pauseDump(); - return; - } - Map map = mWaitingSessions.asMap(); - for (int port : map.keySet()) { - if (net.localPort == port) { - Session session = map.get(port); - if (session != null) { - session.uid = net.uid; - } - mWaitingSessions.invalidate(port); - break; - } - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/UidProvider.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/UidProvider.java deleted file mode 100644 index 080f062..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/net/UidProvider.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.megatronking.netbare.net; - -/** - * This interface provides a known uid for a session. - * - * @author Megatron King - * @since 2019年1月27日 21:31 - */ -public interface UidProvider { - - int UID_UNKNOWN = -1; - - /** - * Returns a known uid for this session, if the uid is unknown should return {@link #UID_UNKNOWN}. - * - * @param session Network session. - * @return A known uid or {@link #UID_UNKNOWN}. - */ - int uid(Session session); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/BaseProxyServer.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/BaseProxyServer.java deleted file mode 100644 index 2035986..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/BaseProxyServer.java +++ /dev/null @@ -1,73 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.proxy; - -import com.github.megatronking.netbare.NetBareLog; - -import java.io.IOException; - -/** - * An abstract base class defined for proxy servers. The local proxy server runs a separated thread - * and loop to process packets. The sub class needs to impl {@link #process()} to handle the packets. - * - * @author Megatron King - * @since 2018年10月10日 00:31 - */ -/* package */ abstract class BaseProxyServer extends ProxyServer implements Runnable { - - /** - * Waiting the specific protocol packets and trying to sent to real remote server. - * - * @throws IOException If an I/O error has occurred. - */ - protected abstract void process() throws IOException; - - private boolean mIsRunning; - - private final Thread mServerThread; - - /* package */ BaseProxyServer(String serverName) { - this.mServerThread = new Thread(this, serverName); - } - - @Override - void startServer() { - mIsRunning = true; - mServerThread.start(); - } - - @Override - void stopServer() { - mIsRunning = false; - mServerThread.interrupt(); - } - - @Override - public void run() { - while (mIsRunning) { - try { - process(); - } catch (IOException e) { - NetBareLog.e(e.getMessage()); - } - } - } - - /* package */ boolean isStopped() { - return !mIsRunning; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/IcmpProxyServerForwarder.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/IcmpProxyServerForwarder.java deleted file mode 100644 index 5bf31b1..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/IcmpProxyServerForwarder.java +++ /dev/null @@ -1,51 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.proxy; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.ip.IcmpHeader; -import com.github.megatronking.netbare.ip.IpHeader; - -import java.io.OutputStream; - -/** - * Forward the Internet Control Message Protocol (ICMP) to proxy server. - * - * @author Megatron King - * @since 2018年10月09日 01:30 - */ -public final class IcmpProxyServerForwarder implements ProxyServerForwarder { - - @Override - public void prepare() { - // TODO - } - - @Override - public void forward(byte[] packet, int len, OutputStream output) { - IpHeader ipHeader = new IpHeader(packet, 0); - IcmpHeader icmpHeader = new IcmpHeader(ipHeader, packet, ipHeader.getHeaderLength()); - NetBareLog.v("ICMP type: " + icmpHeader.getType()); - NetBareLog.v("ICMP code: " + icmpHeader.getCode()); - // TODO transfer to proxy server - } - - @Override - public void release() { - // TODO - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServer.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServer.java deleted file mode 100644 index 509f3d1..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServer.java +++ /dev/null @@ -1,67 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.proxy; - -/** - * A local proxy server receives net packets from VPN and transfer them to the real remote server. - * Every local proxy server runs separated threads and handle specific IP protocols like TCP, UDP - * and so on. The server is managed by {@link ProxyServerForwarder}, use {@link #start()} to - * establish the server and {@link #stop()} to terminate it. - * - * @author Megatron King - * @since 2018年10月10日 00:23 - */ -public abstract class ProxyServer { - - /** - * Establish the server and start receive packets. - */ - /* package */ abstract void startServer(); - - /** - * Terminate this server. - */ - /* package */ abstract void stopServer(); - - /** - * Returns the proxy server IP. - * - * @return The proxy server IP. - */ - /* package */ abstract int getIp(); - - /** - * Returns the proxy server port. - * - * @return The proxy server port. - */ - /* package */ abstract short getPort(); - - /** - * Establish the proxy server. - */ - public final void start() { - startServer(); - } - - /** - * Terminate the proxy server, release resources and close IOs. - */ - public final void stop() { - stopServer(); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServerForwarder.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServerForwarder.java deleted file mode 100644 index ccbc09e..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServerForwarder.java +++ /dev/null @@ -1,47 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.proxy; - -import java.io.OutputStream; - -/** - * An interface needs to be implement by proxy server forwarders. - * - * @author Megatron King - * @since 2018年10月09日 01:24 - */ -public interface ProxyServerForwarder { - - /** - * Prepare the forwarder. - */ - void prepare(); - - /** - * Forward a packet to local proxy server. - * - * @param packet A data packet, the array length is MTU. - * @param len The actual data length in packet array. - * @param output An output stream connects VPN file descriptor. - */ - void forward(byte[] packet, int len, OutputStream output); - - /** - * Release the forwarder. - */ - void release(); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServer.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServer.java deleted file mode 100644 index 9565d53..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServer.java +++ /dev/null @@ -1,216 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.proxy; - -import android.net.VpnService; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.NetBareUtils; -import com.github.megatronking.netbare.gateway.VirtualGateway; -import com.github.megatronking.netbare.net.Session; -import com.github.megatronking.netbare.net.SessionProvider; -import com.github.megatronking.netbare.tunnel.ConnectionShutdownException; -import com.github.megatronking.netbare.tunnel.NioCallback; -import com.github.megatronking.netbare.tunnel.NioTunnel; -import com.github.megatronking.netbare.tunnel.TcpProxyTunnel; -import com.github.megatronking.netbare.tunnel.TcpRemoteTunnel; -import com.github.megatronking.netbare.tunnel.TcpTunnel; -import com.github.megatronking.netbare.tunnel.TcpVATunnel; - -import java.io.EOFException; -import java.io.IOException; -import java.net.ConnectException; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.util.Iterator; -import java.util.Set; - -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLHandshakeException; - -/** - * The TCP proxy server is a nio {@link ServerSocketChannel}, it listens the connections from - * {@link VpnService} and forwards request packets to real remote server. This server uses - * {@link TcpVATunnel} to bind {@link VirtualGateway} and {@link NioTunnel} together. Every TCP - * connection has two channels: {@link TcpProxyTunnel} and {@link TcpRemoteTunnel}. - * The {@link TcpProxyTunnel} is responsible for sending remote server response packets to VPN - * service, and the {@link TcpRemoteTunnel} is responsible for communicating with remote server. - * - * @author Megatron King - * @since 2018年10月11日 17:35 - */ -/* package */ class TcpProxyServer extends BaseProxyServer implements Runnable { - - private final VpnService mVpnService; - - private final Selector mSelector; - private final ServerSocketChannel mServerSocketChannel; - - private int mIp; - private short mPort; - private int mMtu; - - private SessionProvider mSessionProvider; - - /* package */ TcpProxyServer(VpnService vpnService, String ip, int mtu) - throws IOException { - super("TcpProxyServer"); - this.mVpnService = vpnService; - - this.mSelector = Selector.open(); - this.mServerSocketChannel = ServerSocketChannel.open(); - this.mServerSocketChannel.configureBlocking(false); - this.mServerSocketChannel.socket().bind(new InetSocketAddress(0)); - this.mServerSocketChannel.register(mSelector, SelectionKey.OP_ACCEPT); - - this.mIp = NetBareUtils.convertIp(ip); - this.mPort = (short) mServerSocketChannel.socket().getLocalPort(); - this.mMtu = mtu; - - NetBareLog.v("[TCP]proxy server: %s:%d", ip, NetBareUtils.convertPort(mPort)); - } - - void setSessionProvider(SessionProvider sessionProvider) { - this.mSessionProvider = sessionProvider; - } - - - @Override - int getIp() { - return mIp; - } - - @Override - short getPort() { - return mPort; - } - - @Override - public void run() { - NetBareLog.i("[TCP]Server starts running."); - super.run(); - NetBareUtils.closeQuietly(mSelector); - NetBareUtils.closeQuietly(mServerSocketChannel); - NetBareLog.i("[TCP]Server stops running."); - } - - @Override - protected void process() throws IOException { - int select = mSelector.select(); - if (select == 0) { - return; - } - Set selectedKeys = mSelector.selectedKeys(); - if (selectedKeys == null) { - return; - } - Iterator iterator = selectedKeys.iterator(); - while (iterator.hasNext()) { - SelectionKey key = iterator.next(); - try { - if (key.isValid()) { - if (key.isAcceptable()) { - onAccept(); - } else { - Object attachment = key.attachment(); - if (attachment instanceof NioCallback) { - NioCallback callback = (NioCallback) attachment; - try { - if (key.isConnectable()) { - callback.onConnected(); - } else if (key.isReadable()) { - callback.onRead(); - } else if (key.isWritable()) { - callback.onWrite(); - } - } catch (IOException e) { - NioTunnel tunnel = callback.getTunnel(); - if (!tunnel.isClosed()) { - handleException(e); - } - callback.onClosed(); - } - } - } - } - } finally { - iterator.remove(); - } - - } - } - - private void onAccept() throws IOException { - SocketChannel clientChannel = mServerSocketChannel.accept(); - Socket clientSocket = clientChannel.socket(); - - // The client ip is the remote server ip - // The client port is the local port(it is the vpn port not the proxy server port) - String ip = clientSocket.getInetAddress().getHostAddress(); - int port = clientSocket.getPort(); - - // The session should have be saved before the tcp packets be forwarded to proxy server. So - // we can query it by client port. - Session session = mSessionProvider.query((short) port); - if (session == null) { - throw new IOException("No session saved with key: " + port); - } - - int remotePort = NetBareUtils.convertPort(session.remotePort); - - // Connect remote server and dispatch data. - TcpTunnel proxyTunnel = null; - TcpTunnel remoteTunnel = null; - try { - proxyTunnel = new TcpProxyTunnel(clientChannel, mSelector, remotePort); - remoteTunnel = new TcpRemoteTunnel(mVpnService, SocketChannel.open(), - mSelector, ip, remotePort); - TcpVATunnel gatewayTunnel = new TcpVATunnel(session, proxyTunnel, - remoteTunnel, mMtu); - gatewayTunnel.connect(new InetSocketAddress(ip, remotePort)); - } catch (IOException e){ - NetBareUtils.closeQuietly(proxyTunnel); - NetBareUtils.closeQuietly(remoteTunnel); - throw e; - } - } - - private void handleException(IOException e) { - if (e == null || e.getMessage() == null) { - return; - } - if (e instanceof SSLHandshakeException) { - // Client doesn't accept the MITM CA certificate. - NetBareLog.e(e.getMessage()); - } else if (e instanceof ConnectionShutdownException) { - // Connection exception, do not mind this. - NetBareLog.e(e.getMessage()); - } else if (e instanceof ConnectException) { - // Connection timeout - NetBareLog.e(e.getMessage()); - } else if (e instanceof SSLException && (e.getCause() instanceof EOFException)) { - // Connection shutdown manually - NetBareLog.e(e.getMessage()); - } else { - NetBareLog.wtf(e); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServerForwarder.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServerForwarder.java deleted file mode 100644 index d90f047..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServerForwarder.java +++ /dev/null @@ -1,135 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.proxy; - -import android.net.VpnService; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.NetBareUtils; -import com.github.megatronking.netbare.ip.IpHeader; -import com.github.megatronking.netbare.ip.Protocol; -import com.github.megatronking.netbare.ip.TcpHeader; -import com.github.megatronking.netbare.net.Session; -import com.github.megatronking.netbare.net.SessionProvider; -import com.github.megatronking.netbare.net.UidDumper; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * Handshake with local TCP proxy server and then forward packets to it. - * - * @author Megatron King - * @since 2018年10月09日 01:30 - */ -public final class TcpProxyServerForwarder implements ProxyServerForwarder { - - private final SessionProvider mSessionProvider; - private final TcpProxyServer mProxyServer; - - public TcpProxyServerForwarder(VpnService vpnService, String ip, int mtu, - UidDumper dumper) throws IOException { - this.mSessionProvider = new SessionProvider(dumper); - this.mProxyServer = new TcpProxyServer(vpnService, ip, mtu); - this.mProxyServer.setSessionProvider(mSessionProvider); - } - - @Override - public void prepare() { - this.mProxyServer.start(); - } - - @Override - public void forward(byte[] packet, int len, OutputStream output) { - IpHeader ipHeader = new IpHeader(packet, 0); - TcpHeader tcpHeader = new TcpHeader(ipHeader, packet, ipHeader.getHeaderLength()); - - // Src IP & Port - int localIp = ipHeader.getSourceIp(); - short localPort = tcpHeader.getSourcePort(); - - // Dest IP & Port - int remoteIp = ipHeader.getDestinationIp(); - short remotePort = tcpHeader.getDestinationPort(); - - // TCP data size - int tcpDataSize = ipHeader.getDataLength() - tcpHeader.getHeaderLength(); - - NetBareLog.v("ip: %s:%d -> %s:%d", NetBareUtils.convertIp(localIp), - NetBareUtils.convertPort(localPort), NetBareUtils.convertIp(remoteIp), - NetBareUtils.convertPort(remotePort)); - NetBareLog.v("tcp: %s, size: %d", tcpHeader.toString(), tcpDataSize); - - // Tcp handshakes and proxy forward flow. - - // Client: 10.1.10.1:40988 - // Server: 182.254.116.117:80 - // Proxy Server: 10.1.10.1:38283 - - // 10.1.10.1:40988 -> 182.254.116.117:80 SYN - // Forward: 182.254.116.117:40988 -> 10.1.10.1:38283 SYN - - // 10.1.10.1:38283 -> 182.254.116.117:40988 SYN+ACK - // Forward: 182.254.116.117:80 -> 10.1.10.1:40988 SYN+ACK - - // 10.1.10.1:40988 -> 182.254.116.117:80 ACK - // Forward: 182.254.116.117:80 -> 10.1.10.1:38283 ACK - - if (localPort != mProxyServer.getPort()) { - // Client requests to server - Session session = mSessionProvider.ensureQuery(Protocol.TCP, localPort, remotePort, remoteIp); - session.packetIndex++; - - // Forward client request to proxy server. - ipHeader.setSourceIp(remoteIp); - ipHeader.setDestinationIp(mProxyServer.getIp()); - tcpHeader.setDestinationPort(mProxyServer.getPort()); - - ipHeader.updateChecksum(); - tcpHeader.updateChecksum(); - - session.sendDataSize += tcpDataSize; - } else { - // Proxy server responses forward client request. - Session session = mSessionProvider.query(remotePort); - if (session == null) { - NetBareLog.w("No session saved with key: " + remotePort); - return; - } - // Forward proxy server response to client. - ipHeader.setSourceIp(remoteIp); - ipHeader.setDestinationIp(mProxyServer.getIp()); - tcpHeader.setSourcePort(session.remotePort); - - ipHeader.updateChecksum(); - tcpHeader.updateChecksum(); - - session.receiveDataSize += tcpDataSize; - } - - try { - output.write(packet, 0, len); - } catch (IOException e) { - NetBareLog.e(e.getMessage()); - } - } - - @Override - public void release() { - this.mProxyServer.stop(); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServer.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServer.java deleted file mode 100644 index faddeb4..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServer.java +++ /dev/null @@ -1,180 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.proxy; - -import android.net.VpnService; -import android.os.SystemClock; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.NetBareUtils; -import com.github.megatronking.netbare.gateway.VirtualGateway; -import com.github.megatronking.netbare.ip.IpHeader; -import com.github.megatronking.netbare.ip.UdpHeader; -import com.github.megatronking.netbare.net.Session; -import com.github.megatronking.netbare.net.SessionProvider; -import com.github.megatronking.netbare.tunnel.NioCallback; -import com.github.megatronking.netbare.tunnel.NioTunnel; -import com.github.megatronking.netbare.tunnel.Tunnel; -import com.github.megatronking.netbare.tunnel.UdpRemoteTunnel; -import com.github.megatronking.netbare.tunnel.UdpVATunnel; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.nio.channels.DatagramChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -/** - * The UDP proxy server is a virtual server, every packet from {@link UdpProxyServerForwarder} is - * saw as a connection. It use {@link UdpVATunnel} to bind {@link VirtualGateway} and - * {@link NioTunnel} together. Not like TCP, UDP only use {@link UdpRemoteTunnel} to communicate with - * real remote server. - * - * @author Megatron King - * @since 2018年10月11日 17:35 - */ -/* package */ class UdpProxyServer extends BaseProxyServer { - - private static final int SELECTOR_WAIT_TIME = 50; - - private final VpnService mVpnService; - - private int mMtu; - - private final Selector mSelector; - private final Map mTunnels; - - private SessionProvider mSessionProvider; - - /* package */ UdpProxyServer(VpnService vpnService, int mtu) throws IOException { - super("UdpProxyServer"); - this.mVpnService = vpnService; - - this.mMtu = mtu; - - this.mSelector = Selector.open(); - this.mTunnels = new ConcurrentHashMap(); - } - - @Override - int getIp() { - return 0; - } - - @Override - short getPort() { - return 0; - } - - void setSessionProvider(SessionProvider sessionProvider) { - this.mSessionProvider = sessionProvider; - } - - void send(UdpHeader header, OutputStream output) throws IOException { - short localPort = header.getSourcePort(); - UdpVATunnel tunnel = mTunnels.get(localPort); - try { - if (tunnel == null) { - Session session = mSessionProvider.query(localPort); - if (session == null) { - throw new IOException("No session saved with key: " + localPort); - } - - IpHeader ipHeader = header.getIpHeader(); - NioTunnel remoteTunnel = new UdpRemoteTunnel(mVpnService, DatagramChannel.open(), - mSelector, NetBareUtils.convertIp(session.remoteIp), session.remotePort); - tunnel = new UdpVATunnel(session, remoteTunnel, output, mMtu); - tunnel.connect(new InetSocketAddress(NetBareUtils.convertIp(ipHeader.getDestinationIp()), - NetBareUtils.convertPort(header.getDestinationPort()))); - mTunnels.put(header.getSourcePort(), tunnel); - } - tunnel.send(header); - } catch (IOException e) { - mTunnels.remove(localPort); - NetBareUtils.closeQuietly(tunnel); - throw e; - } - } - - @Override - public void run() { - NetBareLog.i("[UDP]Server starts running."); - super.run(); - NetBareUtils.closeQuietly(mSelector); - NetBareLog.i("[UDP]Server stops running."); - } - - @Override - protected void process() throws IOException { - int select = mSelector.select(); - if (select == 0) { - // Wait a short time to let the selector register or interest. - SystemClock.sleep(SELECTOR_WAIT_TIME); - return; - } - Set selectedKeys = mSelector.selectedKeys(); - if (selectedKeys == null) { - return; - } - Iterator iterator = selectedKeys.iterator(); - while (iterator.hasNext()) { - SelectionKey key = iterator.next(); - if (key.isValid()) { - Object attachment = key.attachment(); - if (attachment instanceof NioCallback) { - NioCallback callback = (NioCallback) attachment; - try { - if (key.isReadable()) { - callback.onRead(); - } else if (key.isWritable()) { - callback.onWrite(); - } else if (key.isConnectable()) { - callback.onConnected(); - } - } catch (IOException e) { - callback.onClosed(); - removeTunnel(callback.getTunnel()); - } - } - } - iterator.remove(); - } - } - - @Override - void stopServer() { - super.stopServer(); - for (UdpVATunnel tunnel : mTunnels.values()) { - NetBareUtils.closeQuietly(tunnel); - } - } - - private void removeTunnel(Tunnel tunnel) { - Map tunnels = new HashMap(mTunnels); - for (short key : tunnels.keySet()) { - if (tunnels.get(key).getRemoteChannel() == tunnel) { - mTunnels.remove(key); - } - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServerForwarder.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServerForwarder.java deleted file mode 100644 index d482ebb..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServerForwarder.java +++ /dev/null @@ -1,92 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.proxy; - -import android.net.VpnService; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.NetBareUtils; -import com.github.megatronking.netbare.ip.IpHeader; -import com.github.megatronking.netbare.ip.Protocol; -import com.github.megatronking.netbare.ip.UdpHeader; -import com.github.megatronking.netbare.net.Session; -import com.github.megatronking.netbare.net.SessionProvider; -import com.github.megatronking.netbare.net.UidDumper; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * Unlike TCP proxy server, UDP doesn't need handshake, we can forward packets to it directly. - * - * @author Megatron King - * @since 2018年10月09日 01:30 - */ -public final class UdpProxyServerForwarder implements ProxyServerForwarder { - - private final SessionProvider mSessionProvider; - private final UdpProxyServer mProxyServer; - - public UdpProxyServerForwarder(VpnService vpnService, int mtu, UidDumper dumper) - throws IOException { - this.mSessionProvider = new SessionProvider(dumper); - this.mProxyServer = new UdpProxyServer(vpnService, mtu); - this.mProxyServer.setSessionProvider(mSessionProvider); - } - - @Override - public void prepare() { - this.mProxyServer.start(); - } - - @Override - public void forward(byte[] packet, int len, OutputStream output) { - IpHeader ipHeader = new IpHeader(packet, 0); - UdpHeader udpHeader = new UdpHeader(ipHeader, packet, ipHeader.getHeaderLength()); - - // Src IP & Port - int localIp = ipHeader.getSourceIp(); - short localPort = udpHeader.getSourcePort(); - - // Dest IP & Port - int remoteIp = ipHeader.getDestinationIp(); - short remotePort = udpHeader.getDestinationPort(); - - // UDP data size - int udpDataSize = ipHeader.getDataLength() - udpHeader.getHeaderLength(); - - NetBareLog.v("ip: %s:%d -> %s:%d", NetBareUtils.convertIp(localIp), - NetBareUtils.convertPort(localPort), NetBareUtils.convertIp(remoteIp), - NetBareUtils.convertPort(remotePort)); - NetBareLog.v("udp: %s, size: %d", udpHeader.toString(), udpDataSize); - - Session session = mSessionProvider.ensureQuery(Protocol.UDP, localPort, remotePort, remoteIp); - session.packetIndex++; - - try { - mProxyServer.send(udpHeader, output); - session.sendDataSize += udpDataSize; - } catch (IOException e) { - NetBareLog.e(e.getMessage()); - } - } - - @Override - public void release() { - this.mProxyServer.stop(); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateGenerator.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateGenerator.java deleted file mode 100644 index 982b24b..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateGenerator.java +++ /dev/null @@ -1,253 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ssl; - -import com.github.megatronking.netbare.NetBareUtils; - -import org.bouncycastle.asn1.ASN1EncodableVector; -import org.bouncycastle.asn1.ASN1InputStream; -import org.bouncycastle.asn1.ASN1Sequence; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x500.X500NameBuilder; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x509.BasicConstraints; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.GeneralName; -import org.bouncycastle.asn1.x509.KeyPurposeId; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.X509v3CertificateBuilder; -import org.bouncycastle.cert.bc.BcX509ExtensionUtils; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.math.BigInteger; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.SignatureException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Date; -import java.util.Random; - -/** - * Generates self-signed certificate by {@link JKS}. - * - * @author Megatron King - * @since 2018年11月08日 02:23 - */ -public final class CertificateGenerator { - - private static final String KEY_STORE_TYPE = "PKCS12"; - private static final String KEYGEN_ALGORITHM = "RSA"; - private static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG"; - - private static final String PROVIDER_NAME = BouncyCastleProvider.PROVIDER_NAME; - - private static final int ROOT_KEY_SIZE = 2048; - private static final int SERVER_KEY_SIZE = 1024; - - /** - * The signature algorithm starting with the message digest to use when signing certificates. - * On 64-bit systems this should be set to SHA512, on 32-bit systems this is SHA256. On 64-bit - * systems, SHA512 generally performs better than SHA256; see this question for details: - * http://crypto.stackexchange.com/questions/26336/sha512-faster-than-sha256 - */ - private static final String SIGNATURE_ALGORITHM = (is32BitJvm() ? "SHA256" : "SHA512") + - "WithRSAEncryption"; - - /** - * The milliseconds of 30 day. - */ - private static final long ONE_DAY = 30 * 86400000L; - - /** - * Current time minus 1 year, just in case software clock goes back due to time synchronization. - */ - private static final Date NOT_BEFORE = new Date(System.currentTimeMillis() - ONE_DAY * 365); - - /** - * The maximum possible value in X.509 specification: 9999年12月31日 23:59:59, - * new Date(253402300799000L), but Apple iOS 8 fails with a certificate - * expiration date grater than 6084年1月24日 02:07:59 GMT (issue #6). - * - * Hundred years in the future from starting the proxy should be enough. - */ - private static final Date NOT_AFTER = new Date(System.currentTimeMillis() + ONE_DAY * 365 * 10); - - /** - * Generate a root keystore by a given {@link JKS}. - * - * @param jks A java keystore object. - * @return A root {@link KeyStore}. - */ - public KeyStore generateRoot(JKS jks) - throws KeyStoreException, CertificateException, NoSuchAlgorithmException, - IOException, OperatorCreationException { - KeyPair keyPair = generateKeyPair(ROOT_KEY_SIZE); - - X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); - nameBuilder.addRDN(BCStyle.CN, jks.commonName()); - nameBuilder.addRDN(BCStyle.O, jks.organization()); - nameBuilder.addRDN(BCStyle.OU, jks.organizationalUnitName()); - X500Name issuer = nameBuilder.build(); - - PublicKey pubKey = keyPair.getPublic(); - - X509v3CertificateBuilder generator = new JcaX509v3CertificateBuilder( - issuer, BigInteger.valueOf(randomSerial()), NOT_BEFORE, NOT_AFTER, issuer, pubKey); - generator.addExtension(Extension.subjectKeyIdentifier, false, - createSubjectKeyIdentifier(pubKey)); - generator.addExtension(Extension.basicConstraints, true, - new BasicConstraints(true)); - - KeyUsage usage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.digitalSignature | - KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.cRLSign); - generator.addExtension(Extension.keyUsage, false, usage); - - ASN1EncodableVector purposes = new ASN1EncodableVector(); - purposes.add(KeyPurposeId.id_kp_serverAuth); - purposes.add(KeyPurposeId.id_kp_clientAuth); - purposes.add(KeyPurposeId.anyExtendedKeyUsage); - generator.addExtension(Extension.extendedKeyUsage, false, - new DERSequence(purposes)); - - X509Certificate cert = signCertificate(generator, keyPair.getPrivate()); - - KeyStore result = KeyStore.getInstance(KEY_STORE_TYPE); - result.load(null, null); - result.setKeyEntry(jks.alias(), keyPair.getPrivate(), jks.password(), - new Certificate[] { cert }); - return result; - } - - public KeyStore generateServer(String commonName, JKS jks, - Certificate caCert, PrivateKey caPrivKey) - throws NoSuchAlgorithmException, NoSuchProviderException, - IOException, OperatorCreationException, CertificateException, - InvalidKeyException, SignatureException, KeyStoreException { - - KeyPair keyPair = generateKeyPair(SERVER_KEY_SIZE); - - X500Name issuer = new X509CertificateHolder(caCert.getEncoded()).getSubject(); - BigInteger serial = BigInteger.valueOf(randomSerial()); - X500NameBuilder name = new X500NameBuilder(BCStyle.INSTANCE); - name.addRDN(BCStyle.CN, commonName); - name.addRDN(BCStyle.O, jks.certOrganisation()); - name.addRDN(BCStyle.OU, jks.certOrganizationalUnitName()); - X500Name subject = name.build(); - - X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuer, serial, NOT_BEFORE, - new Date(System.currentTimeMillis() + ONE_DAY), subject, keyPair.getPublic()); - builder.addExtension(Extension.subjectKeyIdentifier, false, - createSubjectKeyIdentifier(keyPair.getPublic())); - builder.addExtension(Extension.basicConstraints, false, - new BasicConstraints(false)); - builder.addExtension(Extension.subjectAlternativeName, false, - new DERSequence(new GeneralName(GeneralName.dNSName, commonName))); - - X509Certificate cert = signCertificate(builder, caPrivKey); - - cert.checkValidity(new Date()); - cert.verify(caCert.getPublicKey()); - - KeyStore result = KeyStore.getInstance(KeyStore.getDefaultType()); - result.load(null, null); - Certificate[] chain = { cert, caCert }; - result.setKeyEntry(jks.alias(), keyPair.getPrivate(), jks.password(), chain); - return result; - } - - public String keyStoreType() { - return KEY_STORE_TYPE; - } - - private KeyPair generateKeyPair(int keySize) throws NoSuchAlgorithmException { - KeyPairGenerator generator = KeyPairGenerator.getInstance(KEYGEN_ALGORITHM); - SecureRandom secureRandom = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM); - generator.initialize(keySize, secureRandom); - return generator.generateKeyPair(); - } - - private long randomSerial() { - final Random rnd = new Random(); - rnd.setSeed(System.currentTimeMillis()); - // prevent browser certificate caches, cause of doubled serial numbers - // using 48bit random number - long sl = ((long) rnd.nextInt()) << 32 | (rnd.nextInt() & 0xFFFFFFFFL); - // let reserve of 16 bit for increasing, serials have to be positive - sl = sl & 0x0000FFFFFFFFFFFFL; - return sl; - } - - private static SubjectKeyIdentifier createSubjectKeyIdentifier(Key key) throws IOException { - ByteArrayInputStream bIn = new ByteArrayInputStream(key.getEncoded()); - ASN1InputStream is = null; - try { - is = new ASN1InputStream(bIn); - ASN1Sequence seq = (ASN1Sequence) is.readObject(); - SubjectPublicKeyInfo info = SubjectPublicKeyInfo.getInstance(seq); - return new BcX509ExtensionUtils().createSubjectKeyIdentifier(info); - } finally { - NetBareUtils.closeQuietly(is); - } - } - - private static X509Certificate signCertificate(X509v3CertificateBuilder certificateBuilder, - PrivateKey signedWithPrivateKey) throws OperatorCreationException, - CertificateException { - ContentSigner signer = new JcaContentSignerBuilder(SIGNATURE_ALGORITHM) - .setProvider(PROVIDER_NAME) - .build(signedWithPrivateKey); - return new JcaX509CertificateConverter() - .setProvider(PROVIDER_NAME) - .getCertificate(certificateBuilder.build(signer)); - } - - /** - * Uses the non-portable system property sun.arch.data.model to help - * determine if we are running on a 32-bit JVM. Since the majority of modern - * systems are 64 bits, this method "assumes" 64 bits and only returns true - * if sun.arch.data.model explicitly indicates a 32-bit JVM. - * - * @return true if we can determine definitively that this is a 32-bit JVM, - * otherwise false - */ - private static boolean is32BitJvm() { - Integer bits = Integer.getInteger("sun.arch.data.model"); - return bits != null && bits == 32; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateInstallActivity.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateInstallActivity.java deleted file mode 100644 index 8196687..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateInstallActivity.java +++ /dev/null @@ -1,82 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ssl; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.security.KeyChain; -import androidx.annotation.Nullable; - -import com.github.megatronking.netbare.NetBareLog; - -import java.io.File; -import java.io.IOException; - -/** - * A translucent activity uses to install self-signed certificate. - * - * @author Megatron King - * @since 2018年11月10日 21:18 - */ -public class CertificateInstallActivity extends Activity { - - private static final int REQUEST_CODE_INSTALL = 1; - public static final String EXTRA_ALIAS = "jks_alias"; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Bundle bundle = getIntent().getExtras(); - if (bundle == null) { - finish(); - return; - } - Intent intent = KeyChain.createInstallIntent(); - intent.putExtras(bundle); - startActivityForResult(intent, REQUEST_CODE_INSTALL); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_CODE_INSTALL && resultCode == RESULT_OK) { - File jsk = new File(getCacheDir(), - getIntent().getStringExtra(EXTRA_ALIAS) + JKS.KEY_JKS_FILE_EXTENSION); - try { - if(!jsk.exists() && !jsk.createNewFile()) { - throw new IOException("Create jks file failed."); - } - } catch (IOException e) { - NetBareLog.wtf(e); - } - } - finish(); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putAll(getIntent().getExtras()); - } - - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - getIntent().putExtras(savedInstanceState); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/JKS.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/JKS.java deleted file mode 100755 index 3bc360d..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/JKS.java +++ /dev/null @@ -1,215 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ssl; - -import android.app.Activity; -import android.app.Application; -import android.content.Context; -import android.content.Intent; -import android.security.KeyChain; -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.NetBareUtils; - -import org.bouncycastle.openssl.jcajce.JcaPEMWriter; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.Writer; -import java.security.KeyStore; -import java.security.cert.Certificate; - -/** - * A java keystore to manage root certificate. - * - * @author Megatron King - * @since 2018年11月10日 20:06 - */ -public class JKS { - private static final String JSK_ALIAS = "易开发抓包证书"; - private static volatile JKS jks; - public static final String KEY_STORE_FILE_EXTENSION = ".p12"; - public static final String KEY_PEM_FILE_EXTENSION = ".pem"; - public static final String KEY_JKS_FILE_EXTENSION = ".jks"; - private final File keystoreDir; - private final String alias; - private final char[] password; - private final String commonName; - private final String organization; - private final String organizationalUnitName; - private final String certOrganization; - private final String certOrganizationalUnitName; - - public JKS(@NonNull Context context, @NonNull String alias, @NonNull char[] password, - @NonNull String commonName, @NonNull String organization, - @NonNull String organizationalUnitName, @NonNull String certOrganization, - @NonNull String certOrganizationalUnitName) { - this.keystoreDir = context.getCacheDir(); - this.alias = alias; - this.password = password; - this.commonName = commonName; - this.organization = organization; - this.organizationalUnitName = organizationalUnitName; - this.certOrganization = certOrganization; - this.certOrganizationalUnitName = certOrganizationalUnitName; - createKeystore(); - } - - public static void init(Application application) { - if (jks == null) { - synchronized (JKS.class) { - if (jks == null) { - jks = new JKS(application, JSK_ALIAS, JSK_ALIAS.toCharArray(), JSK_ALIAS, JSK_ALIAS, JSK_ALIAS, JSK_ALIAS, JSK_ALIAS); - } - } - } - } - - public static JKS getJks() { - return jks; - } - - public static String getJskAlias() { - return JSK_ALIAS; - } - - String alias() { - return alias; - } - - char[] password() { - return password; - } - - String commonName() { - return commonName; - } - - String organization() { - return organization; - } - - String organizationalUnitName() { - return organizationalUnitName; - } - - String certOrganisation() { - return certOrganization; - } - - String certOrganizationalUnitName() { - return certOrganizationalUnitName; - } - - public boolean isInstalled() { - return aliasFile(KEY_STORE_FILE_EXTENSION).exists() && - aliasFile(KEY_PEM_FILE_EXTENSION).exists() && - aliasFile(KEY_JKS_FILE_EXTENSION).exists(); - } - - public File aliasFile(String fileExtension) { - return new File(keystoreDir, alias + fileExtension); - } - - private void createKeystore() { - if (aliasFile(KEY_STORE_FILE_EXTENSION).exists() && - aliasFile(KEY_PEM_FILE_EXTENSION).exists()) { - return; - } - - // Generate keystore in the async thread - new Thread(new Runnable() { - @Override - public void run() { - CertificateGenerator generator = new CertificateGenerator(); - KeyStore keystore; - OutputStream os = null; - Writer sw = null; - JcaPEMWriter pw = null; - try { - keystore = generator.generateRoot(JKS.this); - os = new FileOutputStream(aliasFile(KEY_STORE_FILE_EXTENSION)); - keystore.store(os, password()); - - Certificate cert = keystore.getCertificate(alias()); - sw = new FileWriter(aliasFile(KEY_PEM_FILE_EXTENSION)); - pw = new JcaPEMWriter(sw); - pw.writeObject(cert); - pw.flush(); - NetBareLog.i("Generate keystore succeed."); - } catch (Exception e) { - e.printStackTrace(); - } finally { - NetBareUtils.closeQuietly(os); - NetBareUtils.closeQuietly(sw); - NetBareUtils.closeQuietly(pw); - } - } - }).start(); - } - - /** - * Whether the certificate with given alias has been installed. - * - * @param context Any context. - * @param alias Key store alias. - * @return True if the certificate has been installed. - */ - public static boolean isInstalled(Context context, String alias) { - return new File(context.getCacheDir(), - alias + KEY_JKS_FILE_EXTENSION).exists(); - } - - /** - * Install the self-signed root certificate. - * - * @param context Any context. - * @param name Key chain name. - * @param alias Key store alias. - * @throws IOException If an IO error has occurred. - */ - public static void install(Context context, String name, String alias) - throws IOException { - byte[] keychain; - FileInputStream is = null; - try { - is = new FileInputStream(new File(context.getCacheDir(), - alias + KEY_PEM_FILE_EXTENSION)); - keychain = new byte[is.available()]; - int len = is.read(keychain); - if (len != keychain.length) { - throw new IOException("Install JKS failed, len: " + len); - } - } finally { - NetBareUtils.closeQuietly(is); - } - - Intent intent = new Intent(context, CertificateInstallActivity.class); - intent.putExtra(KeyChain.EXTRA_CERTIFICATE, keychain); - intent.putExtra(KeyChain.EXTRA_NAME, name); - intent.putExtra(CertificateInstallActivity.EXTRA_ALIAS, alias); - if (!(context instanceof Activity)) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - context.startActivity(intent); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLCodec.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLCodec.java deleted file mode 100644 index ff4f6a0..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLCodec.java +++ /dev/null @@ -1,476 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ssl; - -import android.os.Build; -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareLog; - -import java.io.EOFException; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedDeque; - -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLException; - -/** - * A base class for encrypting and decrypting SSL packets. Use {@link CodecCallback} to - * observe actions and receive output packets. - * - *

                SSL handshake steps:

                - * - * client server message - * ====== ====== ======= - * wrap() ... ClientHello - * ... unwrap() ClientHello - * ... wrap() ServerHello/Certificate - * unwrap() ... ServerHello/Certificate - * wrap() ... ClientKeyExchange - * wrap() ... ChangeCipherSpec - * wrap() ... Finished - * ... unwrap() ClientKeyExchange - * ... unwrap() ChangeCipherSpec - * ... unwrap() Finished - * ... wrap() ChangeCipherSpec - * ... wrap() Finished - * unwrap() ... ChangeCipherSpec - * unwrap() ... Finished - * - * @author Megatron King - * @since 2018年11月15日 17:46 - */ -public abstract class SSLCodec { - - /** - * Change cipher spec. - */ - public static final int SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC = - SSLUtils.SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC; - - /** - * Alert. - */ - public static final int SSL_CONTENT_TYPE_ALERT = - SSLUtils.SSL_CONTENT_TYPE_ALERT; - - /** - * Handshake. - */ - public static final int SSL_CONTENT_TYPE_HANDSHAKE = - SSLUtils.SSL_CONTENT_TYPE_HANDSHAKE; - - /** - * Application data. - */ - public static final int SSL_CONTENT_TYPE_APPLICATION_DATA = - SSLUtils.SSL_CONTENT_TYPE_APPLICATION_DATA; - - /** - * HeartBeat Extension. - */ - public static final int SSL_CONTENT_TYPE_EXTENSION_HEARTBEAT = - SSLUtils.SSL_CONTENT_TYPE_EXTENSION_HEARTBEAT; - - /** - * Should larger than generated pem certificate file size. - */ - private static final int DEFAULT_BUFFER_SIZE = 20 * 1024; - - private SSLEngineFactory mSSLEngineFactory; - - private boolean mEngineClosed; - private boolean mHandshakeStarted; - private boolean mHandshakeFinished; - - private Queue mPlaintextBuffers; - - SSLCodec(SSLEngineFactory factory) { - this.mSSLEngineFactory = factory; - this.mPlaintextBuffers = new ConcurrentLinkedDeque(); - } - - /** - * Create an {@link SSLEngine} instance to encode and decode SSL packets. - * - * @param factory A factory produces {@link SSLEngine}. - * @return An instance of {@link SSLEngine}. - * @throws IOException If an I/O error has occurred. - */ - protected abstract SSLEngine createEngine(SSLEngineFactory factory) - throws IOException; - - /** - * Handshake with the client or server and try to decode a SSL encrypt packet. - * - * @param buffer The SSL encrypt packet. - * @param callback A callback to observe actions and receive output packets. - * @throws IOException If an I/O error has occurred. - */ - public void decode(ByteBuffer buffer, @NonNull CodecCallback callback) throws IOException { - int verifyResult = SSLUtils.verifyPacket(buffer); - if (!mHandshakeStarted) { - if (verifyResult == SSLUtils.PACKET_NOT_ENCRYPTED) { - callback.onDecrypt(buffer); - return; - } - } - if (verifyResult == SSLUtils.PACKET_NOT_ENOUGH) { - callback.onPending(buffer); - return; - } - decode(createEngine(mSSLEngineFactory), buffer, callback); - } - - /** - * Try to encrypt a plaintext packet. If SSL handshake has finished, then encode it, - * otherwise add it to queue and wait handshake finished. - * - * @param buffer The plaintext packet. - * @param callback A callback to observe actions and receive output packets. - * @throws IOException If an I/O error has occurred. - */ - public void encode(ByteBuffer buffer, @NonNull CodecCallback callback) throws IOException { - if (!buffer.hasRemaining()) { - return; - } - if (mHandshakeFinished) { - wrap(createEngine(mSSLEngineFactory), buffer, callback); - } else { - mPlaintextBuffers.offer(buffer); - } - } - - private void decode(SSLEngine engine, ByteBuffer input, CodecCallback callback) - throws IOException { - // Give up decrypt SSL packet. - if (engine == null) { - callback.onProcess(input); - return; - } - startDecode(engine, input, callback); - } - - private void startDecode(SSLEngine engine, ByteBuffer input, CodecCallback callback) - throws IOException { - if (mEngineClosed) { - return; - } - if (mHandshakeFinished) { - unwrap(engine, input, callback); - } else { - handshake(engine, input, callback); - } - // Start wrap plaintext to engine if possible. - if (mHandshakeFinished && !mPlaintextBuffers.isEmpty()) { - ByteBuffer plaintextBuffer; - while (!mPlaintextBuffers.isEmpty()) { - plaintextBuffer = mPlaintextBuffers.poll(); - if (plaintextBuffer != null && plaintextBuffer.hasRemaining()) { - wrap(engine, plaintextBuffer, callback); - } - } - } - } - - /* package */ void handshake(SSLEngine engine, ByteBuffer input, CodecCallback callback) - throws IOException { - if (!mHandshakeStarted) { - engine.beginHandshake(); - mHandshakeStarted = true; - } - SSLEngineResult.HandshakeStatus status = engine.getHandshakeStatus(); - while (!mHandshakeFinished) { - if (mEngineClosed) { - throw new IOException("Handshake failed: Engine is closed."); - } - if (status == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { - // Should never happen - throw new IOException("Handshake failed: Invalid handshake status: " + status); - } else if (status == SSLEngineResult.HandshakeStatus.FINISHED) { - mHandshakeFinished = true; - NetBareLog.i("SSL handshake finished!"); - if (input.hasRemaining()) { - decode(engine, input, callback); - } - } else if (status == SSLEngineResult.HandshakeStatus.NEED_WRAP) { - status = handshakeWrap(engine, callback).getHandshakeStatus(); - } else if (status == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) { - // Wait next encrypted buffer. - if (!input.hasRemaining()) { - break; - } - status = handshakeUnwrap(engine, input, callback).getHandshakeStatus(); - } else if (status == SSLEngineResult.HandshakeStatus.NEED_TASK) { - runDelegatedTasks(engine); - } - } - } - - private SSLEngineResult handshakeWrap(SSLEngine engine, CodecCallback callback) - throws IOException { - SSLEngineResult result; - SSLEngineResult.Status status; - ByteBuffer output = allocate(); - while (true) { - result = engineWrap(engine, allocate(0), output); - status = result.getStatus(); - output.flip(); - if (output.hasRemaining()) { - callback.onEncrypt(output); - } - if (status == SSLEngineResult.Status.BUFFER_OVERFLOW) { - output = allocate(engine.getSession().getApplicationBufferSize()); - } else { - if (status == SSLEngineResult.Status.CLOSED) { - mEngineClosed = true; - } - break; - } - } - return result; - } - - private SSLEngineResult handshakeUnwrap(SSLEngine engine, ByteBuffer input, - CodecCallback callback) throws IOException { - SSLEngineResult result; - SSLEngineResult.Status status; - ByteBuffer output = allocate(); - while (true) { - result = engineUnwrap(engine, input, output); - status = result.getStatus(); - output.flip(); - int producedSize = output.remaining(); - if (producedSize> 0) { - callback.onDecrypt(output); - } - if (status == SSLEngineResult.Status.BUFFER_OVERFLOW) { - int bufferSize = engine.getSession().getApplicationBufferSize() - producedSize; - if (bufferSize < 0) { - bufferSize = engine.getSession().getApplicationBufferSize(); - } - output = allocate(bufferSize); - } else if (status == SSLEngineResult.Status.BUFFER_UNDERFLOW) { - // Store the remaining packet and wait next encrypted buffer. - if (input.hasRemaining()) { - callback.onPending(ByteBuffer.wrap(input.array(), input.position(), - input.remaining())); - // Clear all data. - input.position(0); - input.limit(0); - } - break; - } else if (status == SSLEngineResult.Status.CLOSED) { - mEngineClosed = true; - break; - } else { - // It is status OK. - break; - } - } - return result; - } - - private void unwrap(SSLEngine engine, ByteBuffer input, CodecCallback callback) - throws IOException { - ByteBuffer output = null; - while (true) { - if (output == null) { - output = allocate(); - } - SSLEngineResult result = engineUnwrap(engine, input, output); - SSLEngineResult.Status status = result.getStatus(); - output.flip(); - int producedSize = output.remaining(); - if (producedSize> 0) { - callback.onDecrypt(output); - output = null; - } - if (status == SSLEngineResult.Status.BUFFER_OVERFLOW) { - int bufferSize = engine.getSession().getApplicationBufferSize() - producedSize; - if (bufferSize < 0) { - bufferSize = engine.getSession().getApplicationBufferSize(); - } - output = allocate(bufferSize); - } else if (status == SSLEngineResult.Status.BUFFER_UNDERFLOW) { - // Store the remaining packet and wait next encrypted buffer. - if (input.hasRemaining()) { - callback.onPending(ByteBuffer.wrap(input.array(), input.position(), - input.remaining())); - // Clear all data. - input.position(0); - input.limit(0); - } - break; - } else if (status == SSLEngineResult.Status.CLOSED) { - mEngineClosed = true; - break; - } else { - if (!input.hasRemaining()) { - break; - } - } - } - } - - private void wrap(SSLEngine engine, ByteBuffer input, CodecCallback callback) - throws IOException { - ByteBuffer output = allocate(); - while (true) { - SSLEngineResult result = engineWrap(engine, input, output); - SSLEngineResult.Status status = result.getStatus(); - output.flip(); - if (output.hasRemaining()) { - callback.onEncrypt(output); - } - if (status == SSLEngineResult.Status.BUFFER_OVERFLOW) { - output = allocate(engine.getSession().getApplicationBufferSize()); - } else { - if (status == SSLEngineResult.Status.CLOSED) { - mEngineClosed = true; - } - break; - } - } - if (!mEngineClosed && input.hasRemaining()) { - wrap(engine, input, callback); - } - } - - private SSLEngineResult engineWrap(SSLEngine engine, ByteBuffer input, ByteBuffer output) - throws SSLException { - return engine.wrap(input, output); - } - - private SSLEngineResult engineUnwrap(SSLEngine engine, ByteBuffer input, ByteBuffer output) - throws SSLException { - int position = input.position(); - SSLEngineResult result; - // Fixed issue #4 - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) { - // In Android 8.1, the BUFFER_OVERFLOW in the unwrap method will throw an - // SSLException-EOFException. We catch this error and increase the output buffer - // capacity. - while (true) { - int inputRemaining = input.remaining(); - try { - result = engine.unwrap(input, output); - break; - } catch (SSLException e) { - if (!output.hasRemaining()) { - // Copy - ByteBuffer outputTemp = ByteBuffer.allocate(output.capacity() * 2); - output.flip(); - outputTemp.put(output); - output = outputTemp; - } else { - // java.io.EOFException: Read error is an Android 8.1 system bug, - // it will cause #4 and #11. We swallowed this exception and not throw. - if ((e.getCause() instanceof EOFException && inputRemaining == 31 && - input.remaining() == 0 && output.remaining() == output.capacity())) { - // Create a new SSLEngineResult. - result = new SSLEngineResult(SSLEngineResult.Status.OK, - SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING, - inputRemaining, 0); - break; - } else { - throw e; - } - } - } - } - } else { - result = engine.unwrap(input, output); - } - - // This is a workaround for a bug in Android 5.0. Android 5.0 does not correctly update - // the SSLEngineResult.bytesConsumed() in some cases and just return 0. - // - // See: - // - https://android-review.googlesource.com/c/platform/external/conscrypt/+/122080 - // - https://github.com/netty/netty/issues/7758 - if (result.bytesConsumed() == 0) { - int consumed = input.position() - position; - if (consumed != result.bytesConsumed()) { - // Create a new SSLEngineResult with the correct bytesConsumed(). - result = new SSLEngineResult(result.getStatus(), result.getHandshakeStatus(), - consumed, result.bytesProduced()); - } - } - return result; - } - - private void runDelegatedTasks(SSLEngine engine) { - while (true) { - final Runnable task = engine.getDelegatedTask(); - if (task == null) { - break; - } - task.run(); - } - } - - private ByteBuffer allocate(int size) { - return ByteBuffer.allocate(size); - } - - private ByteBuffer allocate() { - return ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); - } - - /** - * A callback to receive {@link SSLCodec} results. - */ - public interface CodecCallback { - - /** - * The packet is not a perfect SSL encrypted packet, should wait next packet and decode - * together. - * - * @param buffer The buffer should be pended in a queue. - */ - void onPending(ByteBuffer buffer); - - /** - * The packet is unable to decrypt or encrypt, should send them to tunnel immediately. - * - * @param buffer Packets should send to tunnel. - * @throws IOException If an I/O error has occurred. - */ - void onProcess(ByteBuffer buffer) throws IOException; - - /** - * Output an encrypted packet. - * - * @param buffer The encrypted packet. - * @throws IOException If an I/O error has occurred. - */ - void onEncrypt(ByteBuffer buffer) throws IOException; - - /** - * Output an decrypted packet, it is a plaintext. - * - * @param buffer The decrypted packet. - * @throws IOException If an I/O error has occurred. - */ - void onDecrypt(ByteBuffer buffer) throws IOException; - - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLEngineFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLEngineFactory.java deleted file mode 100644 index 7669f9b..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLEngineFactory.java +++ /dev/null @@ -1,212 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ssl; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareUtils; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; - -import org.bouncycastle.operator.OperatorCreationException; - -import java.io.FileInputStream; -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.SecureRandom; -import java.security.UnrecoverableKeyException; -import java.security.cert.Certificate; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -/** - * A factory produces client and server MITM {@link SSLEngine}. - * - * @author Megatron King - * @since 2018年11月10日 23:43 - */ -public final class SSLEngineFactory { - - private static final int ALIVE_MINUTES = 10; - private static final int CONCURRENCY_LEVEL = 16; - - /** - * Enforce TLS 1.2 if available, since it's not default up to Java 8. - *

                - * Java 7 disables TLS 1.1 and 1.2 for clients. From Java Cryptography Architecture Oracle Providers Documentation: - * Although SunJSSE in the Java SE 7 release supports TLS 1.1 and TLS 1.2, - * neither version is enabled by default for client connections. Some - * servers do not implement forward compatibility correctly and refuse to - * talk to TLS 1.1 or TLS 1.2 clients. For interoperability, SunJSSE does - * not enable TLS 1.1 or TLS 1.2 by default for client connections. - */ - private static final String SSL_CONTEXT_PROTOCOL = "TLSv1.2"; - - /** - * {@link SSLContext}: Every implementation of the Java platform is required - * to support the following standard SSLContext protocol: TLSv1 - */ - private static final String SSL_CONTEXT_FALLBACK_PROTOCOL = "TLSv1"; - - private final JKS mJKS; - private final Cache mServerSSLContexts; - private final CertificateGenerator mGenerator; - - private Certificate mCaCert; - private PrivateKey mCaPrivKey; - - /** - * Constructs the factory with a self-signed certificate. - * - * @param jks Java keystore of the self-signed certificate. - * @throws GeneralSecurityException If a generic security exception has occurred. - * @throws IOException If an I/O error has occurred. - */ - public SSLEngineFactory(@NonNull JKS jks) throws GeneralSecurityException, IOException { - this.mJKS = jks; - this.mServerSSLContexts = CacheBuilder.newBuilder() - .expireAfterAccess(ALIVE_MINUTES, TimeUnit.MINUTES) - .concurrencyLevel(CONCURRENCY_LEVEL) - .build(); - this.mGenerator = new CertificateGenerator(); - initializeSSLContext(); - } - - /** - * Create a MITM server {@link SSLEngine} with the remote server host. - * - * @param host The remote server host. - * @return A server {@link SSLEngine} instance. - * @throws ExecutionException If an execution error has occurred. - */ - public SSLEngine createServerEngine(@NonNull final String host) throws ExecutionException { - SSLContext ctx = mServerSSLContexts.get(host, new Callable() { - @Override - public SSLContext call() throws GeneralSecurityException, IOException, - OperatorCreationException { - return createServerContext(host); - } - }); - return ctx.createSSLEngine(); - } - - /** - * Create a client {@link SSLEngine} with the remote server IP and port. - * - * @param ip Remote server IP. - * @param port Remote server port. - * @return A client {@link SSLEngine} instance. - * @throws NoSuchAlgorithmException if no Provider supports a SSLContextSpi implementation for - * the specified protocol. - * @throws KeyStoreException If creates KeyManager fail. - * @throws KeyManagementException If creates SSLContext fail. - */ - public SSLEngine createClientEngine(@NonNull final String ip, int port) - throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { - SSLContext sslContext = createSSLContext(); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init((KeyStore) null); - TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); - if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { - throw new KeyManagementException("Unexpected default trust managers:" - + Arrays.toString(trustManagers)); - } - sslContext.init(null, new TrustManager[] { trustManagers[0] }, null); - SSLEngine engine = sslContext.createSSLEngine(ip, port); - List ciphers = new LinkedList(); - for (String each : engine.getEnabledCipherSuites()) { - if (!each.equals("TLS_DHE_RSA_WITH_AES_128_CBC_SHA") && - !each.equals("TLS_DHE_RSA_WITH_AES_256_CBC_SHA")) { - ciphers.add(each); - } - } - engine.setEnabledCipherSuites(ciphers.toArray(new String[ciphers.size()])); - engine.setUseClientMode(true); - engine.setNeedClientAuth(false); - return engine; - } - - private void initializeSSLContext() throws GeneralSecurityException, IOException { - KeyStore ks = loadKeyStore(); - mCaCert = ks.getCertificate(mJKS.alias()); - mCaPrivKey = (PrivateKey) ks.getKey(mJKS.alias(), mJKS.password()); - } - - private KeyStore loadKeyStore() throws GeneralSecurityException, IOException { - KeyStore ks = KeyStore.getInstance(mGenerator.keyStoreType()); - FileInputStream is = null; - try { - is = new FileInputStream(mJKS.aliasFile(JKS.KEY_STORE_FILE_EXTENSION)); - ks.load(is, mJKS.password()); - } finally { - NetBareUtils.closeQuietly(is); - } - return ks; - } - - private SSLContext createServerContext(String host) throws GeneralSecurityException, - IOException, OperatorCreationException { - KeyStore ks = mGenerator.generateServer(host, mJKS, mCaCert, mCaPrivKey); - KeyManager[] keyManagers = getKeyManagers(ks); - return createServerContext(keyManagers); - } - - private SSLContext createServerContext(KeyManager[] keyManagers) throws NoSuchAlgorithmException, - KeyManagementException { - SSLContext result = createSSLContext(); - SecureRandom random = new SecureRandom(); - random.setSeed(System.currentTimeMillis() + 1); - result.init(keyManagers, null, random); - return result; - } - - private SSLContext createSSLContext() throws NoSuchAlgorithmException { - try { - return SSLContext.getInstance(SSL_CONTEXT_PROTOCOL); - } catch (NoSuchAlgorithmException e) { - return SSLContext.getInstance(SSL_CONTEXT_FALLBACK_PROTOCOL); - } - } - - private KeyManager[] getKeyManagers(KeyStore keyStore) throws NoSuchAlgorithmException, - UnrecoverableKeyException, KeyStoreException { - String keyManAlg = KeyManagerFactory.getDefaultAlgorithm(); - KeyManagerFactory kmf = KeyManagerFactory.getInstance(keyManAlg); - kmf.init(keyStore, mJKS.password()); - return kmf.getKeyManagers(); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLRequestCodec.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLRequestCodec.java deleted file mode 100644 index 1d42931..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLRequestCodec.java +++ /dev/null @@ -1,75 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ssl; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.gateway.Request; - -import java.util.concurrent.ExecutionException; - -import javax.net.ssl.SSLEngine; - -/** - * An implementation of {@link SSLCodec} to codec request SSL packets. This codec creates a MITM - * SSL server engine using {@link SSLEngineFactory}, it requires the remote server host. - * - * @author Megatron King - * @since 2018年11月15日 23:23 - */ -public class SSLRequestCodec extends SSLCodec { - - private Request mRequest; - private SSLEngine mEngine; - - /** - * Constructs an instance of {@link SSLCodec} by a factory. - * - * @param factory A factory produces {@link SSLEngine}. - */ - public SSLRequestCodec(SSLEngineFactory factory) { - super(factory); - } - - /** - * Bind a {@link Request} to this codec. - * - * @param request A request has terminated remote server host. - */ - public void setRequest(Request request) { - this.mRequest = request; - } - - @Override - protected SSLEngine createEngine(SSLEngineFactory factory) { - if (mEngine == null) { - String host = mRequest.host(); - if (host == null) { - // Unable to get host. - NetBareLog.e("Failed to get SSL host."); - return null; - } - try { - mEngine = factory.createServerEngine(host); - mEngine.setUseClientMode(false); - mEngine.setNeedClientAuth(false); - } catch (ExecutionException e) { - NetBareLog.e("Failed to create server SSLEngine: " + e.getMessage()); - } - } - return mEngine; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLResponseCodec.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLResponseCodec.java deleted file mode 100644 index 98990fe..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLResponseCodec.java +++ /dev/null @@ -1,114 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ssl; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.gateway.Request; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; - -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLException; - -/** - * An implementation of {@link SSLCodec} to codec response SSL packets. This codec creates a SSL - * client engine using {@link SSLEngineFactory}, it requires the remote server ip and host. - * Before encrypt, should call {@link #prepareHandshake()} to start SSL handshake. - * - * @author Megatron King - * @since 2018年11月16日 01:30 - */ -public class SSLResponseCodec extends SSLCodec { - - private final SSLEngineFactory mSSLEngineFactory; - - private Request mRequest; - private SSLEngine mEngine; - - /** - * Constructs an instance of {@link SSLCodec} by a factory. - * - * @param factory A factory produces {@link SSLEngine}. - */ - public SSLResponseCodec(SSLEngineFactory factory) { - super(factory); - this.mSSLEngineFactory = factory; - } - - /** - * Bind a {@link Request} to this codec. - * - * @param request A request has terminated remote server ip and port. - */ - public void setRequest(Request request) { - this.mRequest = request; - } - - @Override - protected SSLEngine createEngine(SSLEngineFactory factory) { - if (mEngine == null) { - try { - String host = mRequest.host() != null ? mRequest.host() : mRequest.ip(); - mEngine = factory.createClientEngine(host, mRequest.port()); - mEngine.setUseClientMode(true); - } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { - NetBareLog.e("Failed to create client SSLEngine: " + e.getMessage()); - } - } - return mEngine; - } - - /** - * Prepare and start SSL handshake with the remote server. - * - * @throws IOException If an I/O error has occurred. - */ - public void prepareHandshake() throws IOException { - if (mEngine != null) { - // The handshake was started. - return; - } - SSLEngine engine = createEngine(mSSLEngineFactory); - if (engine == null) { - throw new SSLException("Failed to create client SSLEngine."); - } - ByteBuffer input = ByteBuffer.allocate(0); - handshake(engine, input, new CodecCallback() { - @Override - public void onPending(ByteBuffer buffer) { - } - - @Override - public void onProcess(ByteBuffer buffer) { - } - - @Override - public void onEncrypt(ByteBuffer buffer) throws IOException { - // Send to remote server - mRequest.process(buffer); - } - - @Override - public void onDecrypt(ByteBuffer buffer) { - } - }); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLUtils.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLUtils.java deleted file mode 100644 index c21b621..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLUtils.java +++ /dev/null @@ -1,251 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ssl; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.http.HttpProtocol; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -/** - * A SSL utils class. - * - * @author Megatron King - * @since 2018年11月14日 11:38 - */ -public final class SSLUtils { - - /** - * Change cipher spec. - */ - public static final int SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC = 20; - - /** - * Alert. - */ - public static final int SSL_CONTENT_TYPE_ALERT = 21; - - /** - * Handshake. - */ - public static final int SSL_CONTENT_TYPE_HANDSHAKE = 22; - - /** - * Application data. - */ - public static final int SSL_CONTENT_TYPE_APPLICATION_DATA = 23; - - /** - * HeartBeat Extension. - */ - public static final int SSL_CONTENT_TYPE_EXTENSION_HEARTBEAT = 24; - - /** - * The length of the ssl record header (in bytes). - */ - private static final int SSL_RECORD_HEADER_LENGTH = 5; - - /** - * Packet length is not enough to determine. - */ - public static final int PACKET_NOT_ENOUGH = 1; - - /** - * It is a plaintext packet. - */ - public static final int PACKET_NOT_ENCRYPTED = 2; - - /** - * It is a valid SSL packet. - */ - public static final int PACKET_SSL = 3; - - /** - * Verify a packet to see whether it is a valid SSL packet. - * - * @param buffer Encrypted SSL packet. - * @return Verification result, one of {@link #PACKET_NOT_ENOUGH}, {@link #PACKET_NOT_ENCRYPTED}, - * and {@link #PACKET_SSL}. - */ - public static int verifyPacket(ByteBuffer buffer) { - final int position = buffer.position(); - // Get the packet length and wait until we get a packets worth of data to unwrap. - if (buffer.remaining() < SSL_RECORD_HEADER_LENGTH) { - NetBareLog.w("No enough ssl/tls packet length: " + buffer.remaining()); - return PACKET_NOT_ENOUGH; - } - int packetLength = 0; - // SSLv3 or TLS - Check ContentType - boolean tls; - switch (unsignedByte(buffer, position)) { - case SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC: - case SSL_CONTENT_TYPE_ALERT: - case SSL_CONTENT_TYPE_HANDSHAKE: - case SSL_CONTENT_TYPE_APPLICATION_DATA: - case SSL_CONTENT_TYPE_EXTENSION_HEARTBEAT: - tls = true; - break; - default: - // SSLv2 or bad data - tls = false; - } - if (tls) { - // SSLv3 or TLS - Check ProtocolVersion - int majorVersion = unsignedByte(buffer, position + 1); - if (majorVersion == 3) { - // SSLv3 or TLS - packetLength = unsignedShort(buffer, position + 3) + SSL_RECORD_HEADER_LENGTH; - if (packetLength <= SSL_RECORD_HEADER_LENGTH) { - // Neither SSLv3 or TLSv1 (i.e. SSLv2 or bad data) - tls = false; - } - } else { - // Neither SSLv3 or TLSv1 (i.e. SSLv2 or bad data) - tls = false; - } - } - if (!tls) { - // SSLv2 or bad data - Check the version - int headerLength = (unsignedByte(buffer, position) & 0x80) != 0 ? 2 : 3; - int majorVersion = unsignedByte(buffer, position + headerLength + 1); - if (majorVersion == 2 || majorVersion == 3) { - // SSLv2 - packetLength = headerLength == 2 ? - (buffer.getShort(position) & 0x7FFF) + 2 : (buffer.getShort(position) & 0x3FFF) + 3; - if (packetLength <= headerLength) { - NetBareLog.w("No enough ssl/tls packet length, packet: " + packetLength + - " header: " + headerLength); - // No enough data. - return PACKET_NOT_ENOUGH; - } - } else { - // Not encrypted - return PACKET_NOT_ENCRYPTED; - } - } - // Decode SSL data. - if (packetLength> buffer.remaining()) { - NetBareLog.w("No enough ssl/tls packet length, packet: " + packetLength + - " actual: " + buffer.remaining()); - // Wait until the whole packet can be read. - return PACKET_NOT_ENOUGH; - } - return PACKET_SSL; - } - - public static HttpProtocol[] parseClientHelloAlpn(ByteBuffer clienthelloMessage) { - byte[] buffer = clienthelloMessage.array(); - int offset = clienthelloMessage.position(); - int size = clienthelloMessage.remaining(); - int limit = offset + size; - // Client Hello - if (size <= 43 || buffer[offset] != 0x16) { - return null; - } - // Skip 43 byte header - offset += 43; - // Read sessionID - if (offset + 1> limit) { - return null; - } - int sessionIDLength = buffer[offset++] & 0xFF; - offset += sessionIDLength; - - // Read cipher suites - if (offset + 2> limit) { - return null; - } - - int cipherSuitesLength = readShort(buffer, offset) & 0xFFFF; - offset += 2; - offset += cipherSuitesLength; - - // Read Compression method. - if (offset + 1> limit) { - return null; - } - int compressionMethodLength = buffer[offset++] & 0xFF; - offset += compressionMethodLength; - - // Read Extensions - if (offset + 2> limit) { - return null; - } - int extensionsLength = readShort(buffer, offset) & 0xFFFF; - offset += 2; - - if (offset + extensionsLength> limit) { - return null; - } - - while (offset + 4 <= limit) { - int type = readShort(buffer, offset) & 0xFFFF; - offset += 2; - int length = readShort(buffer, offset) & 0xFFFF; - offset += 2; - // TYPE_APPLICATION_LAYER_PROTOCOL_NEGOTIATION = 16 - if (type == 16) { - int protocolCount = readShort(buffer, offset) & 0xFFFF; - offset += 2; - length -= 2; - if (offset + length> limit) { - return null; - } - List httpProtocols = new ArrayList(); - int read = 0; - while (read <= protocolCount) { - int protocolLength = buffer[offset + read]; - read += 1; - HttpProtocol protocol = HttpProtocol.parse(new String(buffer, offset + read, - protocolLength)); - if (protocol == HttpProtocol.HTTP_1_1 || protocol == HttpProtocol.HTTP_2) { - httpProtocols.add(protocol); - } - read += protocolLength; - } - if (httpProtocols.isEmpty()) { - return null; - } else { - HttpProtocol[] protocols = new HttpProtocol[httpProtocols.size()]; - for (int i = 0; i < protocols.length; i++) { - protocols[i] = httpProtocols.get(i); - } - return protocols; - } - } else { - offset += length; - } - - } - return null; - } - - private static int unsignedByte(ByteBuffer buffer, int index) { - return buffer.get(index) & 0x0FF; - } - - private static int unsignedShort(ByteBuffer buffer, int index) { - return buffer.getShort(index) & 0x0FFFF; - } - - private static short readShort(byte[] data, int offset) { - int r = ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF); - return (short) r; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/ConnectionShutdownException.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/ConnectionShutdownException.java deleted file mode 100644 index 86a9687..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/ConnectionShutdownException.java +++ /dev/null @@ -1,38 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.tunnel; - -import java.io.IOException; - -/** - * Thrown when the connection has shutdown due to irresistible reason. - * - * @author Megatron King - * @since 2018年12月20日 18:50 - */ -public class ConnectionShutdownException extends IOException { - - /** - * Constructs an {@code ConnectionShutdownException} with the specified detail message. - * - * @param message The detail message (which is saved for later retrieval by the - * {@link #getMessage()} method). - */ - public ConnectionShutdownException(String message) { - super(message); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioCallback.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioCallback.java deleted file mode 100644 index 784e793..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioCallback.java +++ /dev/null @@ -1,61 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.tunnel; - -import java.io.IOException; - -/** - * A nio selector attachment callback for sending notification to {@link NioTunnel}. - * - * @author Megatron King - * @since 2018年10月15日 23:15 - */ -public interface NioCallback { - - /** - * Invoked when the connection is connected with the terminal. - * - * @throws IOException If an I/O error has occurred. - */ - void onConnected() throws IOException; - - /** - * Invoked when the socket IO is readable. - * - * @throws IOException If an I/O error has occurred. - */ - void onRead() throws IOException; - - /** - * Invoked when the socket IO is writable. - * - * @throws IOException If an I/O error has occurred. - */ - void onWrite() throws IOException; - - /** - * Invoked when the socket IO is closed. - */ - void onClosed(); - - /** - * Returns the tunnel using nio attachment. - * - * @return A tunnel. - */ - NioTunnel getTunnel(); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioTunnel.java deleted file mode 100644 index 189fbd0..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioTunnel.java +++ /dev/null @@ -1,195 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.tunnel; - -import com.github.megatronking.netbare.NetBareUtils; - -import java.io.Closeable; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.spi.AbstractSelectableChannel; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedDeque; - -/** - * An abstract base nio tunnel class uses nio operations, the sub class should provides IO - * operations, such as connect, read and write. - * - * @param An implementation class for selectable channels - * @param A socket protects by VPN service. - * - * @author Megatron King - * @since 2018年11月18日 18:34 - */ -public abstract class NioTunnel implements Closeable, - NioCallback, Tunnel { - - /** - * Let the remote tunnel connects to remote server. - * - * @param address The remote server IP socket address. - * @throws IOException if an I/O error occurs. - */ - public abstract void connect(InetSocketAddress address) throws IOException; - - /** - * Returns the socket should be protected by VPN service. - * - * @return A socket. - */ - public abstract S socket(); - - /** - * Write the packet buffer to remote server. - * - * @param buffer A packet buffer. - * @return The wrote length. - * @throws IOException if an I/O error occurs. - */ - protected abstract int channelWrite(ByteBuffer buffer) throws IOException; - - /** - * Read data from remote server and put it into the given buffer. - * - * @param buffer A buffer to store data. - * @return The read length. - * @throws IOException if an I/O error occurs. - */ - protected abstract int channelRead(ByteBuffer buffer) throws IOException; - - private final T mChannel; - private final Selector mSelector; - private SelectionKey mSelectionKey; - - private Queue mPendingBuffers; - - private NioCallback mCallback; - private boolean mIsClosed; - - NioTunnel(T channel, Selector selector) { - this.mChannel = channel; - this.mSelector = selector; - this.mPendingBuffers = new ConcurrentLinkedDeque(); - } - - @Override - public void onConnected() throws IOException { - if (mCallback != null) { - mCallback.onConnected(); - } - } - - @Override - public void onRead() throws IOException { - if (mCallback != null) { - mCallback.onRead(); - } - } - - @Override - public void onWrite() throws IOException { - if (mCallback != null) { - mCallback.onWrite(); - } - // Write pending buffers. - while (!mPendingBuffers.isEmpty()) { - ByteBuffer buffer = mPendingBuffers.poll(); - int remaining = buffer.remaining(); - int sent = channelWrite(buffer); - if (sent < remaining) { - // Should wait next onWrite. - mPendingBuffers.offer(buffer); - return; - } - } - interestRead(); - } - - @Override - public void onClosed() { - if (mCallback != null) { - mCallback.onClosed(); - } - } - - @Override - public NioTunnel getTunnel() { - return this; - } - - @Override - public void close() { - mIsClosed = true; - mPendingBuffers.clear(); - NetBareUtils.closeQuietly(mChannel); - } - - @Override - public void write(ByteBuffer buffer) throws IOException { - if (mIsClosed) { - return; - } - if (!buffer.hasRemaining()) { - return; - } - mPendingBuffers.offer(buffer); - interestWrite(); - } - - public int read(ByteBuffer buffer) throws IOException { - buffer.clear(); - int len = channelRead(buffer); - if (len> 0) { - buffer.flip(); - } - return len; - } - - public boolean isClosed() { - return mIsClosed; - } - - /* package */ void setNioCallback(NioCallback callback) { - this.mCallback = callback; - } - - /* package */ void prepareRead() throws IOException { - if (mChannel.isBlocking()) { - mChannel.configureBlocking(false); - } - mSelector.wakeup(); - mSelectionKey = mChannel.register(mSelector, SelectionKey.OP_READ, this); - } - - private void interestWrite() { - if (mSelectionKey != null) { - mSelector.wakeup(); - mSelectionKey.interestOps(SelectionKey.OP_WRITE); - } - } - - private void interestRead() { - if (mSelectionKey != null) { - mSelector.wakeup(); - mSelectionKey.interestOps(SelectionKey.OP_READ); - } - } - - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpProxyTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpProxyTunnel.java deleted file mode 100644 index 978f6dd..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpProxyTunnel.java +++ /dev/null @@ -1,75 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.tunnel; - -import com.github.megatronking.netbare.NetBareXLog; -import com.github.megatronking.netbare.ip.Protocol; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.nio.ByteBuffer; -import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; - -/** - * A TCP tunnel communicates with the VPN service. - * - * @author Megatron King - * @since 2018年11月21日 01:40 - */ -public class TcpProxyTunnel extends TcpTunnel { - - private NetBareXLog mLog; - - public TcpProxyTunnel(SocketChannel socketChannel, Selector selector, int remotePort) { - super(socketChannel, selector); - Socket socket = socketChannel.socket(); - this.mLog = new NetBareXLog(Protocol.TCP, socket.getInetAddress().getHostAddress(), - remotePort); - } - - @Override - public void connect(InetSocketAddress address) { - // Nothing to connect - } - - @Override - public void onConnected() throws IOException { - mLog.i("Proxy tunnel is connected."); - super.onConnected(); - } - - @Override - public int read(ByteBuffer buffer) throws IOException { - int len = super.read(buffer); - mLog.i("Read from proxy: " + len); - return len; - } - - @Override - public void write(ByteBuffer buffer) throws IOException { - mLog.i("Write to proxy: " + buffer.remaining()); - super.write(buffer); - } - - @Override - public void close() { - mLog.i("Proxy tunnel is closed."); - super.close(); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpRemoteTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpRemoteTunnel.java deleted file mode 100644 index 5eb660f..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpRemoteTunnel.java +++ /dev/null @@ -1,84 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.tunnel; - -import android.net.VpnService; - -import com.github.megatronking.netbare.NetBareXLog; -import com.github.megatronking.netbare.ip.Protocol; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; - -/** - * A TCP tunnel communicates with the remote server. - * - * @author Megatron King - * @since 2018年11月21日 01:41 - */ -public class TcpRemoteTunnel extends TcpTunnel { - - private final VpnService mVpnService; - - private NetBareXLog mLog; - - public TcpRemoteTunnel(VpnService vpnService, SocketChannel channel, Selector selector, - String remoteIp, int remotePort) { - super(channel, selector); - this.mVpnService = vpnService; - this.mLog = new NetBareXLog(Protocol.TCP, remoteIp, remotePort); - } - - @Override - public void connect(InetSocketAddress address) throws IOException { - if (mVpnService.protect(socket())) { - super.connect(address); - mLog.i("Connect to remote server %s", address); - } else { - throw new IOException("[TCP]Can not protect remote tunnel socket."); - } - } - - @Override - public void onConnected() throws IOException { - mLog.i("Remote tunnel is connected."); - super.onConnected(); - } - - @Override - public int read(ByteBuffer buffer) throws IOException { - int len = super.read(buffer); - mLog.i("Read from remote: " + len); - return len; - } - - @Override - public void write(ByteBuffer buffer) throws IOException { - mLog.i("Write to remote: " + buffer.remaining()); - super.write(buffer); - } - - @Override - public void close() { - mLog.i("Remote tunnel is closed."); - super.close(); - } - - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpTunnel.java deleted file mode 100644 index 84c38aa..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpTunnel.java +++ /dev/null @@ -1,80 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.tunnel; - -import com.github.megatronking.netbare.NetBareLog; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; - -/** - * A TCP protocol implementation based with {@link NioTunnel}. - * - * @author Megatron King - * @since 2018年11月21日 00:36 - */ -public abstract class TcpTunnel extends NioTunnel { - - private final SocketChannel mSocketChannel; - private final Selector mSelector; - - TcpTunnel(SocketChannel socketChannel, Selector selector) { - super(socketChannel, selector); - this.mSocketChannel = socketChannel; - this.mSelector = selector; - } - - @Override - public void connect(InetSocketAddress address) throws IOException { - NetBareLog.i("TCP connects to: %s:%s", - address.getAddress().getHostAddress(), address.getPort()); - if (mSocketChannel.isBlocking()) { - mSocketChannel.configureBlocking(false); - } - mSocketChannel.register(mSelector, SelectionKey.OP_CONNECT, this); - mSocketChannel.connect(address); - } - - @Override - public void onConnected() throws IOException { - if (mSocketChannel.finishConnect()) { - super.onConnected(); - } else { - throw new IOException("[TCP]The tunnel socket is not connected."); - } - } - - @Override - public Socket socket() { - return mSocketChannel.socket(); - } - - @Override - protected int channelRead(ByteBuffer buffer) throws IOException { - return mSocketChannel.read(buffer); - } - - @Override - protected int channelWrite(ByteBuffer buffer) throws IOException { - return mSocketChannel.write(buffer); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpVATunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpVATunnel.java deleted file mode 100644 index 92545f6..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpVATunnel.java +++ /dev/null @@ -1,163 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.tunnel; - -import com.github.megatronking.netbare.NetBareUtils; -import com.github.megatronking.netbare.NetBareVirtualGateway; -import com.github.megatronking.netbare.gateway.Request; -import com.github.megatronking.netbare.gateway.Response; -import com.github.megatronking.netbare.gateway.VirtualGateway; -import com.github.megatronking.netbare.net.Session; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; - -/** - * TCP protocol virtual gateway tunnel wraps {@link TcpProxyTunnel} and {@link TcpRemoteTunnel} as - * client and server. - * - * @author Megatron King - * @since 2018年11月18日 00:19 - */ -public class TcpVATunnel extends VirtualGatewayTunnel { - - private final NioTunnel mRemoteTunnel; - private final NioTunnel mProxyTunnel; - private VirtualGateway mGateway; - - private final int mMtu; - - public TcpVATunnel(Session session, NioTunnel proxyServerTunnel, NioTunnel remoteServerTunnel, int mtu) { - this.mProxyTunnel = proxyServerTunnel; - this.mRemoteTunnel = remoteServerTunnel; - this.mGateway = new NetBareVirtualGateway(session, new Request(mRemoteTunnel), - new Response(mProxyTunnel)); - - this.mMtu = mtu; - - setCallbacks(); - } - - @Override - public VirtualGateway getGateway() { - return mGateway; - } - - @Override - public void connect(InetSocketAddress address) throws IOException { - mRemoteTunnel.connect(address); - } - - private void setCallbacks() { - mProxyTunnel.setNioCallback(new NioCallback() { - @Override - public void onConnected() { - // Nothing to do. - } - - @Override - public void onRead() throws IOException { - if (mProxyTunnel.isClosed()) { - mGateway.sendResponseFinished(); - return; - } - ByteBuffer buffer = ByteBuffer.allocate(mMtu); - int len; - try { - len = mProxyTunnel.read(buffer); - } catch (IOException e) { - throw new ConnectionShutdownException(e.getMessage()); - } - if (len < 0 || mRemoteTunnel.isClosed()) { - NetBareUtils.closeQuietly(mProxyTunnel); - mGateway.sendResponseFinished(); - return; - } - mGateway.sendRequest(buffer); - } - - @Override - public void onWrite() { - // Do nothing - } - - @Override - public void onClosed() { - close(); - } - - @Override - public NioTunnel getTunnel() { - return null; - } - }); - mRemoteTunnel.setNioCallback(new NioCallback() { - @Override - public void onConnected() throws IOException { - // Prepare to read data. - mProxyTunnel.prepareRead(); - mRemoteTunnel.prepareRead(); - } - - @Override - public void onRead() throws IOException { - if (mRemoteTunnel.isClosed()) { - mGateway.sendRequestFinished(); - return; - } - ByteBuffer buffer = ByteBuffer.allocate(mMtu); - int len; - try { - len = mRemoteTunnel.read(buffer); - } catch (IOException e) { - throw new ConnectionShutdownException(e.getMessage()); - } - if (len < 0 || mProxyTunnel.isClosed()) { - NetBareUtils.closeQuietly(mRemoteTunnel); - mGateway.sendRequestFinished(); - return; - } - mGateway.sendResponse(buffer); - } - - @Override - public void onWrite() { - // Do nothing - } - - @Override - public void onClosed() { - close(); - } - - @Override - public NioTunnel getTunnel() { - return null; - } - - }); - } - - @Override - public void close() { - NetBareUtils.closeQuietly(mProxyTunnel); - NetBareUtils.closeQuietly(mRemoteTunnel); - mGateway.sendRequestFinished(); - mGateway.sendResponseFinished(); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/Tunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/Tunnel.java deleted file mode 100644 index cb6dd63..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/Tunnel.java +++ /dev/null @@ -1,37 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.tunnel; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * A tunnel connects a terminal that can be wrote to. - * - * @author Megatron King - * @since 2018年10月25日 17:57 - */ -public interface Tunnel { - - /** - * Write a packet buffer to the terminal. - * - * @param buffer A packet buffer. - * @throws IOException If an I/O error has occurred. - */ - void write(ByteBuffer buffer) throws IOException; - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpRemoteTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpRemoteTunnel.java deleted file mode 100644 index 47359ac..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpRemoteTunnel.java +++ /dev/null @@ -1,76 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.tunnel; - -import android.net.VpnService; - -import com.github.megatronking.netbare.NetBareXLog; -import com.github.megatronking.netbare.ip.Protocol; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.DatagramChannel; -import java.nio.channels.Selector; - -/** - * A UDP tunnel communicates with the remote server. - * - * @author Megatron King - * @since 2018年12月02日 19:49 - */ -public class UdpRemoteTunnel extends UdpTunnel { - - private final VpnService mVpnService; - private NetBareXLog mLog; - - public UdpRemoteTunnel(VpnService vpnService, DatagramChannel channel, Selector selector, - String remoteIp, short remotePort) { - super(channel, selector); - this.mVpnService = vpnService; - this.mLog = new NetBareXLog(Protocol.UDP, remoteIp, remotePort); - } - - @Override - public void connect(InetSocketAddress address) throws IOException { - if (mVpnService.protect(socket())) { - super.connect(address); - mLog.i("Connect to remote server %s", address); - } else { - throw new IOException("[UDP]Can not protect remote tunnel socket."); - } - } - - @Override - public int read(ByteBuffer buffer) throws IOException { - int len = super.read(buffer); - mLog.i("Read from remote: " + len); - return len; - } - - @Override - public void write(ByteBuffer buffer) throws IOException { - mLog.i("Write to remote: " + buffer.remaining()); - super.write(buffer); - } - - @Override - public void close() { - mLog.i("Remote tunnel is closed."); - super.close(); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpTunnel.java deleted file mode 100644 index 9d6fc81..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpTunnel.java +++ /dev/null @@ -1,68 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.tunnel; - -import com.github.megatronking.netbare.NetBareLog; - -import java.io.IOException; -import java.net.DatagramSocket; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.DatagramChannel; -import java.nio.channels.Selector; - -/** - * A UDP protocol implementation based with {@link NioTunnel}. - * - * @author Megatron King - * @since 2018年12月02日 19:24 - */ -public abstract class UdpTunnel extends NioTunnel { - - private final DatagramChannel mDatagramChannel; - - UdpTunnel(DatagramChannel datagramChannel, Selector selector) { - super(datagramChannel, selector); - this.mDatagramChannel = datagramChannel; - } - - @Override - public void connect(InetSocketAddress address) throws IOException { - NetBareLog.i("UDP connects to: %s:%s", - address.getAddress().getHostAddress(), address.getPort()); - if (mDatagramChannel.isBlocking()) { - mDatagramChannel.configureBlocking(false); - } - mDatagramChannel.connect(address); - prepareRead(); - } - - @Override - public DatagramSocket socket() { - return mDatagramChannel.socket(); - } - - @Override - protected int channelWrite(ByteBuffer buffer) throws IOException { - return mDatagramChannel.write(buffer); - } - - @Override - protected int channelRead(ByteBuffer buffer) throws IOException { - return mDatagramChannel.read(buffer); - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpVATunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpVATunnel.java deleted file mode 100644 index 05b5f67..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpVATunnel.java +++ /dev/null @@ -1,197 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -/* - * Copyright (C) 2013 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.github.megatronking.netbare.tunnel; - -import com.github.megatronking.netbare.NetBareLog; -import com.github.megatronking.netbare.NetBareUtils; -import com.github.megatronking.netbare.NetBareVirtualGateway; -import com.github.megatronking.netbare.gateway.Request; -import com.github.megatronking.netbare.gateway.Response; -import com.github.megatronking.netbare.gateway.VirtualGateway; -import com.github.megatronking.netbare.ip.IpHeader; -import com.github.megatronking.netbare.ip.UdpHeader; -import com.github.megatronking.netbare.net.Session; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; - -/** - * UDP protocol virtual gateway tunnel wraps {@link UdpRemoteTunnel} and itself as client and - * server. - * - * @author Megatron King - * @since 2018年11月25日 20:16 - */ -public class UdpVATunnel extends VirtualGatewayTunnel implements NioCallback, - Tunnel { - - private final NioTunnel mRemoteTunnel; - private final OutputStream mOutput; - - private final int mMtu; - - private Session mSession; - private VirtualGateway mGateway; - - private UdpHeader mTemplateHeader; - - public UdpVATunnel(Session session, NioTunnel tunnel, OutputStream output, int mtu) { - this.mRemoteTunnel = tunnel; - this.mOutput = output; - this.mMtu = mtu; - - this.mSession = session; - this.mGateway = new NetBareVirtualGateway(session, - new Request(mRemoteTunnel), new Response(this)); - - this.mRemoteTunnel.setNioCallback(this); - } - - @Override - public void connect(InetSocketAddress address) throws IOException { - mRemoteTunnel.connect(address); - } - - @Override - public VirtualGateway getGateway() { - return mGateway; - } - - @Override - public void onConnected() { - } - - @Override - public void onRead() throws IOException { - if (mRemoteTunnel.isClosed()) { - mGateway.sendRequestFinished(); - mGateway.sendResponseFinished(); - return; - } - ByteBuffer buffer = ByteBuffer.allocate(mMtu); - int len; - try { - len = mRemoteTunnel.read(buffer); - } catch (IOException e) { - throw new ConnectionShutdownException(e.getMessage()); - } - if (len < 0) { - close(); - return; - } - mGateway.sendResponse(buffer); - } - - @Override - public void onWrite() { - } - - @Override - public void onClosed() { - close(); - } - - @Override - public NioTunnel getTunnel() { - return null; - } - - @Override - public void close() { - NetBareUtils.closeQuietly(mRemoteTunnel); - mGateway.sendRequestFinished(); - mGateway.sendResponseFinished(); - } - - public void send(UdpHeader header) { - if (mRemoteTunnel.isClosed()) { - return; - } - // Clone a template by the send data. - if (mTemplateHeader == null) { - mTemplateHeader = createTemplate(header); - } - - try { - mGateway.sendRequest(header.data()); - } catch (IOException e) { - NetBareLog.e(e.getMessage()); - close(); - } - } - - @Override - public void write(ByteBuffer buffer) throws IOException { - // Write to vpn. - UdpHeader header = mTemplateHeader.copy(); - ByteBuffer headerBuffer = header.buffer(); - int headLength = header.getIpHeader().getHeaderLength() + header.getHeaderLength(); - byte[] packet = new byte[headLength + buffer.remaining()]; - headerBuffer.get(packet, 0, headLength); - buffer.get(packet, headLength, packet.length - headLength); - - IpHeader ipHeader = new IpHeader(packet, 0); - ipHeader.setTotalLength((short) packet.length); - - UdpHeader udpHeader = new UdpHeader(ipHeader, packet, ipHeader.getHeaderLength()); - udpHeader.setTotalLength((short) (packet.length - ipHeader.getHeaderLength())); - - ipHeader.updateChecksum(); - udpHeader.updateChecksum(); - - mOutput.write(packet, 0, packet.length); - - mSession.receiveDataSize += packet.length; - } - - public NioTunnel getRemoteChannel() { - return mRemoteTunnel; - } - - private UdpHeader createTemplate(UdpHeader header) { - UdpHeader templateUdp = header.copy(); - IpHeader templateIp = templateUdp.getIpHeader(); - // Swap ip - int sourceIp = templateIp.getSourceIp(); - int destinationIp = templateIp.getDestinationIp(); - templateIp.setSourceIp(destinationIp); - templateIp.setDestinationIp(sourceIp); - // Swap port - short sourcePort = templateUdp.getSourcePort(); - short destinationPort = templateUdp.getDestinationPort(); - templateUdp.setDestinationPort(sourcePort); - templateUdp.setSourcePort(destinationPort); - return templateUdp; - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/VirtualGatewayTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/VirtualGatewayTunnel.java deleted file mode 100644 index 07db766..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/VirtualGatewayTunnel.java +++ /dev/null @@ -1,47 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.tunnel; - -import com.github.megatronking.netbare.gateway.VirtualGateway; - -import java.io.Closeable; -import java.io.IOException; -import java.net.InetSocketAddress; - -/** - * A tunnel uses {@link VirtualGateway} to intercept and filter net packets. - * - * @author Megatron King - * @since 2018年11月21日 09:00 - */ -public abstract class VirtualGatewayTunnel implements Closeable { - - /** - * Connects to the remote server by the given server address. - * - * @param address The server IP socket address. - * @throws IOException If an I/O error has occurred. - */ - public abstract void connect(InetSocketAddress address) throws IOException; - - /** - * Returns the {@link VirtualGateway} this tunnel created. - * - * @return The {@link VirtualGateway} instance. - */ - public abstract VirtualGateway getGateway(); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketCallback.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketCallback.java deleted file mode 100644 index 3b76f4b..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketCallback.java +++ /dev/null @@ -1,66 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.ws; - -import java.io.IOException; - -/** - * A callback to be invoked when the message is decoded. - * - * @author Megatron King - * @since 2019年1月18日 23:56 - */ -public interface WebSocketCallback { - - /** - * Invoked when a text message is decoded. - * - * @param text A text content. - * @throws IOException If an I/O error has occurred. - */ - void onReadMessage(String text) throws IOException; - - /** - * Invoked when a binary message is decoded. - * - * @param binary A binary content. - * @throws IOException If an I/O error has occurred. - */ - void onReadMessage(byte[] binary) throws IOException; - - /** - * Invoked when a ping message is decoded. - * - * @param ping The ping message content. - */ - void onReadPing(byte[] ping); - - /** - * Invoked when a pong message is decoded. - * - * @param pong The pong message content. - */ - void onReadPong(byte[] pong); - - /** - * Invoked when the control frame is closed. - * - * @param code Status code. - * @param reason Close reason. - */ - void onReadClose(int code, String reason); - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketProtocol.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketProtocol.java deleted file mode 100644 index aaf7773..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketProtocol.java +++ /dev/null @@ -1,145 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -/* - * Copyright (C) 2014 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.github.megatronking.netbare.ws; - -/** - * Protocol see: http://tools.ietf.org/html/rfc6455 - * - * @author Megatron King - * @since 2019年1月18日 23:27 - */ -/* package */ final class WebSocketProtocol { - - /** - * Byte 0 flag for whether this is the final fragment in a message. - */ - static final int B0_FLAG_FIN = 0b10000000; - - /** - * Byte 0 reserved flag 1. Must be 0 unless negotiated otherwise. - */ - static final int B0_FLAG_RSV1 = 0b01000000; - - /** - * Byte 0 reserved flag 2. Must be 0 unless negotiated otherwise. - */ - static final int B0_FLAG_RSV2 = 0b00100000; - - /** - * Byte 0 reserved flag 3. Must be 0 unless negotiated otherwise. - */ - static final int B0_FLAG_RSV3 = 0b00010000; - - /** - * Byte 0 mask for the frame opcode. - */ - static final int B0_MASK_OPCODE = 0b00001111; - - /** - * Flag in the opcode which indicates a control frame. - */ - static final int OPCODE_FLAG_CONTROL = 0b00001000; - - /** - * Byte 1 flag for whether the payload data is masked.

                If this flag is set, the next four - * bytes represent the mask key. These bytes appear after any additional bytes specified by {@link - * #B1_MASK_LENGTH}. - */ - static final int B1_FLAG_MASK = 0b10000000; - /** - * Byte 1 mask for the payload length.

                If this value is {@link #PAYLOAD_SHORT}, the next two - * bytes represent the length. If this value is {@link #PAYLOAD_LONG}, the next eight bytes - * represent the length. - */ - static final int B1_MASK_LENGTH = 0b01111111; - - static final int OPCODE_CONTINUATION = 0x0; - static final int OPCODE_TEXT = 0x1; - static final int OPCODE_BINARY = 0x2; - - static final int OPCODE_CONTROL_CLOSE = 0x8; - static final int OPCODE_CONTROL_PING = 0x9; - static final int OPCODE_CONTROL_PONG = 0xa; - - /** - * Maximum length of frame payload. Larger payloads, if supported by the frame type, can use the - * special values {@link #PAYLOAD_SHORT} or {@link #PAYLOAD_LONG}. - */ - static final long PAYLOAD_BYTE_MAX = 125L; - - /** - * Maximum length of close message in bytes. - */ - static final long CLOSE_MESSAGE_MAX = PAYLOAD_BYTE_MAX - 2; - - /** - * Value for {@link #B1_MASK_LENGTH} which indicates the next two bytes are the unsigned length. - */ - static final int PAYLOAD_SHORT = 126; - - /** - * Maximum length of a frame payload to be denoted as {@link #PAYLOAD_SHORT}. - */ - static final long PAYLOAD_SHORT_MAX = 0xffffL; - - /** - * Value for {@link #B1_MASK_LENGTH} which indicates the next eight bytes are the unsigned - * length. - */ - static final int PAYLOAD_LONG = 127; - - /** - * Used when an unchecked exception was thrown in a listener. - */ - static final int CLOSE_CLIENT_GOING_AWAY = 1001; - - /** - * Used when an empty close frame was received (i.e., without a status code). - */ - static final int CLOSE_NO_STATUS_CODE = 1005; - - static String closeCodeExceptionMessage(int code) { - if (code < 1000 || code>= 5000) { - return "Code must be in range [1000,5000): " + code; - } else if ((code>= 1004 && code <= 1006) || (code>= 1012 && code <= 2999)) { - return "Code " + code + " is reserved and may not be used."; - } else { - return null; - } - } - - static void toggleMask(byte[] data, byte[] key) { - for (int i = 0; i < data.length; i++) { - data[i] = (byte) (data[i] ^ key[i % key.length]); - } - } - -} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketReader.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketReader.java deleted file mode 100644 index fa85971..0000000 --- a/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketReader.java +++ /dev/null @@ -1,313 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -/* - * Copyright (C) 2014 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.github.megatronking.netbare.ws; - -import com.github.megatronking.netbare.NetBareUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.net.ProtocolException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -import static java.lang.Integer.toHexString; - -/** - * A web socket frame reader. - * - * @author Megatron King - * @since 2019年1月18日 23:52 - */ -public class WebSocketReader { - - private final InputStream mInput; - private final boolean mClient; - private final WebSocketCallback mCallback; - - private final byte[] mMaskKey; - - private boolean mClosed; - - private int mOpcode; - private long mFrameLength; - private boolean mFinalFrame; - private boolean mControlFrame; - - private List mMessageSegments; - - public WebSocketReader(InputStream input, boolean client, WebSocketCallback callback) { - this.mInput = input; - this.mClient = client; - this.mCallback = callback; - - // Masks are only a concern for server writers. - this.mMaskKey = client ? null : new byte[4]; - - this.mMessageSegments = new ArrayList(1); - } - - /** - * Process the next protocol frame. - */ - public void processNextFrame() throws IOException { - readHeader(); - if (mControlFrame) { - readControlFrame(); - } else { - readMessageFrame(); - } - } - - /** - * Close the input stream. - */ - public void close() { - mClosed = true; - NetBareUtils.closeQuietly(mInput); - } - - private void readHeader() throws IOException { - if (mClosed) { - throw new IOException("The stream is closed."); - } - - // Each frame starts with two bytes of data. - // - // 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 - // +-+-+-+-+-------+ +-+-------------+ - // |F|R|R|R| OP | |M| LENGTH | - // |I|S|S|S| CODE | |A| | - // |N|V|V|V| | |S| | - // | |1|2|3| | |K| | - // +-+-+-+-+-------+ +-+-------------+ - - // Read first byte - int b0 = mInput.read() & 0xff; - - mOpcode = b0 & WebSocketProtocol.B0_MASK_OPCODE; - mFinalFrame = (b0 & WebSocketProtocol.B0_FLAG_FIN) != 0; - mControlFrame = (b0 & WebSocketProtocol.OPCODE_FLAG_CONTROL) != 0; - - // Control frames must be final frames (cannot contain continuations). - if (mControlFrame && !mFinalFrame) { - throw new ProtocolException("Control frames must be final."); - } - - boolean reservedFlag1 = (b0 & WebSocketProtocol.B0_FLAG_RSV1) != 0; - boolean reservedFlag2 = (b0 & WebSocketProtocol.B0_FLAG_RSV2) != 0; - boolean reservedFlag3 = (b0 & WebSocketProtocol.B0_FLAG_RSV3) != 0; - if (reservedFlag1 || reservedFlag2 || reservedFlag3) { - // Reserved flags are for extensions which we currently do not support. - throw new ProtocolException("Reserved flags are unsupported."); - } - - int b1 = mInput.read() & 0xff; - - boolean isMasked = (b1 & WebSocketProtocol.B1_FLAG_MASK) != 0; - if (isMasked == mClient) { - // Masked payloads must be read on the server. Unmasked payloads must be read on the client. - throw new ProtocolException(mClient - ? "Server-sent frames must not be masked." - : "Client-sent frames must be masked."); - } - - // Get frame length, optionally reading from follow-up bytes if indicated by special values. - mFrameLength = b1 & WebSocketProtocol.B1_MASK_LENGTH; - if (mFrameLength == WebSocketProtocol.PAYLOAD_SHORT) { - mFrameLength = readShort() & 0xffffL; // Value is unsigned. - } else if (mFrameLength == WebSocketProtocol.PAYLOAD_LONG) { - mFrameLength = readLong(); - if (mFrameLength < 0) { - throw new ProtocolException( - "Frame length 0x" + Long.toHexString(mFrameLength) + "> 0x7FFFFFFFFFFFFFFF"); - } - } - - if (mControlFrame && mFrameLength> WebSocketProtocol.PAYLOAD_BYTE_MAX) { - throw new ProtocolException("Control frame must be less than " + - WebSocketProtocol.PAYLOAD_BYTE_MAX + "B."); - } - - if (isMasked) { - // Read the masking key as bytes so that they can be used directly for unmasking. - readFully(mMaskKey); - } - - } - - private void readControlFrame() throws IOException { - if (mFrameLength>= Integer.MAX_VALUE) { - throw new IOException("Not support a frame length> " + Integer.MAX_VALUE); - } - WebSocketCallback callback = mCallback; - if (callback == null) { - return; - } - ByteBuffer byteBuffer; - if (mFrameLength> 0) { - byte[] frame = new byte[(int) mFrameLength]; - readFully(frame); - if (!mClient) { - WebSocketProtocol.toggleMask(frame, mMaskKey); - } - byteBuffer = ByteBuffer.wrap(frame); - } else { - byteBuffer = ByteBuffer.allocate(0); - } - switch (mOpcode) { - case WebSocketProtocol.OPCODE_CONTROL_PING: - callback.onReadPing(readFully(byteBuffer)); - break; - case WebSocketProtocol.OPCODE_CONTROL_PONG: - callback.onReadPong(readFully(byteBuffer)); - break; - case WebSocketProtocol.OPCODE_CONTROL_CLOSE: - int code = WebSocketProtocol.CLOSE_NO_STATUS_CODE; - String reason = ""; - long bufferSize = byteBuffer.remaining(); - if (bufferSize == 1) { - throw new ProtocolException("Malformed close payload length of 1."); - } else if (bufferSize != 0) { - code = byteBuffer.getShort(); - reason = new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.remaining()); - String codeExceptionMessage = WebSocketProtocol.closeCodeExceptionMessage(code); - if (codeExceptionMessage != null) { - throw new ProtocolException(codeExceptionMessage); - } - } - callback.onReadClose(code, reason); - break; - default: - throw new ProtocolException("Unknown control opcode: " + toHexString(mOpcode)); - } - } - - private void readMessageFrame() throws IOException { - int opcode = this.mOpcode; - if (opcode != WebSocketProtocol.OPCODE_TEXT && opcode != WebSocketProtocol.OPCODE_BINARY) { - throw new ProtocolException("Unknown opcode: " + toHexString(opcode)); - } - - readMessage(); - - WebSocketCallback callback = mCallback; - if (callback == null) { - return; - } - if (mMessageSegments.isEmpty()) { - throw new ProtocolException("Message frame segment is empty!"); - } - int total = 0; - for (byte[] segment : mMessageSegments) { - total+= segment.length; - } - ByteBuffer byteBuffer = ByteBuffer.allocate(total); - for (byte[] segment : mMessageSegments) { - byteBuffer.put(segment); - } - byteBuffer.flip(); - mMessageSegments.clear(); - if (opcode == WebSocketProtocol.OPCODE_TEXT) { - callback.onReadMessage(new String(byteBuffer.array())); - } else { - callback.onReadMessage(byteBuffer.array()); - } - } - - private void readMessage() throws IOException { - while (true) { - if (mClosed) { - throw new IOException("The stream is closed."); - } - if (mFrameLength <= 0) { - return; - } - if (mFrameLength>= Integer.MAX_VALUE) { - throw new IOException("Not support a frame length> " + Integer.MAX_VALUE); - } - byte[] frame = new byte[(int) mFrameLength]; - readFully(frame); - if (!mClient) { - WebSocketProtocol.toggleMask(frame, mMaskKey); - } - mMessageSegments.add(frame); - - if (mFinalFrame) { - break; // We are exhausted and have no continuations. - } - - readUntilNonControlFrame(); - if (mOpcode != WebSocketProtocol.OPCODE_CONTINUATION) { - throw new ProtocolException("Expected continuation opcode. Got: " + toHexString(mOpcode)); - } - } - } - - private void readUntilNonControlFrame() throws IOException { - while (!mClosed) { - readHeader(); - if (!mControlFrame) { - break; - } - readControlFrame(); - } - } - - private short readShort() throws IOException { - return (short) ((mInput.read() & 0xFF) << 8 - | (mInput.read() & 0xFF)); - } - - private long readLong() throws IOException { - return (mInput.read() & 0xFFL) << 56 - | (mInput.read() & 0xFFL) << 48 - | (mInput.read() & 0xFFL) << 40 - | (mInput.read() & 0xFFL) << 32 - | (mInput.read() & 0xFFL) << 24 - | (mInput.read() & 0xFFL) << 16 - | (mInput.read() & 0xFFL) << 8 - | (mInput.read() & 0xFFL); - } - - private void readFully(byte[] bytes) throws IOException { - for (int i = 0; i < bytes.length; i++) { - bytes[i] = (byte) mInput.read(); - } - } - - private byte[] readFully(ByteBuffer byteBuffer) { - byte[] data = new byte[byteBuffer.remaining()]; - byteBuffer.get(data); - return data; - } - -} diff --git a/netbare-core/src/main/res/values/styles.xml b/netbare-core/src/main/res/values/styles.xml deleted file mode 100644 index fdd919c..0000000 --- a/netbare-core/src/main/res/values/styles.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - \ No newline at end of file diff --git a/netbare-injector/.gitignore b/netbare-injector/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/netbare-injector/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/netbare-injector/build.gradle b/netbare-injector/build.gradle deleted file mode 100644 index 3b52679..0000000 --- a/netbare-injector/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 21 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - -} - -dependencies { - implementation project(':netbare-core') - implementation 'com.android.support:appcompat-v7:28.0.0' -} \ No newline at end of file diff --git a/netbare-injector/src/main/AndroidManifest.xml b/netbare-injector/src/main/AndroidManifest.xml deleted file mode 100644 index 4ad7016..0000000 --- a/netbare-injector/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/Cookie.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/Cookie.java deleted file mode 100644 index af59cae..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/Cookie.java +++ /dev/null @@ -1,581 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.text.TextUtils; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.Locale; -import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * An RFC 6265 Cookie. - * - * @author Megatron King - * @since 2018年12月25日 21:56 - */ -public final class Cookie { - - private static final Pattern YEAR_PATTERN - = Pattern.compile("(\\d{2,4})[^\\d]*"); - private static final Pattern MONTH_PATTERN - = Pattern.compile("(?i)(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec).*"); - private static final Pattern DAY_OF_MONTH_PATTERN - = Pattern.compile("(\\d{1,2})[^\\d]*"); - private static final Pattern TIME_PATTERN - = Pattern.compile("(\\d{1,2}):(\\d{1,2}):(\\d{1,2})[^\\d]*"); - - private static final TimeZone UTC = TimeZone.getTimeZone("GMT"); - - private static final long MAX_DATE = 253402300799999L; - - /** - * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such - * cookies are on the fast path. - */ - private static final ThreadLocal STANDARD_DATE_FORMAT = - new ThreadLocal() { - @Override - protected DateFormat initialValue() { - // Date format specified by RFC 7231 section 7.1.1.1. - DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US); - rfc1123.setLenient(false); - rfc1123.setTimeZone(UTC); - return rfc1123; - } - }; - - public final String name; - public final String value; - public final long expiresAt; - public final String domain; - public final String path; - public final String priority; - public final boolean secure; - public final boolean httpOnly; - - /** - * True if 'expires' or 'max-age' is present. - */ - public final boolean persistent; - - /** - * True unless 'domain' is present. - */ - public final boolean hostOnly; - - private Cookie(String name, String value, long expiresAt, String domain, String path, - boolean secure, boolean httpOnly, boolean hostOnly, boolean persistent, - String priority) { - this.name = name; - this.value = value; - this.expiresAt = expiresAt; - this.domain = domain; - this.path = path; - this.secure = secure; - this.httpOnly = httpOnly; - this.persistent = persistent; - this.hostOnly = hostOnly; - this.priority = priority; - } - - private Cookie(Builder builder) { - this.name = builder.name; - this.value = builder.value; - this.expiresAt = builder.expiresAt; - this.domain = builder.domain; - this.path = builder.path; - this.priority = builder.priority; - this.secure = builder.secure; - this.httpOnly = builder.httpOnly; - this.persistent = builder.persistent; - this.hostOnly = builder.hostOnly; - } - - public String expiresAt() { - if (!persistent || expiresAt == Long.MIN_VALUE) { - // A session date. - return "1969-12-31T23:59:59.000Z"; - } - DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); - format.setLenient(false); - format.setTimeZone(UTC); - return format.format(new Date(expiresAt)); - } - - @Override - public String toString() { - StringBuilder result = new StringBuilder(); - result.append(name); - result.append('='); - result.append(value); - if (persistent) { - if (expiresAt == Long.MIN_VALUE) { - result.append("; max-age=0"); - } else { - result.append("; expires=").append(STANDARD_DATE_FORMAT.get().format(new Date(expiresAt))); - } - } - if (!hostOnly) { - result.append("; domain="); - result.append(domain); - } - result.append("; path=").append(path); - - if (secure) { - result.append("; secure"); - } - if (httpOnly) { - result.append("; httponly"); - } - if (priority != null) { - result.append("; Priority=").append(priority); - } - return result.toString(); - } - - @Override - public boolean equals(@Nullable Object other) { - if (!(other instanceof Cookie)) { - return false; - } - Cookie that = (Cookie) other; - return that.name.equals(name) - && that.value.equals(value) - && that.domain.equals(domain) - && that.path.equals(path) - && that.expiresAt == expiresAt - && that.secure == secure - && that.httpOnly == httpOnly - && that.persistent == persistent - && that.hostOnly == hostOnly - && TextUtils.equals(that.priority, priority); - } - - @Override - public int hashCode() { - int hash = 17; - hash = 31 * hash + name.hashCode(); - hash = 31 * hash + value.hashCode(); - hash = 31 * hash + domain.hashCode(); - hash = 31 * hash + path.hashCode(); - if (priority != null) { - hash = 31 * hash + priority.hashCode(); - } - hash = 31 * hash + (int) (expiresAt ^ (expiresAt>>> 32)); - hash = 31 * hash + (secure ? 0 : 1); - hash = 31 * hash + (httpOnly ? 0 : 1); - hash = 31 * hash + (persistent ? 0 : 1); - hash = 31 * hash + (hostOnly ? 0 : 1); - return hash; - } - - /** - * Builds a cookie. The {@linkplain #name(String)} }, {@linkplain #value(String)}, - * and {@linkplain #domain(String)} values must all be set before calling {@link #build}. - */ - public static final class Builder { - - private String name; - private String value; - private long expiresAt = MAX_DATE; - private String domain; - private String path = "/"; - private boolean secure; - private boolean httpOnly; - private boolean persistent; - private boolean hostOnly; - private String priority; - - /** - * Set the cookie name, must not null. - * - * @param name Cookie name. - * @return The same builder. - */ - public Builder name(@NonNull String name) { - this.name = name; - return this; - } - - /** - * Set the cookie value, must not null. - * - * @param value Cookie value. - * @return The same builder. - */ - public Builder value(@NonNull String value) { - this.value = value; - return this; - } - - /** - * Set the cookie expires time. - * - * @param expiresAt When the cookie expired. - * @return The same builder. - */ - public Builder expiresAt(long expiresAt) { - if (expiresAt <= 0) { - expiresAt = Long.MIN_VALUE; - } - if (expiresAt> MAX_DATE) { - expiresAt = MAX_DATE; - } - this.expiresAt = expiresAt; - this.persistent = true; - return this; - } - - /** - * Set the domain pattern for this cookie. The cookie will match {@code domain} and all of - * its subdomains. - * - * @param domain The domain pattern. - * @return The same builder. - */ - public Builder domain(String domain) { - return domain(domain, false); - } - - /** - * Set the host-only domain for this cookie. The cookie will match {@code domain} but none - * of its subdomains. - * - * @param domain The host-only domain pattern. - * @return The same builder. - */ - public Builder hostOnlyDomain(String domain) { - return domain(domain, true); - } - - private Builder domain(String domain, boolean hostOnly) { - this.domain = domain; - this.hostOnly = hostOnly; - return this; - } - - /** - * Set the path this cookie, must start with '/'. - * - * @param path The path, must start with '/'. - * @return The same builder. - */ - public Builder path(String path) { - this.path = path; - return this; - } - - /** - * Set the cookie is secure. - * - * @return The same builder. - */ - public Builder secure() { - this.secure = true; - return this; - } - - /** - * Set the cookie is http only. - * - * @return The same builder. - */ - public Builder httpOnly() { - this.httpOnly = true; - return this; - } - - /** - * Set the priority for this cookie, it is a chromium extension. - * - * @param priority The priority, such as HIGH. - * @return The same builder. - */ - public Builder priority(String priority) { - this.priority = priority; - return this; - } - - /** - * Build a new cookie instance. - * - * @return A Cookie. - */ - public Cookie build() { - return new Cookie(this); - } - - } - - @Nullable - public static Cookie parseSetCookie(String host, String setCookie) { - int pos = 0; - int limit = setCookie.length(); - int cookiePairEnd = delimiterOffset(setCookie, pos, limit, ';'); - - int pairEqualsSign = delimiterOffset(setCookie, pos, cookiePairEnd, '='); - if (pairEqualsSign == cookiePairEnd) { - return null; - } - - String cookieName = trimSubstring(setCookie, pos, pairEqualsSign); - if (cookieName.isEmpty() || indexOfControlOrNonAscii(cookieName) != -1) { - return null; - } - - String cookieValue = trimSubstring(setCookie, pairEqualsSign + 1, cookiePairEnd); - if (indexOfControlOrNonAscii(cookieValue) != -1) { - return null; - } - - long expiresAt = MAX_DATE; - String domain = null; - String path = null; - long deltaSeconds = -1L; - boolean secureOnly = false; - boolean httpOnly = false; - boolean hostOnly = true; - boolean persistent = false; - String priority = null; - - pos = cookiePairEnd + 1; - while (pos < limit) { - int attributePairEnd = delimiterOffset(setCookie, pos, limit, ';'); - - int attributeEqualsSign = delimiterOffset(setCookie, pos, attributePairEnd, '='); - String attributeName = trimSubstring(setCookie, pos, attributeEqualsSign); - String attributeValue = attributeEqualsSign < attributePairEnd - ? trimSubstring(setCookie, attributeEqualsSign + 1, attributePairEnd) - : ""; - - if (attributeName.equalsIgnoreCase("expires")) { - try { - expiresAt = parseExpires(attributeValue, attributeValue.length()); - persistent = true; - } catch (IllegalArgumentException e) { - // Ignore this attribute, it isn't recognizable as a date. - } - } else if (attributeName.equalsIgnoreCase("max-age")) { - try { - deltaSeconds = parseMaxAge(attributeValue); - persistent = true; - } catch (NumberFormatException e) { - // Ignore this attribute, it isn't recognizable as a max age. - } - } else if (attributeName.equalsIgnoreCase("domain")) { - domain = attributeValue; - } else if (attributeName.equalsIgnoreCase("path")) { - path = attributeValue; - } else if (attributeName.equalsIgnoreCase("secure")) { - secureOnly = true; - } else if (attributeName.equalsIgnoreCase("httponly")) { - httpOnly = true; - } else if (attributeName.equalsIgnoreCase("priority")) { - priority = attributeValue; - } - - pos = attributePairEnd + 1; - } - - if (TextUtils.isEmpty(domain)) { - domain = host; - } - - // If 'Max-Age' is present, it takes precedence over 'Expires', regardless of the order the two - // attributes are declared in the cookie string. - if (deltaSeconds == Long.MIN_VALUE) { - expiresAt = Long.MIN_VALUE; - } else if (deltaSeconds != -1L) { - long deltaMilliseconds = deltaSeconds <= (Long.MAX_VALUE / 1000) ? deltaSeconds * 1000 - : Long.MAX_VALUE; - long currentTimeMillis = System.currentTimeMillis(); - expiresAt = currentTimeMillis + deltaMilliseconds; - if (expiresAt> MAX_DATE) { - // Handle overflow. - expiresAt = MAX_DATE; - } - } - return new Cookie(cookieName, cookieValue, expiresAt, domain, path, secureOnly, httpOnly, - hostOnly, persistent, priority); - } - - private static int delimiterOffset(String input, int pos, int limit, char delimiter) { - for (int i = pos; i < limit; i++) { - if (input.charAt(i) == delimiter) { - return i; - } - } - return limit; - } - - private static String trimSubstring(String string, int pos, int limit) { - int start = skipLeadingAsciiWhitespace(string, pos, limit); - int end = skipTrailingAsciiWhitespace(string, start, limit); - return string.substring(start, end); - } - - private static int skipLeadingAsciiWhitespace(String input, int pos, int limit) { - for (int i = pos; i < limit; i++) { - switch (input.charAt(i)) { - case '\t': - case '\n': - case '\f': - case '\r': - case ' ': - continue; - default: - return i; - } - } - return limit; - } - - private static int skipTrailingAsciiWhitespace(String input, int pos, int limit) { - for (int i = limit - 1; i>= pos; i--) { - switch (input.charAt(i)) { - case '\t': - case '\n': - case '\f': - case '\r': - case ' ': - continue; - default: - return i + 1; - } - } - return pos; - } - - private static int indexOfControlOrNonAscii(String input) { - for (int i = 0, length = input.length(); i < length; i++) { - char c = input.charAt(i); - if (c <= '\u001f' || c>= '\u007f') { - return i; - } - } - return -1; - } - - private static long parseExpires(String s, int limit) { - int pos = dateCharacterOffset(s, 0, limit, false); - - int hour = -1; - int minute = -1; - int second = -1; - int dayOfMonth = -1; - int month = -1; - int year = -1; - Matcher matcher = TIME_PATTERN.matcher(s); - - while (pos < limit) { - int end = dateCharacterOffset(s, pos + 1, limit, true); - matcher.region(pos, end); - - if (hour == -1 && matcher.usePattern(TIME_PATTERN).matches()) { - hour = Integer.parseInt(matcher.group(1)); - minute = Integer.parseInt(matcher.group(2)); - second = Integer.parseInt(matcher.group(3)); - } else if (dayOfMonth == -1 && matcher.usePattern(DAY_OF_MONTH_PATTERN).matches()) { - dayOfMonth = Integer.parseInt(matcher.group(1)); - } else if (month == -1 && matcher.usePattern(MONTH_PATTERN).matches()) { - String monthString = matcher.group(1).toLowerCase(Locale.US); - month = MONTH_PATTERN.pattern().indexOf(monthString) / 4; // Sneaky! jan=1, dec=12. - } else if (year == -1 && matcher.usePattern(YEAR_PATTERN).matches()) { - year = Integer.parseInt(matcher.group(1)); - } - - pos = dateCharacterOffset(s, end + 1, limit, false); - } - - // Convert two-digit years into four-digit years. 99 becomes 1999, 15 becomes 2015. - if (year>= 70 && year <= 99) { - year += 1900; - } - if (year>= 0 && year <= 69) { - year += 2000; - } - - // If any partial is omitted or out of range, return -1. The date is impossible. Note that leap - // seconds are not supported by this syntax. - if (year < 1601) { - throw new IllegalArgumentException(); - } - if (month == -1) { - throw new IllegalArgumentException(); - } - if (dayOfMonth < 1 || dayOfMonth> 31) { - throw new IllegalArgumentException(); - } - if (hour < 0 || hour> 23) { - throw new IllegalArgumentException(); - } - if (minute < 0 || minute> 59) { - throw new IllegalArgumentException(); - } - if (second < 0 || second> 59) { - throw new IllegalArgumentException(); - } - - Calendar calendar = new GregorianCalendar(UTC); - calendar.setLenient(false); - calendar.set(Calendar.YEAR, year); - calendar.set(Calendar.MONTH, month - 1); - calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); - calendar.set(Calendar.HOUR_OF_DAY, hour); - calendar.set(Calendar.MINUTE, minute); - calendar.set(Calendar.SECOND, second); - calendar.set(Calendar.MILLISECOND, 0); - return calendar.getTimeInMillis(); - } - - private static int dateCharacterOffset(String input, int pos, int limit, boolean invert) { - for (int i = pos; i < limit; i++) { - int c = input.charAt(i); - boolean dateCharacter = (c < ' ' && c != '\t') || (c>= '\u007f') - || (c>= '0' && c <= '9') - || (c>= 'a' && c <= 'z') - || (c>= 'A' && c <= 'Z') - || (c == ':'); - if (dateCharacter == !invert) { - return i; - } - } - return limit; - } - - private static long parseMaxAge(String s) { - try { - long parsed = Long.parseLong(s); - return parsed <= 0L ? Long.MIN_VALUE : parsed; - } catch (NumberFormatException e) { - // Check if the value is an integer (positive or negative) that's too big for a long. - if (s.matches("-?\\d+")) { - return s.startsWith("-") ? Long.MIN_VALUE : Long.MAX_VALUE; - } - throw e; - } - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpBody.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpBody.java deleted file mode 100644 index e4a9472..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpBody.java +++ /dev/null @@ -1,27 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import com.github.megatronking.netbare.stream.Stream; - -/** - * An abstract HTTP body type object, it declares that this object represents the HTTP body. - * - * @author Megatron King - * @since 2018年12月15日 21:09 - */ -public abstract class HttpBody implements Stream { -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpHeaderPart.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpHeaderPart.java deleted file mode 100644 index 7be08c0..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpHeaderPart.java +++ /dev/null @@ -1,204 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import android.net.Uri; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.github.megatronking.netbare.utils.CaseInsensitiveLinkedMap; -import com.github.megatronking.netbare.stream.Stream; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * A base class of http header part, it contains the request/status line and headers. - * - * @author Megatron King - * @since 2018年12月11日 23:30 - */ -/* package */ abstract class HttpHeaderPart implements Stream { - - private final HttpProtocol protocol; - private final Uri uri; - private final Map> headers; - - HttpHeaderPart(@NonNull HttpProtocol protocol, @NonNull Uri uri, - @NonNull Map> headers) { - this.protocol = protocol; - this.uri = uri; - this.headers = headers; - } - - /** - * Get the http request uri. - * - * @return A {@link Uri} object. - */ - @NonNull - public Uri uri() { - return this.uri; - } - - /** - * Get the http protocol. - * - * @return The http protocol. - */ - @NonNull - public HttpProtocol protocol() { - return this.protocol; - } - - /** - * Get a collection of http headers from the header part. - * - * @return A map collection. - */ - @NonNull - public Map> headers() { - return this.headers; - } - - /** - * Get a list of header values by name. - * - * @param name The name of a header. - * @return a list of header values. - */ - @Nullable - public List headers(@NonNull String name) { - return this.headers.get(name); - } - - /** - * Get the last value corresponding to the specified name, or null. - * - * @param name The name of a header. - * @return The value of the specified header. - */ - @Nullable - public String header(@NonNull String name) { - List headers = headers(name); - return headers != null ? headers.get(headers.size() - 1) : null; - } - - /** - * Create a new builder for this header, the new builder will do a shallow copy of this header - * part. - * - * @return A Builder to generate a header part instance. - */ - protected abstract Builder newBuilder(); - - static abstract class Builder { - - HttpProtocol protocol; - Uri uri; - Map> headers; - - Builder(HttpProtocol protocol, Uri uri, Map> headers) { - this.protocol = protocol; - this.uri = uri; - this.headers = headers; - } - - Builder(HttpHeaderPart httpHeader) { - this.protocol = httpHeader.protocol; - this.uri = httpHeader.uri; - this.headers = new CaseInsensitiveLinkedMap(httpHeader.headers); - } - - Builder removeHeader(@NonNull String name) { - this.headers.remove(name); - return this; - } - - Builder removeHeader(@NonNull String name, int index) { - List header = headers.get(name); - if (header != null) { - if (header.size()> index) { - header.remove(index); - } - } - if (header == null || header.isEmpty()) { - headers.remove(name); - } - return this; - } - - Builder replaceHeader(@NonNull String name, @NonNull String value) { - List header = headers.get(name); - if (header == null) { - header = new ArrayList(1); - headers.put(name, header); - } else { - header.clear(); - } - header.add(value); - return this; - } - - Builder updateHeader(@NonNull String name, @NonNull String value, int index) { - List header = headers.get(name); - if (header == null) { - header = new ArrayList(1); - headers.put(name, header); - } - if (header.size()> index) { - header.remove(index); - header.add(index, value); - } - return this; - } - - Builder updateOrAddHeader(@NonNull String name, @NonNull String value, int index) { - List header = headers.get(name); - if (header == null) { - header = new ArrayList(1); - headers.put(name, header); - } - if (header.size()> index) { - header.remove(index); - header.add(index, value); - } else { - header.add(value); - } - return this; - } - - Builder addHeader(@NonNull String name, @NonNull String value) { - List header = headers.get(name); - if (header == null) { - header = new ArrayList(1); - headers.put(name, header); - } - header.add(value); - return this; - } - - Builder removeHeaders() { - headers.clear(); - return this; - } - - abstract HttpHeaderPart build(); - - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpInjectInterceptor.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpInjectInterceptor.java deleted file mode 100644 index 7f3f954..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpInjectInterceptor.java +++ /dev/null @@ -1,136 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import android.net.Uri; -import android.os.Process; -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.injector.HttpInjector; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * A HTTP inject interceptor holds a {@link HttpInjector}. - * - * @author Megatron King - * @since 2018年12月29日 22:25 - */ -public final class HttpInjectInterceptor extends HttpIndexInterceptor { - - private boolean mShouldInjectRequest; - private boolean mShouldInjectResponse; - - private HttpInjector mHttpInjector; - - private HttpInjectInterceptor(HttpInjector httpInjector) { - this.mHttpInjector = httpInjector; - } - - @Override - protected final void intercept(@NonNull final HttpRequestChain chain, - @NonNull ByteBuffer buffer, int index) throws IOException { - if (chain.request().uid() == Process.myUid()) { - chain.process(buffer); - return; - } - if (index == 0) { - mShouldInjectRequest = mHttpInjector.sniffRequest(chain.request()); - } - if (!mShouldInjectRequest) { - chain.process(buffer); - return; - } - if (index == 0) { - mHttpInjector.onRequestInject(buildHeader(chain.request()), new HttpRequestInjectorCallback(chain)); - } else { - mHttpInjector.onRequestInject(chain.request(), new HttpRawBody(buffer), new HttpRequestInjectorCallback(chain)); - } - } - - @Override - protected final void intercept(@NonNull final HttpResponseChain chain, - @NonNull ByteBuffer buffer, int index) throws IOException { - if (chain.response().uid() == Process.myUid()) { - chain.process(buffer); - return; - } - if (index == 0) { - mShouldInjectResponse = mHttpInjector.sniffResponse(chain.response()); - } - if (!mShouldInjectResponse) { - chain.process(buffer); - return; - } - if (index == 0) { - mHttpInjector.onResponseInject(buildHeader(chain.response()), - new HttpResponseInjectorCallback(chain)); - } else { - mHttpInjector.onResponseInject(chain.response(), new HttpRawBody(buffer), - new HttpResponseInjectorCallback(chain)); - } - } - - @Override - protected void onRequestFinished(@NonNull HttpRequest request) { - super.onRequestFinished(request); - if (mShouldInjectRequest) { - mHttpInjector.onRequestFinished(request); - } - mShouldInjectRequest = false; - } - - @Override - protected void onResponseFinished(@NonNull HttpResponse response) { - super.onResponseFinished(response); - if (mShouldInjectResponse) { - mHttpInjector.onResponseFinished(response); - } - mShouldInjectResponse = false; - } - - private HttpRequestHeaderPart buildHeader(HttpRequest request) { - return new HttpRequestHeaderPart.Builder(request.httpProtocol(), Uri.parse(request.url()), - request.requestHeaders(), request.method()) - .build(); - } - - private HttpResponseHeaderPart buildHeader(HttpResponse response) { - return new HttpResponseHeaderPart.Builder(response.httpProtocol(), Uri.parse(response.url()), - response.responseHeaders(), response.code(), response.message()) - .build(); - } - - /** - * A factory produces {@link HttpInjectInterceptor} instance. - * - * @param httpInjector A HTTP injector. - * @return An instance of {@link HttpInjectInterceptor}. - */ - public static HttpInterceptorFactory createFactory(final HttpInjector httpInjector) { - return new HttpInterceptorFactory() { - - @NonNull - @Override - public HttpInterceptor create() { - return new HttpInjectInterceptor(httpInjector); - } - - }; - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRawBody.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRawBody.java deleted file mode 100644 index f3fcb87..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRawBody.java +++ /dev/null @@ -1,45 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import androidx.annotation.NonNull; - -import java.nio.ByteBuffer; - -/** - * HTTP body object contains a raw buffer. - * - * @author Megatron King - * @since 2018年12月15日 21:15 - */ -public class HttpRawBody extends HttpBody { - - private ByteBuffer mRawBuffer; - - public HttpRawBody(ByteBuffer buffer) { - ByteBuffer rawBuffer = ByteBuffer.allocate(buffer.remaining()); - rawBuffer.put(buffer.array(), buffer.position(), buffer.remaining()); - rawBuffer.flip(); - this.mRawBuffer = rawBuffer; - } - - @NonNull - @Override - public ByteBuffer toBuffer() { - return mRawBuffer; - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestHeaderPart.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestHeaderPart.java deleted file mode 100644 index afafc94..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestHeaderPart.java +++ /dev/null @@ -1,233 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import android.net.Uri; -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareUtils; - -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Map; - -/** - * The http request header part, it contains the request line and request headers. - * - * @author Megatron King - * @since 2018年12月11日 23:59 - */ -public final class HttpRequestHeaderPart extends HttpHeaderPart { - - private final HttpMethod method; - - HttpRequestHeaderPart(HttpProtocol protocol, HttpMethod method, Uri uri, - Map> headers) { - super(protocol, uri, headers); - this.method = method; - } - - /** - * Get the request method from the header part. - * - * @return The request method. - */ - public HttpMethod method() { - return this.method; - } - - /** - * Get the request path from the header part. - * - * @return The request path. - */ - public String path() { - String path = uri().getPath(); - int pathIndex = uri().toString().indexOf(path); - return uri().toString().substring(pathIndex); - } - - @Override - public Builder newBuilder() { - return new Builder(this); - } - - /** - * Builder class for {@link HttpRequestHeaderPart}. - */ - public static class Builder extends HttpHeaderPart.Builder { - - private HttpMethod method; - - Builder(HttpProtocol protocol, Uri uri, Map> headers, - HttpMethod method) { - super(protocol, uri, headers); - this.method = method; - } - - Builder(HttpRequestHeaderPart requestHeader) { - super(requestHeader); - this.method = requestHeader.method; - } - - /** - * Set a new http request method. - * - * @param method The http request method. - * @return The same builder. - */ - public Builder method(@NonNull HttpMethod method) { - this.method = method; - return this; - } - - /** - * Set a new http request uri. - * - * @param uri The http request uri. - * @return The same builder. - */ - public Builder uri(@NonNull Uri uri) { - this.uri = uri; - return this; - } - - /** - * Add a http header. - * - * @param name The name of the header. - * @param value The value of the header. - * @return The same builder. - */ - @Override - public Builder addHeader(@NonNull String name, @NonNull String value) { - return (Builder) super.addHeader(name, value); - } - - /** - * Replace the http header by name. If the name has multiple values, the values will be all - * removed. - * - * @param name The name of the header. - * @param value The value of the header. - * @return The same builder. - */ - @Override - public Builder replaceHeader(@NonNull String name, @NonNull String value) { - return (Builder) super.replaceHeader(name, value); - } - - /** - * Update the http header by name and index. If the index of values not exists, then do - * nothing. - * - * @param name The name of the header. - * @param value The value of the header. - * @param index The value index in list. - * @return The same builder. - * - * @see #updateOrAddHeader(String, String, int) - */ - @Override - public Builder updateHeader(@NonNull String name, @NonNull String value, int index) { - return (Builder) super.updateHeader(name, value, index); - } - - /** - * Update the http header by name and index. If the index of values not exists, then append - * a new header. - * - * @param name The name of the header. - * @param value The value of the header. - * @param index The value index in list. - * @return The same builder. - * - * @see #updateHeader(String, String, int) - */ - @Override - public Builder updateOrAddHeader(@NonNull String name, @NonNull String value, int index) { - return (Builder) super.updateOrAddHeader(name, value, index); - } - - /** - * Remove the http header by name. If the name has multiple values, the values will be all - * removed. - * - * @param name The name of the header. - * @return The same builder. - */ - @Override - public Builder removeHeader(@NonNull String name) { - return (Builder) super.removeHeader(name); - } - - /** - * Remove the http header by name and index. If the index of values not exists, then do - * nothing. - * - * @param name The name of the header. - * @return The same builder. - */ - @Override - public Builder removeHeader(@NonNull String name, int index) { - return (Builder) super.removeHeader(name, index); - } - - /** - * Remove all http headers. - * - * @return The same builder. - */ - @Override - public Builder removeHeaders() { - return (Builder) super.removeHeaders(); - } - - /** - * Build a {@link HttpRequestHeaderPart} instance. - * - * @return The instance. - */ - @Override - public HttpRequestHeaderPart build() { - return new HttpRequestHeaderPart(protocol, method, uri, headers); - } - - } - - @NonNull - @Override - public ByteBuffer toBuffer() { - StringBuilder builder = new StringBuilder(); - builder.append(method.name()); - builder.append(" "); - builder.append(path()); - builder.append(" "); - builder.append(protocol().toString()); - builder.append(NetBareUtils.LINE_END); - for (Map.Entry> entry : headers().entrySet()) { - for (String value : entry.getValue()) { - builder.append(entry.getKey()); - builder.append(": "); - builder.append(value); - builder.append(NetBareUtils.LINE_END); - } - } - builder.append(NetBareUtils.LINE_END); - return ByteBuffer.wrap(builder.toString().getBytes()); - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestInjectorCallback.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestInjectorCallback.java deleted file mode 100644 index acec681..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestInjectorCallback.java +++ /dev/null @@ -1,59 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import com.github.megatronking.netbare.stream.Stream; -import com.github.megatronking.netbare.injector.InjectorCallback; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.LinkedHashMap; - -/** - * A HTTP request packets injector callback. - * - * @author Megatron King - * @since 2018年12月15日 21:55 - */ -public class HttpRequestInjectorCallback implements InjectorCallback { - - private HttpRequestChain mChain; - - /** - * Constructs a {@link InjectorCallback} for HTTP request. - * - * @param chain A {@link HttpInterceptor} chain. - */ - public HttpRequestInjectorCallback(HttpRequestChain chain) { - this.mChain = chain; - } - - @Override - public void onFinished(Stream stream) throws IOException { - ByteBuffer byteBuffer = stream.toBuffer(); - if (stream instanceof HttpRequestHeaderPart) { - HttpRequestHeaderPart header = (HttpRequestHeaderPart) stream; - HttpSession session = mChain.request().session(); - session.method = header.method(); - session.protocol = header.protocol(); - session.path = header.path(); - session.requestHeaders = new LinkedHashMap(header.headers()); - session.reqBodyOffset = byteBuffer.remaining(); - } - mChain.process(byteBuffer); - } - -} \ No newline at end of file diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseHeaderPart.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseHeaderPart.java deleted file mode 100644 index 47cd257..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseHeaderPart.java +++ /dev/null @@ -1,239 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import android.net.Uri; -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareUtils; - -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Map; - -/** - * The http response header part, it contains the status line and response headers. - * - * @author Megatron King - * @since 2018年12月11日 23:43 - */ -public final class HttpResponseHeaderPart extends HttpHeaderPart { - - private final int code; - private final String message; - - HttpResponseHeaderPart(int code, String message, HttpProtocol protocol, Uri uri, - Map> headers) { - super(protocol, uri, headers); - this.code = code; - this.message = message; - } - - /** - * Get the response code from the header part. - * - * @return The response code. - */ - public int code() { - return this.code; - } - - /** - * Get the response message from the header part. - * - * @return The response message. - */ - public String message() { - return this.message; - } - - @Override - public Builder newBuilder() { - return new Builder(this); - } - - /** - * Builder class for {@link HttpResponseHeaderPart}. - */ - public static class Builder extends HttpHeaderPart.Builder { - - private int code; - private String message; - - Builder(HttpProtocol protocol, Uri uri, Map> headers, - int code, String message) { - super(protocol, uri, headers); - this.code = code; - this.message = message; - } - - Builder(HttpResponseHeaderPart responseHeader) { - super(responseHeader); - this.code = responseHeader.code; - this.message = responseHeader.message; - } - - /** - * Set a new http response code. - * - * @param code The http response code. - * @return The same builder. - */ - public Builder code(int code) { - this.code = code; - return this; - } - - /** - * Set a new http response message, the message should match with the response code. - * - * @param message The http response message. - * @return The same builder. - */ - public Builder message(@NonNull String message) { - if (message.contains(NetBareUtils.LINE_END)) { - throw new IllegalArgumentException("The message contains line end symbol."); - } - this.message = message; - return this; - } - - /** - * Add a http header. - * - * @param name The name of the header. - * @param value The value of the header. - * @return The same builder. - */ - @Override - public Builder addHeader(@NonNull String name, @NonNull String value) { - return (Builder) super.addHeader(name, value); - } - - /** - * Replace the http header by name. If the name has multiple values, the values will be all - * removed. - * - * @param name The name of the header. - * @param value The value of the header. - * @return The same builder. - */ - @Override - public Builder replaceHeader(@NonNull String name, @NonNull String value) { - return (Builder) super.replaceHeader(name, value); - } - - /** - * Update the http header by name and index. If the index of values not exists, then do - * nothing. - * - * @param name The name of the header. - * @param value The value of the header. - * @param index The value index in list. - * @return The same builder. - * - * @see #updateOrAddHeader(String, String, int) - */ - @Override - public Builder updateHeader(@NonNull String name, @NonNull String value, int index) { - return (Builder) super.updateHeader(name, value, index); - } - - /** - * Update the http header by name and index. If the index of values not exists, then append - * a new header. - * - * @param name The name of the header. - * @param value The value of the header. - * @param index The value index in list. - * @return The same builder. - * - * @see #updateHeader(String, String, int) - */ - @Override - public Builder updateOrAddHeader(@NonNull String name, @NonNull String value, int index) { - return (Builder) super.updateOrAddHeader(name, value, index); - } - - /** - * Remove the http header by name. If the name has multiple values, the values will be all - * removed. - * - * @param name The name of the header. - * @return The same builder. - */ - @Override - public Builder removeHeader(@NonNull String name) { - return (Builder) super.removeHeader(name); - } - - /** - * Remove the http header by name and index. If the index of values not exists, then do - * nothing. - * - * @param name The name of the header. - * @return The same builder. - */ - @Override - public Builder removeHeader(@NonNull String name, int index) { - return (Builder) super.removeHeader(name, index); - } - - /** - * Remove all http headers. - * - * @return The same builder. - */ - @Override - public Builder removeHeaders() { - return (Builder) super.removeHeaders(); - } - - /** - * Build a {@link HttpResponseHeaderPart} instance. - * - * @return The instance. - */ - @Override - public HttpResponseHeaderPart build() { - return new HttpResponseHeaderPart(code, message, protocol, uri, headers); - } - - } - - @NonNull - @Override - public ByteBuffer toBuffer() { - StringBuilder builder = new StringBuilder(); - builder.append(protocol().toString()); - builder.append(" "); - builder.append(code()); - builder.append(" "); - builder.append(message()); - builder.append(NetBareUtils.LINE_END); - for (Map.Entry> entry : headers().entrySet()) { - for (String value : entry.getValue()) { - builder.append(entry.getKey()); - builder.append(": "); - builder.append(value); - builder.append(NetBareUtils.LINE_END); - } - } - builder.append(NetBareUtils.LINE_END); - return ByteBuffer.wrap(builder.toString().getBytes()); - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseInjectorCallback.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseInjectorCallback.java deleted file mode 100644 index 7001a65..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseInjectorCallback.java +++ /dev/null @@ -1,58 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.http; - -import com.github.megatronking.netbare.stream.Stream; -import com.github.megatronking.netbare.injector.InjectorCallback; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.LinkedHashMap; - -/** - * A HTTP response packets injector callback. - * - * @author Megatron King - * @since 2018年12月15日 21:55 - */ -public class HttpResponseInjectorCallback implements InjectorCallback { - - private HttpResponseChain mChain; - - /** - * Constructs a {@link InjectorCallback} for HTTP response. - * - * @param chain A {@link HttpInterceptor} chain. - */ - public HttpResponseInjectorCallback(HttpResponseChain chain) { - this.mChain = chain; - } - - @Override - public void onFinished(Stream stream) throws IOException { - ByteBuffer byteBuffer = stream.toBuffer(); - if (stream instanceof HttpResponseHeaderPart) { - HttpResponseHeaderPart header = (HttpResponseHeaderPart) stream; - HttpSession session = mChain.response().session(); - session.code = header.code(); - session.message = header.message(); - session.responseHeaders = new LinkedHashMap(header.headers()); - session.resBodyOffset = byteBuffer.remaining(); - } - mChain.process(byteBuffer); - } - -} \ No newline at end of file diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/AppHttpInjector.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/AppHttpInjector.java deleted file mode 100644 index 064ea75..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/AppHttpInjector.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.megatronking.netbare.injector; - -import androidx.annotation.NonNull; -import com.github.megatronking.netbare.http.HttpRequest; -import com.github.megatronking.netbare.http.HttpResponse; - -import java.util.Map; - -public class AppHttpInjector extends SimpleHttpInjector { - @Override - public boolean sniffResponse(@NonNull HttpResponse response) { - return super.sniffResponse(response); - } -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/BlockedHttpInjector.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/BlockedHttpInjector.java deleted file mode 100644 index bdadf25..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/BlockedHttpInjector.java +++ /dev/null @@ -1,63 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.injector; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.http.HttpBody; -import com.github.megatronking.netbare.http.HttpRequest; -import com.github.megatronking.netbare.http.HttpRequestHeaderPart; -import com.github.megatronking.netbare.http.HttpResponse; -import com.github.megatronking.netbare.http.HttpResponseHeaderPart; - -/** - * An injector can block http request and response. Add your custom rules in - * {@link #sniffRequest(HttpRequest)} or {@link #sniffResponse(HttpResponse)} to filter the request. - * - * @author Megatron King - * @since 2019年1月2日 20:50 - */ -public abstract class BlockedHttpInjector implements HttpInjector { - - @Override - public final void onRequestInject(@NonNull HttpRequestHeaderPart header, - @NonNull InjectorCallback callback) { - } - - @Override - public final void onResponseInject(@NonNull HttpResponseHeaderPart header, - @NonNull InjectorCallback callback) { - } - - @Override - public final void onRequestInject(@NonNull HttpRequest request, @NonNull HttpBody body, - @NonNull InjectorCallback callback) { - } - - @Override - public final void onResponseInject(@NonNull HttpResponse response, @NonNull HttpBody body, - @NonNull InjectorCallback callback) { - } - - @Override - public void onRequestFinished(@NonNull HttpRequest request) { - } - - @Override - public void onResponseFinished(@NonNull HttpResponse response) { - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/HttpInjector.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/HttpInjector.java deleted file mode 100644 index ef13bdd..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/HttpInjector.java +++ /dev/null @@ -1,115 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.injector; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.http.HttpBody; -import com.github.megatronking.netbare.http.HttpRequest; -import com.github.megatronking.netbare.http.HttpRequestHeaderPart; -import com.github.megatronking.netbare.http.HttpResponse; -import com.github.megatronking.netbare.http.HttpResponseHeaderPart; -import com.github.megatronking.netbare.stream.Stream; - -import java.io.IOException; - -/** - * An injector for HTTP protocol packets, you can block and modify HTTP headers and bodies in this - * injector. - *

                - * Remember do not hold those methods for a long time. - *

                - * - * @author Megatron King - * @since 2018年12月15日 21:50 - */ -public interface HttpInjector { - - /** - * Determine should the injector apply to this request. - * - * @param request Http request session. - * @return True if do injection to this request. - */ - boolean sniffRequest(@NonNull HttpRequest request); - - /** - * Determine should the injector apply to this response. - * - * @param response Http response session. - * @return True if do injection to this response. - */ - boolean sniffResponse(@NonNull HttpResponse response); - - /** - * Inject the http request header part, call {@link InjectorCallback#onFinished(Stream)} after - * the injection. - * - * @param header Http header part. - * @param callback A injection finish callback. - * @throws IOException If an I/O error has occurred. - */ - void onRequestInject(@NonNull HttpRequestHeaderPart header, @NonNull InjectorCallback callback) - throws IOException; - - /** - * Inject the http response header part, call {@link InjectorCallback#onFinished(Stream)} after - * the injection. - * - * @param header Http header part. - * @param callback A injection finish callback. - * @throws IOException If an I/O error has occurred. - */ - void onResponseInject(@NonNull HttpResponseHeaderPart header, @NonNull InjectorCallback callback) - throws IOException; - - /** - * Inject the http request body part, call {@link InjectorCallback#onFinished(Stream)} after - * the injection. - * - * @param body Http body part. - * @param callback A injection finish callback. - * @throws IOException If an I/O error has occurred. - */ - void onRequestInject(@NonNull HttpRequest request, @NonNull HttpBody body, - @NonNull InjectorCallback callback) throws IOException; - - /** - * Inject the http response body part, call {@link InjectorCallback#onFinished(Stream)} after - * the injection. - * - * @param body Http body part. - * @param callback A injection finish callback. - * @throws IOException If an I/O error has occurred. - */ - void onResponseInject(@NonNull HttpResponse response, @NonNull HttpBody body, - @NonNull InjectorCallback callback) throws IOException; - - /** - * Notify the injector that the request is finished. - * - * @param request Http request session. - */ - void onRequestFinished(@NonNull HttpRequest request); - - /** - * Notify the injector that the response is finished. - * - * @param response Http response session. - */ - void onResponseFinished(@NonNull HttpResponse response); - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/InjectorCallback.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/InjectorCallback.java deleted file mode 100644 index ec3d68c..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/InjectorCallback.java +++ /dev/null @@ -1,38 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.injector; - -import com.github.megatronking.netbare.stream.Stream; - -import java.io.IOException; - -/** - * A callback invoked when the injector finished the injection. - * - * @author Megatron King - * @since 2018年12月15日 21:55 - */ -public interface InjectorCallback { - - /** - * Invoked when the injector finished the injection. - * - * @param stream The injected stream. - * @throws IOException If an I/O error has occurred. - */ - void onFinished(Stream stream) throws IOException; - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/SimpleHttpInjector.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/SimpleHttpInjector.java deleted file mode 100644 index 669e776..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/SimpleHttpInjector.java +++ /dev/null @@ -1,78 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.injector; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.http.HttpBody; -import com.github.megatronking.netbare.http.HttpRequest; -import com.github.megatronking.netbare.http.HttpRequestHeaderPart; -import com.github.megatronking.netbare.http.HttpResponse; -import com.github.megatronking.netbare.http.HttpResponseHeaderPart; - -import java.io.IOException; - -/** - * Convenience class for simple injectors and allows extending only methods that are necessary. - * - * @author Megatron King - * @since 2019年1月2日 20:55 - */ -public abstract class SimpleHttpInjector implements HttpInjector { - - @Override - public boolean sniffRequest(@NonNull HttpRequest request) { - return false; - } - - @Override - public boolean sniffResponse(@NonNull HttpResponse response) { - return false; - } - - @Override - public void onRequestInject(@NonNull HttpRequestHeaderPart header, - @NonNull InjectorCallback callback) throws IOException { - callback.onFinished(header); - } - - @Override - public void onResponseInject(@NonNull HttpResponseHeaderPart header, - @NonNull InjectorCallback callback) throws IOException { - callback.onFinished(header); - } - - @Override - public void onRequestInject(@NonNull HttpRequest request, @NonNull HttpBody body, - @NonNull InjectorCallback callback) throws IOException { - callback.onFinished(body); - } - - @Override - public void onResponseInject(@NonNull HttpResponse response, @NonNull HttpBody body, - @NonNull InjectorCallback callback) throws IOException { - callback.onFinished(body); - } - - @Override - public void onRequestFinished(@NonNull HttpRequest request) { - } - - @Override - public void onResponseFinished(@NonNull HttpResponse response) { - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/io/ByteBufferInputStream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/io/ByteBufferInputStream.java deleted file mode 100644 index cd85cc1..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/io/ByteBufferInputStream.java +++ /dev/null @@ -1,39 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.io; - -import java.io.ByteArrayInputStream; -import java.nio.ByteBuffer; - -/** - * A {@link ByteArrayInputStream} constructs by a {@link ByteBuffer}. - * - * @author Megatron King - * @since 2018年12月23日 15:24 - */ -public class ByteBufferInputStream extends ByteArrayInputStream { - - /** - * Constructs a new {@link ByteArrayInputStream} by a {@link ByteBuffer}. - * - * @param buffer A buffer has remaining data. - */ - public ByteBufferInputStream(ByteBuffer buffer) { - super(buffer.array(), buffer.position(), buffer.remaining()); - } - - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/io/HttpBodyInputStream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/io/HttpBodyInputStream.java deleted file mode 100644 index 8008c5e..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/io/HttpBodyInputStream.java +++ /dev/null @@ -1,39 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.io; - -import com.github.megatronking.netbare.http.HttpBody; - -import java.io.ByteArrayInputStream; - -/** - * A {@link ByteArrayInputStream} constructs by a {@link HttpBody}. - * - * @author Megatron King - * @since 2018年12月23日 15:24 - */ -public class HttpBodyInputStream extends ByteBufferInputStream { - - /** - * Constructs a new {@link ByteArrayInputStream} by a {@link HttpBody}. - * - * @param body A HTTP body stream. - */ - public HttpBodyInputStream(HttpBody body) { - super(body.toBuffer()); - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/BufferStream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/BufferStream.java deleted file mode 100644 index 7e29037..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/BufferStream.java +++ /dev/null @@ -1,47 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.stream; - -import androidx.annotation.NonNull; - -import java.nio.ByteBuffer; - -/** - * A {@link ByteBuffer} stream data, the buffer will be output with nothing changes. - * - * @author Megatron King - * @since 2018年12月23日 11:15 - */ -public class BufferStream implements Stream { - - private ByteBuffer mBuffer; - - /** - * Constructs a {@link ByteStream} by a {@link ByteBuffer}. - * - * @param buffer A byte buffer. - */ - public BufferStream(@NonNull ByteBuffer buffer) { - this.mBuffer = buffer; - } - - @NonNull - @Override - public ByteBuffer toBuffer() { - return mBuffer; - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/ByteStream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/ByteStream.java deleted file mode 100644 index bfa1916..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/ByteStream.java +++ /dev/null @@ -1,62 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.stream; - -import androidx.annotation.NonNull; - -import java.nio.ByteBuffer; - -/** - * A stream creates by a byte array. - * - * @author Megatron King - * @since 2018年12月23日 11:15 - */ -public class ByteStream implements Stream { - - private final ByteBuffer mBuffer; - - /** - * Constructs a new stream by a byte array. - * - * @param bytes The bytes to be wrapped into {@link ByteBuffer}. - */ - public ByteStream(byte[] bytes) { - this(bytes, 0, bytes.length); - } - - /** - * Constructs a new stream by a byte array. - * - * @param bytes The bytes to be wrapped into {@link ByteBuffer}. - * @param offset The index of the first byte in array. - * @param length The number of bytes. - */ - public ByteStream(byte[] bytes, int offset, int length) { - // Do not use wrap. - ByteBuffer buffer = ByteBuffer.allocate(length); - buffer.put(bytes, offset, length); - buffer.flip(); - this.mBuffer = buffer; - } - - @NonNull - @Override - public ByteBuffer toBuffer() { - return mBuffer; - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/Stream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/Stream.java deleted file mode 100644 index 97c9a60..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/Stream.java +++ /dev/null @@ -1,38 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.stream; - -import androidx.annotation.NonNull; - -import java.nio.ByteBuffer; - -/** - * Stream is special data type using in NetBare. - * - * @author Megatron King - * @since 2018年12月15日 21:05 - */ -public interface Stream { - - /** - * Converts the stream data type to {@link ByteBuffer}. - * - * @return A {@link ByteBuffer}. - */ - @NonNull - ByteBuffer toBuffer(); - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/StringStream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/StringStream.java deleted file mode 100644 index 6e07e84..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/StringStream.java +++ /dev/null @@ -1,39 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.stream; - -import androidx.annotation.NonNull; - -import java.nio.ByteBuffer; - -/** - * A stream creates by a string. - * - * @author Megatron King - * @since 2018年12月23日 11:30 - */ -public class StringStream extends ByteStream { - - /** - * Constructs a new stream by a string. - * - * @param string The string to be wrapped into {@link ByteBuffer}. - */ - public StringStream(@NonNull String string) { - super(string.getBytes()); - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/TinyFileStream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/TinyFileStream.java deleted file mode 100644 index fa984b8..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/TinyFileStream.java +++ /dev/null @@ -1,78 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.stream; - -import androidx.annotation.NonNull; - -import com.github.megatronking.netbare.NetBareUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; - -/** - * A tiny file stream, the file will be loaded into memory, the file size should be less than - * {@link #MAX_FILE_LENGTH}. - * - * @author Megatron King - * @since 2018年12月23日 11:39 - */ -public class TinyFileStream implements Stream { - - /** - * The max file size of this stream supports. - */ - public static final int MAX_FILE_LENGTH = 64 * 1024; - - private final File mFile; - - /** - * Constructs a stream by a tiny file. - * - * @param file A tiny file. - * @throws IOException - */ - public TinyFileStream(@NonNull File file) throws IOException { - if (file.length()> MAX_FILE_LENGTH) { - throw new IOException("Only support file size < " + MAX_FILE_LENGTH); - } - this.mFile = file; - } - - @NonNull - @Override - public ByteBuffer toBuffer() { - ByteBuffer byteBuffer; - FileInputStream fis = null; - FileChannel channel = null; - try { - fis = new FileInputStream(mFile); - channel = fis.getChannel(); - byteBuffer = ByteBuffer.allocate(fis.available()); - channel.read(byteBuffer); - byteBuffer.flip(); - } catch (IOException e) { - byteBuffer = ByteBuffer.allocate(0); - } finally { - NetBareUtils.closeQuietly(channel); - NetBareUtils.closeQuietly(fis); - } - return byteBuffer; - } - -} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/utils/CaseInsensitiveLinkedMap.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/utils/CaseInsensitiveLinkedMap.java deleted file mode 100644 index 7935711..0000000 --- a/netbare-injector/src/main/java/com/github/megatronking/netbare/utils/CaseInsensitiveLinkedMap.java +++ /dev/null @@ -1,362 +0,0 @@ -/* NetBare - An android network capture and injection library. - * Copyright (C) 2018-2019 Megatron King - * Copyright (C) 2018-2019 GuoShi - * - * NetBare is free software: you can redistribute it and/or modify it under the terms - * of the GNU General Public License as published by the Free Software Found- - * ation, either version 3 of the License, or (at your option) any later version. - * - * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with NetBare. - * If not, see . - */ -package com.github.megatronking.netbare.utils; - -import androidx.annotation.NonNull; - -import java.util.AbstractMap; -import java.util.AbstractSet; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; - -/** - * {@link CaseInsensitiveLinkedMap} is a {@link LinkedHashMap} from String keys to - * values which is case-insensitive and case-preserving with respect to the keys in the map. - * - * This class is not necessarily thread safe. - * - * @author Megatron King - * @since 2018年12月21日 23:55 - */ -public class CaseInsensitiveLinkedMap extends AbstractMap { - - private final Map map; - - private static final class KeySet extends AbstractSet { - - private static final class KeySetIterator implements Iterator { - - private Iterator iterator; - - private KeySetIterator(Iterator iterator) { - this.iterator = iterator; - } - - @Override - public boolean hasNext() { - return this.iterator.hasNext(); - } - - @Override - public String next() { - return this.iterator.next().toString(); - } - - @Override - public void remove() { - this.iterator.remove(); - } - } - - private final Set keySet; - - private KeySet(Set keySet) { - this.keySet = keySet; - } - - /** - * Not supported for sets returned by Map.keySet. - */ - @Override - public boolean add(String o) { - throw new UnsupportedOperationException("Map.keySet must return a Set which does not support add"); - } - - /** - * Not supported for sets returned by Map.keySet. - */ - @Override - public boolean addAll(@NonNull Collection c) { - throw new UnsupportedOperationException("Map.keySet must return a Set which does not support addAll"); - } - - @Override - public void clear() { - this.keySet.clear(); - } - - @Override - public boolean contains(Object o) { - return o instanceof String && this.keySet.contains(CaseInsensitiveKey.objectToKey(o)); - } - - @Override - public Iterator iterator() { - return new KeySetIterator(this.keySet.iterator()); - } - - @Override - public boolean remove(Object o) { - // The following can throw ClassCastException which conforms to the method specification. - return this.keySet.remove(CaseInsensitiveKey.objectToKey(o)); - } - - @Override - public int size() { - return this.keySet.size(); - } - - } - - private static final class EntrySet extends AbstractSet> { - - private static final class MapEntry implements Entry { - - private final Entry entry; - - private MapEntry(Entry entry) { - this.entry = entry; - } - - @Override - public String getKey() { - return this.entry.getKey().toString(); - } - - @Override - public V getValue() { - return this.entry.getValue(); - } - - @Override - public V setValue(V value) { - return this.entry.setValue(value); - } - - private Entry getEntry() { - return this.entry; - } - } - - private static final class EntrySetIterator implements Iterator> { - - private final Iterator> iterator; - - private EntrySetIterator(Iterator> iterator) { - this.iterator = iterator; - } - - @Override - public boolean hasNext() { - return this.iterator.hasNext(); - } - - @Override - public Entry next() { - return new MapEntry(this.iterator.next()); - } - - @Override - public void remove() { - this.iterator.remove(); - } - - } - - private final Set> entrySet; - - private final CaseInsensitiveLinkedMap map; - - private EntrySet(Set> entrySet, CaseInsensitiveLinkedMap map) { - this.entrySet = entrySet; - this.map = map; - } - - /** - * Not supported for sets returned by Map.entrySet. - */ - @Override - public boolean add(Entry o) { - throw new UnsupportedOperationException("Map.entrySet must return a Set which does not support add"); - } - - /** - * Not supported for sets returned by Map.entrySet. - */ - @Override - public boolean addAll(@NonNull Collection> c) { - throw new UnsupportedOperationException("Map.entrySet must return a Set which does not support addAll"); - } - - @Override - public void clear() { - this.entrySet.clear(); - } - - @SuppressWarnings("unchecked") - @Override - public boolean contains(Object o) { - if (o instanceof Entry) { - Entry e = (Entry) o; - V value = this.map.get(e.getKey()); - return value.equals(e.getValue()); - } - return false; - } - - @Override - public Iterator> iterator() { - return new EntrySetIterator(this.entrySet.iterator()); - } - - @SuppressWarnings("unchecked") - @Override - public boolean remove(Object o) { - return this.entrySet.remove(((MapEntry) o).getEntry()); - } - - @Override - public int size() { - return this.entrySet.size(); - } - - } - - static final class CaseInsensitiveKey { - - private final String key; - - private CaseInsensitiveKey(String key) { - this.key = key; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + key.toLowerCase().hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - CaseInsensitiveKey other = (CaseInsensitiveKey) obj; - return key == null ? other.key == null : key.equalsIgnoreCase(other.key); - } - - @Override - public String toString() { - return key; - } - - /** - * Convert the given key Object to a {@link CaseInsensitiveKey}. - *

                - * Pre-condition: key instanceof String - * - * @param key the key to be converted - * @return the CaseInsensitiveKey corresponding to the given key - */ - public static CaseInsensitiveKey objectToKey(Object key) { - return new CaseInsensitiveKey((String) key); - } - - } - - public CaseInsensitiveLinkedMap(int initialCapacity) { - this.map = new LinkedHashMap(initialCapacity); - } - - public CaseInsensitiveLinkedMap() { - this.map = new LinkedHashMap(); - } - - @SuppressWarnings("unchecked") - public CaseInsensitiveLinkedMap(Map map) { - this.map = new LinkedHashMap(map.size()); - if (map instanceof CaseInsensitiveLinkedMap) { - this.map.putAll(((CaseInsensitiveLinkedMap) map).map); - } else { - for (Entry entry : map.entrySet()) { - put(entry.getKey(), entry.getValue()); - } - } - } - - public CaseInsensitiveLinkedMap(CaseInsensitiveLinkedMap map) { - this.map = new LinkedHashMap(map.size()); - this.map.putAll(map.map); - } - - @Override - public void clear() { - this.map.clear(); - } - - @Override - public boolean containsKey(Object key) { - return key instanceof String && this.map.containsKey(CaseInsensitiveKey.objectToKey(key)); - } - - @Override - public boolean containsValue(Object value) { - return this.map.containsValue(value); - } - - @Override - @NonNull - public Set> entrySet() { - return new EntrySet(this.map.entrySet(), this); - } - - @Override - public V get(Object key) { - return key instanceof String ? this.map.get(CaseInsensitiveKey.objectToKey(key)) : null; - } - - @Override - @NonNull - public Set keySet() { - return new KeySet(this.map.keySet()); - } - - @Override - public V put(String key, V value) { - if (key == null) { - throw new NullPointerException("CaseInsensitiveLinkedMap does not permit null keys"); - } - return this.map.put(CaseInsensitiveKey.objectToKey(key), value); - } - - @Override - public V remove(Object key) { - return key instanceof String ? this.map.remove(CaseInsensitiveKey.objectToKey(key)) : null; - } - - @Override - public int size() { - return this.map.size(); - } - - @Override - @NonNull - public Collection values() { - return this.map.values(); - } - -} diff --git a/netbare-sample/build.gradle b/netbare-sample/build.gradle deleted file mode 100644 index faf40a8..0000000 --- a/netbare-sample/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion 28 - defaultConfig { - applicationId "com.github.megatronking.netbare.sample" - minSdkVersion 21 - targetSdkVersion 27 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support.constraint:constraint-layout:1.1.3' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' - implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.11' - - implementation 'com.google.code.gson:gson:2.8.2' - implementation project(':basemoduleimport') - -} -repositories { - mavenCentral() -} diff --git a/netbare-sample/proguard-rules.pro b/netbare-sample/proguard-rules.pro deleted file mode 100644 index f1b4245..0000000 --- a/netbare-sample/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/netbare-sample/src/main/AndroidManifest.xml b/netbare-sample/src/main/AndroidManifest.xml deleted file mode 100644 index 8f56281..0000000 --- a/netbare-sample/src/main/AndroidManifest.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/App.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/App.kt deleted file mode 100644 index 5a7e6f7..0000000 --- a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/App.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.megatronking.netbare.sample - -import android.app.Application -import com.wrbug.developerhelper.basewidgetimport.BaseModule - -class App : Application() { - - companion object { - - private lateinit var sInstance: App - - fun getInstance(): App { - return sInstance - } - } - - override fun onCreate() { - super.onCreate() - sInstance = this - // 创建自签证书 - BaseModule.init(this) - } - - - -} \ No newline at end of file diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/AppService.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/AppService.kt deleted file mode 100644 index 99cd666..0000000 --- a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/AppService.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.megatronking.netbare.sample - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.graphics.BitmapFactory -import android.os.Build -import androidx.core.app.NotificationCompat -import com.github.megatronking.netbare.NetBareService - -class AppService : NetBareService() { - - companion object { - - private const val CHANNEL_ID = "com.github.megatronking.netbare.sample.NOTIFICATION_CHANNEL_ID" - - } - - override fun onCreate() { - super.onCreate() - if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.O) { - val notificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) { - notificationManager.createNotificationChannel(NotificationChannel(CHANNEL_ID, - getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW)) - } - } - } - - override fun notificationId(): Int { - return 100 - } - - override fun createNotification(): Notification { - val intent = Intent(this, MainActivity::class.java) - intent.addCategory(Intent.CATEGORY_LAUNCHER) - intent.action = Intent.ACTION_MAIN - val pendingIntent = PendingIntent.getActivity(this, 0, intent, - PendingIntent.FLAG_UPDATE_CURRENT) - return NotificationCompat.Builder(this, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_launcher_foreground) - .setContentTitle(getString(R.string.app_name)) - .setContentText(getString(R.string.app_name)) - .setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)) - .setOngoing(true) - .setContentIntent(pendingIntent) - .build() - } - -} \ No newline at end of file diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/BaiduLogoInjector.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/BaiduLogoInjector.kt deleted file mode 100644 index 9db4969..0000000 --- a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/BaiduLogoInjector.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.github.megatronking.netbare.sample - -import android.util.Log -import com.github.megatronking.netbare.http.HttpBody -import com.github.megatronking.netbare.http.HttpResponse -import com.github.megatronking.netbare.http.HttpResponseHeaderPart -import com.github.megatronking.netbare.injector.InjectorCallback -import com.github.megatronking.netbare.injector.SimpleHttpInjector -import com.github.megatronking.netbare.stream.BufferStream -import java.nio.ByteBuffer - -/** - * 注入器范例1:替换百度首页logo - * - * 启动NetBare服务后,用浏览器App打开百度首页,Logo将会被替换成此sample项目raw目录下的图片。 - * 注意:如果浏览器有图片缓存,记得先把缓存清理掉! - * - * @author Megatron King - * @since 2018年12月30日 00:18 - */ -class BaiduLogoInjector : SimpleHttpInjector() { - - companion object { - const val TAG = "BaiduLogoInjector" - } - - override fun sniffResponse(response: HttpResponse): Boolean { - // 请求url匹配时才进行注入 - val shouldInject = "https://m.baidu.com/static/index/plus/plus_logo.png".equals( - response.url()) - if (shouldInject) { - Log.i(TAG, "Start Miss. Du logo injection!") - } - return shouldInject - } - - override fun onResponseInject(header: HttpResponseHeaderPart, callback: InjectorCallback) { - // 响应体大小变化,一定要先更新header中的content-length - val newHeader = header.newBuilder() - .replaceHeader("content-length", "10764") - .build() - callback.onFinished(newHeader) - Log.i(TAG, "Inject header completed!") - } - - override fun onResponseInject(response: HttpResponse, body: HttpBody, callback: InjectorCallback) { - // 替换图片请求响应体 - val injectIOStream = App.getInstance().resources.openRawResource(R.raw.baidu_inject_logo) - val injectStream = BufferStream(ByteBuffer.wrap(injectIOStream.readBytes())) - injectIOStream.close() - callback.onFinished(injectStream) - Log.i(TAG, "Inject body completed!") - } - -} \ No newline at end of file diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/HttpUrlPrintInterceptor.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/HttpUrlPrintInterceptor.kt deleted file mode 100644 index 699ce30..0000000 --- a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/HttpUrlPrintInterceptor.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.github.megatronking.netbare.sample - -import android.util.Log -import com.github.megatronking.netbare.http.* -import java.nio.ByteBuffer -import java.nio.CharBuffer -import java.nio.charset.Charset -import java.nio.charset.CharsetDecoder - -/** - * 拦截器反例1:打印请求url - * - * @author Megatron King - * @since 2019年1月2日 22:05 - */ -class HttpUrlPrintInterceptor : HttpIndexInterceptor() { - - companion object { - const val TAG = "URL" - - fun createFactory(): HttpInterceptorFactory { - return HttpInterceptorFactory { HttpUrlPrintInterceptor() } - } - } - - override fun intercept(chain: HttpRequestChain, buffer: ByteBuffer, index: Int) { - if (index == 0) { - // 一个请求可能会有多个数据包,故此方法会多次触发,这里只在收到第一个包的时候打印 - Log.i(TAG, "Request: " + chain.request().url()) - } - // 调用process将数据发射给下一个拦截器,否则数据将不会发给服务器 - chain.process(buffer) - } - - override fun intercept(chain: HttpResponseChain, buffer: ByteBuffer, index: Int) { - chain.process(buffer) - } - - override fun onResponseFinished(response: HttpResponse) { - super.onResponseFinished(response) - } - -} diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/MainActivity.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/MainActivity.kt deleted file mode 100644 index 3922dbd..0000000 --- a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/MainActivity.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.github.megatronking.netbare.sample - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.widget.Button -import androidx.appcompat.app.AppCompatActivity -import com.github.megatronking.netbare.NetBare -import com.github.megatronking.netbare.NetBareConfig -import com.github.megatronking.netbare.NetBareListener -import com.github.megatronking.netbare.http.HttpInjectInterceptor -import com.github.megatronking.netbare.http.HttpInterceptorFactory -import com.github.megatronking.netbare.http.HttpVirtualGatewayFactory -import com.github.megatronking.netbare.ssl.JKS -import com.wrbug.developerhelper.basecommon.startActivityForResultOk -import java.io.IOException - -class MainActivity : AppCompatActivity(), NetBareListener { - - companion object { - private const val REQUEST_CODE_PREPARE = 1 - } - - private lateinit var mNetBare: NetBare - - private lateinit var mActionButton: Button - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - - mNetBare = NetBare.get() - - mActionButton = findViewById(R.id.action) - mActionButton.setOnClickListener { - if (mNetBare.isActive) { - mNetBare.stop() - } else { - prepareNetBare() - } - } - - // 监听NetBare服务的启动和停止 - mNetBare.registerNetBareListener(this) - } - - override fun onDestroy() { - super.onDestroy() - mNetBare.unregisterNetBareListener(this) - mNetBare.stop() - } - - override fun onServiceStarted() { - mActionButton.setText(R.string.stop_netbare) - } - - override fun onServiceStopped() { - mActionButton.setText(R.string.start_netbare) - mActionButton.invalidate() - } - - private fun prepareNetBare() { - // 安装自签证书 - if (!JKS.isInstalled(this, JKS.getJskAlias())) { - try { - JKS.install(this, JKS.getJskAlias(), JKS.getJskAlias()) - } catch (e: IOException) { - // 安装失败 - } - return - } - // 配置VPN - val intent = NetBare.get().prepare() - if (intent != null) { - startActivityForResultOk(intent){ - prepareNetBare() - } - return - } - // 启动NetBare服务 - mNetBare.start( - NetBareConfig.defaultHttpConfig( - JKS.getJks(), - interceptorFactories() - ).newBuilder().dumpUid(true).build() - ) - } - - private fun interceptorFactories(): List { - // 拦截器范例1:打印请求url - val interceptor1 = HttpUrlPrintInterceptor.createFactory() - // 注入器范例1:替换百度首页logo - val injector1 = HttpInjectInterceptor.createFactory(BaiduLogoInjector()) - // 注入器范例2:修改发朋友圈定位 - val injector2 = HttpInjectInterceptor.createFactory(WechatLocationInjector()) - val injector3 = HttpInjectInterceptor.createFactory(PresellLocationInjector()) - // 可以添加其它的拦截器,注入器 - // ... - return listOf(interceptor1, injector1, injector2, injector3) - } - - -} diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/PresellLocationInjector.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/PresellLocationInjector.kt deleted file mode 100644 index 0c15243..0000000 --- a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/PresellLocationInjector.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.github.megatronking.netbare.sample - -import android.util.Log -import com.github.megatronking.netbare.NetBareUtils -import com.github.megatronking.netbare.http.HttpBody -import com.github.megatronking.netbare.http.HttpResponse -import com.github.megatronking.netbare.http.HttpResponseHeaderPart -import com.github.megatronking.netbare.injector.InjectorCallback -import com.github.megatronking.netbare.injector.SimpleHttpInjector -import com.github.megatronking.netbare.io.HttpBodyInputStream -import com.github.megatronking.netbare.stream.ByteStream -import com.google.gson.JsonParser -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.InputStreamReader -import java.io.Reader -import java.nio.ByteBuffer -import java.nio.CharBuffer -import java.nio.charset.Charset -import java.nio.charset.CharsetDecoder -import java.util.* -import java.util.zip.DeflaterOutputStream -import java.util.zip.GZIPInputStream -import java.util.zip.InflaterInputStream - -/** - * 注入器范例2:修改发朋友圈定位 - * - * 启动NetBare服务后,打开朋友圈发状态->所在位置,会发现POI都变成被修改地点附近的。 - * - * @author Sundy - * @since 2019年1月2日 22:17 - */ -class PresellLocationInjector : SimpleHttpInjector() { - - companion object { - const val TAG = "WechatLocationInjector" - } - - private var mHoldResponseHeader: HttpResponseHeaderPart? = null - - override fun sniffResponse(response: HttpResponse): Boolean { - // 请求url匹配时才进行注入 - val shouldInject = response.url().startsWith("http://boss-apk.10.1.133.14.xip.io/") - if (shouldInject) { - Log.i(TAG, "Start wechat location injection!") - } - return shouldInject - } - - override fun onResponseInject(header: HttpResponseHeaderPart, callback: InjectorCallback) { - // 由于响应体大小不确定,这里先hold住header(需要在后面修改content-length) - mHoldResponseHeader = header - } - - override fun onResponseInject(response: HttpResponse, body: HttpBody, callback: InjectorCallback) { - if (mHoldResponseHeader == null) { - // 一般不会发生 - return - } - var his: HttpBodyInputStream? = null - try { -// his = HttpBodyInputStream(body) -// var bytes: ByteArray = ByteArray(his.available()) -// his.read(bytes) -// val str = String(bytes) - val str = String(uncompress(body.toBuffer().array())!!) - Log.i(TAG, str) - } finally { - NetBareUtils.closeQuietly(his) - } - mHoldResponseHeader = null - super.onResponseInject(response, body, callback) - } - - fun getString(buffer: ByteBuffer?): String { - var charset: Charset? = null - var decoder: CharsetDecoder? = null - var charBuffer: CharBuffer? = null - try { - charset = Charset.forName("UTF-8") - decoder = charset!!.newDecoder() - // charBuffer = decoder.decode(buffer);//用这个的话,只能输出来一次结果,第二次显示为空 - charBuffer = decoder!!.decode(buffer?.asReadOnlyBuffer()) - return charBuffer!!.toString() - } catch (ex: Exception) { - ex.printStackTrace() - return "" - } - - } - - fun uncompress(bytes: ByteArray?): ByteArray? { - if (bytes == null || bytes.isEmpty()) { - return null - } - val out = ByteArrayOutputStream() - val `in` = ByteArrayInputStream(bytes) - try { - val ungzip = GZIPInputStream(`in`) - val buffer = ByteArray(256) - var n: Int = ungzip.read(buffer) - while (n>= 0) { - out.write(buffer, 0, n) - n = ungzip.read(buffer) - } - } catch (e: Exception) { - e.printStackTrace() - } - - return out.toByteArray() - } - - -} diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/TestHttpIntercepter.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/TestHttpIntercepter.kt deleted file mode 100644 index a5ba225..0000000 --- a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/TestHttpIntercepter.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.megatronking.netbare.sample - -import com.github.megatronking.netbare.http.* -import java.nio.ByteBuffer - -class TestHttpIntercepter : HttpInterceptor() { - - override fun intercept(chain: HttpRequestChain, buffer: ByteBuffer) { - // 对Http请求包进行自定义处理 - // 将Http请求发射出去,交给下个拦截器或者发给服务器 - chain.process(buffer) - } - - override fun intercept(chain: HttpResponseChain, buffer: ByteBuffer) { - // 对Http响应包进行自定义处理 - // 将Http响应发射出去,交给下个拦截器或者发给客户端 - chain.process(buffer) - } - - override fun onRequestFinished(request: HttpRequest) { - // Http请求包已全部发送完成 - } - - override fun onResponseFinished(response: HttpResponse) { - val url = response.url() - if (url==null) { - - } - // Http响应包已全部发送完成 - } - -} \ No newline at end of file diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/WechatLocationInjector.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/WechatLocationInjector.kt deleted file mode 100644 index 14924ff..0000000 --- a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/WechatLocationInjector.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.github.megatronking.netbare.sample - -import android.util.Log -import com.github.megatronking.netbare.NetBareUtils -import com.github.megatronking.netbare.http.HttpBody -import com.github.megatronking.netbare.http.HttpResponse -import com.github.megatronking.netbare.http.HttpResponseHeaderPart -import com.github.megatronking.netbare.injector.InjectorCallback -import com.github.megatronking.netbare.injector.SimpleHttpInjector -import com.github.megatronking.netbare.io.HttpBodyInputStream -import com.github.megatronking.netbare.stream.ByteStream -import com.google.gson.JsonParser -import java.io.ByteArrayOutputStream -import java.io.InputStreamReader -import java.io.Reader -import java.util.zip.DeflaterOutputStream -import java.util.zip.InflaterInputStream - -/** - * 注入器范例2:修改发朋友圈定位 - * - * 启动NetBare服务后,打开朋友圈发状态->所在位置,会发现POI都变成被修改地点附近的。 - * - * @author Megatron King - * @since 2019年1月2日 22:17 - */ -class WechatLocationInjector : SimpleHttpInjector() { - - companion object { - const val TAG = "WechatLocationInjector" - } - - private var mHoldResponseHeader: HttpResponseHeaderPart? = null - - override fun sniffResponse(response: HttpResponse): Boolean { - // 请求url匹配时才进行注入 - val shouldInject = response.url().startsWith("https://lbs.map.qq.com/loc") - if (shouldInject) { - Log.i(TAG, "Start wechat location injection!") - } - return shouldInject - } - - override fun onResponseInject(header: HttpResponseHeaderPart, callback: InjectorCallback) { - // 由于响应体大小不确定,这里先hold住header(需要在后面修改content-length) - mHoldResponseHeader = header - } - - override fun onResponseInject(response: HttpResponse, body: HttpBody, callback: InjectorCallback) { - if (mHoldResponseHeader == null) { - // 一般不会发生 - return - } - var his: HttpBodyInputStream? = null - var reader: Reader? = null - var fos: DeflaterOutputStream? = null - var bos: ByteArrayOutputStream? = null - try { - his = HttpBodyInputStream(body) - // 数据使用了zlib编码,这里需要先解码 - reader = InputStreamReader(InflaterInputStream(his)) - val element = JsonParser().parse(reader) - if (element == null || !element.isJsonObject) { - return - } - val locationNode = element.asJsonObject.get("location") - if (locationNode == null || !locationNode.isJsonObject) { - return - } - // 替换经纬度,这里是珠峰的经纬度,装逼很厉害的地方 - val location = locationNode.asJsonObject - location.addProperty("latitude", 27.99136f) - location.addProperty("longitude", 86.88915f) - val injectedBody = element.toString() - // 重新使用zlib编码 - bos = ByteArrayOutputStream() - fos = DeflaterOutputStream(bos) - fos.write(injectedBody.toByteArray()) - fos.finish() - fos.flush() - val injectBodyData = bos.toByteArray() - // 更新header的content-length - val injectHeader = mHoldResponseHeader!! - .newBuilder() - .replaceHeader("Content-Length", injectBodyData.size.toString()) - .build() - // 先将header发射出去 - callback.onFinished(injectHeader) - // 再将响应体发射出去 - callback.onFinished(ByteStream(injectBodyData)) - Log.i(TAG, "Inject wechat location completed!") - } finally { - NetBareUtils.closeQuietly(his) - NetBareUtils.closeQuietly(reader) - NetBareUtils.closeQuietly(fos) - NetBareUtils.closeQuietly(bos) - } - mHoldResponseHeader = null - } - -} diff --git a/netbare-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/netbare-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index c7bd21d..0000000 --- a/netbare-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - diff --git a/netbare-sample/src/main/res/drawable/ic_launcher_background.xml b/netbare-sample/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index d5fccc5..0000000 --- a/netbare-sample/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/netbare-sample/src/main/res/layout/activity_main.xml b/netbare-sample/src/main/res/layout/activity_main.xml deleted file mode 100644 index a0ec0ea..0000000 --- a/netbare-sample/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - -