From 4e20047683b6804c910d6c1cbc0370e4e250ab90 Mon Sep 17 00:00:00 2001 From: Arcy Date: Tue, 23 Aug 2022 11:37:25 +0200 Subject: [PATCH 1/2] Update Project Structure Updated the project build files + Added GetSharedLibraries in IPackageManagerProxy --- Bcore/black-fake/build.gradle | 33 +- .../android/location/LocationRequest.java | 3 +- Bcore/black-hook/build.gradle | 37 +- Bcore/build.gradle | 48 +- Bcore/pine-core/build.gradle | 18 +- Bcore/pine-xposed-res/build.gradle | 22 +- Bcore/pine-xposed/build.gradle | 18 +- .../org/apache/commons/lang3/ArrayUtils.java | 4 +- .../org/apache/commons/lang3/StringUtils.java | 2 +- .../external/misc-helper/pthread_helper.cc | 34 +- .../core/modules/assembler/assembler-arm64.h | 2 +- Bcore/src/main/cpp/IO.cpp | 6 +- .../dispatcher/AppJobServiceDispatcher.java | 2 +- .../niunaijun/blackbox/core/NativeCore.java | 2 - .../blackbox/core/env/AppSystemEnv.java | 2 +- .../core/system/BProcessManagerService.java | 8 +- .../accounts/BAccountManagerService.java | 2 +- .../system/pm/BPackageManagerService.java | 31 +- .../core/system/pm/PackageManagerCompat.java | 4 +- .../blackbox/entity/location/BCell.java | 2 +- .../delegate/BaseInstrumentationDelegate.java | 3 + .../fake/service/IActivityManagerProxy.java | 2 +- .../service/IFingerprintManagerProxy.java | 3 + .../fake/service/IPackageManagerProxy.java | 18 +- .../fake/service/IShortcutManagerProxy.java | 5 +- .../service/IStorageStatsManagerProxy.java | 1 - .../service/context/LocationListenerStub.java | 4 +- .../blackbox/utils/HackAppUtils.java | 4 +- .../niunaijun/blackbox/utils/Reflector.java | 14 +- .../utils/compat/ActivityManagerCompat.java | 2 +- .../utils/compat/StrictModeCompat.java | 2 +- Bcore/src/main/res/xml/filepath.xml | 2 - android-mirror/build.gradle | 15 +- app/build.gradle | 76 ++- .../blackbox/ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 8 +- .../java/top/niunaijun/blackbox/app/App.kt | 35 ++ .../top/niunaijun/blackbox/app/AppManager.kt | 33 ++ .../rocker/BaseActivityLifecycleCallback.kt | 27 ++ .../blackbox/app/rocker/RockerManager.kt | 81 ++++ .../top/niunaijun/blackbox/bean/AppInfo.kt | 11 + .../blackbox/bean/FakeLocationBean.kt | 13 + .../top/niunaijun/blackbox/bean/GmsBean.kt | 11 + .../blackbox/bean/InstalledAppBean.kt | 11 + .../niunaijun/blackbox/bean/XpModuleInfo.kt | 18 + .../biz/cache/AppSharedPreferenceDelegate.kt | 65 +++ .../niunaijun/blackbox/data/AppsRepository.kt | 251 ++++++++++ .../niunaijun/blackbox/data/AppsSortCompon.kt | 21 + .../blackbox/data/FakeLocationRepository.kt | 74 +++ .../niunaijun/blackbox/data/GmsRepository.kt | 65 +++ .../niunaijun/blackbox/data/XpRepository.kt | 58 +++ .../niunaijun/blackbox/util/ContextUtil.kt | 22 + .../niunaijun/blackbox/util/InjectionUtil.kt | 44 ++ .../top/niunaijun/blackbox/util/MathUtil.java | 77 ++++ .../top/niunaijun/blackbox/util/ResUtil.kt | 11 + .../top/niunaijun/blackbox/util/Resolution.kt | 291 ++++++++++++ .../niunaijun/blackbox/util/ShortcutUtil.kt | 80 ++++ .../top/niunaijun/blackbox/util/ToastEx.kt | 28 ++ .../niunaijun/blackbox/util/ViewBindingEx.kt | 31 ++ .../blackbox/view/apps/AppsAdapter.kt | 35 ++ .../blackbox/view/apps/AppsFactory.kt | 18 + .../blackbox/view/apps/AppsFragment.kt | 341 ++++++++++++++ .../blackbox/view/apps/AppsTouchCallBack.kt | 28 ++ .../blackbox/view/apps/AppsViewModel.kt | 56 +++ .../blackbox/view/base/BaseActivity.kt | 32 ++ .../blackbox/view/base/BaseViewModel.kt | 30 ++ .../blackbox/view/base/LoadingActivity.kt | 40 ++ .../blackbox/view/fake/FakeLocationAdapter.kt | 38 ++ .../blackbox/view/fake/FakeLocationFactory.kt | 19 + .../view/fake/FakeLocationViewModel.kt | 34 ++ .../blackbox/view/fake/FakeManagerActivity.kt | 168 +++++++ .../view/fake/FollowMyLocationOverlay.kt | 145 ++++++ .../niunaijun/blackbox/view/gms/GmsAdapter.kt | 35 ++ .../niunaijun/blackbox/view/gms/GmsFactory.kt | 18 + .../blackbox/view/gms/GmsManagerActivity.kt | 127 +++++ .../blackbox/view/gms/GmsViewModel.kt | 36 ++ .../blackbox/view/list/ListActivity.kt | 163 +++++++ .../blackbox/view/list/ListAdapter.kt | 36 ++ .../blackbox/view/list/ListFactory.kt | 19 + .../blackbox/view/list/ListViewModel.kt | 35 ++ .../blackbox/view/main/BlackBoxLoader.kt | 124 +++++ .../blackbox/view/main/MainActivity.kt | 181 ++++++++ .../blackbox/view/main/ShortcutActivity.kt | 26 ++ .../blackbox/view/main/ViewPagerAdapter.kt | 29 ++ .../blackbox/view/main/WelcomeActivity.kt | 31 ++ .../blackbox/view/setting/SettingActivity.kt | 30 ++ .../blackbox/view/setting/SettingFragment.kt | 102 ++++ .../niunaijun/blackbox/view/xp/XpActivity.kt | 135 ++++++ .../niunaijun/blackbox/view/xp/XpAdapter.kt | 37 ++ .../niunaijun/blackbox/view/xp/XpFactory.kt | 19 + .../niunaijun/blackbox/view/xp/XpViewModel.kt | 35 ++ .../niunaijun/blackbox/widget/EnFloatView.kt | 50 ++ .../niunaijun/blackbox/widget/RockerView.java | 434 ++++++++++++++++++ app/src/main/res/layout/view_float_rocker.xml | 2 +- .../top/niunaijun/blackbox/ExampleUnitTest.kt | 17 + build.gradle | 45 +- gradle.properties | 11 +- gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 5 +- gradlew | 53 ++- gradlew.bat | 43 +- settings.gradle | 31 +- 102 files changed, 4393 insertions(+), 321 deletions(-) create mode 100644 app/src/androidTest/java/top/niunaijun/blackbox/ExampleInstrumentedTest.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/app/App.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/app/AppManager.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/app/rocker/BaseActivityLifecycleCallback.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/app/rocker/RockerManager.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/bean/AppInfo.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/bean/FakeLocationBean.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/bean/GmsBean.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/bean/InstalledAppBean.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/bean/XpModuleInfo.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/biz/cache/AppSharedPreferenceDelegate.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/data/AppsRepository.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/data/AppsSortCompon.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/data/FakeLocationRepository.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/data/GmsRepository.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/data/XpRepository.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/util/ContextUtil.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/util/InjectionUtil.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/util/MathUtil.java create mode 100644 app/src/main/java/top/niunaijun/blackbox/util/ResUtil.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/util/Resolution.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/util/ShortcutUtil.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/util/ToastEx.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/util/ViewBindingEx.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/apps/AppsAdapter.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/apps/AppsFactory.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/apps/AppsFragment.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/apps/AppsTouchCallBack.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/apps/AppsViewModel.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/base/BaseActivity.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/base/BaseViewModel.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/base/LoadingActivity.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/fake/FakeLocationAdapter.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/fake/FakeLocationFactory.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/fake/FakeLocationViewModel.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/fake/FakeManagerActivity.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/fake/FollowMyLocationOverlay.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/gms/GmsAdapter.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/gms/GmsFactory.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/gms/GmsManagerActivity.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/gms/GmsViewModel.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/list/ListActivity.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/list/ListAdapter.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/list/ListFactory.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/list/ListViewModel.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/main/BlackBoxLoader.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/main/MainActivity.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/main/ShortcutActivity.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/main/ViewPagerAdapter.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/main/WelcomeActivity.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/setting/SettingActivity.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/setting/SettingFragment.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/xp/XpActivity.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/xp/XpAdapter.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/xp/XpFactory.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/view/xp/XpViewModel.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/widget/EnFloatView.kt create mode 100644 app/src/main/java/top/niunaijun/blackbox/widget/RockerView.java create mode 100644 app/src/test/java/top/niunaijun/blackbox/ExampleUnitTest.kt diff --git a/Bcore/black-fake/build.gradle b/Bcore/black-fake/build.gradle index d11b89f1..57cfbc7c 100644 --- a/Bcore/black-fake/build.gradle +++ b/Bcore/black-fake/build.gradle @@ -1,15 +1,15 @@ plugins { id 'com.android.library' + id 'org.jetbrains.kotlin.android' } -apply plugin: 'kotlin-android' android { - compileSdkVersion 30 - buildToolsVersion "30.0.3" + compileSdk rootProject.ext.compileSdk + buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { - minSdkVersion 21 - targetSdkVersion rootProject.ext.targetSdkVersion + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdk consumerProguardFiles "consumer-rules.pro" } @@ -25,24 +25,19 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } - aaptOptions { - cruncherEnabled = false - useNewCruncher = false - } - testOptions { unitTests.returnDefaultValues = true } - - lintOptions { - checkReleaseBuilds false + lint { abortOnError false - warningsAsErrors false - disable "UnusedResources", 'RestrictedApi' - textOutput "stdout" - textReport false checkOnly 'NewApi', 'InlinedApi' + checkReleaseBuilds false + disable 'UnusedResources', 'RestrictedApi' + textOutput file('stdout') + textReport false + warningsAsErrors false } + } tasks.withType(Javadoc) { @@ -56,7 +51,3 @@ dependencies { implementation "androidx.core:core-ktx:$ktx_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } - -repositories { - mavenCentral() -} \ No newline at end of file diff --git a/Bcore/black-fake/src/main/java/android/location/LocationRequest.java b/Bcore/black-fake/src/main/java/android/location/LocationRequest.java index ad683321..83e8a84b 100644 --- a/Bcore/black-fake/src/main/java/android/location/LocationRequest.java +++ b/Bcore/black-fake/src/main/java/android/location/LocationRequest.java @@ -26,6 +26,5 @@ public int describeContents() { } @Override - public void writeToParcel(Parcel dest, int flags) { - } + public void writeToParcel(Parcel dest, int flags) { } } diff --git a/Bcore/black-hook/build.gradle b/Bcore/black-hook/build.gradle index e67b7592..be95aa76 100644 --- a/Bcore/black-hook/build.gradle +++ b/Bcore/black-hook/build.gradle @@ -1,15 +1,15 @@ plugins { id 'com.android.library' + id 'org.jetbrains.kotlin.android' } -apply plugin: 'kotlin-android' android { - compileSdkVersion 30 - buildToolsVersion "30.0.3" + compileSdk rootProject.ext.compileSdk + buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { - minSdkVersion 21 - targetSdkVersion rootProject.ext.targetSdkVersion + minSdkVersion rootProject.ext.minSdkVersion + targetSdk rootProject.ext.targetSdk consumerProguardFiles "consumer-rules.pro" } @@ -26,24 +26,19 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } - aaptOptions { - cruncherEnabled = false - useNewCruncher = false - } - testOptions { unitTests.returnDefaultValues = true } - - lintOptions { - checkReleaseBuilds false + lint { abortOnError false - warningsAsErrors false - disable "UnusedResources", 'RestrictedApi' - textOutput "stdout" - textReport false checkOnly 'NewApi', 'InlinedApi' + checkReleaseBuilds false + disable 'UnusedResources', 'RestrictedApi' + textOutput file('stdout') + textReport false + warningsAsErrors false } + } tasks.withType(Javadoc) { @@ -53,12 +48,8 @@ tasks.withType(Javadoc) { } dependencies { - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.google.android.material:material:1.3.0' + implementation 'androidx.appcompat:appcompat:1.5.0' + implementation 'com.google.android.material:material:1.6.1' implementation "androidx.core:core-ktx:$ktx_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } - -repositories { - mavenCentral() -} \ No newline at end of file diff --git a/Bcore/build.gradle b/Bcore/build.gradle index 66e5dff8..db52a55c 100644 --- a/Bcore/build.gradle +++ b/Bcore/build.gradle @@ -1,12 +1,14 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} android { - compileSdkVersion 32 + compileSdk rootProject.ext.compileSdk defaultConfig { - minSdkVersion 21 - targetSdkVersion rootProject.ext.targetSdkVersion + minSdkVersion rootProject.ext.minSdkVersion + targetSdk rootProject.ext.targetSdk consumerProguardFiles "consumer-rules.pro" ndk { @@ -37,14 +39,18 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } - aaptOptions { - cruncherEnabled = false - useNewCruncher = false - } - testOptions { unitTests.returnDefaultValues = true } + lint { + abortOnError false + checkOnly 'NewApi', 'InlinedApi' + checkReleaseBuilds false + disable 'UnusedResources', 'RestrictedApi' + textOutput file('stdout') + textReport false + warningsAsErrors false + } defaultConfig { ndk { @@ -52,23 +58,10 @@ android { } } - lintOptions { - checkReleaseBuilds false - abortOnError false - warningsAsErrors false - disable "UnusedResources", 'RestrictedApi' - textOutput "stdout" - textReport false - checkOnly 'NewApi', 'InlinedApi' - } buildFeatures { prefab true } - - packagingOptions { - exclude '**/libshadowhook.so' - } } tasks.withType(Javadoc) { @@ -79,12 +72,11 @@ tasks.withType(Javadoc) { dependencies { implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.appcompat:appcompat:1.5.0' implementation "com.github.CodingGay.BlackReflection:core:$rootProject.ext.blackReflection" implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3' - implementation 'com.bytedance.android:shadowhook:1.0.3' - implementation 'com.iqiyi.xcrash:xcrash-android-lib:3.0.0' + implementation 'com.iqiyi.xcrash:xcrash-android-lib:3.1.0' implementation project(':Bcore:black-fake') implementation project(':Bcore:black-hook') @@ -96,7 +88,3 @@ dependencies { implementation "androidx.core:core-ktx:$ktx_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } - -repositories { - mavenCentral() -} \ No newline at end of file diff --git a/Bcore/pine-core/build.gradle b/Bcore/pine-core/build.gradle index 27ed57cb..9ab4af3f 100644 --- a/Bcore/pine-core/build.gradle +++ b/Bcore/pine-core/build.gradle @@ -1,16 +1,16 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} android { - compileSdkVersion rootProject.ext.compileSdkVersion + compileSdk rootProject.ext.compileSdk buildToolsVersion rootProject.ext.buildToolsVersion ndkVersion rootProject.ext.ndkVersion defaultConfig { - minSdkVersion 19 - targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1 - versionName "1.0" + minSdkVersion rootProject.ext.minSdkVersion + targetSdk rootProject.ext.targetSdk testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' @@ -49,7 +49,3 @@ dependencies { implementation "androidx.core:core-ktx:$ktx_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } - -repositories { - mavenCentral() -} \ No newline at end of file diff --git a/Bcore/pine-xposed-res/build.gradle b/Bcore/pine-xposed-res/build.gradle index d1c0ccad..1137ecc7 100644 --- a/Bcore/pine-xposed-res/build.gradle +++ b/Bcore/pine-xposed-res/build.gradle @@ -1,12 +1,14 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} android { - compileSdkVersion rootProject.ext.compileSdkVersion + compileSdk rootProject.ext.compileSdk defaultConfig { - minSdkVersion 19 - targetSdkVersion rootProject.ext.targetSdkVersion + minSdkVersion rootProject.ext.minSdkVersion + targetSdk rootProject.ext.targetSdk testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' @@ -33,11 +35,11 @@ android { cmake { path "src/main/cpp/CMakeLists.txt" } - }*/ + } buildFeatures { - // prefab true - } + prefab true + }*/ } dependencies { @@ -48,7 +50,3 @@ dependencies { implementation "androidx.core:core-ktx:$ktx_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } - -repositories { - mavenCentral() -} diff --git a/Bcore/pine-xposed/build.gradle b/Bcore/pine-xposed/build.gradle index a6c81c14..d04a5164 100644 --- a/Bcore/pine-xposed/build.gradle +++ b/Bcore/pine-xposed/build.gradle @@ -1,15 +1,15 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} android { - compileSdkVersion rootProject.ext.compileSdkVersion + compileSdk rootProject.ext.compileSdk buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { - minSdkVersion 19 - targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1 - versionName "1.0" + minSdkVersion rootProject.ext.minSdkVersion + targetSdk rootProject.ext.targetSdk testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' @@ -35,7 +35,3 @@ dependencies { implementation "androidx.core:core-ktx:$ktx_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } - -repositories { - mavenCentral() -} \ No newline at end of file diff --git a/Bcore/pine-xposed/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ArrayUtils.java b/Bcore/pine-xposed/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ArrayUtils.java index 8ca830e1..c8a65089 100644 --- a/Bcore/pine-xposed/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ArrayUtils.java +++ b/Bcore/pine-xposed/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ArrayUtils.java @@ -3439,7 +3439,7 @@ public static T[] addAll(T[] array1, T... array2) { * - safer, in case check turns out to be too strict */ final Class type2 = array2.getClass().getComponentType(); - if (!type1.isAssignableFrom(type2)){ + if (!type1.isAssignableFrom(type2)) { throw new IllegalArgumentException("Cannot store "+type2.getName()+" in an array of " +type1.getName(), ase); } @@ -3711,7 +3711,7 @@ public static double[] addAll(double[] array1, double... array2) { */ public static T[] add(T[] array, T element) { Class type; - if (array != null){ + if (array != null) { type = array.getClass(); } else if (element != null) { type = element.getClass(); diff --git a/Bcore/pine-xposed/src/main/apacheCommonsLang/external/org/apache/commons/lang3/StringUtils.java b/Bcore/pine-xposed/src/main/apacheCommonsLang/external/org/apache/commons/lang3/StringUtils.java index e4b2fadd..a7830ce6 100644 --- a/Bcore/pine-xposed/src/main/apacheCommonsLang/external/org/apache/commons/lang3/StringUtils.java +++ b/Bcore/pine-xposed/src/main/apacheCommonsLang/external/org/apache/commons/lang3/StringUtils.java @@ -3598,7 +3598,7 @@ public static String removeStart(String str, String remove) { if (isEmpty(str) || isEmpty(remove)) { return str; } - if (str.startsWith(remove)){ + if (str.startsWith(remove)) { return str.substring(remove.length()); } return str; diff --git a/Bcore/src/main/cpp/Dobby/external/misc-helper/pthread_helper.cc b/Bcore/src/main/cpp/Dobby/external/misc-helper/pthread_helper.cc index a539fc1d..99a48c5c 100644 --- a/Bcore/src/main/cpp/Dobby/external/misc-helper/pthread_helper.cc +++ b/Bcore/src/main/cpp/Dobby/external/misc-helper/pthread_helper.cc @@ -9,9 +9,9 @@ int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void { uintptr_t handle = _beginthread((windows_thread)start_routine,0,arg); thread->handle = (HANDLE)handle; - if(thread->handle == (HANDLE)-1){ + if (thread->handle == (HANDLE)-1) { return 1; - }else{ + } else { return 0; } } @@ -30,9 +30,9 @@ void pthread_exit(void *value_ptr) int pthread_join(pthread_t thread, void **value_ptr) { DWORD retvalue = WaitForSingleObject(thread.handle,INFINITE); - if(retvalue == WAIT_OBJECT_0){ + if (retvalue == WAIT_OBJECT_0) { return 0; - }else{ + } else { return EINVAL; } } @@ -73,10 +73,10 @@ int pthread_mutex_destroy(pthread_mutex_t *mutex) int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr) { HANDLE handle = CreateMutex(NULL,FALSE,NULL); - if(handle != NULL){ + if (handle != NULL) { mutex->handle = handle; return 0; - }else{ + } else { return 1; } } @@ -84,9 +84,9 @@ int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr) int pthread_mutex_lock(pthread_mutex_t *mutex) { DWORD retvalue = WaitForSingleObject(mutex->handle,INFINITE); - if(retvalue == WAIT_OBJECT_0){ + if (retvalue == WAIT_OBJECT_0) { return 0; - }else{ + } else { return EINVAL; } } @@ -94,11 +94,11 @@ int pthread_mutex_lock(pthread_mutex_t *mutex) int pthread_mutex_trylock(pthread_mutex_t *mutex) { DWORD retvalue = WaitForSingleObject(mutex->handle,0); - if(retvalue == WAIT_OBJECT_0){ + if (retvalue == WAIT_OBJECT_0) { return 0; - }else if(retvalue == WAIT_TIMEOUT){ + } else if (retvalue == WAIT_TIMEOUT) { return EBUSY; - }else{ + } else { return EINVAL; } } @@ -113,28 +113,28 @@ int pthread_mutex_unlock(pthread_mutex_t *mutex) int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *)) { DWORD dkey = TlsAlloc(); - if(dkey != 0xFFFFFFFF){ + if (dkey != 0xFFFFFFFF) { *key = dkey; return 0; - }else{ + } else { return EAGAIN; } } int pthread_key_delete(pthread_key_t key) { - if(TlsFree(key)){ + if (TlsFree(key)) { return 0; - }else{ + } else { return EINVAL; } } int pthread_setspecific(pthread_key_t key, const void *pointer) { - if(TlsSetValue(key,(LPVOID)pointer)){ + if (TlsSetValue(key,(LPVOID)pointer)) { return 0; - }else{ + } else { return EINVAL; } } diff --git a/Bcore/src/main/cpp/Dobby/source/core/modules/assembler/assembler-arm64.h b/Bcore/src/main/cpp/Dobby/source/core/modules/assembler/assembler-arm64.h index 4aeb4c01..f3faa470 100644 --- a/Bcore/src/main/cpp/Dobby/source/core/modules/assembler/assembler-arm64.h +++ b/Bcore/src/main/cpp/Dobby/source/core/modules/assembler/assembler-arm64.h @@ -104,7 +104,7 @@ class PseudoLabel : public Label { private: #if 0 // From a design perspective, these fix-function write as callback, maybe beeter. - void FixLdr(PseudoLabelInstruction *instruction){ + void FixLdr(PseudoLabelInstruction *instruction) { // dummy }; #endif diff --git a/Bcore/src/main/cpp/IO.cpp b/Bcore/src/main/cpp/IO.cpp index 105e1614..bc32ece0 100644 --- a/Bcore/src/main/cpp/IO.cpp +++ b/Bcore/src/main/cpp/IO.cpp @@ -44,7 +44,7 @@ const char *IO::redirectPath(const char *__path) { IO::RelocateInfo info = *iterator; if (strstr(__path, info.targetPath) && !strstr(__path, "/blackbox/")) { char *ret = replace(__path, info.targetPath, info.relocatePath); - // ALOGD("redirectPath %s => %s", __path, ret); + //ALOGD("redirectPath %s => %s", __path, ret); return ret; } } @@ -52,7 +52,7 @@ const char *IO::redirectPath(const char *__path) { } //#ifdef __arm__ https://developer.android.com/games/optimize/64-bit?hl=zh-cn -HOOK_JNI(void *, openat, int fd, const char *pathname, int flags, int mode){ +HOOK_JNI(void *, openat, int fd, const char *pathname, int flags, int mode) { // 执行 stack 清理(不可省略),只需调用一次 // SHADOWHOOK_STACK_SCOPE(); list::iterator white_iterator; @@ -75,7 +75,7 @@ HOOK_JNI(void *, openat, int fd, const char *pathname, int flags, int mode){ return orig_openat(fd, pathname, flags, mode); } -HOOK_JNI(FILE *, popen, const char* cmd, const char* mode){ +HOOK_JNI(FILE *, popen, const char* cmd, const char* mode) { // 执行 stack 清理(不可省略),只需调用一次 // SHADOWHOOK_STACK_SCOPE(); diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/app/dispatcher/AppJobServiceDispatcher.java b/Bcore/src/main/java/top/niunaijun/blackbox/app/dispatcher/AppJobServiceDispatcher.java index 55268c98..9034bcc7 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/app/dispatcher/AppJobServiceDispatcher.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/app/dispatcher/AppJobServiceDispatcher.java @@ -91,7 +91,7 @@ JobService getJobService(int jobId) { } try { JobRecord record = BlackBoxCore.getBJobManager().queryJobRecord(BActivityThread.getAppProcessName(), jobId); - if (record == null){ + if (record == null) { return null; } record.mJobService = BActivityThread.currentActivityThread().createJobService(record.mServiceInfo); diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/core/NativeCore.java b/Bcore/src/main/java/top/niunaijun/blackbox/core/NativeCore.java index daa05166..c11a9d4a 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/core/NativeCore.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/core/NativeCore.java @@ -16,8 +16,6 @@ import static top.niunaijun.blackbox.core.env.BEnvironment.EMPTY_JAR; -import com.bytedance.shadowhook.ShadowHook; - /** * Created by Milk on 4/9/21. * * ∧_∧ diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/core/env/AppSystemEnv.java b/Bcore/src/main/java/top/niunaijun/blackbox/core/env/AppSystemEnv.java index d68a76f7..e85dde51 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/core/env/AppSystemEnv.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/core/env/AppSystemEnv.java @@ -59,7 +59,7 @@ public class AppSystemEnv { sXposedPackages.add("de.robv.android.xposed.installer"); // sPreInstallPackages.add("com.huawei.hwid"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < 29){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < 29) { //解决Android 9三星浏览器闪退问题 } else { diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/core/system/BProcessManagerService.java b/Bcore/src/main/java/top/niunaijun/blackbox/core/system/BProcessManagerService.java index c8954dda..deccf9c0 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/core/system/BProcessManagerService.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/core/system/BProcessManagerService.java @@ -108,17 +108,17 @@ public ProcessRecord startProcessLocked(String packageName, String processName, return app; } - private void killProcess(final ProcessRecord app){ + private void killProcess(final ProcessRecord app) { // make sure process be killed - if(app.pid > 0 ){ + if(app.pid > 0) { Process.killProcess(app.pid); - }else{ + } else { try { ActivityManager manager = (ActivityManager) BlackBoxCore.getContext().getSystemService(Context.ACTIVITY_SERVICE); List runningAppProcesses = manager.getRunningAppProcesses(); for (ActivityManager.RunningAppProcessInfo runningAppProcess : runningAppProcesses) { int bpid = parseBPid(runningAppProcess.processName); - if(bpid != -1 && app.bpid == bpid){ + if (bpid != -1 && app.bpid == bpid) { Slog.d(TAG, "force kill process: "+app.processName+", pid: "+runningAppProcess.pid+", bpid: "+bpid); Process.killProcess(runningAppProcess.pid); } diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/core/system/accounts/BAccountManagerService.java b/Bcore/src/main/java/top/niunaijun/blackbox/core/system/accounts/BAccountManagerService.java index 15da59af..15227843 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/core/system/accounts/BAccountManagerService.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/core/system/accounts/BAccountManagerService.java @@ -862,7 +862,7 @@ protected String readAuthTokenInternal(BUserAccounts accounts, Account account, private void completeCloningAccount(IAccountManagerResponse response, final Bundle accountCredentials, final Account account, final BUserAccounts targetUser, - final int parentUserId){ + final int parentUserId) { new Session(targetUser, response, account.type, false, false /* stripAuthTokenFromResult */, account.name, false /* authDetailsRequired */) { diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/core/system/pm/BPackageManagerService.java b/Bcore/src/main/java/top/niunaijun/blackbox/core/system/pm/BPackageManagerService.java index 6a986c0e..a03b336f 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/core/system/pm/BPackageManagerService.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/core/system/pm/BPackageManagerService.java @@ -15,6 +15,7 @@ import android.content.pm.ServiceInfo; import android.net.Uri; import android.os.Binder; +import android.os.Build; import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; @@ -66,7 +67,6 @@ public class BPackageManagerService extends IBPackageManagerService.Stub impleme private static final BUserManagerService sUserManager = BUserManagerService.get(); private final List mPackageMonitors = new ArrayList<>(); - final Map mPackages = mSettings.mPackages; final Object mInstallLock = new Object(); @@ -80,22 +80,21 @@ public BPackageManagerService() { filter.addAction("android.intent.action.PACKAGE_ADDED"); filter.addAction("android.intent.action.PACKAGE_REMOVED"); filter.addDataScheme("package"); + BroadcastReceiver mPackageChangedHandler = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (!TextUtils.isEmpty(action)) { + if ("android.intent.action.PACKAGE_ADDED".equals(action) || "android.intent.action.PACKAGE_REMOVED".equals(action)) { + mSettings.scanPackage(); + } + } + } + }; BlackBoxCore.getContext() .registerReceiver(mPackageChangedHandler, filter); } - private final BroadcastReceiver mPackageChangedHandler = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (!TextUtils.isEmpty(action)) { - if ("android.intent.action.PACKAGE_ADDED".equals(action) || "android.intent.action.PACKAGE_REMOVED".equals(action)) { - mSettings.scanPackage(); - } - } - } - }; - @Override public ApplicationInfo getApplicationInfo(String packageName, int flags, int userId) { if (!sUserManager.exists(userId)) return null; @@ -771,14 +770,16 @@ static String fixProcessName(String defProcessName, String processName) { * Update given flags based on encryption status of current user. */ private int updateFlags(int flags, int userId) { - if ((flags & (PackageManager.MATCH_DIRECT_BOOT_UNAWARE + if ((flags & (MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DIRECT_BOOT_AWARE)) != 0) { // Caller expressed an explicit opinion about what encryption // aware/unaware components they want to see, so fall through and // give them what they want } else { // Caller expressed no opinion, so match based on user state - flags |= PackageManager.MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + flags |= PackageManager.MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE; + } } return flags; } diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/core/system/pm/PackageManagerCompat.java b/Bcore/src/main/java/top/niunaijun/blackbox/core/system/pm/PackageManagerCompat.java index 71a9570a..7af8d7d7 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/core/system/pm/PackageManagerCompat.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/core/system/pm/PackageManagerCompat.java @@ -192,8 +192,8 @@ public static PackageInfo generatePackageInfo(BPackage p, int flags, long firstI base = BlackBoxCore.getPackageManager().getPackageInfo(p.packageName, flags); } catch (PackageManager.NameNotFoundException ignored) { } - if(base != null){ - if(base.splitNames != null){ + if (base != null) { + if (base.splitNames != null) { pi.splitNames = base.splitNames; } } diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/entity/location/BCell.java b/Bcore/src/main/java/top/niunaijun/blackbox/entity/location/BCell.java index 38b247d8..55df5bc3 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/entity/location/BCell.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/entity/location/BCell.java @@ -67,7 +67,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.TYPE); } - public BCell(){} + public BCell() {} public BCell(int MCC, int MNC, int LAC, int CID) { this.TYPE = PHONE_TYPE_GSM; this.MCC = MCC; diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/delegate/BaseInstrumentationDelegate.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/delegate/BaseInstrumentationDelegate.java index 38b693e5..a6e368c3 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/delegate/BaseInstrumentationDelegate.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/delegate/BaseInstrumentationDelegate.java @@ -20,6 +20,8 @@ import android.view.KeyEvent; import android.view.MotionEvent; +import androidx.annotation.RequiresApi; + import top.niunaijun.blackbox.BlackBoxCore; import top.niunaijun.blackbox.app.BActivityThread; import top.niunaijun.blackbox.app.configuration.AppLifecycleCallback; @@ -55,6 +57,7 @@ public void sendStatus(int resultCode, Bundle results) { mBaseInstrumentation.sendStatus(resultCode, results); } + @RequiresApi(api = Build.VERSION_CODES.O) @Override public void addResults(Bundle results) { mBaseInstrumentation.addResults(results); diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IActivityManagerProxy.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IActivityManagerProxy.java index 1887728a..03e55146 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IActivityManagerProxy.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IActivityManagerProxy.java @@ -115,7 +115,7 @@ protected Object hook(Object who, Method method, Object[] args) throws Exception /*try { BlackBoxCore.getBPackageManager().resolveContentProvider((String) auth, GET_META_DATA, BActivityThread.getUserId()); - } catch (Exception e){ + } catch (Exception e) { Log.e(TAG, "BlackBoxCore.getBPackageManager().resolveContentProvider Failed"); e.printStackTrace(); } diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IFingerprintManagerProxy.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IFingerprintManagerProxy.java index 4bfa0ebc..2865c583 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IFingerprintManagerProxy.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IFingerprintManagerProxy.java @@ -1,8 +1,11 @@ package top.niunaijun.blackbox.fake.service; import android.content.Context; +import android.os.Build; import android.os.IBinder; +import androidx.annotation.RequiresApi; + import black.android.os.BRServiceManager; import black.android.view.BRIGraphicsStatsStub; import top.niunaijun.blackbox.fake.hook.BinderInvocationStub; diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IPackageManagerProxy.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IPackageManagerProxy.java index 2469877d..6fb7d169 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IPackageManagerProxy.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IPackageManagerProxy.java @@ -20,6 +20,8 @@ import top.niunaijun.blackbox.BlackBoxCore; import top.niunaijun.blackbox.app.BActivityThread; import top.niunaijun.blackbox.core.env.AppSystemEnv; +import top.niunaijun.blackbox.core.system.pm.BPackageManagerService; +import top.niunaijun.blackbox.core.system.pm.BPackageSettings; import top.niunaijun.blackbox.fake.hook.BinderInvocationStub; import top.niunaijun.blackbox.fake.hook.MethodHook; import top.niunaijun.blackbox.fake.hook.ProxyMethod; @@ -347,8 +349,20 @@ protected Object hook(Object who, Method method, Object[] args) throws Throwable public static class GetSharedLibraries extends MethodHook { @Override protected Object hook(Object who, Method method, Object[] args) throws Throwable { - // todo - return ParceledListSliceCompat.create(new ArrayList<>()); + String packageName = (String) args[0]; + BPackageSettings packageSettings = BPackageManagerService.get().getBPackageSetting(packageName); + if (packageSettings != null) { + ArrayList packageLibraries = new ArrayList<>(); + if (packageSettings.pkg.usesLibraries != null) { + packageLibraries.addAll(packageSettings.pkg.usesLibraries); + } + + if (packageSettings.pkg.usesOptionalLibraries != null) { + packageLibraries.addAll(packageSettings.pkg.usesOptionalLibraries); + } + return ParceledListSliceCompat.create(packageLibraries); + } + return method.invoke(who, args); } } diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IShortcutManagerProxy.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IShortcutManagerProxy.java index 5d823158..42f30765 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IShortcutManagerProxy.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IShortcutManagerProxy.java @@ -3,6 +3,9 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ShortcutInfo; +import android.os.Build; + +import androidx.annotation.RequiresApi; import java.lang.reflect.Method; import java.util.ArrayList; @@ -61,7 +64,7 @@ protected void onBindMethod() { addMethodHook(new PkgMethodProxy("removeAllDynamicShortcuts")); addMethodHook(new PkgMethodProxy("removeDynamicShortcuts")); addMethodHook(new PkgMethodProxy("removeLongLivedShortcuts")); - addMethodHook(new PkgMethodProxy("getManifestShortcuts"){ + addMethodHook(new PkgMethodProxy("getManifestShortcuts") { @Override protected Object hook(Object who, Method method, Object[] args) throws Throwable { return ParceledListSliceCompat.create(new ArrayList()); diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IStorageStatsManagerProxy.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IStorageStatsManagerProxy.java index 980d310f..bb3c294b 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IStorageStatsManagerProxy.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IStorageStatsManagerProxy.java @@ -12,7 +12,6 @@ /** * Created by BlackBox on 2022/3/3. */ -//@TargetApi(Build.VERSION_CODES.O) public class IStorageStatsManagerProxy extends BinderInvocationStub { public IStorageStatsManagerProxy() { super(BRServiceManager.get().getService(Context.STORAGE_STATS_SERVICE)); diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/context/LocationListenerStub.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/context/LocationListenerStub.java index 44d2af81..dbc6cf66 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/context/LocationListenerStub.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/context/LocationListenerStub.java @@ -48,12 +48,12 @@ public static class OnLocationChanged extends MethodHook { @Override protected Object hook(Object who, Method method, Object[] args) throws Throwable { - if(args[0] instanceof List){ + if (args[0] instanceof List) { List locations = (List) args[0]; locations.clear(); locations.add(BLocationManager.get().getLocation(BActivityThread.getUserId(), BActivityThread.getAppPackageName()).convert2SystemLocation()); args[0] = locations; - }else if(args[0] instanceof Location){ + } else if(args[0] instanceof Location) { args[0] = BLocationManager.get().getLocation(BActivityThread.getUserId(), BActivityThread.getAppPackageName()).convert2SystemLocation(); } return method.invoke(who, args); diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/utils/HackAppUtils.java b/Bcore/src/main/java/top/niunaijun/blackbox/utils/HackAppUtils.java index b23ef67d..997097db 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/utils/HackAppUtils.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/utils/HackAppUtils.java @@ -46,13 +46,13 @@ private static void injectLine(ClassLoader classLoader) { } } - public static void hackApp(String packageName, ClassLoader classLoader){ + public static void hackApp(String packageName, ClassLoader classLoader) { /*if ("jp.naver.line.android".equals(packageName)) { injectLine(classLoader); }*/ } - public static void hackActivity(String packageName, ClassLoader classLoader){ + public static void hackActivity(String packageName, ClassLoader classLoader) { /*if ("com.tencent.mobileqq".equals(packageName)) { enableQQLogOutput(classLoader); }*/ diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/utils/Reflector.java b/Bcore/src/main/java/top/niunaijun/blackbox/utils/Reflector.java index d3b7cf29..e52894a6 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/utils/Reflector.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/utils/Reflector.java @@ -2,6 +2,9 @@ import android.annotation.SuppressLint; +import android.os.Build; + +import androidx.annotation.RequiresApi; import org.lsposed.hiddenapibypass.HiddenApiBypass; @@ -143,16 +146,17 @@ protected Field findInstanceField(String name) throws NoSuchFieldException { @SuppressWarnings("unchecked") protected Field findStaticField(String name) throws NoSuchFieldException { - List allStaticFields = HiddenApiBypass.getStaticFields(mType); - for (Field f : allStaticFields) { - if (f.getName().equals(name)) { - return f; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + List allStaticFields = HiddenApiBypass.getStaticFields(mType); + for (Field f : allStaticFields) { + if (f.getName().equals(name)) { + return f; + } } } throw new NoSuchFieldException(); } - @SuppressWarnings("unchecked") public R get() throws Exception { return get(mCaller); } diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/utils/compat/ActivityManagerCompat.java b/Bcore/src/main/java/top/niunaijun/blackbox/utils/compat/ActivityManagerCompat.java index 4062df7c..410a7299 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/utils/compat/ActivityManagerCompat.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/utils/compat/ActivityManagerCompat.java @@ -102,7 +102,7 @@ public static void setActivityOrientation(Activity activity, int orientation) { IBinder token = BRActivity.get(parent).mToken(); try { BRIActivityManager.get(BRActivityManagerNative.get().getDefault()).setRequestedOrientation(token, orientation); - } catch (Throwable ex){ + } catch (Throwable ex) { ex.printStackTrace(); } } diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/utils/compat/StrictModeCompat.java b/Bcore/src/main/java/top/niunaijun/blackbox/utils/compat/StrictModeCompat.java index c9fcafda..d2ab8931 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/utils/compat/StrictModeCompat.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/utils/compat/StrictModeCompat.java @@ -9,7 +9,7 @@ public class StrictModeCompat { public static int PENALTY_DEATH_ON_FILE_URI_EXPOSURE = BRStrictMode.get().PENALTY_DEATH_ON_FILE_URI_EXPOSURE() == null ? (0x04 << 24) : BRStrictMode.get().PENALTY_DEATH_ON_FILE_URI_EXPOSURE(); - public static void disableDeathOnFileUriExposure(){ + public static void disableDeathOnFileUriExposure() { try { BRStrictMode.get().disableDeathOnFileUriExposure(); } catch (Throwable e) { diff --git a/Bcore/src/main/res/xml/filepath.xml b/Bcore/src/main/res/xml/filepath.xml index 6e11ec24..080bee98 100644 --- a/Bcore/src/main/res/xml/filepath.xml +++ b/Bcore/src/main/res/xml/filepath.xml @@ -8,6 +8,4 @@ - - diff --git a/android-mirror/build.gradle b/android-mirror/build.gradle index 1ac45a5a..dcf87d4f 100644 --- a/android-mirror/build.gradle +++ b/android-mirror/build.gradle @@ -1,13 +1,12 @@ plugins { id 'com.android.library' + id 'org.jetbrains.kotlin.android' } -apply plugin: 'kotlin-android' android { - compileSdkVersion rootProject.ext.compileSdkVersion - - aidlPackageWhiteList "android/app/IServiceConnection.aidl" - aidlPackageWhiteList "android/accounts/IAccountManagerResponse.aidl" + compileSdk rootProject.ext.compileSdk + aidlPackagedList "android/app/IServiceConnection.aidl" + aidlPackagedList "android/accounts/IAccountManagerResponse.aidl" defaultConfig { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -28,14 +27,10 @@ android { } dependencies { - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.appcompat:appcompat:1.5.0' implementation project(':Bcore:black-fake') implementation "com.github.CodingGay.BlackReflection:core:$rootProject.ext.blackReflection" annotationProcessor "com.github.CodingGay.BlackReflection:compiler:$rootProject.ext.blackReflection" implementation "androidx.core:core-ktx:$ktx_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } - -repositories { - mavenCentral() -} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 73b03568..8a39fb43 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,25 +1,25 @@ plugins { id 'com.android.application' - id 'kotlin-android' + id 'org.jetbrains.kotlin.android' } -apply plugin: 'kotlin-android' android { - compileSdkVersion 32 + compileSdk rootProject.ext.compileSdk defaultConfig { - applicationId "top.niunaijun.blackboxa" - minSdkVersion 21 - targetSdkVersion rootProject.ext.targetSdkVersion + applicationId "top.niunaijun.blackbox" + minSdk rootProject.ext.minSdkVersion + targetSdk rootProject.ext.targetSdk versionCode 12 versionName "2.1.4" flavorDimensions "BlackBox32" + multiDexEnabled true } productFlavors { BlackBox32 { - applicationId "top.niunaijun.blackboxa32" + applicationId "top.niunaijun.blackbox32" ndk { abiFilters "armeabi-v7a" } @@ -29,7 +29,7 @@ android { } BlackBox64 { - applicationId "top.niunaijun.blackboxa64" + applicationId "top.niunaijun.blackbox64" ndk { abiFilters "arm64-v8a" } @@ -39,7 +39,7 @@ android { } BlackBox32Beta { - applicationId "top.niunaijun.blackboxa32_beta" + applicationId "top.niunaijun.blackbox32_beta" ndk { abiFilters "armeabi-v7a" } @@ -49,7 +49,7 @@ android { } BlackBox64Beta { - applicationId "top.niunaijun.blackboxa64_beta" + applicationId "top.niunaijun.blackbox64_beta" ndk { abiFilters "arm64-v8a" } @@ -86,30 +86,25 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } - aaptOptions { - cruncherEnabled = false - useNewCruncher = false - } - testOptions { unitTests.returnDefaultValues = true } - lintOptions { - checkReleaseBuilds false - abortOnError false - warningsAsErrors false - disable "UnusedResources", 'RestrictedApi' - textOutput "stdout" - textReport false - checkOnly 'NewApi', 'InlinedApi' - } sourceSets { main { jniLibs.srcDirs = ['libs'] } } + lint { + abortOnError false + checkOnly 'NewApi', 'InlinedApi' + checkReleaseBuilds false + disable 'UnusedResources', 'RestrictedApi' + textOutput file('stdout') + textReport false + warningsAsErrors false + } } tasks.withType(Javadoc) { @@ -118,34 +113,29 @@ tasks.withType(Javadoc) { options.addStringOption('charSet', 'UTF-8') } -repositories { - mavenCentral() -} - dependencies { - implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) implementation project(':Bcore') //android - implementation 'com.google.android.material:material:1.3.0' - implementation 'androidx.appcompat:appcompat:1.3.0-rc01' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation "androidx.recyclerview:recyclerview:1.2.0" + implementation 'com.google.android.material:material:1.6.1' + implementation 'androidx.appcompat:appcompat:1.6.0-beta01' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.viewpager2:viewpager2:1.0.0' - implementation "androidx.activity:activity-ktx:1.3.1" - implementation "androidx.fragment:fragment-ktx:1.3.3" - implementation "androidx.preference:preference-ktx:1.1.1" + implementation 'androidx.activity:activity-ktx:1.5.1' + implementation 'androidx.fragment:fragment-ktx:1.5.2' + implementation 'androidx.preference:preference-ktx:1.2.0' //coroutines - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' //viewModel liveData lifecycle - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' //third - implementation 'com.tbuonomo:dotsindicator:4.2' + implementation 'com.tbuonomo:dotsindicator:4.3' //viewPager2 指示点 implementation 'com.afollestad.material-dialogs:core:3.3.0' implementation 'com.afollestad.material-dialogs:input:3.3.0' @@ -160,7 +150,7 @@ dependencies { //图标角标 // osmdroid - implementation 'org.osmdroid:osmdroid-android:6.1.11' + implementation 'org.osmdroid:osmdroid-android:6.1.14' //adapter implementation 'com.gitee.cbfg5210:RVAdapter:0.3.7' @@ -168,4 +158,4 @@ dependencies { implementation 'com.imuxuan:floatingview:1.6' implementation "androidx.core:core-ktx:$ktx_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} +} \ No newline at end of file diff --git a/app/src/androidTest/java/top/niunaijun/blackbox/ExampleInstrumentedTest.kt b/app/src/androidTest/java/top/niunaijun/blackbox/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..457b0d26 --- /dev/null +++ b/app/src/androidTest/java/top/niunaijun/blackbox/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package top.niunaijun.blackbox + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("top.niunaijun.blackbox", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9f536b04..011676bb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,8 @@ + package="top.niunaijun.blackbox"> + @@ -23,6 +24,7 @@ @@ -37,10 +39,10 @@ android:name=".view.main.ShortcutActivity" android:excludeFromRecents="true" android:exported="true" /> - + - + \ No newline at end of file diff --git a/app/src/main/java/top/niunaijun/blackbox/app/App.kt b/app/src/main/java/top/niunaijun/blackbox/app/App.kt new file mode 100644 index 00000000..5200fa54 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/app/App.kt @@ -0,0 +1,35 @@ +package top.niunaijun.blackbox.app + +import android.annotation.SuppressLint +import android.app.Application +import android.content.Context + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/4/29 21:21 + */ +class App : Application() { + companion object { + @SuppressLint("StaticFieldLeak") + @Volatile + private lateinit var mContext: Context + + @JvmStatic + fun getContext(): Context { + return mContext + } + } + + override fun attachBaseContext(base: Context?) { + super.attachBaseContext(base) + mContext = base!! + AppManager.doAttachBaseContext(base) + } + + override fun onCreate() { + super.onCreate() + AppManager.doOnCreate() + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/app/AppManager.kt b/app/src/main/java/top/niunaijun/blackbox/app/AppManager.kt new file mode 100644 index 00000000..f75a6dd4 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/app/AppManager.kt @@ -0,0 +1,33 @@ +package top.niunaijun.blackbox.app + +import android.content.Context +import android.content.SharedPreferences +import top.niunaijun.blackbox.view.main.BlackBoxLoader + +object AppManager { + @JvmStatic + val mBlackBoxLoader by lazy { + BlackBoxLoader() + } + + @JvmStatic + val mRemarkSharedPreferences: SharedPreferences by lazy { + App.getContext().getSharedPreferences("UserRemark",Context.MODE_PRIVATE) + } + + fun doAttachBaseContext(context: Context) { + try { + mBlackBoxLoader.attachBaseContext(context) + mBlackBoxLoader.addLifecycleCallback() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun doOnCreate() { + mBlackBoxLoader.doOnCreate() + initThirdService() + } + + private fun initThirdService() { } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/app/rocker/BaseActivityLifecycleCallback.kt b/app/src/main/java/top/niunaijun/blackbox/app/rocker/BaseActivityLifecycleCallback.kt new file mode 100644 index 00000000..b4bf71ec --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/app/rocker/BaseActivityLifecycleCallback.kt @@ -0,0 +1,27 @@ +package top.niunaijun.blackbox.app.rocker + +import android.app.Activity +import android.app.Application +import android.os.Bundle + +/** + * + * @Description: + * @Author: kotlinMiku + * @CreateDate: 2022/3/19 20:08 + */ +interface BaseActivityLifecycleCallback : Application.ActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { } + + override fun onActivityStarted(activity: Activity) { } + + override fun onActivityResumed(activity: Activity) { } + + override fun onActivityPaused(activity: Activity) { } + + override fun onActivityStopped(activity: Activity) { } + + override fun onActivitySaveInstanceState(activity: Activity, p1: Bundle) { } + + override fun onActivityDestroyed(activity: Activity) { } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/app/rocker/RockerManager.kt b/app/src/main/java/top/niunaijun/blackbox/app/rocker/RockerManager.kt new file mode 100644 index 00000000..f6a27c28 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/app/rocker/RockerManager.kt @@ -0,0 +1,81 @@ +package top.niunaijun.blackbox.app.rocker + +import android.app.Activity +import android.app.Application +import android.view.Gravity +import android.widget.FrameLayout +import android.widget.RelativeLayout +import com.imuxuan.floatingview.FloatingMagnetView +import com.imuxuan.floatingview.FloatingView +import top.niunaijun.blackbox.app.App +import top.niunaijun.blackbox.entity.location.BLocation +import top.niunaijun.blackbox.fake.frameworks.BLocationManager +import top.niunaijun.blackbox.widget.EnFloatView +import kotlin.math.cos +import kotlin.math.sin + +/** + * + * @Description: + * @Author: kotlinMiku + * @CreateDate: 2022/3/19 19:37 + */ +object RockerManager { + private const val TAG = "RockerManager" + private const val Ea = 6378137 //赤道半径 + private const val Eb = 6356725 //极半径 + + fun init(application: Application?, userId: Int) { + if (application == null || !BLocationManager.isFakeLocationEnable()) { + return + } + + val enFloatView = initFloatView() + if (enFloatView is EnFloatView) { + enFloatView.setListener { angle: Float, distance: Float -> + changeLocation(distance, angle, application.packageName,userId) + } + } + + application.registerActivityLifecycleCallbacks(object : BaseActivityLifecycleCallback { + override fun onActivityStarted(activity: Activity) { + super.onActivityStarted(activity) + FloatingView.get().attach(activity) + } + + override fun onActivityStopped(activity: Activity) { + super.onActivityStopped(activity) + FloatingView.get().detach(activity) + } + }) + } + + private fun initFloatView(): FloatingMagnetView? { + val params = FrameLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT + ) + + params.gravity = Gravity.START or Gravity.CENTER + val view = EnFloatView(App.getContext()) + view.layoutParams = params + + FloatingView.get().customView(view) + return FloatingView.get().view + } + + private fun changeLocation(distance: Float, angle: Float, packageName: String, userId: Int) { + val location = BLocationManager.get().getLocation(userId, packageName) + + val dx = distance * sin(angle * Math.PI / 180.0) + val dy = distance * cos(angle * Math.PI / 180.0) + + val ec = Eb + (Ea - Eb) * (90.0 - location.latitude) / 90.0 + val ed = ec * cos(location.latitude * Math.PI / 180) + val newLng = (dx / ed + location.longitude * Math.PI / 180.0) * 180.0 / Math.PI + val newLat = (dy / ec + location.latitude * Math.PI / 180.0) * 180.0 / Math.PI + val newLocation = BLocation(newLat, newLng) + + BLocationManager.get().setLocation(userId, packageName, newLocation) + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/bean/AppInfo.kt b/app/src/main/java/top/niunaijun/blackbox/bean/AppInfo.kt new file mode 100644 index 00000000..9fb0347b --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/bean/AppInfo.kt @@ -0,0 +1,11 @@ +package top.niunaijun.blackbox.bean + +import android.graphics.drawable.Drawable + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/4/29 21:57 + */ +data class AppInfo(val name: String, val icon: Drawable, val packageName: String, val sourceDir: String, val isXpModule: Boolean) diff --git a/app/src/main/java/top/niunaijun/blackbox/bean/FakeLocationBean.kt b/app/src/main/java/top/niunaijun/blackbox/bean/FakeLocationBean.kt new file mode 100644 index 00000000..f722a63b --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/bean/FakeLocationBean.kt @@ -0,0 +1,13 @@ +package top.niunaijun.blackbox.bean + +import android.graphics.drawable.Drawable +import top.niunaijun.blackbox.entity.location.BLocation + +data class FakeLocationBean( + val userID: Int, + val name: String, + val icon: Drawable, + val packageName: String, + var fakeLocationPattern: Int, + var fakeLocation: BLocation? +) diff --git a/app/src/main/java/top/niunaijun/blackbox/bean/GmsBean.kt b/app/src/main/java/top/niunaijun/blackbox/bean/GmsBean.kt new file mode 100644 index 00000000..730468c3 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/bean/GmsBean.kt @@ -0,0 +1,11 @@ +package top.niunaijun.blackbox.bean + +/** + * + * @Description: + * @Author: BlackBox + * @CreateDate: 2022/3/2 21:30 + */ +data class GmsBean(val userID: Int, val userName: String, var isInstalledGms: Boolean) + +data class GmsInstallBean(val userID: Int, val success: Boolean, val msg: String) diff --git a/app/src/main/java/top/niunaijun/blackbox/bean/InstalledAppBean.kt b/app/src/main/java/top/niunaijun/blackbox/bean/InstalledAppBean.kt new file mode 100644 index 00000000..6967c41a --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/bean/InstalledAppBean.kt @@ -0,0 +1,11 @@ +package top.niunaijun.blackbox.bean + +import android.graphics.drawable.Drawable + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2022/3/6 19:26 + */ +data class InstalledAppBean(val name: String, val icon: Drawable, val packageName: String, val sourceDir: String, val isInstall: Boolean) diff --git a/app/src/main/java/top/niunaijun/blackbox/bean/XpModuleInfo.kt b/app/src/main/java/top/niunaijun/blackbox/bean/XpModuleInfo.kt new file mode 100644 index 00000000..9380d6cc --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/bean/XpModuleInfo.kt @@ -0,0 +1,18 @@ +package top.niunaijun.blackbox.bean + +import android.graphics.drawable.Drawable + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/5/2 21:03 + */ +data class XpModuleInfo( + val name: String, + val desc: String, + val packageName: String, + val version: String, + var enable:Boolean, + val icon: Drawable +) diff --git a/app/src/main/java/top/niunaijun/blackbox/biz/cache/AppSharedPreferenceDelegate.kt b/app/src/main/java/top/niunaijun/blackbox/biz/cache/AppSharedPreferenceDelegate.kt new file mode 100644 index 00000000..5edfbd39 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/biz/cache/AppSharedPreferenceDelegate.kt @@ -0,0 +1,65 @@ +package top.niunaijun.blackbox.biz.cache + +import android.content.Context +import android.text.TextUtils +import androidx.core.content.edit +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * + * @desc:目前只支持 5种基本数据类型,如果要支持obj,请继承该类并重写他的相关方法 findData/putData + * + * @author: mini + * @created by 2021/5/10 + */ +open class AppSharedPreferenceDelegate(context: Context, private val default: Data, spName: String? = null) : ReadWriteProperty { + private val mSharedPreferences by lazy { + val tmpCacheName = if (TextUtils.isEmpty(spName)) { + AppSharedPreferenceDelegate::class.java.simpleName + } else { + spName + } + return@lazy context.getSharedPreferences(tmpCacheName, Context.MODE_PRIVATE) + } + + override fun getValue(thisRef: Any, property: KProperty<*>): Data { + return findData(property.name, default) + } + + override fun setValue(thisRef: Any, property: KProperty<*>, value: Data?) { + putData(property.name, value) + } + + @Suppress("UNCHECKED_CAST") + private fun findData(key: String, default: Data): Data { + with(mSharedPreferences) { + val result: Any = when (default) { + is Int -> getInt(key, default) + is Long -> getLong(key, default) + is Float -> getFloat(key, default) + is String -> getString(key, default)!! + is Boolean -> getBoolean(key, default) + else -> throw IllegalArgumentException("This type $default can not be saved into sharedPreferences") + } + return result as? Data ?: default + } + } + + private fun putData(key: String, value: Data?) { + mSharedPreferences.edit { + if (value == null) { + remove(key) + } else { + when (value) { + is Int -> putInt(key, value) + is Long -> putLong(key, value) + is Float -> putFloat(key, value) + is String -> putString(key, value) + is Boolean -> putBoolean(key, value) + else -> throw IllegalArgumentException("This type $default can not be saved into Preferences") + } + } + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/data/AppsRepository.kt b/app/src/main/java/top/niunaijun/blackbox/data/AppsRepository.kt new file mode 100644 index 00000000..900e2eb6 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/data/AppsRepository.kt @@ -0,0 +1,251 @@ +package top.niunaijun.blackbox.data + +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.util.Log +import android.webkit.URLUtil +import androidx.core.content.edit +import androidx.lifecycle.MutableLiveData +import top.niunaijun.blackbox.BlackBoxCore +import top.niunaijun.blackbox.BlackBoxCore.getPackageManager +import top.niunaijun.blackbox.utils.AbiUtils +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.app.AppManager +import top.niunaijun.blackbox.bean.AppInfo +import top.niunaijun.blackbox.bean.InstalledAppBean +import top.niunaijun.blackbox.util.getString +import java.io.File + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/4/29 23:05 + */ +class AppsRepository { + private val TAG: String = "AppsRepository" + private var mInstalledList = mutableListOf() + + fun previewInstallList() { + synchronized(mInstalledList) { + val installedList = mutableListOf() + val installedApplications: List = if (Build.VERSION.SDK_INT >= 33) { + getPackageManager().getInstalledApplications( + PackageManager.ApplicationInfoFlags.of(0)) + } else { + getPackageManager().getInstalledApplications(0) + } + + for (installedApplication in installedApplications) { + val file = File(installedApplication.sourceDir) + if ((installedApplication.flags and ApplicationInfo.FLAG_SYSTEM) != 0) { + continue + } + + if (!AbiUtils.isSupport(file)) { + continue + } + + val isXpModule = BlackBoxCore.get().isXposedModule(file) + + val info = AppInfo( + installedApplication.loadLabel(getPackageManager()).toString(), + installedApplication.loadIcon(getPackageManager()), + installedApplication.packageName, + installedApplication.sourceDir, + isXpModule + ) + installedList.add(info) + } + + installedList.sortWith { a, b -> + if (a.name > b.name) { + 1 + } else { + -1 + } + } + + this.mInstalledList.clear() + this.mInstalledList.addAll(installedList) + } + } + + fun getInstalledAppList( + userID: Int, + loadingLiveData: MutableLiveData, + appsLiveData: MutableLiveData> + ) { + loadingLiveData.postValue(true) + synchronized(mInstalledList) { + val blackBoxCore = BlackBoxCore.get() + Log.d(TAG, mInstalledList.joinToString(",")) + val newInstalledList = mInstalledList.map { + InstalledAppBean( + it.name, + it.icon, + it.packageName, + it.sourceDir, + blackBoxCore.isInstalled(it.packageName, userID) + ) + } + appsLiveData.postValue(newInstalledList) + loadingLiveData.postValue(false) + } + } + + fun getInstalledModuleList( + loadingLiveData: MutableLiveData, + appsLiveData: MutableLiveData> + ) { + loadingLiveData.postValue(true) + synchronized(mInstalledList) { + val blackBoxCore = BlackBoxCore.get() + val moduleList = mInstalledList.filter { + it.isXpModule + }.map { + InstalledAppBean( + it.name, + it.icon, + it.packageName, + it.sourceDir, + blackBoxCore.isInstalledXposedModule(it.packageName) + ) + } + appsLiveData.postValue(moduleList) + loadingLiveData.postValue(false) + } + } + + fun getVmInstallList(userId: Int, appsLiveData: MutableLiveData>) { + val sortListData = + AppManager.mRemarkSharedPreferences.getString("AppList$userId", "") + val sortList = sortListData?.split(",") + + val applicationList = BlackBoxCore.get().getInstalledApplications(0, userId) + val appInfoList = mutableListOf() + applicationList.also { + if (sortList.isNullOrEmpty()) { + return@also + } + it.sortWith(AppsSortComparator(sortList)) + }.forEach { + val info = AppInfo( + it.loadLabel(getPackageManager()).toString(), + it.loadIcon(getPackageManager()), + it.packageName, + it.sourceDir, + isInstalledXpModule(it.packageName) + ) + + appInfoList.add(info) + } + + appsLiveData.postValue(appInfoList) + } + + private fun isInstalledXpModule(packageName: String): Boolean { + BlackBoxCore.get().installedXPModules.forEach { + if (packageName == it.packageName) { + return@isInstalledXpModule true + } + } + return false + } + + fun installApk(source: String, userId: Int, resultLiveData: MutableLiveData) { + val blackBoxCore = BlackBoxCore.get() + val installResult = if (URLUtil.isValidUrl(source)) { + val uri = Uri.parse(source) + blackBoxCore.installPackageAsUser(uri, userId) + } else { + blackBoxCore.installPackageAsUser(source, userId) + } + + if (installResult.success) { + updateAppSortList(userId, installResult.packageName, true) + resultLiveData.postValue(getString(R.string.install_success)) + } else { + resultLiveData.postValue(getString(R.string.install_fail, installResult.msg)) + } + scanUser() + } + + fun unInstall(packageName: String, userID: Int, resultLiveData: MutableLiveData) { + BlackBoxCore.get().uninstallPackageAsUser(packageName, userID) + updateAppSortList(userID, packageName, false) + scanUser() + resultLiveData.postValue(getString(R.string.uninstall_success)) + } + + fun launchApk(packageName: String, userId: Int, launchLiveData: MutableLiveData) { + val result = BlackBoxCore.get().launchApk(packageName, userId) + launchLiveData.postValue(result) + } + + fun clearApkData(packageName: String, userID: Int, resultLiveData: MutableLiveData) { + BlackBoxCore.get().clearPackage(packageName, userID) + resultLiveData.postValue(getString(R.string.clear_success)) + } + + /** + * 倒序递归扫描用户, + * 如果用户是空的,就删除用户,删除用户备注,删除应用排序列表 + */ + private fun scanUser() { + val blackBoxCore = BlackBoxCore.get() + val userList = blackBoxCore.users + + if (userList.isEmpty()) { + return + } + + val id = userList.last().id + if (blackBoxCore.getInstalledApplications(0, id).isEmpty()) { + blackBoxCore.deleteUser(id) + AppManager.mRemarkSharedPreferences.edit { + remove("Remark$id") + remove("AppList$id") + } + scanUser() + } + } + + /** + * 更新排序列表 + * @param userID Int + * @param pkg String + * @param isAdd Boolean true是添加,false是移除 + */ + private fun updateAppSortList(userID: Int, pkg: String, isAdd: Boolean) { + val savedSortList = + AppManager.mRemarkSharedPreferences.getString("AppList$userID", "") + + val sortList = linkedSetOf() + if (savedSortList != null) { + sortList.addAll(savedSortList.split(",")) + } + + if (isAdd) { + sortList.add(pkg) + } else { + sortList.remove(pkg) + } + + AppManager.mRemarkSharedPreferences.edit { + putString("AppList$userID", sortList.joinToString(",")) + } + } + + /** + * 保存排序后的apk顺序 + */ + fun updateApkOrder(userID: Int, dataList: List) { + AppManager.mRemarkSharedPreferences.edit { + putString("AppList$userID", + dataList.joinToString(",") { it.packageName }) + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/data/AppsSortCompon.kt b/app/src/main/java/top/niunaijun/blackbox/data/AppsSortCompon.kt new file mode 100644 index 00000000..1508a1ee --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/data/AppsSortCompon.kt @@ -0,0 +1,21 @@ +package top.niunaijun.blackbox.data + +import android.content.pm.ApplicationInfo + +/** + * + * @Description: app sort + * @Author: BlackBox + * @CreateDate: 2022/2/27 23:21 + */ +class AppsSortComparator(private val sortedList: List) : Comparator { + override fun compare(o1: ApplicationInfo?, o2: ApplicationInfo?): Int { + if (o1 == null || o2 == null) { + return 0 + } + + val first = sortedList.indexOf(o1.packageName) + val second = sortedList.indexOf(o2.packageName) + return first - second + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/data/FakeLocationRepository.kt b/app/src/main/java/top/niunaijun/blackbox/data/FakeLocationRepository.kt new file mode 100644 index 00000000..bdced0bb --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/data/FakeLocationRepository.kt @@ -0,0 +1,74 @@ +package top.niunaijun.blackbox.data + +import android.content.pm.ApplicationInfo +import android.util.Log +import androidx.lifecycle.MutableLiveData +import top.niunaijun.blackbox.BlackBoxCore +import top.niunaijun.blackbox.entity.location.BLocation +import top.niunaijun.blackbox.fake.frameworks.BLocationManager +import top.niunaijun.blackbox.bean.FakeLocationBean + +/** + * getInstalledApplications and query fake location of each of them. + * mode and location configuration are respectively concept. + * Location config just store the location but mode decides whether turns on it + * each application has three pattern.Global: use global fake location, + * self: use own config, close: use real config + * @Description: + * @Author: BlackBoxing + * @CreateDate: 2022/3/12 21:14 + */ +class FakeLocationRepository { + private val TAG: String = "FakeLocationRepository" + + fun setPattern(userId: Int, pkg: String, pattern: Int) { + BLocationManager.get().setPattern(userId, pkg, pattern) + } + + private fun getPattern(userId: Int, pkg: String): Int { + return BLocationManager.get().getPattern(userId, pkg) + } + + private fun getLocation(userId: Int, pkg: String): BLocation? { + return BLocationManager.get().getLocation(userId, pkg) + } + + fun setLocation(userId: Int, pkg: String, location: BLocation) { + BLocationManager.get().setLocation(userId, pkg, location) + } + + fun getInstalledAppList( + userID: Int, + appsFakeLiveData: MutableLiveData> + ) { + val installedList = mutableListOf() + val installedApplications: List = + BlackBoxCore.get().getInstalledApplications(0, userID) + //List -> List + for (installedApplication in installedApplications) { + /*val file = File(installedApplication.sourceDir) + if ((installedApplication.flags and ApplicationInfo.FLAG_SYSTEM) != 0) { + continue + } + + if (!AbiUtils.isSupport(file)) { + continue + } + + val isXpModule = BlackBoxCore.get().isXposedModule(file)*/ + val info = FakeLocationBean( + userID, + installedApplication.loadLabel(BlackBoxCore.getPackageManager()).toString(), + installedApplication.loadIcon(BlackBoxCore.getPackageManager()), + installedApplication.packageName, + getPattern(userID, installedApplication.packageName), + getLocation(userID, installedApplication.packageName) + ) + + installedList.add(info) + } + + Log.d(TAG, installedList.joinToString(",")) + appsFakeLiveData.postValue(installedList) + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/data/GmsRepository.kt b/app/src/main/java/top/niunaijun/blackbox/data/GmsRepository.kt new file mode 100644 index 00000000..ebf03d7b --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/data/GmsRepository.kt @@ -0,0 +1,65 @@ +package top.niunaijun.blackbox.data + +import androidx.lifecycle.MutableLiveData +import top.niunaijun.blackbox.BlackBoxCore +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.app.AppManager +import top.niunaijun.blackbox.bean.GmsBean +import top.niunaijun.blackbox.bean.GmsInstallBean +import top.niunaijun.blackbox.util.getString + +/** + * + * @Description: + * @Author: BlackBox + * @CreateDate: 2022/3/2 21:14 + */ +class GmsRepository { + fun getGmsInstalledList(mInstalledLiveData: MutableLiveData>) { + val userList = arrayListOf() + BlackBoxCore.get().users.forEach { + val userId = it.id + val userName = + AppManager.mRemarkSharedPreferences.getString("Remark$userId", "User $userId") ?: "" + val isInstalled = BlackBoxCore.get().isInstallGms(userId) + val bean = GmsBean(userId, userName, isInstalled) + userList.add(bean) + } + + mInstalledLiveData.postValue(userList) + } + + fun installGms( + userID: Int, + mUpdateInstalledLiveData: MutableLiveData + ) { + val installResult = BlackBoxCore.get().installGms(userID) + val result = if (installResult.success) { + getString(R.string.install_success) + } else { + getString(R.string.install_fail, installResult.msg) + } + + val bean = GmsInstallBean(userID,installResult.success,result) + mUpdateInstalledLiveData.postValue(bean) + } + + fun uninstallGms( + userID: Int, + mUpdateInstalledLiveData: MutableLiveData + ) { + var isSuccess = false + if (BlackBoxCore.get().isInstallGms(userID)) { + isSuccess = BlackBoxCore.get().uninstallGms(userID) + } + + val result = if (isSuccess) { + getString(R.string.uninstall_success) + } else { + getString(R.string.uninstall_fail) + } + + val bean = GmsInstallBean(userID,isSuccess,result) + mUpdateInstalledLiveData.postValue(bean) + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/data/XpRepository.kt b/app/src/main/java/top/niunaijun/blackbox/data/XpRepository.kt new file mode 100644 index 00000000..74a010d5 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/data/XpRepository.kt @@ -0,0 +1,58 @@ +package top.niunaijun.blackbox.data + +import android.net.Uri +import android.webkit.URLUtil +import androidx.lifecycle.MutableLiveData +import top.niunaijun.blackbox.BlackBoxCore +import top.niunaijun.blackbox.BlackBoxCore.getPackageManager +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.bean.XpModuleInfo +import top.niunaijun.blackbox.util.getString + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/5/2 20:55 + */ +class XpRepository { + fun getInstallModules(modulesLiveData: MutableLiveData>) { + val moduleList = BlackBoxCore.get().installedXPModules + val result = mutableListOf() + moduleList.forEach { + val info = XpModuleInfo( + it.name, + it.desc, + it.packageName, + it.packageInfo.versionName, + it.enable, + it.application.loadIcon(getPackageManager()) + ) + result.add(info) + } + + modulesLiveData.postValue(result) + } + + fun installModule(source: String, resultLiveData: MutableLiveData) { + val blackBoxCore = BlackBoxCore.get() + val installResult = if (URLUtil.isValidUrl(source)) { + val uri = Uri.parse(source) + blackBoxCore.installXPModule(uri) + } else { + //source == packageName + blackBoxCore.installXPModule(source) + } + + if (installResult.success) { + resultLiveData.postValue(getString(R.string.install_success)) + } else { + resultLiveData.postValue(getString(R.string.install_fail, installResult.msg)) + } + } + + fun unInstallModule(packageName: String, resultLiveData: MutableLiveData) { + BlackBoxCore.get().uninstallXPModule(packageName) + resultLiveData.postValue(getString(R.string.remove_success)) + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/util/ContextUtil.kt b/app/src/main/java/top/niunaijun/blackbox/util/ContextUtil.kt new file mode 100644 index 00000000..4bc7299b --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/util/ContextUtil.kt @@ -0,0 +1,22 @@ +package top.niunaijun.blackbox.util + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings + +/** + * + * @Description: + * @Author: kotlinMiku + * @CreateDate: 2022/4/17 16:32 + */ +object ContextUtil { + fun Context.openAppSystemSettings() { + startActivity(Intent().apply { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + data = Uri.fromParts("package", packageName, null) + }) + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/util/InjectionUtil.kt b/app/src/main/java/top/niunaijun/blackbox/util/InjectionUtil.kt new file mode 100644 index 00000000..cc71d02a --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/util/InjectionUtil.kt @@ -0,0 +1,44 @@ +package top.niunaijun.blackbox.util + +import top.niunaijun.blackbox.data.AppsRepository +import top.niunaijun.blackbox.data.FakeLocationRepository +import top.niunaijun.blackbox.data.GmsRepository +import top.niunaijun.blackbox.data.XpRepository +import top.niunaijun.blackbox.view.apps.AppsFactory +import top.niunaijun.blackbox.view.fake.FakeLocationFactory +import top.niunaijun.blackbox.view.gms.GmsFactory +import top.niunaijun.blackbox.view.list.ListFactory +import top.niunaijun.blackbox.view.xp.XpFactory + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/4/29 22:38 + */ +object InjectionUtil { + private val appsRepository = AppsRepository() + private val xpRepository = XpRepository() + private val gmsRepository = GmsRepository() + private val fakeLocationRepository = FakeLocationRepository() + + fun getAppsFactory() : AppsFactory { + return AppsFactory(appsRepository) + } + + fun getListFactory(): ListFactory { + return ListFactory(appsRepository) + } + + fun getXpFactory():XpFactory{ + return XpFactory(xpRepository) + } + + fun getGmsFactory():GmsFactory{ + return GmsFactory(gmsRepository) + } + + fun getFakeLocationFactory():FakeLocationFactory{ + return FakeLocationFactory(fakeLocationRepository) + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/util/MathUtil.java b/app/src/main/java/top/niunaijun/blackbox/util/MathUtil.java new file mode 100644 index 00000000..dbd3a589 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/util/MathUtil.java @@ -0,0 +1,77 @@ +package top.niunaijun.blackbox.util; + +import android.graphics.Point; +import android.graphics.PointF; + +public class MathUtil { + public MathUtil() { + } + + /** + * Get the distance between two points. + * + * @param A Point A + * @param B Point B + * @return the distance between point A and point B. + */ + public static int getDistance(PointF A, PointF B) { + return (int) Math.sqrt(Math.pow(A.x - B.x, 2) + Math.pow(A.y - B.y, 2)); + } + + /** + * Get the distance between two points. + */ + public static int getDistance(float x1, float y1, float x2, float y2) { + return (int) Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); + } + + /** + * Get the coordinates of a point on the line by cut length. + * + * @param A Point A + * @param B Point B + * @param cutLength cut length + * @return the point. + */ + public static Point getPointByCutLength(Point A, Point B, int cutLength) { + float radian = getRadian(A, B); + return new Point(A.x + (int) (cutLength * Math.cos(radian)), A.y + (int) (cutLength * Math.sin(radian))); + } + + /** + * Get the radian between current line(determined by point A and B) and horizontal line. + * + * @param A point A + * @param B point B + * @return the radian + */ + public static float getRadian(Point A, Point B) { + float lenA = B.x - A.x; + float lenB = B.y - A.y; + float lenC = (float) Math.sqrt(lenA * lenA + lenB * lenB); + float radian = (float) Math.acos(lenA / lenC); + radian = radian * (B.y < A.y ? -1 : 1); + return radian; + } + + + /** + * angle to radian + * + * @param angle angle + * @return radian + */ + public static double angle2Radian(double angle) { + return angle / 180 * Math.PI; + } + + /** + * radian to angle + * + * @param radian radian + * @return angle + */ + public static double radian2Angle(double radian) { + return radian / Math.PI * 180; + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/util/ResUtil.kt b/app/src/main/java/top/niunaijun/blackbox/util/ResUtil.kt new file mode 100644 index 00000000..be2cfec1 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/util/ResUtil.kt @@ -0,0 +1,11 @@ +package top.niunaijun.blackbox.util + +import androidx.annotation.StringRes +import top.niunaijun.blackbox.app.App + +fun getString(@StringRes id: Int, vararg arg: String): String { + if (arg.isEmpty()) { + return App.getContext().getString(id) + } + return App.getContext().getString(id,*arg) +} diff --git a/app/src/main/java/top/niunaijun/blackbox/util/Resolution.kt b/app/src/main/java/top/niunaijun/blackbox/util/Resolution.kt new file mode 100644 index 00000000..4a445472 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/util/Resolution.kt @@ -0,0 +1,291 @@ +package top.niunaijun.blackbox.util + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.KeyguardManager +import android.content.Context +import android.graphics.Point +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.DisplayMetrics +import android.view.Display +import android.view.View +import android.view.WindowManager +import android.view.inputmethod.InputMethodManager +import androidx.core.hardware.display.DisplayManagerCompat +import java.lang.Exception +import java.lang.reflect.Field + +object Resolution { + private const val TAG = "UtilsScreen" + + /** + * Gets the width of the display, in pixels. + * + * + * Note that this value should not be used for computing layouts, since a + * device will typically have screen decoration (such as a status bar) along + * the edges of the display that reduce the amount of application space + * available from the size returned here. Layouts should instead use the + * window size. + * + * + * The size is adjusted based on the current rotation of the display. + * + * + * The size returned by this method does not necessarily represent the + * actual raw size (native resolution) of the display. The returned size may + * be adjusted to exclude certain system decoration elements that are always + * visible. It may also be scaled to provide compatibility with older + * applications that were originally designed for smaller displays. + * + * @return Screen width in pixels. + */ + fun getScreenWidth(context: Context): Int { + return getScreenSize(context, null).x + } + + /** + * Gets the height of the display, in pixels. + * + * + * Note that this value should not be used for computing layouts, since a + * device will typically have screen decoration (such as a status bar) along + * the edges of the display that reduce the amount of application space + * available from the size returned here. Layouts should instead use the + * window size. + * + * + * The size is adjusted based on the current rotation of the display. + * + * + * The size returned by this method does not necessarily represent the + * actual raw size (native resolution) of the display. The returned size may + * be adjusted to exclude certain system decoration elements that are always + * visible. It may also be scaled to provide compatibility with older + * applications that were originally designed for smaller displays. + * + * @return Screen height in pixels. + */ + fun getScreenHeight(context: Context): Int { + return getScreenSize(context, null).y + } + + /** + * Gets the size of the display, in pixels. + * + * + * Note that this value should not be used for computing layouts, since a + * device will typically have screen decoration (such as a status bar) along + * the edges of the display that reduce the amount of application space + * available from the size returned here. Layouts should instead use the + * window size. + * + * + * The size is adjusted based on the current rotation of the display. + * + * + * The size returned by this method does not necessarily represent the + * actual raw size (native resolution) of the display. The returned size may + * be adjusted to exclude certain system decoration elements that are always + * visible. It may also be scaled to provide compatibility with older + * applications that were originally designed for smaller displays. + * + * @param outSize null-ok. If it is null, will create a Point instance inside, + * otherwise use it to fill the output. NOTE if it is not null, + * it will be the returned value. + * @return Screen size in pixels, the x is the width, the y is the height. + */ + @SuppressLint("NewApi") + fun getScreenSize(context: Context, outSize: Point?): Point { + val wm = context + .getSystemService(Context.WINDOW_SERVICE) as WindowManager + val ret = outSize ?: Point() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val defaultDisplay = DisplayManagerCompat.getInstance(context).getDisplay(Display.DEFAULT_DISPLAY) + val displayContext = context.createDisplayContext(defaultDisplay!!) + ret.x = displayContext.resources.displayMetrics.widthPixels + ret.y = displayContext.resources.displayMetrics.heightPixels + } else { + val defaultDisplay = wm.defaultDisplay + val displayMetrics = DisplayMetrics() + defaultDisplay.getMetrics(displayMetrics) + ret.x = displayMetrics.widthPixels + ret.y = displayMetrics.heightPixels + } + return ret + } + + /** + * This method converts dp unit to equivalent pixels, depending on device density. + * + * @param dp A value in dp (density independent pixels) unit. Which we need to convert into pixels + * @param context Context to get resources and device specific display metrics + * @return A float value to represent px equivalent to dp depending on device density + */ + fun convertDpToPixel(dp: Float, context: Context): Float { + val resources = context.resources + val metrics = resources.displayMetrics + return dp * (metrics.densityDpi / 160f) + } + + /** + * This method converts device specific pixels to density independent pixels. + * + * @param px A value in px (pixels) unit. Which we need to convert into db + * @param context Context to get resources and device specific display metrics + * @return A float value to represent dp equivalent to px value + */ + fun convertPixelsToDp(px: Float, context: Context): Float { + val resources = context.resources + val metrics = resources.displayMetrics + return px / (metrics.densityDpi / 160f) + } + /////////////////////////////////////////////////////////////////////// + /** + * 获取屏幕密度 + */ + fun getDensity(context: Context?): Float { + var density = 0f + if (context == null) { + return density + } + try { + density = context.resources.displayMetrics.density + } catch (ignored: Exception) { + } + return density + } + + /** + * 检查分辨率是否为本机 + */ + fun checkPix(context: Activity, width: Int, height: Int): Boolean { + return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) { + val metrics = DisplayMetrics() + context.windowManager.defaultDisplay.getRealMetrics(metrics) + metrics.widthPixels == width && metrics.heightPixels == height + } else { + getScreenPixWidth(context) == width && getScreenPixHeight( + context + ) == height + } + } + + /** + * 获取屏幕分辨率:宽 + */ + private fun getScreenPixWidth(context: Context): Int { + return context.resources.displayMetrics.widthPixels + } + + /** + * 获取屏幕分辨率:高 + */ + private fun getScreenPixHeight(context: Context): Int { + return context.resources.displayMetrics.heightPixels + } + + /** + * dipתpx + */ + fun dipToPx(context: Context, dip: Int): Int { + return (dip * context.resources.displayMetrics.density + 0.5f).toInt() + } + + /** + * pxתdip + */ + fun pxToDip(context: Context, pxValue: Float): Int { + val scale = context.resources.displayMetrics.density + return (pxValue / scale + 0.5f).toInt() + } + + /** + * 将sp值转换为px值,保证文字大小不变 + * + * @param context + * @param spValue + * @return + */ + fun sp2px(context: Context, spValue: Float): Int { + val fontScale = context.resources.displayMetrics.scaledDensity + return (spValue * fontScale + 0.5f).toInt() + } + + /** + * 隐藏软键盘 + */ + fun hideInputMethod(view: View) { + val imm = view.context + .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(view.windowToken, 0) + } + + /** + * 显示软键盘 + */ + private fun showInputMethod(view: View) { + val imm = view.context + .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) + } + + /** + * 多少时间后显示软键盘 + */ + fun showInputMethod(view: View, delayMillis: Long) { + // 显示输入法 + Handler(Looper.getMainLooper()!!).postDelayed({ showInputMethod(view) }, delayMillis) + } + + /** + * 判断手机是否在锁屏状态 true锁屏 false未锁屏 + */ + fun isScreenLocked(c: Context): Boolean { + val mKeyguardManager = c + .getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + !mKeyguardManager.isKeyguardLocked + } else { + !mKeyguardManager.inKeyguardRestrictedInputMode() + } + } + + fun getBarHeight(context: Context): Int { + val c: Class<*>? + val obj: Any + val field: Field? + val x: Int + var sbar = 38 //默认为38,貌似大部分是这样的 + try { + c = Class.forName("com.android.internal.R\$dimen") + obj = c.newInstance() + field = c.getField("status_bar_height") + x = field?.get(obj).toString().toInt() + sbar = context.resources.getDimensionPixelSize(x) + } catch (e1: Exception) { + e1.printStackTrace() + } + return sbar + } + + //http://stackoverflow.com/questions/20264268/how-to-get-height-and-width-of-navigation-bar-programmatically + //获取屏幕下方导航栏高度 + fun getNavigationBarSize(context: Context): Point { + val appUsableSize = getScreenSize(context, null) + + // navigation bar on the right + /*if (appUsableSize.x < realScreenSize.x) { + return new Point(realScreenSize.x - appUsableSize.x, appUsableSize.y); + }*/ + + // navigation bar at the bottom + return if (appUsableSize.y < appUsableSize.y) { + Point(appUsableSize.x, appUsableSize.y - appUsableSize.y) + } else Point() + + // navigation bar is not present + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/util/ShortcutUtil.kt b/app/src/main/java/top/niunaijun/blackbox/util/ShortcutUtil.kt new file mode 100644 index 00000000..413f55f1 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/util/ShortcutUtil.kt @@ -0,0 +1,80 @@ +package top.niunaijun.blackbox.util + +import android.content.Context +import android.content.Intent +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import androidx.core.graphics.drawable.toBitmap +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.input.input +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.app.App +import top.niunaijun.blackbox.app.AppManager +import top.niunaijun.blackbox.bean.AppInfo +import top.niunaijun.blackbox.util.ContextUtil.openAppSystemSettings +import top.niunaijun.blackbox.view.main.ShortcutActivity + +/** + * + * @Description: 桌面快捷方式 工具类 + * @Author: BlackBox + * @CreateDate: 2022/2/27 22:56 + */ +object ShortcutUtil { + /** + * 创建桌面快捷方式 + * @param userID Int userID + * @param info AppInfo + */ + fun createShortcut(context: Context,userID: Int, info: AppInfo) { + if (ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { + val labelName = info.name + userID + val intent = Intent(context, ShortcutActivity::class.java) + .setAction(Intent.ACTION_MAIN) + .putExtra("pkg", info.packageName) + .putExtra("userId", userID) + MaterialDialog(context).show { + title(res = R.string.app_shortcut) + input( + hintRes = R.string.shortcut_name, + prefill = labelName + ) { _, input -> + val shortcutInfo: ShortcutInfoCompat = + ShortcutInfoCompat.Builder(context, info.packageName + userID) + .setIntent(intent) + .setShortLabel(input) + .setLongLabel(input) + .setIcon(IconCompat.createWithBitmap(info.icon.toBitmap())) + .build() + + ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null) + showAllowPermissionDialog(context) + } + positiveButton(R.string.done) + negativeButton(R.string.cancel) + } + } else { + toast(R.string.cannot_create_shortcut) + } + } + + private fun showAllowPermissionDialog(context: Context) { + if (!AppManager.mBlackBoxLoader.showShortcutPermissionDialog()) { + return + } + + MaterialDialog(context).show { + title(R.string.try_add_shortcut) + message(R.string.add_shortcut_fail_msg) + positiveButton(R.string.done) + negativeButton(R.string.permission_setting) { + App.getContext().openAppSystemSettings() + } + + neutralButton(R.string.no_reminders) { + AppManager.mBlackBoxLoader.invalidShortcutPermissionDialog(false) + } + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/util/ToastEx.kt b/app/src/main/java/top/niunaijun/blackbox/util/ToastEx.kt new file mode 100644 index 00000000..1869c9dd --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/util/ToastEx.kt @@ -0,0 +1,28 @@ +package top.niunaijun.blackbox.util + +import android.content.Context +import android.widget.Toast +import androidx.annotation.StringRes +import top.niunaijun.blackbox.app.App + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/5/2 0:13 + */ +var toastImpl:Toast? = null + +fun Context.toast(msg:String) { + toastImpl?.cancel() + toastImpl = Toast.makeText(this,msg,Toast.LENGTH_SHORT) + toastImpl?.show() +} + +fun toast(msg: String) { + App.getContext().toast(msg) +} + +fun toast(@StringRes msgID:Int) { + toast(getString(msgID)) +} diff --git a/app/src/main/java/top/niunaijun/blackbox/util/ViewBindingEx.kt b/app/src/main/java/top/niunaijun/blackbox/util/ViewBindingEx.kt new file mode 100644 index 00000000..5bcd17ed --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/util/ViewBindingEx.kt @@ -0,0 +1,31 @@ +package top.niunaijun.blackbox.util + +import android.app.Activity +import android.app.Dialog +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.viewbinding.ViewBinding + +/** + * + * @Description: viewBinding 扩展类 + * @Author: wukaicheng + * @CreateDate: 2021/4/29 21:23 + */ +inline fun Activity.inflate(): Lazy = lazy { + inflateBinding(layoutInflater) +} + +inline fun Fragment.inflate(): Lazy = lazy { + inflateBinding(layoutInflater) +} + +inline fun Dialog.inflate(): Lazy = lazy { + inflateBinding(layoutInflater) +} + +inline fun inflateBinding(layoutInflater: LayoutInflater): T { + val method = T::class.java.getMethod("inflate", LayoutInflater::class.java) + return method.invoke(null, layoutInflater) as T +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsAdapter.kt b/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsAdapter.kt new file mode 100644 index 00000000..f318409e --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsAdapter.kt @@ -0,0 +1,35 @@ +package top.niunaijun.blackbox.view.apps + +import android.view.View +import android.view.ViewGroup +import cbfg.rvadapter.RVHolder +import cbfg.rvadapter.RVHolderFactory +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.bean.AppInfo +import top.niunaijun.blackbox.databinding.ItemAppBinding + +/** + * + * @Description: 软件显示界面适配器 + * @Author: wukaicheng + * @CreateDate: 2021/4/29 21:52 + */ +class AppsAdapter : RVHolderFactory() { + override fun createViewHolder(parent: ViewGroup?, viewType: Int, item: Any): RVHolder { + return AppsVH(inflate(R.layout.item_app,parent)) + } + + class AppsVH(itemView:View):RVHolder(itemView) { + private val binding = ItemAppBinding.bind(itemView) + + override fun setContent(item: AppInfo, isSelected: Boolean, payload: Any?) { + binding.icon.setImageDrawable(item.icon) + binding.name.text = item.name + if (item.isXpModule) { + binding.cornerLabel.visibility = View.VISIBLE + } else { + binding.cornerLabel.visibility = View.INVISIBLE + } + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsFactory.kt b/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsFactory.kt new file mode 100644 index 00000000..8485205d --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsFactory.kt @@ -0,0 +1,18 @@ +package top.niunaijun.blackbox.view.apps + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import top.niunaijun.blackbox.data.AppsRepository + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/4/29 22:36 + */ +@Suppress("UNCHECKED_CAST") +class AppsFactory(private val appsRepository: AppsRepository) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T { + return AppsViewModel(appsRepository) as T + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsFragment.kt b/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsFragment.kt new file mode 100644 index 00000000..152e85c6 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsFragment.kt @@ -0,0 +1,341 @@ +package top.niunaijun.blackbox.view.apps + +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import cbfg.rvadapter.RVAdapter +import com.afollestad.materialdialogs.MaterialDialog +import top.niunaijun.blackbox.BlackBoxCore +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.bean.AppInfo +import top.niunaijun.blackbox.databinding.FragmentAppsBinding +import top.niunaijun.blackbox.util.InjectionUtil +import top.niunaijun.blackbox.util.ShortcutUtil +import top.niunaijun.blackbox.util.inflate +import top.niunaijun.blackbox.util.toast +import top.niunaijun.blackbox.view.base.LoadingActivity +import top.niunaijun.blackbox.view.main.MainActivity +import java.util.* +import kotlin.math.abs +import android.view.MotionEvent +import android.graphics.Point + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/4/29 22:21 + */ +class AppsFragment : Fragment() { + var userID: Int = 0 + private lateinit var viewModel: AppsViewModel + private lateinit var mAdapter: RVAdapter + private val viewBinding: FragmentAppsBinding by inflate() + private var popupMenu: PopupMenu? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel = + ViewModelProvider(this, InjectionUtil.getAppsFactory()).get(AppsViewModel::class.java) + userID = requireArguments().getInt("userID", 0) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + viewBinding.stateView.showEmpty() + + mAdapter = + RVAdapter(requireContext(), AppsAdapter()).bind(viewBinding.recyclerView) + + viewBinding.recyclerView.adapter = mAdapter + viewBinding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 4) + + val touchCallBack = AppsTouchCallBack { from, to -> + onItemMove(from, to) + viewModel.updateSortLiveData.postValue(true) + } + + val itemTouchHelper = ItemTouchHelper(touchCallBack) + itemTouchHelper.attachToRecyclerView(viewBinding.recyclerView) + mAdapter.setItemClickListener { _, data, _ -> + showLoading() + viewModel.launchApk(data.packageName, userID) + } + + interceptTouch() + setOnLongClick() + return viewBinding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initData() + } + + override fun onStart() { + super.onStart() + viewModel.getInstalledApps(userID) + } + + /** + * 拖拽优化 + */ + private fun interceptTouch() { + val point = Point() + viewBinding.recyclerView.setOnTouchListener { _, e -> + when (e.action) { + MotionEvent.ACTION_UP -> { + if (!isMove(point, e)) { + popupMenu?.show() + } + popupMenu = null + point.set(0, 0) + } + + MotionEvent.ACTION_MOVE -> { + if (point.x == 0 && point.y == 0) { + point.x = e.rawX.toInt() + point.y = e.rawY.toInt() + } + isDownAndUp(point, e) + + if (isMove(point, e)) { + popupMenu?.dismiss() + } + } + } + return@setOnTouchListener false + } + } + + private fun isMove(point: Point, e: MotionEvent): Boolean { + val max = 40 + val x = point.x + val y = point.y + + val xU = abs(x - e.rawX) + val yU = abs(y - e.rawY) + return xU > max || yU > max + } + + private fun isDownAndUp(point: Point, e: MotionEvent) { + val min = 10 + val y = point.y + val yU = y - e.rawY + + if (abs(yU) > min) { + (requireActivity() as MainActivity).showFloatButton(yU < 0) + } + } + + private fun onItemMove(fromPosition:Int, toPosition:Int) { + if (fromPosition < toPosition) { + for (i in fromPosition until toPosition) { + Collections.swap(mAdapter.getItems(), i, i + 1) + } + } else { + for (i in fromPosition downTo toPosition + 1) { + Collections.swap(mAdapter.getItems(), i, i - 1) + } + } + mAdapter.notifyItemMoved(fromPosition, toPosition) + } + + private fun setOnLongClick() { + mAdapter.setItemLongClickListener { view, data, _ -> + popupMenu = PopupMenu(requireContext(), view).also { +// mAdapter.setItemClickListener { view, data, _ -> +// PopupMenu(requireContext(),view).also { + it.inflate(R.menu.app_menu) + it.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.app_remove -> { + if (data.isXpModule) { + toast(R.string.uninstall_module_toast) + } else { + unInstallApk(data) + } + } + + R.id.app_open -> { + showLoading() + viewModel.launchApk(data.packageName, userID) + } + + R.id.app_clear -> { + clearApk(data) + } + + R.id.app_stop -> { + stopApk(data) + } + + R.id.app_shortcut -> { + ShortcutUtil.createShortcut(requireContext(), userID, data) + } + } + return@setOnMenuItemClickListener true + } + it.show() + } + } + } + + /*interceptTouch() + setOnLongClick() + return viewBinding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initData() + } + + override fun onStart() { + super.onStart() + viewModel.getInstalledApps(userID) + } + + private fun onItemMove(fromPosition:Int, toPosition:Int) { + if (fromPosition < toPosition) { + for (i in fromPosition until toPosition) { + Collections.swap(mAdapter.getItems(), i, i + 1) + } + } else { + for (i in fromPosition downTo toPosition + 1) { + Collections.swap(mAdapter.getItems(), i, i - 1) + } + } + mAdapter.notifyItemMoved(fromPosition, toPosition) + }*/ + + private fun initData() { + viewBinding.stateView.showLoading() + viewModel.getInstalledApps(userID) + viewModel.appsLiveData.observe(viewLifecycleOwner) { + if (it != null) { + mAdapter.setItems(it) + if (it.isEmpty()) { + viewBinding.stateView.showEmpty() + } else { + viewBinding.stateView.showContent() + } + } + } + + viewModel.resultLiveData.observe(viewLifecycleOwner) { + if (!TextUtils.isEmpty(it)) { + hideLoading() + requireContext().toast(it) + viewModel.getInstalledApps(userID) + scanUser() + } + } + + viewModel.launchLiveData.observe(viewLifecycleOwner) { + it?.run { + hideLoading() + if (!it) { + toast(R.string.start_fail) + } + } + } + + viewModel.updateSortLiveData.observe(viewLifecycleOwner) { + if (this::mAdapter.isInitialized) { + viewModel.updateApkOrder(userID, mAdapter.getItems()) + } + } + } + + override fun onStop() { + super.onStop() + viewModel.resultLiveData.value = null + viewModel.launchLiveData.value = null + } + + private fun unInstallApk(info: AppInfo) { + MaterialDialog(requireContext()).show { + title(R.string.uninstall_app) + message(text = getString(R.string.uninstall_app_hint, info.name)) + positiveButton(R.string.done) { + showLoading() + viewModel.unInstall(info.packageName, userID) + } + negativeButton(R.string.cancel) + } + } + + /** + * 强行停止软件 + * @param info AppInfo + */ + private fun stopApk(info: AppInfo) { + MaterialDialog(requireContext()).show { + title(R.string.app_stop) + message(text = getString(R.string.app_stop_hint,info.name)) + positiveButton(R.string.done) { + BlackBoxCore.get().stopPackage(info.packageName, userID) + toast(getString(R.string.is_stop,info.name)) + } + negativeButton(R.string.cancel) + } + } + + /** + * 清除软件数据 + * @param info AppInfo + */ + private fun clearApk(info: AppInfo) { + MaterialDialog(requireContext()).show { + title(R.string.app_clear) + message(text = getString(R.string.app_clear_hint,info.name)) + positiveButton(R.string.done) { + showLoading() + viewModel.clearApkData(info.packageName, userID) + } + negativeButton(R.string.cancel) + } + } + + fun installApk(source: String) { + showLoading() + viewModel.install(source, userID) + } + + private fun scanUser() { + (requireActivity() as MainActivity).scanUser() + } + + private fun showLoading() { + if (requireActivity() is LoadingActivity) { + (requireActivity() as LoadingActivity).showLoading() + } + } + + private fun hideLoading() { + if (requireActivity() is LoadingActivity) { + (requireActivity() as LoadingActivity).hideLoading() + } + } + + companion object { + fun newInstance(userID:Int): AppsFragment { + val fragment = AppsFragment() + val bundle = bundleOf("userID" to userID) + fragment.arguments = bundle + return fragment + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsTouchCallBack.kt b/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsTouchCallBack.kt new file mode 100644 index 00000000..3b1987ac --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsTouchCallBack.kt @@ -0,0 +1,28 @@ +package top.niunaijun.blackbox.view.apps + +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView + +class AppsTouchCallBack(private val onMoveBlock: (from: Int, to: Int) -> Unit) : + ItemTouchHelper.Callback() { + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 0) + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val fromPosition = viewHolder.bindingAdapterPosition + val toPosition = target.bindingAdapterPosition + onMoveBlock(fromPosition, toPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsViewModel.kt b/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsViewModel.kt new file mode 100644 index 00000000..b1fc585f --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/apps/AppsViewModel.kt @@ -0,0 +1,56 @@ +package top.niunaijun.blackbox.view.apps + +import androidx.lifecycle.MutableLiveData +import top.niunaijun.blackbox.bean.AppInfo +import top.niunaijun.blackbox.data.AppsRepository +import top.niunaijun.blackbox.view.base.BaseViewModel + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/4/29 22:36 + */ +class AppsViewModel(private val repo: AppsRepository) : BaseViewModel() { + val appsLiveData = MutableLiveData>() + val resultLiveData = MutableLiveData() + val launchLiveData = MutableLiveData() + //利用LiveData只更新最后一次的特性,用来保存app顺序 + val updateSortLiveData = MutableLiveData() + + fun getInstalledApps(userId: Int) { + launchOnUI { + repo.getVmInstallList(userId, appsLiveData) + } + } + + fun install(source: String, userID: Int) { + launchOnUI { + repo.installApk(source, userID, resultLiveData) + } + } + + fun unInstall(packageName: String, userID: Int) { + launchOnUI { + repo.unInstall(packageName, userID, resultLiveData) + } + } + + fun clearApkData(packageName: String,userID: Int) { + launchOnUI { + repo.clearApkData(packageName,userID,resultLiveData) + } + } + + fun launchApk(packageName: String, userID: Int) { + launchOnUI { + repo.launchApk(packageName, userID, launchLiveData) + } + } + + fun updateApkOrder(userID: Int,dataList:List) { + launchOnUI { + repo.updateApkOrder(userID,dataList) + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/base/BaseActivity.kt b/app/src/main/java/top/niunaijun/blackbox/view/base/BaseActivity.kt new file mode 100644 index 00000000..a2407ba3 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/base/BaseActivity.kt @@ -0,0 +1,32 @@ +package top.niunaijun.blackbox.view.base + +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar + +/** + * + * @Description:BaseActivity + * @Author: wukaicheng + * @CreateDate: 2021/5/4 15:58 + */ +open class BaseActivity : AppCompatActivity() { + protected fun initToolbar(toolbar: Toolbar,title:Int, showBack: Boolean = false, onBack: (() -> Unit)? = null) { + setSupportActionBar(toolbar) + toolbar.setTitle(title) + if (showBack) { + supportActionBar?.let { + it.setDisplayHomeAsUpEnabled(true) + toolbar.setNavigationOnClickListener { + if (onBack != null) { + onBack() + } + finish() + } + } + } + } + + protected fun currentUserID():Int{ + return intent.getIntExtra("userID", 0) + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/base/BaseViewModel.kt b/app/src/main/java/top/niunaijun/blackbox/view/base/BaseViewModel.kt new file mode 100644 index 00000000..d0fd2d8f --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/base/BaseViewModel.kt @@ -0,0 +1,30 @@ +package top.niunaijun.blackbox.view.base + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.* + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/4/29 23:33 + */ +open class BaseViewModel : ViewModel() { + fun launchOnUI(block: suspend CoroutineScope.() -> Unit) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + try { + block() + } catch (e: Throwable) { + e.printStackTrace() + } + } + } + } + + override fun onCleared() { + super.onCleared() + viewModelScope.cancel() + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/base/LoadingActivity.kt b/app/src/main/java/top/niunaijun/blackbox/view/base/LoadingActivity.kt new file mode 100644 index 00000000..4f9ab258 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/base/LoadingActivity.kt @@ -0,0 +1,40 @@ +package top.niunaijun.blackbox.view.base + +import android.view.KeyEvent +import com.roger.catloadinglibrary.CatLoadingView +import top.niunaijun.blackbox.R + +/** + * + * @Description: loading activity + * @Author: BlackBox + * @CreateDate: 2022/3/2 21:49 + */ +abstract class LoadingActivity : BaseActivity() { + private lateinit var loadingView: CatLoadingView + + fun showLoading() { + if (!this::loadingView.isInitialized) { + loadingView = CatLoadingView() + } + + if (!loadingView.isAdded) { + loadingView.setBackgroundColor(R.color.primary) + loadingView.show(supportFragmentManager, "") + supportFragmentManager.executePendingTransactions() + loadingView.setClickCancelAble(false) + loadingView.dialog?.setOnKeyListener { _, keyCode, _ -> + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { + return@setOnKeyListener true + } + false + } + } + } + + fun hideLoading() { + if (this::loadingView.isInitialized) { + loadingView.dismiss() + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/fake/FakeLocationAdapter.kt b/app/src/main/java/top/niunaijun/blackbox/view/fake/FakeLocationAdapter.kt new file mode 100644 index 00000000..aa2709ec --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/fake/FakeLocationAdapter.kt @@ -0,0 +1,38 @@ +package top.niunaijun.blackbox.view.fake + +import android.view.View +import android.view.ViewGroup +import cbfg.rvadapter.RVHolder +import cbfg.rvadapter.RVHolderFactory +import top.niunaijun.blackbox.fake.frameworks.BLocationManager +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.bean.FakeLocationBean +import top.niunaijun.blackbox.databinding.ItemFakeBinding +import top.niunaijun.blackbox.util.getString + +/** + * + * @Description: 软件显示界面适配器 + * @Author: BlackBoxing + * @CreateDate: 2022/3/14 + */ +class FakeLocationAdapter : RVHolderFactory() { + override fun createViewHolder(parent: ViewGroup?, viewType: Int, item: Any): RVHolder { + return FakeLocationVH(inflate(R.layout.item_fake,parent)) + } + + class FakeLocationVH(itemView:View):RVHolder(itemView) { + private val binding = ItemFakeBinding.bind(itemView) + override fun setContent(item: FakeLocationBean, isSelected: Boolean, payload: Any?) { + binding.icon.setImageDrawable(item.icon) + binding.name.text = item.name + if (item.fakeLocation == null || item.fakeLocationPattern == BLocationManager.CLOSE_MODE) { + binding.fakeLocation.text = getString(R.string.real_location) + } else { + binding.fakeLocation.text = + String.format("%f, %f", item.fakeLocation!!.latitude, item.fakeLocation!!.longitude) + } + binding.cornerLabel.visibility = View.VISIBLE + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/fake/FakeLocationFactory.kt b/app/src/main/java/top/niunaijun/blackbox/view/fake/FakeLocationFactory.kt new file mode 100644 index 00000000..7329a080 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/fake/FakeLocationFactory.kt @@ -0,0 +1,19 @@ +package top.niunaijun.blackbox.view.fake + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import top.niunaijun.blackbox.data.FakeLocationRepository + +/** + * + * @Author: BlackBoxing + * @CreateDate: 2022/3/14 + */ +class FakeLocationFactory(private val repo: FakeLocationRepository) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return FakeLocationViewModel(repo) as T + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/fake/FakeLocationViewModel.kt b/app/src/main/java/top/niunaijun/blackbox/view/fake/FakeLocationViewModel.kt new file mode 100644 index 00000000..6f9f151a --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/fake/FakeLocationViewModel.kt @@ -0,0 +1,34 @@ +package top.niunaijun.blackbox.view.fake + +import androidx.lifecycle.MutableLiveData +import top.niunaijun.blackbox.entity.location.BLocation +import top.niunaijun.blackbox.bean.FakeLocationBean +import top.niunaijun.blackbox.data.FakeLocationRepository +import top.niunaijun.blackbox.view.base.BaseViewModel + +/** + * + * @Author: BlackBoxing + * @CreateDate: 2022/3/14 + */ +class FakeLocationViewModel(private val mRepo: FakeLocationRepository) : BaseViewModel() { + val appsLiveData = MutableLiveData>() + + fun getInstallAppList(userID: Int) { + launchOnUI { + mRepo.getInstalledAppList(userID, appsLiveData) + } + } + + fun setPattern(userId: Int, pkg: String, pattern: Int) { + launchOnUI { + mRepo.setPattern(userId, pkg, pattern) + } + } + + fun setLocation(userId: Int, pkg: String, location: BLocation) { + launchOnUI { + mRepo.setLocation(userId, pkg, location) + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/fake/FakeManagerActivity.kt b/app/src/main/java/top/niunaijun/blackbox/view/fake/FakeManagerActivity.kt new file mode 100644 index 00000000..8b03dc8a --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/fake/FakeManagerActivity.kt @@ -0,0 +1,168 @@ +package top.niunaijun.blackbox.view.fake + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import cbfg.rvadapter.RVAdapter +import com.afollestad.materialdialogs.MaterialDialog +import com.ferfalk.simplesearchview.SimpleSearchView +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.bean.FakeLocationBean +import top.niunaijun.blackbox.databinding.ActivityListBinding +import top.niunaijun.blackbox.entity.location.BLocation +import top.niunaijun.blackbox.fake.frameworks.BLocationManager +import top.niunaijun.blackbox.util.InjectionUtil +import top.niunaijun.blackbox.util.inflate +import top.niunaijun.blackbox.util.toast +import top.niunaijun.blackbox.view.base.BaseActivity + +/** + * + * @Author: BlackBoxing + * @CreateDate: 2022/3/14 + */ +class FakeManagerActivity : BaseActivity() { + val TAG: String = "FakeManagerActivity" + private val viewBinding: ActivityListBinding by inflate() + //private lateinit var mAdapter: ListAdapter + private lateinit var mAdapter: RVAdapter + private lateinit var viewModel: FakeLocationViewModel + private var appList: List = ArrayList() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(viewBinding.root) + + initToolbar(viewBinding.toolbarLayout.toolbar, R.string.fake_location, true) + + mAdapter = RVAdapter(this,FakeLocationAdapter()).bind(viewBinding.recyclerView) + .setItemClickListener { _, data, _ -> + + val intent = Intent(this, FollowMyLocationOverlay::class.java) + intent.putExtra("location", data.fakeLocation) + intent.putExtra("pkg", data.packageName) + + locationResult.launch(intent) + }.setItemLongClickListener { _, item, position -> + disableFakeLocation(item,position) + } + + viewBinding.recyclerView.layoutManager = LinearLayoutManager(this) + initSearchView() + initViewModel() + onBackPressedDispatcher + .addCallback(onBackPressedCallback) + } + + private fun disableFakeLocation(item: FakeLocationBean,position:Int) { + MaterialDialog(this).show { + title(R.string.close_fake_location) + message(text = getString(R.string.close_app_fake_location,item.name)) + negativeButton(R.string.cancel) + positiveButton(R.string.done) { + BLocationManager.disableFakeLocation(currentUserID(), item.packageName) + toast(getString(R.string.close_fake_location_success,item.name)) + item.fakeLocationPattern = BLocationManager.CLOSE_MODE + mAdapter.replaceAt(position,item) + } + } + } + + private fun initSearchView() { + viewBinding.searchView.setOnQueryTextListener(object : + SimpleSearchView.OnQueryTextListener { + override fun onQueryTextChange(newText: String): Boolean { + filterApp(newText) + return true + } + + override fun onQueryTextCleared(): Boolean { + return true + } + + override fun onQueryTextSubmit(query: String): Boolean { + return true + } + }) + } + + private fun initViewModel() { + viewModel = ViewModelProvider(this, InjectionUtil.getFakeLocationFactory()).get( + FakeLocationViewModel::class.java + ) + loadAppList() + viewBinding.toolbarLayout.toolbar.setTitle(R.string.fake_location) + + viewModel.appsLiveData.observe(this) { + if (it != null) { + this.appList = it + viewBinding.searchView.setQuery("", false) + filterApp("") + if (it.isNotEmpty()) { + viewBinding.stateView.showContent() + } else { + viewBinding.stateView.showEmpty() + } + } + } + } + + private fun loadAppList() { + viewBinding.stateView.showLoading() + viewModel.getInstallAppList(currentUserID()) + } + + private val locationResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + it.data?.let { data -> + val latitude = data.getDoubleExtra("latitude", 0.0) + val longitude = data.getDoubleExtra("longitude", 0.0) + val pkg = data.getStringExtra("pkg")!! + + viewModel.setPattern(currentUserID(), pkg, BLocationManager.OWN_MODE) + viewModel.setLocation(currentUserID(), pkg, BLocation(latitude, longitude)) + + toast(getString(R.string.set_location,latitude.toString(), longitude.toString())) + + loadAppList() + } + } + } + + private val onBackPressedCallback = + object: OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (viewBinding.searchView.isSearchOpen) { + viewBinding.searchView.closeSearch() + } + finish() + } + } + + private fun filterApp(newText: String) { + val newList = this.appList.filter { + it.name.contains(newText, true) or it.packageName.contains(newText, true) + } + mAdapter.setItems(newList) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_search, menu) + val item = menu.findItem(R.id.list_search) + viewBinding.searchView.setMenuItem(item) + return true + } + + companion object { + fun start(context: Context) { + val intent = Intent(context, FakeManagerActivity::class.java) + context.startActivity(intent) + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/fake/FollowMyLocationOverlay.kt b/app/src/main/java/top/niunaijun/blackbox/view/fake/FollowMyLocationOverlay.kt new file mode 100644 index 00000000..188c82cd --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/fake/FollowMyLocationOverlay.kt @@ -0,0 +1,145 @@ +package top.niunaijun.blackbox.view.fake + +import android.app.Activity +import android.os.Build +import android.os.Bundle +import android.view.inputmethod.InputMethodManager +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.preference.PreferenceManager +import org.osmdroid.config.Configuration +import org.osmdroid.events.MapEventsReceiver +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.overlay.MapEventsOverlay +import org.osmdroid.views.overlay.Marker +import top.niunaijun.blackbox.entity.location.BLocation +import top.niunaijun.blackbox.databinding.ActivityOsmdroidBinding +import top.niunaijun.blackbox.util.inflate +import top.niunaijun.blackbox.util.toast + +/** + * + * @Author: BlackBoxing + * @CreateDate: 2022/3/14 + */ +class FollowMyLocationOverlay : AppCompatActivity() { + val TAG: String = "FollowMyLocationOverlay" + private val REQUEST_PERMISSIONS_REQUEST_CODE = 1 + private val binding: ActivityOsmdroidBinding by inflate() + lateinit var startPoint: GeoPoint + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + //handle permissions first, before map is created. not depicted here + + //load/initialize the osmdroid configuration, this can be done + //This won't work unless you have imported this: org.osmdroid.config.Configuration.* + Configuration.getInstance().load(this, PreferenceManager.getDefaultSharedPreferences(this)) + //setting this before the layout is inflated is a good idea + //it 'should' ensure that the map has a writable location for the map cache, even without permissions + //if no tiles are displayed, you can try overriding the cache path using Configuration.getInstance().setCachePath + //see also StorageUtils + //note, the load method also sets the HTTP User Agent to your application's package name, if you abuse osm's + //tile servers will get you banned based on this string. + + //inflate and create the map + setContentView(binding.root) + + val location: BLocation? = if (Build.VERSION.SDK_INT >= 33) { + intent.getParcelableExtra("location", BLocation::class.java) + } else { + intent.getParcelableExtra("location") + } + startPoint = if (location == null) { + GeoPoint(30.2736, 120.1563) + } else { + GeoPoint(location.latitude, location.longitude) + } + + val startMarker = Marker(binding.map) + startMarker.position = startPoint + startMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) + + binding.map.overlays.add(startMarker) + val mReceive: MapEventsReceiver = object : MapEventsReceiver { + override fun singleTapConfirmedHelper(p: GeoPoint): Boolean { + startPoint = p + startMarker.position = p + startMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) + binding.map.overlays.add(startMarker) + toast(p.latitude.toString() + " - " + p.longitude) + return false + } + + override fun longPressHelper(p: GeoPoint): Boolean { + return false + } + } + binding.map.overlays.add(MapEventsOverlay(mReceive)) + val mapController = binding.map.controller + mapController.setZoom(12.5) + //val startPoint = GeoPoint(30.2736, 120.1563) + mapController.setCenter(startPoint) + binding.map.setTileSource(TileSourceFactory.MAPNIK) + onBackPressedDispatcher + .addCallback(onBackPressedCallback) + } + + private val onBackPressedCallback = + object: OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finishWithResult(startPoint) + } + } + + override fun onResume() { + super.onResume() + //this will refresh the osmdroid configuration on resuming. + //if you make changes to the configuration, use + //SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this) + //Configuration.getInstance().load(this, PreferenceManager.getDefaultSharedPreferences(this)) + binding.map.onResume() //needed for compass, my location overlays, v6.0.0 and up + } + + override fun onPause() { + super.onPause() + //this will refresh the osmdroid configuration on resuming. + //if you make changes to the configuration, use + //SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this) + //Configuration.getInstance().save(this, prefs) + binding.map.onPause() //needed for compass, my location overlays, v6.0.0 and up + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + val permissionsToRequest = ArrayList() + var i = 0 + while (i < grantResults.size) { + permissionsToRequest.add(permissions[i]) + i++ + } + if (permissionsToRequest.size > 0) { + ActivityCompat.requestPermissions( + this, + permissionsToRequest.toTypedArray(), + REQUEST_PERMISSIONS_REQUEST_CODE + ) + } + } + + private fun finishWithResult(geoPoint: GeoPoint) { + intent.putExtra("latitude", geoPoint.latitude) + intent.putExtra("longitude", geoPoint.longitude) + setResult(Activity.RESULT_OK, intent) + val imm: InputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + window.peekDecorView()?.run { + imm.hideSoftInputFromWindow(windowToken, 0) + } + finish() + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/gms/GmsAdapter.kt b/app/src/main/java/top/niunaijun/blackbox/view/gms/GmsAdapter.kt new file mode 100644 index 00000000..d93f03a9 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/gms/GmsAdapter.kt @@ -0,0 +1,35 @@ +package top.niunaijun.blackbox.view.gms + +import android.view.View +import android.view.ViewGroup +import cbfg.rvadapter.RVHolder +import cbfg.rvadapter.RVHolderFactory +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.bean.GmsBean +import top.niunaijun.blackbox.databinding.ItemGmsBinding + +/** + * + * @Description: + * @Author: BlackBox + * @CreateDate: 2022/3/2 21:13 + */ +class GmsAdapter : RVHolderFactory() { + override fun createViewHolder(parent: ViewGroup?, viewType: Int, item: Any): RVHolder { + return GmsVH(inflate(R.layout.item_gms,parent)) + } + + class GmsVH(itemView:View):RVHolder(itemView) { + private val binding = ItemGmsBinding.bind(itemView) + + override fun setContent(item: GmsBean, isSelected: Boolean, payload: Any?) { + binding.tvTitle.text = item.userName + binding.checkbox.isChecked = item.isInstalledGms + binding.checkbox.setOnCheckedChangeListener { buttonView, _ -> + if (buttonView.isPressed) { + binding.root.performClick() + } + } + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/gms/GmsFactory.kt b/app/src/main/java/top/niunaijun/blackbox/view/gms/GmsFactory.kt new file mode 100644 index 00000000..2d0d1ed4 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/gms/GmsFactory.kt @@ -0,0 +1,18 @@ +package top.niunaijun.blackbox.view.gms + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import top.niunaijun.blackbox.data.GmsRepository + +/** + * + * @Description: + * @Author: BlackBox + * @CreateDate: 2022/3/2 21:15 + */ +class GmsFactory(private val repo:GmsRepository): ViewModelProvider.NewInstanceFactory() { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return GmsViewModel(repo) as T + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/gms/GmsManagerActivity.kt b/app/src/main/java/top/niunaijun/blackbox/view/gms/GmsManagerActivity.kt new file mode 100644 index 00000000..ef1b169f --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/gms/GmsManagerActivity.kt @@ -0,0 +1,127 @@ +package top.niunaijun.blackbox.view.gms + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.widget.Switch +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import cbfg.rvadapter.RVAdapter +import com.afollestad.materialdialogs.MaterialDialog +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.bean.GmsBean +import top.niunaijun.blackbox.databinding.ActivityGmsBinding +import top.niunaijun.blackbox.util.InjectionUtil +import top.niunaijun.blackbox.util.inflate +import top.niunaijun.blackbox.util.toast +import top.niunaijun.blackbox.view.base.LoadingActivity + +/** + * + * @Description: gms manager activity + * @Author: BlackBox + * @CreateDate: 2022/3/2 21:06 + */ +class GmsManagerActivity : LoadingActivity() { + private lateinit var viewModel: GmsViewModel + private lateinit var mAdapter: RVAdapter + private val viewBinding: ActivityGmsBinding by inflate() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(viewBinding.root) + initToolbar(viewBinding.toolbarLayout.toolbar, R.string.gms_manager, true) + initViewModel() + + initRecyclerView() + } + + private fun initViewModel() { + viewModel = ViewModelProvider(this, InjectionUtil.getGmsFactory())[GmsViewModel::class.java] + showLoading() + + viewModel.mInstalledLiveData.observe(this) { + hideLoading() + mAdapter.setItems(it) + } + + viewModel.mUpdateInstalledLiveData.observe(this) { result -> + if (result == null) { + return@observe + } + + val items = mAdapter.getItems() + for (index in items.indices) { + val bean = items[index] + if (bean.userID == result.userID) { + if (result.success) { + bean.isInstalledGms = !bean.isInstalledGms + } + mAdapter.replaceAt( index,bean) + break + } + } + + hideLoading() + if (result.success) { + toast(result.msg) + } else { + MaterialDialog(this).show { + title(R.string.gms_manager) + message(text = result.msg) + positiveButton(R.string.done) + } + } + } + + viewModel.getInstalledUser() + } + + private fun initRecyclerView() { + mAdapter = RVAdapter(this, GmsAdapter()).bind(viewBinding.recyclerView) + .setItemClickListener { view, item, _ -> + val checkbox = view.findViewById(R.id.checkbox) + if (item.isInstalledGms) { + uninstallGms(item.userID, checkbox) + } else { + installGms(item.userID, checkbox) + } + } + viewBinding.recyclerView.layoutManager = LinearLayoutManager(this) + } + + private fun installGms(userID: Int, checkbox: Switch) { + MaterialDialog(this).show { + title(R.string.enable_gms) + message(R.string.enable_gms_hint) + positiveButton(R.string.done) { + showLoading() + viewModel.installGms(userID) + } + negativeButton(R.string.cancel) { + checkbox.isChecked = !checkbox.isChecked + } + } + } + + private fun uninstallGms(userID: Int, checkbox: Switch) { + MaterialDialog(this).show { + title(R.string.disable_gms) + message(R.string.disable_gms_hint) + positiveButton(R.string.done) { + showLoading() + viewModel.uninstallGms(userID) + } + negativeButton(R.string.cancel) { + checkbox.isChecked = !checkbox.isChecked + } + } + } + + companion object{ + fun start(context: Context) { + val intent = Intent(context,GmsManagerActivity::class.java) + context.startActivity(intent) + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/gms/GmsViewModel.kt b/app/src/main/java/top/niunaijun/blackbox/view/gms/GmsViewModel.kt new file mode 100644 index 00000000..28205318 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/gms/GmsViewModel.kt @@ -0,0 +1,36 @@ +package top.niunaijun.blackbox.view.gms + +import androidx.lifecycle.MutableLiveData +import top.niunaijun.blackbox.bean.GmsBean +import top.niunaijun.blackbox.bean.GmsInstallBean +import top.niunaijun.blackbox.data.GmsRepository +import top.niunaijun.blackbox.view.base.BaseViewModel + +/** + * + * @Description: gms viewModel + * @Author: BlackBox + * @CreateDate: 2022/3/2 21:11 + */ +class GmsViewModel(private val mRepo: GmsRepository) : BaseViewModel() { + val mInstalledLiveData = MutableLiveData>() + val mUpdateInstalledLiveData = MutableLiveData() + + fun getInstalledUser() { + launchOnUI { + mRepo.getGmsInstalledList(mInstalledLiveData) + } + } + + fun installGms(userID: Int) { + launchOnUI { + mRepo.installGms(userID,mUpdateInstalledLiveData) + } + } + + fun uninstallGms(userID: Int) { + launchOnUI { + mRepo.uninstallGms(userID,mUpdateInstalledLiveData) + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/list/ListActivity.kt b/app/src/main/java/top/niunaijun/blackbox/view/list/ListActivity.kt new file mode 100644 index 00000000..5d326b25 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/list/ListActivity.kt @@ -0,0 +1,163 @@ +package top.niunaijun.blackbox.view.list + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.inputmethod.InputMethodManager +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import cbfg.rvadapter.RVAdapter +import com.ferfalk.simplesearchview.SimpleSearchView +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.bean.InstalledAppBean +import top.niunaijun.blackbox.databinding.ActivityListBinding +import top.niunaijun.blackbox.util.InjectionUtil +import top.niunaijun.blackbox.util.inflate +import top.niunaijun.blackbox.view.base.BaseActivity + +class ListActivity : BaseActivity() { + private val viewBinding: ActivityListBinding by inflate() + private lateinit var mAdapter: RVAdapter + private lateinit var viewModel: ListViewModel + private var appList: List = ArrayList() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(viewBinding.root) + + initToolbar(viewBinding.toolbarLayout.toolbar, R.string.installed_app, true) + + mAdapter = RVAdapter(this,ListAdapter()).bind(viewBinding.recyclerView).setItemClickListener { _, item, _ -> + finishWithResult(item.packageName) + } + + viewBinding.recyclerView.layoutManager = LinearLayoutManager(this) + + initSearchView() + initViewModel() + onBackPressedDispatcher + .addCallback(onBackPressedCallback) + } + + private fun initSearchView() { + viewBinding.searchView.setOnQueryTextListener(object : SimpleSearchView.OnQueryTextListener { + override fun onQueryTextChange(newText: String): Boolean { + filterApp(newText) + return true + } + + override fun onQueryTextCleared(): Boolean { + return true + } + + override fun onQueryTextSubmit(query: String): Boolean { + return true + } + }) + } + + private fun initViewModel() { + viewModel = ViewModelProvider(this, InjectionUtil.getListFactory()).get(ListViewModel::class.java) + val onlyShowXp = intent.getBooleanExtra("onlyShowXp", false) + val userID = intent.getIntExtra("userID",0) + + if (onlyShowXp) { + viewModel.getInstalledModules() + viewBinding.toolbarLayout.toolbar.setTitle(R.string.installed_module) + } else { + viewModel.getInstallAppList(userID) + viewBinding.toolbarLayout.toolbar.setTitle(R.string.installed_app) + } + + viewModel.loadingLiveData.observe(this) { + if (it) { + viewBinding.stateView.showLoading() + } else { + viewBinding.stateView.showContent() + } + } + + viewModel.appsLiveData.observe(this) { + if (it != null) { + this.appList = it + viewBinding.searchView.setQuery("", false) + filterApp("") + if (it.isNotEmpty()) { + viewBinding.stateView.showContent() + viewModel.previewInstalledList() + } else { + viewBinding.stateView.showEmpty() + } + } + } + } + + private fun filterApp(newText: String) { + val newList = this.appList.filter { + it.name.contains(newText, true) or it.packageName.contains(newText, true) + } + mAdapter.setItems(newList) + } + + private val openDocumentedResult = registerForActivityResult(ActivityResultContracts.GetContent()) { + it?.run { + finishWithResult(it.toString()) + } + } + + private val onBackPressedCallback = + object: OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (viewBinding.searchView.isSearchOpen) { + viewBinding.searchView.closeSearch() + } + finish() + } + } + + private fun finishWithResult(source: String) { + intent.putExtra("source", source) + setResult(Activity.RESULT_OK, intent) + val imm: InputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + window.peekDecorView()?.run { + imm.hideSoftInputFromWindow(windowToken, 0) + } + finish() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.list_choose) { + openDocumentedResult.launch("application/vnd.android.package-archive") + } + return true + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_list, menu) + val item = menu.findItem(R.id.list_search) + viewBinding.searchView.setMenuItem(item) + + return true + } + + override fun onStop() { + super.onStop() + viewModel.loadingLiveData.postValue(true) + viewModel.loadingLiveData.removeObservers(this) + viewModel.appsLiveData.postValue(null) + viewModel.appsLiveData.removeObservers(this) + } + + companion object { + fun start(context: Context, onlyShowXp: Boolean) { + val intent = Intent(context, ListActivity::class.java) + intent.putExtra("onlyShowXp", onlyShowXp) + context.startActivity(intent) + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/list/ListAdapter.kt b/app/src/main/java/top/niunaijun/blackbox/view/list/ListAdapter.kt new file mode 100644 index 00000000..34b90ab2 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/list/ListAdapter.kt @@ -0,0 +1,36 @@ +package top.niunaijun.blackbox.view.list + +import android.view.View +import android.view.ViewGroup +import cbfg.rvadapter.RVHolder +import cbfg.rvadapter.RVHolderFactory +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.bean.InstalledAppBean +import top.niunaijun.blackbox.databinding.ItemPackageBinding + +/** + * + * @Description: 软件显示界面适配器 + * @Author: wukaicheng + * @CreateDate: 2021/4/29 21:52 + */ +class ListAdapter : RVHolderFactory() { + override fun createViewHolder(parent: ViewGroup?, viewType: Int, item: Any): RVHolder { + return ListVH(inflate(R.layout.item_package,parent)) + } + + class ListVH(itemView:View) :RVHolder(itemView) { + private val binding = ItemPackageBinding.bind(itemView) + + override fun setContent(item: InstalledAppBean, isSelected: Boolean, payload: Any?) { + binding.icon.setImageDrawable(item.icon) + binding.name.text = item.name + binding.packageName.text = item.packageName + binding.cornerLabel.visibility = if (item.isInstall) { + View.VISIBLE + } else { + View.GONE + } + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/list/ListFactory.kt b/app/src/main/java/top/niunaijun/blackbox/view/list/ListFactory.kt new file mode 100644 index 00000000..0558ca0b --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/list/ListFactory.kt @@ -0,0 +1,19 @@ +package top.niunaijun.blackbox.view.list + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import top.niunaijun.blackbox.data.AppsRepository + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/4/29 22:36 + */ +@Suppress("UNCHECKED_CAST") +class ListFactory(private val appsRepository: AppsRepository) : ViewModelProvider.NewInstanceFactory() { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return ListViewModel(appsRepository) as T + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/list/ListViewModel.kt b/app/src/main/java/top/niunaijun/blackbox/view/list/ListViewModel.kt new file mode 100644 index 00000000..5f38ff52 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/list/ListViewModel.kt @@ -0,0 +1,35 @@ +package top.niunaijun.blackbox.view.list + +import androidx.lifecycle.MutableLiveData +import top.niunaijun.blackbox.bean.InstalledAppBean +import top.niunaijun.blackbox.data.AppsRepository +import top.niunaijun.blackbox.view.base.BaseViewModel + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/4/29 22:36 + */ +class ListViewModel(private val repo: AppsRepository) : BaseViewModel() { + val appsLiveData = MutableLiveData>() + val loadingLiveData = MutableLiveData() + + fun previewInstalledList() { + launchOnUI{ + repo.previewInstallList() + } + } + + fun getInstallAppList(userID:Int) { + launchOnUI { + repo.getInstalledAppList(userID, loadingLiveData, appsLiveData) + } + } + + fun getInstalledModules() { + launchOnUI { + repo.getInstalledModuleList(loadingLiveData, appsLiveData) + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/main/BlackBoxLoader.kt b/app/src/main/java/top/niunaijun/blackbox/view/main/BlackBoxLoader.kt new file mode 100644 index 00000000..dcab1fd5 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/main/BlackBoxLoader.kt @@ -0,0 +1,124 @@ +package top.niunaijun.blackbox.view.main + +import android.app.Application +import android.content.Context +import android.util.Log +import top.niunaijun.blackbox.BlackBoxCore +import top.niunaijun.blackbox.app.BActivityThread +import top.niunaijun.blackbox.app.configuration.AppLifecycleCallback +import top.niunaijun.blackbox.app.configuration.ClientConfiguration +import top.niunaijun.blackbox.app.App +import top.niunaijun.blackbox.biz.cache.AppSharedPreferenceDelegate +import java.io.File + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/5/6 23:38 + */ +class BlackBoxLoader { + private var mHideRoot by AppSharedPreferenceDelegate(App.getContext(), false) + private var mHideXposed by AppSharedPreferenceDelegate(App.getContext(), false) + private var mDaemonEnable by AppSharedPreferenceDelegate(App.getContext(), false) + private var mShowShortcutPermissionDialog by AppSharedPreferenceDelegate(App.getContext(), true) + + fun hideRoot(): Boolean { + return mHideRoot + } + + fun invalidHideRoot(hideRoot: Boolean) { + this.mHideRoot = hideRoot + } + + fun hideXposed(): Boolean { + return mHideXposed + } + + fun invalidHideXposed(hideXposed: Boolean) { + this.mHideXposed = hideXposed + } + + fun daemonEnable(): Boolean { + return mDaemonEnable + } + + fun invalidDaemonEnable(enable: Boolean) { + this.mDaemonEnable = enable + } + + fun showShortcutPermissionDialog(): Boolean { + return mShowShortcutPermissionDialog + } + + fun invalidShortcutPermissionDialog(show: Boolean) { + this.mShowShortcutPermissionDialog = show + } + + fun addLifecycleCallback() { + BlackBoxCore.get().addAppLifecycleCallback(object : AppLifecycleCallback() { + override fun beforeCreateApplication( + packageName: String?, + processName: String?, + context: Context?, + userId: Int + ) { + Log.d( + TAG, + "beforeCreateApplication: pkg $packageName, processName $processName,userID:${BActivityThread.getUserId()}" + ) + } + + override fun beforeApplicationOnCreate( + packageName: String?, + processName: String?, + application: Application?, + userId: Int + ) { + Log.d(TAG, "beforeApplicationOnCreate: pkg $packageName, processName $processName") + } + + override fun afterApplicationOnCreate( + packageName: String?, + processName: String?, + application: Application?, + userId: Int + ) { + Log.d(TAG, "afterApplicationOnCreate: pkg $packageName, processName $processName") + //RockerManager.init(application, userId) + } + }) + } + + fun attachBaseContext(context: Context) { + BlackBoxCore.get().doAttachBaseContext(context, object : ClientConfiguration() { + override fun getHostPackageName(): String { + return context.packageName + } + + override fun isHideRoot(): Boolean { + return mHideRoot + } + + override fun isHideXposed(): Boolean { + return mHideXposed + } + + override fun isEnableDaemonService(): Boolean { + return mDaemonEnable + } + + override fun requestInstallPackage(file: File?, userId: Int): Boolean { + return false + } + }) + } + + fun doOnCreate() { + BlackBoxCore.get().doCreate() + } + + companion object { + val TAG: String = BlackBoxLoader::class.java.simpleName + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/main/MainActivity.kt b/app/src/main/java/top/niunaijun/blackbox/view/main/MainActivity.kt new file mode 100644 index 00000000..49ee3b22 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/main/MainActivity.kt @@ -0,0 +1,181 @@ +package top.niunaijun.blackbox.view.main + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.edit +import androidx.viewpager2.widget.ViewPager2 +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.input.input +import top.niunaijun.blackbox.BlackBoxCore +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.app.App +import top.niunaijun.blackbox.app.AppManager +import top.niunaijun.blackbox.databinding.ActivityMainBinding +import top.niunaijun.blackbox.util.Resolution +import top.niunaijun.blackbox.util.inflate +import top.niunaijun.blackbox.view.apps.AppsFragment +import top.niunaijun.blackbox.view.base.LoadingActivity +import top.niunaijun.blackbox.view.fake.FakeManagerActivity +import top.niunaijun.blackbox.view.list.ListActivity +import top.niunaijun.blackbox.view.setting.SettingActivity + +class MainActivity : LoadingActivity() { + private val viewBinding: ActivityMainBinding by inflate() + private lateinit var mViewPagerAdapter: ViewPagerAdapter + private val fragmentList = mutableListOf() + private var currentUser = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(viewBinding.root) + initToolbar(viewBinding.toolbarLayout.toolbar, R.string.app_name) + initViewPager() + initFab() + initToolbarSubTitle() + } + + private fun initToolbarSubTitle() { + updateUserRemark(0) + //hack code + viewBinding.toolbarLayout.toolbar.getChildAt(1).setOnClickListener { + MaterialDialog(this).show { + title(res = R.string.userRemark) + input( + hintRes = R.string.userRemark, + prefill = viewBinding.toolbarLayout.toolbar.subtitle + ) { _, input -> + AppManager.mRemarkSharedPreferences.edit { + putString("Remark$currentUser", input.toString()) + viewBinding.toolbarLayout.toolbar.subtitle = input + } + } + positiveButton(res = R.string.done) + negativeButton(res = R.string.cancel) + } + } + } + + private fun initViewPager() { + val userList = BlackBoxCore.get().users + userList.forEach { + fragmentList.add(AppsFragment.newInstance(it.id)) + } + + currentUser = userList.firstOrNull()?.id ?: 0 + fragmentList.add(AppsFragment.newInstance(userList.size)) + + mViewPagerAdapter = ViewPagerAdapter(this) + mViewPagerAdapter.replaceData(fragmentList) + viewBinding.viewPager.adapter = mViewPagerAdapter + viewBinding.dotsIndicator.attachTo(viewBinding.viewPager) + viewBinding.viewPager.registerOnPageChangeCallback(object : + ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + currentUser = fragmentList[position].userID + updateUserRemark(currentUser) + showFloatButton(true) + } + }) + } + + private fun initFab() { + viewBinding.fab.setOnClickListener { + val userId = viewBinding.viewPager.currentItem + val intent = Intent(this, ListActivity::class.java) + intent.putExtra("userID", userId) + apkPathResult.launch(intent) + } + } + + fun showFloatButton(show: Boolean) { + val tranY: Float = Resolution.convertDpToPixel(120F, App.getContext()) + val time = 200L + if (show) { + viewBinding.fab.animate().translationY(0f).alpha(1f).setDuration(time) + .start() + } else { + viewBinding.fab.animate().translationY(tranY).alpha(0f).setDuration(time) + .start() + } + } + + fun scanUser() { + val userList = BlackBoxCore.get().users + + if (fragmentList.size == userList.size) { + fragmentList.add(AppsFragment.newInstance(fragmentList.size)) + } else if (fragmentList.size > userList.size + 1) { + fragmentList.removeLast() + } + + mViewPagerAdapter.notifyDataSetChanged() + } + + private fun updateUserRemark(userId: Int) { + var remark = AppManager.mRemarkSharedPreferences.getString("Remark$userId", "User $userId") + if (remark.isNullOrEmpty()) { + remark = "User $userId" + } + + viewBinding.toolbarLayout.toolbar.subtitle = remark + } + + private val apkPathResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + it.data?.let { data -> + val userId = data.getIntExtra("userID", 0) + val source = data.getStringExtra("source") + if (source != null) { + fragmentList[userId].installApk(source) + } + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.main_git -> { + val intent = + Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/BlackBoxing/BlackBox")) + startActivity(intent) + } + + R.id.main_setting -> { + SettingActivity.start(this) + } + + R.id.main_tg -> { + val intent = + Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/blackboxing")) + startActivity(intent) + } + + R.id.fake_location -> { + //toast("Still Developing") + val intent = Intent(this, FakeManagerActivity::class.java) + intent.putExtra("userID", currentUser) + startActivity(intent) + } + } + return true + } + + companion object { + fun start(context: Context) { + val intent = Intent(context, MainActivity::class.java) + context.startActivity(intent) + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/main/ShortcutActivity.kt b/app/src/main/java/top/niunaijun/blackbox/view/main/ShortcutActivity.kt new file mode 100644 index 00000000..90eaa322 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/main/ShortcutActivity.kt @@ -0,0 +1,26 @@ +package top.niunaijun.blackbox.view.main + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import top.niunaijun.blackbox.BlackBoxCore + +/** + * + * @Description: 快捷方式跳转activity + * @Author: wukaicheng + * @CreateDate: 2022/2/11 23:13 + */ +class ShortcutActivity:AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pkg = intent.getStringExtra("pkg") + val userID = intent.getIntExtra("userId",0) + + lifecycleScope.launch { + BlackBoxCore.get().launchApk(pkg,userID) + finish() + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/main/ViewPagerAdapter.kt b/app/src/main/java/top/niunaijun/blackbox/view/main/ViewPagerAdapter.kt new file mode 100644 index 00000000..f7fa6fed --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/main/ViewPagerAdapter.kt @@ -0,0 +1,29 @@ +package top.niunaijun.blackbox.view.main + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import top.niunaijun.blackbox.view.apps.AppsFragment + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/4/29 22:00 + */ +class ViewPagerAdapter(appCompatActivity: AppCompatActivity) : FragmentStateAdapter(appCompatActivity) { + private var fragmentList = mutableListOf() + + fun replaceData(list: MutableList) { + this.fragmentList = list + notifyDataSetChanged() + } + + override fun getItemCount(): Int { + return fragmentList.size + } + + override fun createFragment(position: Int): Fragment { + return fragmentList[position] + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/main/WelcomeActivity.kt b/app/src/main/java/top/niunaijun/blackbox/view/main/WelcomeActivity.kt new file mode 100644 index 00000000..0d793801 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/main/WelcomeActivity.kt @@ -0,0 +1,31 @@ +package top.niunaijun.blackbox.view.main + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import top.niunaijun.blackbox.util.InjectionUtil +import top.niunaijun.blackbox.view.list.ListViewModel + +class WelcomeActivity : AppCompatActivity() { + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + jump() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + previewInstalledAppList() + jump() + } + + private fun jump() { + MainActivity.start(this) + finish() + } + + private fun previewInstalledAppList() { + val viewModel = ViewModelProvider(this,InjectionUtil.getListFactory()).get(ListViewModel::class.java) + viewModel.previewInstalledList() + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/setting/SettingActivity.kt b/app/src/main/java/top/niunaijun/blackbox/view/setting/SettingActivity.kt new file mode 100644 index 00000000..fdea635d --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/setting/SettingActivity.kt @@ -0,0 +1,30 @@ +package top.niunaijun.blackbox.view.setting + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.databinding.ActivitySettingBinding +import top.niunaijun.blackbox.util.inflate +import top.niunaijun.blackbox.view.base.BaseActivity + +class SettingActivity : BaseActivity() { + private val viewBinding: ActivitySettingBinding by inflate() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(viewBinding.root) + initToolbar(viewBinding.toolbarLayout.toolbar, R.string.setting, true) + supportFragmentManager.beginTransaction() + .replace(R.id.fragment, SettingFragment()) + .commit() + } + + companion object { + fun start(context: Context) { + val intent = Intent(context,SettingActivity::class.java) + intent.action = Intent.ACTION_OPEN_DOCUMENT + context.startActivity(intent) + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/setting/SettingFragment.kt b/app/src/main/java/top/niunaijun/blackbox/view/setting/SettingFragment.kt new file mode 100644 index 00000000..dee072b0 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/setting/SettingFragment.kt @@ -0,0 +1,102 @@ +package top.niunaijun.blackbox.view.setting + +import android.content.Intent +import android.os.Bundle +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat +import top.niunaijun.blackbox.BlackBoxCore +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.app.AppManager +import top.niunaijun.blackbox.util.toast +import top.niunaijun.blackbox.view.gms.GmsManagerActivity +import top.niunaijun.blackbox.view.xp.XpActivity + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/5/6 22:13 + */ +class SettingFragment : PreferenceFragmentCompat() { + private lateinit var xpEnable: SwitchPreferenceCompat + private lateinit var xpModule: Preference + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.setting, rootKey) + + xpEnable = findPreference("xp_enable")!! + xpEnable.isChecked = BlackBoxCore.get().isXPEnable + + xpEnable.setOnPreferenceChangeListener { _, newValue -> + BlackBoxCore.get().isXPEnable = (newValue == true) + true + } + //xp模块跳转 + xpModule = findPreference("xp_module")!! + xpModule.setOnPreferenceClickListener { + val intent = Intent(requireActivity(), XpActivity::class.java) + requireContext().startActivity(intent) + true + } + initGms() + + invalidHideState{ + val xpHidePreference: Preference = (findPreference("xp_hide")!!) + val hideXposed = AppManager.mBlackBoxLoader.hideXposed() + xpHidePreference.setDefaultValue(hideXposed) + xpHidePreference + } + + invalidHideState{ + val rootHidePreference: Preference = (findPreference("root_hide")!!) + val hideRoot = AppManager.mBlackBoxLoader.hideRoot() + rootHidePreference.setDefaultValue(hideRoot) + rootHidePreference + } + + invalidHideState { + val daemonPreference: Preference = (findPreference("daemon_enable")!!) + val mDaemonEnable = AppManager.mBlackBoxLoader.daemonEnable() + daemonPreference.setDefaultValue(mDaemonEnable) + daemonPreference + } + } + + private fun initGms() { + val gmsManagerPreference: Preference = (findPreference("gms_manager")!!) + + if (BlackBoxCore.get().isSupportGms) { + gmsManagerPreference.setOnPreferenceClickListener { + GmsManagerActivity.start(requireContext()) + true + } + } else { + gmsManagerPreference.summary = getString(R.string.no_gms) + gmsManagerPreference.isEnabled = false + } + } + + private fun invalidHideState(block: () -> Preference) { + val pref = block() + pref.setOnPreferenceChangeListener { preference, newValue -> + val tmpHide = (newValue == true) + when (preference.key) { + "xp_hide" -> { + AppManager.mBlackBoxLoader.invalidHideXposed(tmpHide) + } + + "root_hide" -> { + AppManager.mBlackBoxLoader.invalidHideRoot(tmpHide) + } + + "daemon_enable" -> { + AppManager.mBlackBoxLoader.invalidDaemonEnable(tmpHide) + } + } + + toast(R.string.restart_module) + return@setOnPreferenceChangeListener true + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/xp/XpActivity.kt b/app/src/main/java/top/niunaijun/blackbox/view/xp/XpActivity.kt new file mode 100644 index 00000000..8fc31ed2 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/xp/XpActivity.kt @@ -0,0 +1,135 @@ +package top.niunaijun.blackbox.view.xp + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import cbfg.rvadapter.RVAdapter +import com.afollestad.materialdialogs.MaterialDialog +import top.niunaijun.blackbox.BlackBoxCore +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.bean.XpModuleInfo +import top.niunaijun.blackbox.databinding.ActivityXpBinding +import top.niunaijun.blackbox.util.InjectionUtil +import top.niunaijun.blackbox.util.inflate +import top.niunaijun.blackbox.util.toast +import top.niunaijun.blackbox.view.base.LoadingActivity +import top.niunaijun.blackbox.view.list.ListActivity + +/** + * + * @Description: xposed模块管理界面 + * @Author: wukaicheng + * @CreateDate: 2021/5/2 20:25 + */ +class XpActivity : LoadingActivity() { + private val viewBinding: ActivityXpBinding by inflate() + private lateinit var viewModel: XpViewModel + private lateinit var mAdapter: RVAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(viewBinding.root) + initToolbar(viewBinding.toolbarLayout.toolbar, R.string.xp_setting, true) + + viewModel = ViewModelProvider(this, InjectionUtil.getXpFactory()).get(XpViewModel::class.java) + + initRecyclerView() + initFab() + } + + private fun observeLiveData() { + viewBinding.stateView.showLoading() + viewModel.getInstalledModule() + viewModel.appsLiveData.observe(this) { + if (it.isNullOrEmpty()) { + viewBinding.stateView.showEmpty() + } else { + mAdapter.setItems(it) + viewBinding.stateView.showContent() + } + } + + viewModel.resultLiveData.observe(this) { + if (!TextUtils.isEmpty(it)) { + hideLoading() + toast(it) + viewModel.getInstalledModule() + } + } + } + + private fun initRecyclerView() { + mAdapter = RVAdapter(this, XpAdapter()).bind(viewBinding.recyclerView) + .setItemClickListener { _, item, position -> + item.enable = !item.enable + BlackBoxCore.get().setModuleEnable(item.packageName, item.enable) + mAdapter.replaceAt(position, item) + toast(R.string.restart_module) + }.setItemLongClickListener { _, item, _ -> + unInstallModule(item.packageName) + } + + viewBinding.recyclerView.layoutManager = LinearLayoutManager(this) + viewBinding.stateView.showEmpty() + } + + private fun initFab() { + viewBinding.fab.setOnClickListener { + val intent = Intent(this, ListActivity::class.java) + intent.putExtra("onlyShowXp", true) + apkPathResult.launch(intent) + } + } + + override fun onStart() { + super.onStart() + observeLiveData() + } + + override fun onStop() { + super.onStop() + viewModel.appsLiveData.value = null + viewModel.appsLiveData.removeObservers(this) + viewModel.resultLiveData.value = null + viewModel.resultLiveData.removeObservers(this) + } + + private fun unInstallModule(packageName: String) { + MaterialDialog(this).show { + title(R.string.uninstall_module) + message(R.string.uninstall_module_hint) + positiveButton(R.string.done) { + showLoading() + viewModel.unInstallModule(packageName) + } + negativeButton(R.string.cancel) + } + } + + private fun installModule(source: String) { + showLoading() + viewModel.installModule(source) + } + + private val apkPathResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + it.data?.let { data -> + val source = data.getStringExtra("source") + if (source != null) { + installModule(source) + } + } + } + } + + companion object { + fun start(context: Context) { + val intent = Intent(context, XpActivity::class.java) + context.startActivity(intent) + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/xp/XpAdapter.kt b/app/src/main/java/top/niunaijun/blackbox/view/xp/XpAdapter.kt new file mode 100644 index 00000000..a1a36233 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/xp/XpAdapter.kt @@ -0,0 +1,37 @@ +package top.niunaijun.blackbox.view.xp + +import android.view.View +import android.view.ViewGroup +import cbfg.rvadapter.RVHolder +import cbfg.rvadapter.RVHolderFactory +import top.niunaijun.blackbox.R +import top.niunaijun.blackbox.bean.XpModuleInfo +import top.niunaijun.blackbox.databinding.ItemXpBinding + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/5/2 21:32 + */ +class XpAdapter : RVHolderFactory() { + override fun createViewHolder(parent: ViewGroup?, viewType: Int, item: Any): RVHolder { + return XpVH(inflate(R.layout.item_xp, parent)) + } + + class XpVH(itemView: View) : RVHolder(itemView) { + private val binding = ItemXpBinding.bind(itemView) + + override fun setContent(item: XpModuleInfo, isSelected: Boolean, payload: Any?) { + binding.icon.setImageDrawable(item.icon) + binding.name.text = item.name + binding.desc.text = item.desc + binding.enable.isChecked = item.enable + binding.enable.setOnCheckedChangeListener { buttonView, _ -> + if (buttonView.isPressed) { + binding.root.performClick() + } + } + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/xp/XpFactory.kt b/app/src/main/java/top/niunaijun/blackbox/view/xp/XpFactory.kt new file mode 100644 index 00000000..fe18ce3f --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/xp/XpFactory.kt @@ -0,0 +1,19 @@ +package top.niunaijun.blackbox.view.xp + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import top.niunaijun.blackbox.data.XpRepository + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/5/2 20:56 + */ +@Suppress("UNCHECKED_CAST") +class XpFactory(private val repo:XpRepository): ViewModelProvider.NewInstanceFactory() { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return XpViewModel(repo) as T + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/view/xp/XpViewModel.kt b/app/src/main/java/top/niunaijun/blackbox/view/xp/XpViewModel.kt new file mode 100644 index 00000000..b9d3692f --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/view/xp/XpViewModel.kt @@ -0,0 +1,35 @@ +package top.niunaijun.blackbox.view.xp + +import androidx.lifecycle.MutableLiveData +import top.niunaijun.blackbox.bean.XpModuleInfo +import top.niunaijun.blackbox.data.XpRepository +import top.niunaijun.blackbox.view.base.BaseViewModel + +/** + * + * @Description: + * @Author: wukaicheng + * @CreateDate: 2021/5/2 20:55 + */ +class XpViewModel(private val repo: XpRepository) : BaseViewModel() { + val appsLiveData = MutableLiveData>() + val resultLiveData = MutableLiveData() + + fun getInstalledModule() { + launchOnUI { + repo.getInstallModules(appsLiveData) + } + } + + fun installModule(source:String) { + launchOnUI { + repo.installModule(source, resultLiveData) + } + } + + fun unInstallModule(packageName: String) { + launchOnUI { + repo.unInstallModule(packageName, resultLiveData) + } + } +} diff --git a/app/src/main/java/top/niunaijun/blackbox/widget/EnFloatView.kt b/app/src/main/java/top/niunaijun/blackbox/widget/EnFloatView.kt new file mode 100644 index 00000000..3b7a2a8d --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/widget/EnFloatView.kt @@ -0,0 +1,50 @@ +package top.niunaijun.blackbox.widget + +import android.content.Context +import android.view.MotionEvent +import com.imuxuan.floatingview.FloatingMagnetView +import top.niunaijun.blackbox.R + +/** + * + * @Description: rocker parent + * @Author: kotlinMiku + * @CreateDate: 2022/3/20 16:58 + */ +class EnFloatView(mContext: Context) : FloatingMagnetView(mContext) { + private val TAG = "RockerManager" + private var rockerView: RockerView? = null + private var mListener: LocationListener? = null + + init { + inflate(mContext, R.layout.view_float_rocker, this) + initRockerView() + } + + private fun initRockerView() { + rockerView = findViewById(R.id.rocker) + rockerView?.setListener { type, currentAngle, currentDistance -> + if (type == RockerView.EVENT_CLOCK && currentAngle != -1F) { + val realAngle = currentAngle + val realDistance = currentDistance * 0.001F + //拉满的话,大概就是一秒五米 + mListener?.invoke(realAngle, realDistance) + } + } + } + + override fun onTouchEvent(event: MotionEvent?): Boolean { + if (event?.action == MotionEvent.ACTION_DOWN) { + rockerView?.setCanMove(false) + } else if (event?.action == MotionEvent.ACTION_UP) { + rockerView?.setCanMove(true) + } + return super.onTouchEvent(event) + } + + fun setListener(listener: LocationListener) { + this.mListener = listener + } +} + +typealias LocationListener = (angle: Float, distance: Float) -> Unit \ No newline at end of file diff --git a/app/src/main/java/top/niunaijun/blackbox/widget/RockerView.java b/app/src/main/java/top/niunaijun/blackbox/widget/RockerView.java new file mode 100644 index 00000000..08a98942 --- /dev/null +++ b/app/src/main/java/top/niunaijun/blackbox/widget/RockerView.java @@ -0,0 +1,434 @@ +package top.niunaijun.blackbox.widget; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; + +import androidx.annotation.NonNull; + +import top.niunaijun.blackbox.util.MathUtil; + +/** + * A custom view for game or others. + *

+ * Author: GcsSloop + * Created Date: 16/5/24 + * Copyright (C) 2016 GcsSloop. + * GitHub: https://github.com/GcsSloop + */ +public class RockerView extends SurfaceView implements Runnable, SurfaceHolder.Callback { + private static final int DEFAULT_AREA_RADIUS = 100; + private static final int DEFAULT_ROCKER_RADIUS = 35; + + private static final int DEFAULT_AREA_COLOR = Color.argb(128,0,0,0); + private static final int DEFAULT_ROCKER_COLOR = Color.argb(128,0,0,0); + + private static final int DEFAULT_REFRESH_CYCLE = 30; + private static final int DEFAULT_CALLBACK_CYCLE = 300; + + private SurfaceHolder mHolder; + private static boolean mDrawOk = true; + private static boolean mCallbackOk = true; + + private Paint mPaint; + + /** + * The rocker active area center position. + * usually, it is the center of this view. + */ + private Point mAreaPosition; + + /** + * The Rocker position. + * usually, it as same asmAreaPosition . + * if this view touched, it will follow the touch position. + *

+ * we get position information from this. + */ + private Point mRockerPosition; + + private int mAreaRadius = -1; + private int mRockerRadius = -1; + + private int mAreaColor; + private int mRockerColor; + private Bitmap mAreaBitmap; + private Bitmap mRockerBitmap; + + private boolean canMove = true; + + private RockerListener mListener; + public static final int EVENT_ACTION = 1; + public static final int EVENT_CLOCK = 2; + + private int mRefreshCycle = DEFAULT_REFRESH_CYCLE; + private int mCallbackCycle = DEFAULT_CALLBACK_CYCLE; + + /*Life Cycle***********************************************************************************/ + public RockerView(Context context) { + this(context, null); + } + + public RockerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RockerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + // init attrs + initAttrs(); + + // set paint + setPaint(); + + if (isInEditMode()) { + return; + } + + // config surfaceView + configSurfaceView(); + + // config surfaceHolder + configSurfaceHolder(); + } + + private void initAttrs() { + mAreaColor = DEFAULT_AREA_COLOR; + mRockerColor = DEFAULT_ROCKER_COLOR; + mAreaRadius = DEFAULT_AREA_RADIUS; + mRockerRadius = DEFAULT_ROCKER_RADIUS; + } + + private void setPaint() { + mPaint = new Paint(); + mPaint.setAntiAlias(true); + } + + private void configSurfaceView() { + setKeepScreenOn(true); // do not lock screen when surfaceView is running. + setFocusable(true); // make sure this surfaceView can get focus from keyboard. + setFocusableInTouchMode(true); // make sure this surfaceView can get focus from touch. + setZOrderOnTop(true); // make sure this surface is placed on top of the window + } + + private void configSurfaceHolder() { + mHolder = getHolder(); + mHolder.addCallback(this); + mHolder.setFormat(PixelFormat.TRANSPARENT); //设置背景透明 + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measureWidth, measureHeight; + int defaultWidth = (mAreaRadius + mRockerRadius) * 2; + + int widthsize = MeasureSpec.getSize(widthMeasureSpec); //取出宽度的确切数值 + int widthmode = MeasureSpec.getMode(widthMeasureSpec); //取出宽度的测量模式 + + int heightsize = MeasureSpec.getSize(heightMeasureSpec); //取出高度的确切数值 + int heightmode = MeasureSpec.getMode(heightMeasureSpec); //取出高度的测量模式 + + if (widthmode == MeasureSpec.AT_MOST || widthmode == MeasureSpec.UNSPECIFIED || widthsize < 0) { + measureWidth = defaultWidth; + } else { + measureWidth = widthsize; + } + + if (heightmode == MeasureSpec.AT_MOST || heightmode == MeasureSpec.UNSPECIFIED || heightsize < 0) { + measureHeight = defaultWidth; + } else { + measureHeight = heightsize; + } + + setMeasuredDimension(measureWidth, measureHeight); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mAreaPosition = new Point(w / 2, h / 2); + mRockerPosition = new Point(mAreaPosition); + + // this need subtract the view padding + int tempRadius = Math.min(w - getPaddingLeft() - getPaddingRight(), h - getPaddingTop() - getPaddingBottom()); + tempRadius /= 2; + if (mAreaRadius == -1) + mAreaRadius = (int) (tempRadius * 0.75); + if (mRockerRadius == -1) + mRockerRadius = (int) (tempRadius * 0.25); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + try { + Thread mDrawThread = new Thread(this); + mDrawThread.start(); + // listener callback + Thread mCallbackThread = new Thread(() -> { + while (mCallbackOk) { + // listener callback + listenerCallback(); + try { + Thread.sleep(mCallbackCycle); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + mCallbackThread.start(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mDrawOk = false; + mCallbackOk = false; + } + + @Override + protected void onVisibilityChanged(@NonNull View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + if (visibility == VISIBLE) { + mDrawOk = true; + mCallbackOk = true; + } else { + mDrawOk = false; + mCallbackOk = false; + } + } + + /*Event Response*******************************************************************************/ + @Override + public boolean onTouchEvent(MotionEvent event) { + try { + int len = MathUtil.getDistance(mAreaPosition.x, mAreaPosition.y, event.getX(), event.getY()); + if (event.getAction() == MotionEvent.ACTION_DOWN) { + //如果屏幕接触点不在摇杆挥动范围内,则不处理 + if (len > mAreaRadius) { + return true; + } + } + + if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (len <= mAreaRadius) { + //如果手指在摇杆活动范围内,则摇杆处于手指触摸位置 + mRockerPosition.set((int) event.getX(), (int) event.getY()); + } else { + //设置摇杆位置,使其处于手指触摸方向的 摇杆活动范围边缘 + mRockerPosition = MathUtil.getPointByCutLength(mAreaPosition, + new Point((int) event.getX(), (int) event.getY()), mAreaRadius); + } + if (mListener != null) { + float radian = MathUtil.getRadian(mAreaPosition, new Point((int) event.getX(), (int) event.getY())); + float angle = RockerView.this.getAngleConvert(radian); + float distance = MathUtil.getDistance(mAreaPosition.x, mAreaPosition.y, event.getX(), event.getY()); + mListener.callback(EVENT_ACTION, angle, distance); + } + } + //如果手指离开屏幕,则摇杆返回初始位置 + if (event.getAction() == MotionEvent.ACTION_UP) { + mRockerPosition = new Point(mAreaPosition); + if (mListener != null) { + mListener.callback(EVENT_ACTION, -1, 0); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + + /*Thread - draw view***************************************************************************/ + @Override + public void run() { + if (isInEditMode()) { + return; + } + Canvas canvas = null; + while (mDrawOk) { + boolean canMove = this.canMove; + try { + if (canMove) { + canvas = mHolder.lockCanvas(); + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + + drawArea(canvas); + drawRocker(canvas); + } + Thread.sleep(mRefreshCycle); // 休眠 + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (canvas != null && canMove) { + mHolder.unlockCanvasAndPost(canvas); + } + } + } + } + + private void drawArea(Canvas canvas) { + if (mAreaBitmap != null) { + mPaint.setColor(Color.BLACK); + Rect src = new Rect(0, 0, mAreaBitmap.getWidth(), mAreaBitmap.getHeight()); + Rect dst = new Rect( + mAreaPosition.x - mAreaRadius, + mAreaPosition.y - mAreaRadius, + mAreaPosition.x + mAreaRadius, + mAreaPosition.y + mAreaRadius); + canvas.drawBitmap(mAreaBitmap, src, dst, mPaint); + } else { + mPaint.setColor(mAreaColor); + canvas.drawCircle(mAreaPosition.x, mAreaPosition.y, mAreaRadius, mPaint); + } + } + + private void drawRocker(Canvas canvas) { + if (mRockerBitmap != null) { + mPaint.setColor(Color.BLACK); + Rect src = new Rect(0, 0, mRockerBitmap.getWidth(), mRockerBitmap.getHeight()); + Rect dst = new Rect( + mRockerPosition.x - mRockerRadius, + mRockerPosition.y - mRockerRadius, + mRockerPosition.x + mRockerRadius, + mRockerPosition.y + mRockerRadius); + canvas.drawBitmap(mRockerBitmap, src, dst, mPaint); + } else { + mPaint.setColor(mRockerColor); + canvas.drawCircle(mRockerPosition.x, mRockerPosition.y, mRockerRadius, mPaint); + } + } + + private void listenerCallback() { + if (mListener != null) { + if (mRockerPosition.x == mAreaPosition.x && mRockerPosition.y == mAreaPosition.y) { + mListener.callback(EVENT_CLOCK, -1, 0); + } else { + float radian = MathUtil.getRadian(mAreaPosition, new Point(mRockerPosition.x, mRockerPosition.y)); + float angle = RockerView.this.getAngleConvert(radian); + float distance = MathUtil.getDistance(mAreaPosition.x, mAreaPosition.y, mRockerPosition.x, mRockerPosition.y); + mListener.callback(EVENT_CLOCK, angle, distance); + } + } + } + + //获取摇杆偏移角度 上方中间为0,左为负,右为正 + private float getAngleConvert(float radian) { + return 90 + Math.round(radian / Math.PI * 180); + } + + // for preview + @Override + protected void onDraw(Canvas canvas) { + if (isInEditMode()) { + canvas.drawColor(Color.WHITE); + drawArea(canvas); + drawRocker(canvas); + } + } + + /*Getter Setter********************************************************************************/ + public void setCanMove(boolean isMove) { + this.canMove = isMove; + } + + public int getAreaRadius() { + return mAreaRadius; + } + + public void setAreaRadius(int areaRadius) { + mAreaRadius = areaRadius; + } + + public int getRockerRadius() { + return mRockerRadius; + } + + public void setRockerRadius(int rockerRadius) { + mRockerRadius = rockerRadius; + } + + public Bitmap getAreaBitmap() { + return mAreaBitmap; + } + + public void setAreaBitmap(Bitmap areaBitmap) { + mAreaBitmap = areaBitmap; + } + + public Bitmap getRockerBitmap() { + return mRockerBitmap; + } + + public void setRockerBitmap(Bitmap rockerBitmap) { + mRockerBitmap = rockerBitmap; + } + + public int getRefreshCycle() { + return mRefreshCycle; + } + + public void setRefreshCycle(int refreshCycle) { + mRefreshCycle = refreshCycle; + } + + public int getCallbackCycle() { + return mCallbackCycle; + } + + public void setCallbackCycle(int callbackCycle) { + mCallbackCycle = callbackCycle; + } + + public int getAreaColor() { + return mAreaColor; + } + + public void setAreaColor(int areaColor) { + mAreaColor = areaColor; + mAreaBitmap = null; + } + + public int getRockerColor() { + return mRockerColor; + } + + public void setRockerColor(int rockerColor) { + mRockerColor = rockerColor; + mRockerBitmap = null; + } + + public void setListener(@NonNull RockerListener listener) { + mListener = listener; + } + + /*Rocker Listener******************************************************************************/ + /** + * rocker listener + */ + public interface RockerListener { + /** + * you can get some event from this method + * + * @param eventType The event type, EVENT_ACTION or EVENT_CLOCK + * @param currentAngle The current angle + * @param currentDistance The current distance (px) + */ + void callback(int eventType, float currentAngle, float currentDistance); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/view_float_rocker.xml b/app/src/main/res/layout/view_float_rocker.xml index a2f7a905..fea49cb6 100644 --- a/app/src/main/res/layout/view_float_rocker.xml +++ b/app/src/main/res/layout/view_float_rocker.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="wrap_content"> - DQ>P;GjjD{w zH}lENr;dU&FbEU?00aa80D$0M0RRB{U*7-#kbjS|qAG&4l5%47zyJ#WrfA#1$1Ctx zf&Z_d{GW=lf^w2#qRJ|CvSJUi(^E3iv~=^Z(zH}F)3Z%V3`@+rNB7gTVU{Bb~90p|f+0(v;nz01EG7yDMX9@S~__vVgv%rS$+?IH+oZ03D5zYrv|^ zC1J)SruYHmCki$jLBlTaE5&dFG9-kq3!^i>^UQL`%gn6)jz54$WDmeYdsBE9;PqZ_ zoGd=P4+|(-u4U1dbAVQrFWoNgNd;0nrghPFbQrJctO>nwDdI`Q^i0XJDUYm|T|RWc zZ3^Qgo_Qk$%Fvjj-G}1NB#ZJqIkh;kX%V{THPqOyiq)d)0+(r9o(qKlSp*hmK#iIY zA^)Vr$-Hz<#SF=0@tL@;dCQsm`V9s1vYNq}K1B)!XSK?=I1)tX+bUV52$YQu*0%fnWEukW>mxkz+%3-S!oguE8u#MGzST8_Dy^#U?fA@S#K$S@9msUiX!gd_ow>08w5)nX{-KxqMOo7d?k2&?Vf z&diGDtZr(0cwPe9z9FAUSD9KC)7(n^lMWuayCfxzy8EZsns%OEblHFSzP=cL6}?J| z0U$H!4S_TVjj<`6dy^2j`V`)mC;cB%* z8{>_%E1^FH!*{>4a7*C1v>~1*@TMcLK{7nEQ!_igZC}ikJ$*<$yHy>7)oy79A~#xE zWavoJOIOC$5b6*q*F_qN1>2#MY)AXVyr$6x4b=$x^*aqF*L?vmj>Mgv+|ITnw_BoW zO?jwHvNy^prH{9$rrik1#fhyU^MpFqF2fYEt(;4`Q&XWOGDH8k6M=%@fics4ajI;st# zCU^r1CK&|jzUhRMv;+W~6N;u<;#DI6cCw-otsc@IsN3MoSD^O`eNflIoR~l4*&-%RBYk@gb^|-JXs&~KuSEmMxB}xSb z@K76cXD=Y|=I&SNC2E+>Zg?R6E%DGCH5J1nU!A|@eX9oS(WPaMm==k2s_ueCqdZw| z&hqHp)47`c{BgwgvY2{xz%OIkY1xDwkw!<0veB#yF4ZKJyabhyyVS`gZepcFIk%e2 zTcrmt2@-8`7i-@5Nz>oQWFuMC_KlroCl(PLSodswHqJ3fn<;gxg9=}~3x_L3P`9Sn zChIf}8vCHvTriz~T2~FamRi?rh?>3bX1j}%bLH+uFX+p&+^aXbOK7clZxdU~6Uxgy z8R=obwO4dL%pmVo*Ktf=lH6hnlz_5k3cG;m8lgaPp~?eD!Yn2kf)tU6PF{kLyn|oI@eQ`F z3IF7~Blqg8-uwUuWZScRKn%c2_}dXB6Dx_&xR*n9M9LXasJhtZdr$vBY!rP{c@=)& z#!?L$2UrkvClwQO>U*fSMs67oSj2mxiJ$t;E|>q%Kh_GzzWWO&3;ufU%2z%ucBU8H z3WIwr$n)cfCXR&>tyB7BcSInK>=ByZA%;cVEJhcg<#6N{aZC4>K41XF>ZgjG`z_u& zGY?;Ad?-sgiOnI`oppF1o1Gurqbi*;#x2>+SSV6|1^G@ooVy@fg?wyf@0Y!UZ4!}nGuLeC^l)6pwkh|oRY`s1Pm$>zZ3u-83T|9 zGaKJIV3_x+u1>cRibsaJpJqhcm%?0-L;2 zitBrdRxNmb0OO2J%Y&Ym(6*`_P3&&5Bw157{o7LFguvxC$4&zTy#U=W*l&(Q2MNO} zfaUwYm{XtILD$3864IA_nn34oVa_g^FRuHL5wdUd)+W-p-iWCKe8m_cMHk+=? zeKX)M?Dt(|{r5t7IenkAXo%&EXIb-i^w+0CX0D=xApC=|Xy(`xy+QG^UyFe z+#J6h_&T5i#sV)hj3D4WN%z;2+jJcZxcI3*CHXGmOF3^)JD5j&wfX)e?-|V0GPuA+ zQFot%aEqGNJJHn$!_}#PaAvQ^{3-Ye7b}rWwrUmX53(|~i0v{}G_sI9uDch_brX&6 zWl5Ndj-AYg(W9CGfQf<6!YmY>Ey)+uYd_JNXH=>|`OH-CDCmcH(0%iD_aLlNHKH z7bcW-^5+QV$jK?R*)wZ>r9t}loM@XN&M-Pw=F#xn(;u3!(3SXXY^@=aoj70;_=QE9 zGghsG3ekq#N||u{4We_25U=y#T*S{4I{++Ku)> zQ!DZW;pVcn>b;&g2;YE#+V`v*Bl&Y-i@X6D*OpNA{G@JAXho&aOk(_j^weW{#3X5Y z%$q_wpb07EYPdmyH(1^09i$ca{O<}7) zRWncXdSPgBE%BM#by!E>tdnc$8RwUJg1*x($6$}ae$e9Knj8gvVZe#bLi!<+&BkFj zg@nOpDneyc+hU9P-;jmOSMN|*H#>^Ez#?;%C3hg_65leSUm;iz)UkW)jX#p)e&S&M z1|a?wDzV5NVnlhRBCd_;F87wp>6c<&nkgvC+!@KGiIqWY4l}=&1w7|r6{oBN8xyzh zG$b#2=RJp_iq6)#t5%yLkKx(0@D=C3w+oiXtSuaQ%I1WIb-eiE$d~!)b@|4XLy!CZ z9p=t=%3ad@Ep+<9003D2KZ5VyP~_n$=;~r&YUg5UZ0KVD&tR1DHy9x)qWtKJp#Kq# zP*8p#W(8JJ_*h_3W}FlvRam?<4Z+-H77^$Lvi+#vmhL9J zJ<1SV45xi;SrO2f=-OB(7#iNA5)x1uNC-yNxUw|!00vcW2PufRm>e~toH;M0Q85MQLWd?3O{i8H+5VkR@l9Dg-ma ze2fZ%>G(u5(k9EHj2L6!;(KZ8%8|*-1V|B#EagbF(rc+5iL_5;Eu)L4Z-V;0HfK4d z*{utLse_rvHZeQ>V5H=f78M3Ntg1BPxFCVD{HbNA6?9*^YIq;B-DJd{Ca2L#)qWP? zvX^NhFmX?CTWw&Ns}lgs;r3i+Bq@y}Ul+U%pzOS0Fcv9~aB(0!>GT0)NO?p=25LjN z2bh>6RhgqD7bQj#k-KOm@JLgMa6>%-ok1WpOe)FS^XOU{c?d5shG(lIn3GiVBxmg`u%-j=)^v&pX1JecJics3&jvPI)mDut52? z3jEA)DM%}BYbxxKrizVYwq?(P&19EXlwD9^-6J+4!}9{ywR9Gk42jjAURAF&EO|~N z)?s>$Da@ikI4|^z0e{r`J8zIs>SpM~Vn^{3fArRu;?+43>lD+^XtUcY1HidJwnR6+ z!;oG2=B6Z_=M%*{z-RaHc(n|1RTKQdNjjV!Pn9lFt^4w|AeN06*j}ZyhqZ^!-=cyGP_ShV1rGxkx8t zB;8`h!S{LD%ot``700d0@Grql(DTt4Awgmi+Yr0@#jbe=2#UkK%rv=OLqF)9D7D1j z!~McAwMYkeaL$~kI~90)5vBhBzWYc3Cj1WI0RS`z000R8-@ET0dA~*r(gSiCJmQMN&4%1D zyVNf0?}sBH8zNbBLn>~(W{d3%@kL_eQ6jEcR{l>C|JK z(R-fA!z|TTRG40|zv}7E@PqCAXP3n`;%|SCQ|ZS%ym$I{`}t3KPL&^l5`3>yah4*6 zifO#{VNz3)?ZL$be;NEaAk9b#{tV?V7 zP|wf5YA*1;s<)9A4~l3BHzG&HH`1xNr#%){4xZ!jq%o=7nN*wMuXlFV{HaiQLJ`5G zBhDi#D(m`Q1pLh@Tq+L;OwuC52RdW7b8}~60WCOK5iYMUad9}7aWBuILb({5=z~YF zt?*Jr5NG+WadM{mDL>GyiByCuR)hd zA=HM?J6l1Xv0Dl+LW@w$OTcEoOda^nFCw*Sy^I@$sSuneMl{4ys)|RY#9&NxW4S)9 zq|%83IpslTLoz~&vTo!Ga@?rj_kw{|k{nv+w&Ku?fyk4Ki4I?);M|5Axm)t+BaE)D zm(`AQ#k^DWrjbuXoJf2{Aj^KT zFb1zMSqxq|vceV+Mf-)$oPflsO$@*A0n0Z!R{&(xh8s}=;t(lIy zv$S8x>m;vQNHuRzoaOo?eiWFe{0;$s`Bc+Osz~}Van${u;g(su`3lJ^TEfo~nERfP z)?aFzpDgnLYiERsKPu|0tq4l2wT)Atr6Qb%m-AUn6HnCue*yWICp7TjW$@sO zm5rm4aTcPQ(rfi7a`xP7cKCFrJD}*&_~xgLyr^-bmsL}y;A5P|al8J3WUoBSjqu%v zxC;mK!g(7r6RRJ852Z~feoC&sD3(6}^5-uLK8o)9{8L_%%rItZK9C){UxB|;G>JbP zsRRtS4-3B*5c+K2kvmgZK8472%l>3cntWUOVHxB|{Ay~aOg5RN;{PJgeVD*H%ac+y!h#wi%o2bF2Ca8IyMyH{>4#{E_8u^@+l-+n=V}Sq?$O z{091@v%Bd*3pk0^2UtiF9Z+(a@wy6 zUdw8J*ze$K#=$48IBi1U%;hmhO>lu!uU;+RS}p&6@rQila7WftH->*A4=5W|Fmtze z)7E}jh@cbmr9iup^i%*(uF%LG&!+Fyl@LFA-}Ca#bxRfDJAiR2dt6644TaYw1Ma79 zt8&DYj31j^5WPNf5P&{)J?WlCe@<3u^78wnd(Ja4^a>{^Tw}W>|Cjt^If|7l^l)^Q zbz|7~CF(k_9~n|h;ysZ+jHzkXf(*O*@5m zLzUmbHp=x!Q|!9NVXyipZ3)^GuIG$k;D)EK!a5=8MFLI_lpf`HPKl=-Ww%z8H_0$j ztJ||IfFG1lE9nmQ0+jPQy zCBdKkjArH@K7jVcMNz);Q(Q^R{d5G?-kk;Uu_IXSyWB)~KGIizZL(^&qF;|1PI7!E zTP`%l)gpX|OFn&)M%txpQ2F!hdA~hX1Cm5)IrdljqzRg!f{mN%G~H1&oqe`5eJCIF zHdD7O;AX-{XEV(a`gBFJ9ews#CVS2y!&>Cm_dm3C8*n3MA*e67(WC?uP@8TXuMroq z{#w$%z@CBIkRM7?}Xib+>hRjy?%G!fiw8! z8(gB+8J~KOU}yO7UGm&1g_MDJ$IXS!`+*b*QW2x)9>K~Y*E&bYMnjl6h!{17_8d!%&9D`a7r&LKZjC<&XOvTRaKJ1 zUY@hl5^R&kZl3lU3njk`3dPzxj$2foOL26r(9zsVF3n_F#v)s5vv3@dgs|lP#eylq62{<-vczqP!RpVBTgI>@O6&sU>W|do17+#OzQ7o5A$ICH z?GqwqnK^n2%LR;$^oZM;)+>$X3s2n}2jZ7CdWIW0lnGK-b#EG01)P@aU`pg}th&J-TrU`tIpb5t((0eu|!u zQz+3ZiOQ^?RxxK4;zs=l8q!-n7X{@jSwK(iqNFiRColuEOg}!7cyZi`iBX4g1pNBj zAPzL?P^Ljhn;1$r8?bc=#n|Ed7wB&oHcw()&*k#SS#h}jO?ZB246EGItsz*;^&tzp zu^YJ0=lwsi`eP_pU8}6JA7MS;9pfD;DsSsLo~ogzMNP70@@;Fm8f0^;>$Z>~}GWRw!W5J3tNX*^2+1f3hz{~rIzJo z6W%J(H!g-eI_J1>0juX$X4Cl6i+3wbc~k146UIX&G22}WE>0ga#WLsn9tY(&29zBvH1$`iWtTe zG2jYl@P!P)eb<5DsR72BdI7-zP&cZNI{7q3e@?N8IKc4DE#UVr->|-ryuJXk^u^>4 z$3wE~=q390;XuOQP~TNoDR?#|NSPJ%sTMInA6*rJ%go|=YjGe!B>z6u$IhgQSwoV* zjy3F2#I>uK{42{&IqP59)Y(1*Z>>#W8rCf4_eVsH)`v!P#^;BgzKDR`ARGEZzkNX+ zJUQu=*-ol=Xqqt5=`=pA@BIn@6a9G8C{c&`i^(i+BxQO9?YZ3iu%$$da&Kb?2kCCo zo7t$UpSFWqmydXf@l3bVJ=%K?SSw)|?srhJ-1ZdFu*5QhL$~-IQS!K1s@XzAtv6*Y zl8@(5BlWYLt1yAWy?rMD&bwze8bC3-GfNH=p zynNFCdxyX?K&G(ZZ)afguQ2|r;XoV^=^(;Cku#qYn4Lus`UeKt6rAlFo_rU`|Rq z&G?~iWMBio<78of-2X(ZYHx~=U0Vz4btyXkctMKdc9UM!vYr~B-(>)(Hc|D zMzkN4!PBg%tZoh+=Gba!0++d193gbMk2&krfDgcbx0jI92cq?FFESVg0D$>F+bil} zY~$)|>1HZsX=5sAZ2WgPB5P=8X#TI+NQ(M~GqyVB53c6IdX=k>Wu@A0Svf5#?uHaF zsYn|koIi3$(%GZ2+G+7Fv^lHTb#5b8sAHSTnL^qWZLM<(1|9|QFw9pnRU{svj}_Al zL)b9>fN{QiA($8peNEJyy`(a{&uh-T4_kdZFIVsKKVM(?05}76EEz?#W za^fiZOAd14IJ4zLX-n7Lq0qlQ^lW8Cvz4UKkV9~P}>sq0?xD3vg+$4vLm~C(+ zM{-3Z#qnZ09bJ>}j?6ry^h+@PfaD7*jZxBEY4)UG&daWb??6)TP+|3#Z&?GL?1i+280CFsE|vIXQbm| zM}Pk!U`U5NsNbyKzkrul-DzwB{X?n3E6?TUHr{M&+R*2%yOiXdW-_2Yd6?38M9Vy^ z*lE%gA{wwoSR~vN0=no}tP2Ul5Gk5M(Xq`$nw#ndFk`tcpd5A=Idue`XZ!FS>Q zG^0w#>P4pPG+*NC9gLP4x2m=cKP}YuS!l^?sHSFftZy{4CoQrb_ z^20(NnG`wAhMI=eq)SsIE~&Gp9Ne0nD4%Xiu|0Fj1UFk?6avDqjdXz{O1nKao*46y zT8~iA%Exu=G#{x=KD;_C&M+Zx4+n`sHT>^>=-1YM;H<72k>$py1?F3#T1*ef9mLZw z5naLQr?n7K;2l+{_uIw*_1nsTn~I|kkCgrn;|G~##hM;9l7Jy$yJfmk+&}W@JeKcF zx@@Woiz8qdi|D%aH3XTx5*wDlbs?dC1_nrFpm^QbG@wM=i2?Zg;$VK!c^Dp8<}BTI zyRhAq@#%2pGV49*Y5_mV4+OICP|%I(dQ7x=6Ob}>EjnB_-_18*xrY?b%-yEDT(wrO z9RY2QT0`_OpGfMObKHV;QLVnrK%mc?$WAdIT`kJQT^n%GuzE7|9@k3ci5fYOh(287 zuIbg!GB3xLg$YN=n)^pHGB0jH+_iIiC=nUcD;G6LuJsjn2VI1cyZx=a?ShCsF==QK z;q~*m&}L<-cb+mDDXzvvrRsybcgQ;Vg21P(uLv5I+eGc7o7tc6`;OA9{soHFOz zT~2?>Ts}gprIX$wRBb4yE>ot<8+*Bv`qbSDv*VtRi|cyWS>)Fjs>fkNOH-+PX&4(~ z&)T8Zam2L6puQl?;5zg9h<}k4#|yH9czHw;1jw-pwBM*O2hUR6yvHATrI%^mvs9q_ z&ccT0>f#eDG<^WG^q@oVqlJrhxH)dcq2cty@l3~|5#UDdExyXUmLQ}f4#;6fI{f^t zDCsgIJ~0`af%YR%Ma5VQq-p21k`vaBu6WE?66+5=XUd%Ay%D$irN>5LhluRWt7 zov-=f>QbMk*G##&DTQyou$s7UqjjW@k6=!I@!k+S{pP8R(2=e@io;N8E`EOB;OGoI zw6Q+{X1_I{OO0HPpBz!X!@`5YQ2)t{+!?M_iH25X(d~-Zx~cXnS9z>u?+If|iNJbx zyFU2d1!ITX64D|lE0Z{dLRqL1Ajj=CCMfC4lD3&mYR_R_VZ>_7_~|<^o*%_&jevU+ zQ4|qzci=0}Jydw|LXLCrOl1_P6Xf@c0$ieK2^7@A9UbF{@V_0p%lqW|L?5k>bVM8|p5v&2g;~r>B8uo<4N+`B zH{J)h;SYiIVx@#jI&p-v3dwL5QNV1oxPr8J%ooezTnLW>i*3Isb49%5i!&ac_dEXv zvXmVUck^QHmyrF8>CGXijC_R-y(Qr{3Zt~EmW)-nC!tiH`wlw5D*W7Pip;T?&j%kX z6DkZX4&}iw>hE(boLyjOoupf6JpvBG8}jIh!!VhnD0>}KSMMo{1#uU6kiFcA04~|7 zVO8eI&x1`g4CZ<2cYUI(n#wz2MtVFHx47yE5eL~8bot~>EHbevSt}LLMQX?odD{Ux zJMnam{d)W4da{l7&y-JrgiU~qY3$~}_F#G7|MxT)e;G{U`In&?`j<5D->}cb{}{T(4DF0BOk-=1195KB-E*o@c?`>y#4=dMtYtSY=&L{!TAjFVcq0y@AH`vH! z$41+u!Ld&}F^COPgL(EE{0X7LY&%D7-(?!kjFF7=qw<;`V{nwWBq<)1QiGJgUc^Vz ztMUlq1bZqKn17|6x6iAHbWc~l1HcmAxr%$Puv!znW)!JiukwIrqQ00|H$Z)OmGG@= zv%A8*4cq}(?qn4rN6o`$Y))(MyXr8R<2S^J+v(wmFmtac!%VOfN?&(8Nr!T@kV`N; z*Q33V3t`^rN&aBiHet)18wy{*wi1=W!B%B-Q6}SCrUl$~Hl{@!95ydml@FK8P=u4s z4e*7gV2s=YxEvskw2Ju!2%{8h01rx-3`NCPc(O zH&J0VH5etNB2KY6k4R@2Wvl^Ck$MoR3=)|SEclT2ccJ!RI9Nuter7u9@;sWf-%um;GfI!=eEIQ2l2p_YWUd{|6EG ze{yO6;lMc>;2tPrsNdi@&1K6(1;|$xe8vLgiouj%QD%gYk`4p{Ktv9|j+!OF-P?@p z;}SV|oIK)iwlBs+`ROXkhd&NK zzo__r!B>tOXpBJMDcv!Mq54P+n4(@dijL^EpO1wdg~q+!DT3lB<>9AANSe!T1XgC=J^)IP0XEZ()_vpu!!3HQyJhwh?r`Ae%Yr~b% zO*NY9t9#qWa@GCPYOF9aron7thfWT`eujS4`t2uG6)~JRTI;f(ZuoRQwjZjp5Pg34 z)rp$)Kr?R+KdJ;IO;pM{$6|2y=k_siqvp%)2||cHTe|b5Ht8&A{wazGNca zX$Ol?H)E_R@SDi~4{d-|8nGFhZPW;Cts1;08TwUvLLv&_2$O6Vt=M)X;g%HUr$&06 zISZb(6)Q3%?;3r~*3~USIg=HcJhFtHhIV(siOwV&QkQe#J%H9&E21!C*d@ln3E@J* zVqRO^<)V^ky-R|%{(9`l-(JXq9J)1r$`uQ8a}$vr9E^nNiI*thK8=&UZ0dsFN_eSl z(q~lnD?EymWLsNa3|1{CRPW60>DSkY9YQ;$4o3W7Ms&@&lv9eH!tk~N&dhqX&>K@} zi1g~GqglxkZ5pEFkllJ)Ta1I^c&Bt6#r(QLQ02yHTaJB~- zCcE=5tmi`UA>@P=1LBfBiqk)HB4t8D?02;9eXj~kVPwv?m{5&!&TFYhu>3=_ zsGmYZ^mo*-j69-42y&Jj0cBLLEulNRZ9vXE)8~mt9C#;tZs;=#M=1*hebkS;7(aGf zcs7zH(I8Eui9UU4L--))yy`&d&$In&VA2?DAEss4LAPCLd>-$i?lpXvn!gu^JJ$(DoUlc6wE98VLZ*z`QGQov5l4Fm_h?V-;mHLYDVOwKz7>e4+%AzeO>P6v}ndPW| zM>m#6Tnp7K?0mbK=>gV}=@k*0Mr_PVAgGMu$j+pWxzq4MAa&jpCDU&-5eH27Iz>m^ zax1?*HhG%pJ((tkR(V(O(L%7v7L%!_X->IjS3H5kuXQT2!ow(;%FDE>16&3r){!ex zhf==oJ!}YU89C9@mfDq!P3S4yx$aGB?rbtVH?sHpg?J5C->!_FHM%Hl3#D4eplxzQ zRA+<@LD%LKSkTk2NyWCg7u=$%F#;SIL44~S_OGR}JqX}X+=bc@swpiClB`Zbz|f!4 z7Ysah7OkR8liXfI`}IIwtEoL}(URrGe;IM8%{>b1SsqXh)~w}P>yiFRaE>}rEnNkT z!HXZUtxUp1NmFm)Dm@-{FI^aRQqpSkz}ZSyKR%Y}YHNzBk)ZIp} zMtS=aMvkgWKm9&oTcU0?S|L~CDqA+sHpOxwnswF-fEG)cXCzUR?ps@tZa$=O)=L+5 zf%m58cq8g_o}3?Bhh+c!w4(7AjxwQ3>WnVi<{{38g7yFboo>q|+7qs<$8CPXUFAN< zG&}BHbbyQ5n|qqSr?U~GY{@GJ{(Jny{bMaOG{|IkUj7tj^9pa9|FB_<+KHLxSxR;@ zHpS$4V)PP+tx}22fWx(Ku9y+}Ap;VZqD0AZW4gCDTPCG=zgJmF{|x;(rvdM|2|9a}cex6xrMkERnkE;}jvU-kmzd%_J50$M`lIPCKf+^*zL=@LW`1SaEc%=m zQ+lT06Gw+wVwvQ9fZ~#qd430v2HndFsBa9WjD0P}K(rZYdAt^5WQIvb%D^Q|pkVE^ zte$&#~zmULFACGfS#g=2OLOnIf2Of-k!(BIHjs77nr!5Q1*I9 z1%?=~#Oss!rV~?-6Gm~BWJiA4mJ5TY&iPm_$)H1_rTltuU1F3I(qTQ^U$S>%$l z)Wx1}R?ij0idp@8w-p!Oz{&*W;v*IA;JFHA9%nUvVDy7Q8woheC#|8QuDZb-L_5@R zOqHwrh|mVL9b=+$nJxM`3eE{O$sCt$UK^2@L$R(r^-_+z?lOo+me-VW=Zw z-Bn>$4ovfWd%SPY`ab-u9{INc*k2h+yH%toDHIyqQ zO68=u`N}RIIs7lsn1D){)~%>ByF<>i@qFb<-axvu(Z+6t7v<^z&gm9McRB~BIaDn$ z#xSGT!rzgad8o>~kyj#h1?7g96tOcCJniQ+*#=b7wPio>|6a1Z?_(TS{)KrPe}(8j z!#&A=k(&Pj^F;r)CI=Z{LVu>uj!_W1q4b`N1}E(i%;BWjbEcnD=mv$FL$l?zS6bW!{$7j1GR5ocn94P2u{ z70tAAcpqtQo<@cXw~@i-@6B23;317|l~S>CB?hR5qJ%J3EFgyBdJd^fHZu7AzHF(BQ!tyAz^L0`X z23S4Fe{2X$W0$zu9gm%rg~A>ijaE#GlYlrF9$ds^QtaszE#4M(OLVP2O-;XdT(XIC zatwzF*)1c+t~c{L=fMG8Z=k5lv>U0;C{caN1NItnuSMp)6G3mbahu>E#sj&oy94KC zpH}8oEw{G@N3pvHhp{^-YaZeH;K+T_1AUv;IKD<=mv^&Ueegrb!yf`4VlRl$M?wsl zZyFol(2|_QM`e_2lYSABpKR{{NlxlDSYQNkS;J66aT#MSiTx~;tUmvs-b*CrR4w=f z8+0;*th6kfZ3|5!Icx3RV11sp=?`0Jy3Fs0N4GZQMN=8HmT6%x9@{Dza)k}UwL6JT zHRDh;%!XwXr6yuuy`4;Xsn0zlR$k%r%9abS1;_v?`HX_hI|+EibVnlyE@3aL5vhQq zlIG?tN^w@0(v9M*&L+{_+RQZw=o|&BRPGB>e5=ys7H`nc8nx)|-g;s7mRc7hg{GJC zAe^vCIJhajmm7C6g! zL&!WAQ~5d_5)00?w_*|*H>3$loHrvFbitw#WvLB!JASO?#5Ig5$Ys10n>e4|3d;tS zELJ0|R4n3Az(Fl3-r^QiV_C;)lQ1_CW{5bKS15U|E9?ZgLec@%kXr84>5jV2a5v=w z?pB1GPdxD$IQL4)G||B_lI+A=08MUFFR4MxfGOu07vfIm+j=z9tp~5i_6jb`tR>qV z$#`=BQ*jpCjm$F0+F)L%xRlnS%#&gro6PiRfu^l!EVan|r3y}AHJQOORGx4~ z&<)3=K-tx518DZyp%|!EqpU!+X3Et7n2AaC5(AtrkW>_57i}$eqs$rupubg0a1+WO zGHZKLN2L0D;ab%{_S1Plm|hx8R?O14*w*f&2&bB050n!R2by zw!@XOQx$SqZ5I<(Qu$V6g>o#A!JVwErWv#(Pjx=KeS0@hxr4?13zj#oWwPS(7Ro|v z>Mp@Kmxo79q|}!5qtX2-O@U&&@6s~!I&)1WQIl?lTnh6UdKT_1R640S4~f=_xoN3- zI+O)$R@RjV$F=>Ti7BlnG1-cFKCC(t|Qjm{SalS~V-tX#+2ekRhwmN zZr`8{QF6y~Z!D|{=1*2D-JUa<(1Z=;!Ei!KiRNH?o{p5o3crFF=_pX9O-YyJchr$~ zRC`+G+8kx~fD2k*ZIiiIGR<8r&M@3H?%JVOfE>)})7ScOd&?OjgAGT@WVNSCZ8N(p zuQG~76GE3%(%h1*vUXg$vH{ua0b`sQ4f0*y=u~lgyb^!#CcPJa2mkSEHGLsnO^kb$ zru5_l#nu=Y{rSMWiYx?nO{8I!gH+?wEj~UM?IrG}E|bRIBUM>UlY<`T1EHpRr36vv zBi&dG8oxS|J$!zoaq{+JpJy+O^W(nt*|#g32bd&K^w-t>!Vu9N!k9eA8r!Xc{utY> zg9aZ(D2E0gL#W0MdjwES-7~Wa8iubPrd?8-$C4BP?*wok&O8+ykOx{P=Izx+G~hM8 z*9?BYz!T8~dzcZr#ux8kS7u7r@A#DogBH8km8Ry4slyie^n|GrTbO|cLhpqgMdsjX zJ_LdmM#I&4LqqsOUIXK8gW;V0B(7^$y#h3h>J0k^WJfAMeYek%Y-Dcb_+0zPJez!GM zAmJ1u;*rK=FNM0Nf}Y!!P9c4)HIkMnq^b;JFd!S3?_Qi2G#LIQ)TF|iHl~WKK6JmK zbv7rPE6VkYr_%_BT}CK8h=?%pk@3cz(UrZ{@h40%XgThP*-Oeo`T0eq9 zA8BnWZKzCy5e&&_GEsU4*;_k}(8l_&al5K-V*BFM=O~;MgRkYsOs%9eOY6s6AtE*<7GQAR2ulC3RAJrG_P1iQK5Z~&B z&f8X<>yJV6)oDGIlS$Y*D^Rj(cszTy5c81a5IwBr`BtnC6_e`ArI8CaTX_%rx7;cn zR-0?J_LFg*?(#n~G8cXut(1nVF0Oka$A$1FGcERU<^ggx;p@CZc?3UB41RY+wLS`LWFNSs~YP zuw1@DNN3lTd|jDL7gjBsd9}wIw}4xT2+8dBQzI00m<@?c2L%>}QLfK5%r!a-iII`p zX@`VEUH)uj^$;7jVUYdADQ2k*!1O3WdfgF?OMtUXNpQ1}QINamBTKDuv19^{$`8A1 zeq%q*O0mi@(%sZU>Xdb0Ru96CFqk9-L3pzLVsMQ`Xpa~N6CR{9Rm2)A|CI21L(%GW zh&)Y$BNHa=FD+=mBw3{qTgw)j0b!Eahs!rZnpu)z!!E$*eXE~##yaXz`KE5(nQM`s zD!$vW9XH)iMxu9R>r$VlLk9oIR%HxpUiW=BK@4U)|1WNQ=mz9a z^!KkO=>GaJ!GBXm{KJj^;kh-MkUlEQ%lza`-G&}C5y1>La1sR6hT=d*NeCnuK%_LV zOXt$}iP6(YJKc9j-Fxq~*ItVUqljQ8?oaysB-EYtFQp9oxZ|5m0^Hq(qV!S+hq#g( z?|i*H2MIr^Kxgz+3vIljQ*Feejy6S4v~jKEPTF~Qhq!(ms5>NGtRgO5vfPPc4Z^AM zTj!`5xEreIN)vaNxa|q6qWdg>+T`Ol0Uz)ckXBXEGvPNEL3R8hB3=C5`@=SYgAju1 z!)UBr{2~=~xa{b8>x2@C7weRAEuatC)3pkRhT#pMPTpSbA|tan%U7NGMvzmF?c!V8 z=pEWxbdXbTAGtWTyI?Fml%lEr-^AE}w#l(<7OIw;ctw}imYax&vR4UYNJZK6P7ZOd zP87XfhnUHxCUHhM@b*NbTi#(-8|wcv%3BGNs#zRCVV(W?1Qj6^PPQa<{yaBwZ`+<`w|;rqUY_C z&AeyKwwf*q#OW-F()lir=T^<^wjK65Lif$puuU5+tk$;e_EJ;Lu+pH>=-8=PDhkBg z8cWt%@$Sc#C6F$Vd+0507;{OOyT7Hs%nKS88q-W!$f~9*WGBpHGgNp}=C*7!RiZ5s zn1L_DbKF@B8kwhDiLKRB@lsXVVLK|ph=w%_`#owlf@s@V(pa`GY$8h%;-#h@TsO|Y8V=n@*!Rog7<7Cid%apR|x zOjhHCyfbIt%+*PCveTEcuiDi%Wx;O;+K=W?OFUV%)%~6;gl?<0%)?snDDqIvkHF{ zyI02)+lI9ov42^hL>ZRrh*HhjF9B$A@=H94iaBESBF=eC_KT$8A@uB^6$~o?3Wm5t1OIaqF^~><2?4e3c&)@wKn9bD? zoeCs;H>b8DL^F&>Xw-xjZEUFFTv>JD^O#1E#)CMBaG4DX9bD(Wtc8Rzq}9soQ8`jf zeSnHOL}<+WVSKp4kkq&?SbETjq6yr@4%SAqOG=9E(3YeLG9dtV+8vmzq+6PFPk{L; z(&d++iu=^F%b+ea$i2UeTC{R*0Isk;vFK!no<;L+(`y`3&H-~VTdKROkdyowo1iqR zbVW(3`+(PQ2>TKY>N!jGmGo7oeoB8O|P_!Ic@ zZ^;3dnuXo;WJ?S+)%P>{Hcg!Jz#2SI(s&dY4QAy_vRlmOh)QHvs_7c&zkJCmJGVvV zX;Mtb>QE+xp`KyciG$Cn*0?AK%-a|=o!+7x&&yzHQOS>8=B*R=niSnta^Pxp1`=md z#;$pS$4WCT?mbiCYU?FcHGZ#)kHVJTTBt^%XE(Q};aaO=Zik0UgLcc0I(tUpt(>|& zcxB_|fxCF7>&~5eJ=Dpn&5Aj{A^cV^^}(7w#p;HG&Q)EaN~~EqrE1qKrMAc&WXIE;>@<&)5;gD2?={Xf@Mvn@OJKw=8Mgn z!JUFMwD+s==JpjhroT&d{$kQAy%+d`a*XxDEVxy3`NHzmITrE`o!;5ClXNPb4t*8P zzAivdr{j_v!=9!^?T3y?gzmqDWX6mkzhIzJ-3S{T5bcCFMr&RPDryMcdwbBuZbsgN zGrp@^i?rcfN7v0NKGzDPGE#4yszxu=I_`MI%Z|10nFjU-UjQXXA?k8Pk|OE<(?ae) zE%vG#eZAlj*E7_3dx#Zz4kMLj>H^;}33UAankJiDy5ZvEhrjr`!9eMD8COp}U*hP+ zF}KIYx@pkccIgyxFm#LNw~G&`;o&5)2`5aogs`1~7cMZQ7zj!%L4E`2yzlQN6REX20&O<9 zKV6fyr)TScJPPzNTC2gL+0x#=u>(({{D7j)c-%tvqls3#Y?Z1m zV5WUE)zdJ{$p>yX;^P!UcXP?UD~YM;IRa#Rs5~l+*$&nO(;Ers`G=0D!twR(0GF@c zHl9E5DQI}Oz74n zfKP>&$q0($T4y$6w(p=ERAFh+>n%iaeRA%!T%<^+pg?M)@ucY<&59$x9M#n+V&>}=nO9wCV{O~lg&v#+jcUj(tQ z`0u1YH)-`U$15a{pBkGyPL0THv1P|4e@pf@3IBZS4dVJPo#H>pWq%Lr0YS-SeWash z8R7=jb28KPMI|_lo#GEO|5B?N_e``H*23{~a!AmUJ+fb4HX-%QI@lSEUxKlGV7z7Q zSKw@-TR>@1RL%w{x}dW#k1NgW+q4yt2Xf1J62Bx*O^WG8OJ|FqI4&@d3_o8Id@*)4 zYrk=>@!wv~mh7YWv*bZhxqSmFh2Xq)o=m;%n$I?GSz49l1$xRpPu_^N(vZ>*>Z<04 z2+rP70oM=NDysd!@fQdM2OcyT?3T^Eb@lIC-UG=Bw{BjQ&P`KCv$AcJ;?`vdZ4){d z&gkoUK{$!$$K`3*O-jyM1~p-7T*qb)Ys>Myt^;#1&a%O@x8A+E>! zY8=eD`ZG)LVagDLBeHg>=atOG?Kr%h4B%E6m@J^C+U|y)XX@f z8oyJDW|9g=<#f<{JRr{y#~euMnv)`7j=%cHWLc}ngjq~7k**6%4u>Px&W%4D94(r* z+akunK}O0DC2A%Xo9jyF;DobX?!1I(7%}@7F>i%&nk*LMO)bMGg2N+1iqtg+r(70q zF5{Msgsm5GS7DT`kBsjMvOrkx&|EU!{{~gL4d2MWrAT=KBQ-^zQCUq{5PD1orxlIL zq;CvlWx#f1NWvh`hg011I%?T_s!e38l*lWVt|~z-PO4~~1g)SrJ|>*tXh=QfXT)%( z+ex+inPvD&O4Ur;JGz>$sUOnWdpSLcm1X%aQDw4{dB!cnj`^muI$CJ2%p&-kULVCE z>$eMR36kN$wCPR+OFDM3-U(VOrp9k3)lI&YVFqd;Kpz~K)@Fa&FRw}L(SoD z9B4a+hQzZT-BnVltst&=kq6Y(f^S4hIGNKYBgMxGJ^;2yrO}P3;r)(-I-CZ)26Y6? z&rzHI_1GCvGkgy-t1E;r^3Le30|%$ebDRu2+gdLG)r=A~Qz`}~&L@aGJ{}vVs_GE* zVUjFnzHiXfKQbpv&bR&}l2bzIjAooB)=-XNcYmrGmBh(&iu@o!^hn0^#}m2yZZUK8 zufVm7Gq0y`Mj;9b>`c?&PZkU0j4>IL=UL&-Lp3j&47B5pAW4JceG{!XCA)kT<%2nqCxj<)uy6XR_uws~>_MEKPOpAQ!H zkn>FKh)<9DwwS*|Y(q?$^N!6(51O0 z^JM~Ax{AI1Oj$fs-S5d4T7Z_i1?{%0SsIuQ&r8#(JA=2iLcTN+?>wOL532%&dMYkT z*T5xepC+V6zxhS@vNbMoi|i)=rpli@R9~P!39tWbSSb904ekv7D#quKbgFEMTb48P zuq(VJ+&L8aWU(_FCD$3^uD!YM%O^K(dvy~Wm2hUuh6bD|#(I39Xt>N1Y{ZqXL`Fg6 zKQ?T2htHN!(Bx;tV2bfTtIj7e)liN-29s1kew>v(D^@)#v;}C4-G=7x#;-dM4yRWm zyY`cS21ulzMK{PoaQ6xChEZ}o_#}X-o}<&0)$1#3we?+QeLt;aVCjeA)hn!}UaKt< zat1fHEx13y-rXNMvpUUmCVzocPmN~-Y4(YJvQ#db)4|%B!rBsgAe+*yor~}FrNH08 z3V!97S}D7d$zbSD{$z;@IYMxM6aHdypIuS*pr_U6;#Y!_?0i|&yU*@16l z*dcMqDQgfNBf}?quiu4e>H)yTVfsp#f+Du0@=Kc41QockXkCkvu>FBd6Q+@FL!(Yx z2`YuX#eMEiLEDhp+9uFqME_E^faV&~9qjBHJkIp~%$x^bN=N)K@kvSVEMdDuzA0sn z88CBG?`RX1@#hQNd`o^V{37)!w|nA)QfiYBE^m=yQKv-fQF+UCMcuEe1d4BH7$?>b zJl-r9@0^Ie=)guO1vOd=i$_4sz>y3x^R7n4ED!5oXL3@5**h(xr%Hv)_gILarO46q+MaDOF%ChaymKoI6JU5Pg;7#2n9-18|S1;AK+ zgsn6;k6-%!QD>D?cFy}8F;r@z8H9xN1jsOBw2vQONVqBVEbkiNUqgw~*!^##ht>w0 zUOykwH=$LwX2j&nLy=@{hr)2O&-wm-NyjW7n~Zs9UlH;P7iP3 zI}S(r0YFVYacnKH(+{*)Tbw)@;6>%=&Th=+Z6NHo_tR|JCI8TJiXv2N7ei7M^Q+RM z?9o`meH$5Yi;@9XaNR#jIK^&{N|DYNNbtdb)XW1Lv2k{E>;?F`#Pq|&_;gm~&~Zc9 zf+6ZE%{x4|{YdtE?a^gKyzr}dA>OxQv+pq|@IXL%WS0CiX!V zm$fCePA%lU{%pTKD7|5NJHeXg=I0jL@$tOF@K*MI$)f?om)D63K*M|r`gb9edD1~Y zc|w7N)Y%do7=0{RC|AziW7#am$)9jciRJ?IWl9PE{G3U+$%FcyKs_0Cgq`=K3@ttV z9g;M!3z~f_?P%y3-ph%vBMeS@p7P&Ea8M@97+%XEj*(1E6vHj==d zjsoviB>j^$_^OI_DEPvFkVo(BGRo%cJeD){6Uckei=~1}>sp299|IRjhXe)%?uP0I zF5+>?0#Ye}T^Y$u_rc4=lPcq4K^D(TZG-w30-YiEM=dcK+4#o*>lJ8&JLi+3UcpZk z!^?95S^C0ja^jwP`|{<+3cBVog$(mRdQmadS+Vh~z zS@|P}=|z3P6uS+&@QsMp0no9Od&27O&14zHXGAOEy zh~OKpymK5C%;LLb467@KgIiVwYbYd6wFxI{0-~MOGfTq$nBTB!{SrWmL9Hs}C&l&l#m?s*{tA?BHS4mVKHAVMqm63H<|c5n0~k)-kbg zXidai&9ZUy0~WFYYKT;oe~rytRk?)r8bptITsWj(@HLI;@=v5|XUnSls7$uaxFRL+ zRVMGuL3w}NbV1`^=Pw*0?>bm8+xfeY(1PikW*PB>>Tq(FR`91N0c2&>lL2sZo5=VD zQY{>7dh_TX98L2)n{2OV=T10~*YzX27i2Q7W86M4$?gZIXZaBq#sA*{PH8){|GUi;oM>e?ua7eF4WFuFYZSG| zze?srg|5Ti8Og{O zeFxuw9!U+zhyk?@w zjsA6(oKD=Ka;A>Ca)oPORxK+kxH#O@zhC!!XS4@=swnuMk>t+JmLmFiE^1aX3f<)D@`%K0FGK^gg1a1j>zi z2KhV>sjU7AX3F$SEqrXSC}fRx64GDoc%!u2Yag68Lw@w9v;xOONf@o)Lc|Uh3<21ctTYu-mFZuHk*+R{GjXHIGq3p)tFtQp%TYqD=j1&y)>@zxoxUJ!G@ zgI0XKmP6MNzw>nRxK$-Gbzs}dyfFzt>#5;f6oR27ql!%+{tr+(`(>%51|k`ML} zY4eE)Lxq|JMas(;JibNQds1bUB&r}ydMQXBY4x(^&fY_&LlQC)3hylc$~8&~|06-D z#T+%66rYbHX%^KuqJED_wuGB+=h`nWA!>1n0)3wZrBG3%`b^Ozv6__dNa@%V14|!D zQ?o$z5u0^8`giv%qE!BzZ!3j;BlDlJDk)h@9{nSQeEk!z9RGW) z${RSF3phEM*ce*>Xdp}585vj$|40=&S{S-GTiE?Op*vY&Lvr9}BO$XWy80IF+6@%n z5*2ueT_g@ofP#u5pxb7n*fv^Xtt7&?SRc{*2Ka-*!BuOpf}neHGCiHy$@Ka1^Dint z;DkmIL$-e)rj4o2WQV%Gy;Xg(_Bh#qeOsTM2f@KEe~4kJ8kNLQ+;(!j^bgJMcNhvklP5Z6I+9Fq@c&D~8Fb-4rmDT!MB5QC{Dsb;BharP*O;SF4& zc$wj-7Oep7#$WZN!1nznc@Vb<_Dn%ga-O#J(l=OGB`dy=Sy&$(5-n3zzu%d7E#^8`T@}V+5B;PP8J14#4cCPw-SQTdGa2gWL0*zKM z#DfSXs_iWOMt)0*+Y>Lkd=LlyoHjublNLefhKBv@JoC>P7N1_#> zv=mLWe96%EY;!ZGSQDbZWb#;tzqAGgx~uk+-$+2_8U`!ypbwXl z^2E-FkM1?lY@yt8=J3%QK+xaZ6ok=-y%=KXCD^0r!5vUneW>95PzCkOPO*t}p$;-> ze5j-BLT_;)cZQzR2CEsm@rU7GZfFtdp*a|g4wDr%8?2QkIGasRfDWT-Dvy*U{?IHT z*}wGnzdlSptl#ZF^sf)KT|BJs&kLG91^A6ls{CzFprZ6-Y!V0Xysh%9p%iMd7HLsS zN+^Un$tDV)T@i!v?3o0Fsx2qI(AX_$dDkBzQ@fRM%n zRXk6hb9Py#JXUs+7)w@eo;g%QQ95Yq!K_d=z{0dGS+pToEI6=Bo8+{k$7&Z zo4>PH(`ce8E-Ps&uv`NQ;U$%t;w~|@E3WVOCi~R4oj5wP?%<*1C%}Jq%a^q~T7u>K zML5AKfQDv6>PuT`{SrKHRAF+^&edg6+5R_#H?Lz3iGoWo#PCEd0DS;)2U({{X#zU^ zw_xv{4x7|t!S)>44J;KfA|DC?;uQ($l+5Vp7oeqf7{GBF9356nx|&B~gs+@N^gSdd zvb*>&W)|u#F{Z_b`f#GVtQ`pYv3#||N{xj1NgB<#=Odt6{eB%#9RLt5v zIi|0u70`#ai}9fJjKv7dE!9ZrOIX!3{$z_K5FBd-Kp-&e4(J$LD-)NMTp^_pB`RT; zftVVlK2g@+1Ahv2$D){@Y#cL#dUj9*&%#6 zd2m9{1NYp>)6=oAvqdCn5#cx{AJ%S8skUgMglu2*IAtd+z1>B&`MuEAS(D(<6X#Lj z?f4CFx$)M&$=7*>9v1ER4b6!SIz-m0e{o0BfkySREchp?WdVPpQCh!q$t>?rL!&Jg zd#heM;&~A}VEm8Dvy&P|J*eAV&w!&Nx6HFV&B8jJFVTmgLaswn!cx$&%JbTsloz!3 zMEz1d`k==`Ueub_JAy_&`!ogbwx27^ZXgFNAbx=g_I~5nO^r)}&myw~+yY*cJl4$I znNJ32M&K=0(2Dj_>@39`3=FX!v3nZHno_@q^!y}%(yw0PqOo=);6Y@&ylVe>nMOZ~ zd>j#QQSBn3oaWd;qy$&5(5H$Ayi)0haAYO6TH>FR?rhqHmNOO+(})NB zLI@B@v0)eq!ug`>G<@htRlp3n!EpU|n+G+AvXFrWSUsLMBfL*ZB`CRsIVHNTR&b?K zxBgsN0BjfB>UVcJ|x%=-zb%OV7lmZc& zxiupadZVF7)6QuhoY;;FK2b*qL0J-Rn-8!X4ZY$-ZSUXV5DFd7`T41c(#lAeLMoeT z4%g655v@7AqT!i@)Edt5JMbN(=Q-6{=L4iG8RA%}w;&pKmtWvI4?G9pVRp|RTw`g0 zD5c12B&A2&P6Ng~8WM2eIW=wxd?r7A*N+&!Be7PX3s|7~z=APxm=A?5 zt>xB4WG|*Td@VX{Rs)PV0|yK`oI3^xn(4c_j&vgxk_Y3o(-`_5o`V zRTghg6%l@(qodXN;dB#+OKJEEvhfcnc#BeO2|E(5df-!fKDZ!%9!^BJ_4)9P+9Dq5 zK1=(v?KmIp34r?z{NEWnLB3Px{XYwy-akun4F7xTRr2^zeYW{gcK9)>aJDdU5;w5@ zak=<+-PLH-|04pelTb%ULpuuuJC7DgyT@D|p{!V!0v3KpDnRjANN12q6SUR3mb9<- z>2r~IApQGhstZ!3*?5V z8#)hJ0TdZg0M-BK#nGFP>$i=qk82DO z7h;Ft!D5E15OgW)&%lej*?^1~2=*Z5$2VX>V{x8SC+{i10BbtUk9@I#Vi&hX)q

Q!LwySI{Bnv%Sm)yh{^sSVJ8&h_D-BJ_YZe5eCaAWU9b$O2c z$T|{vWVRtOL!xC0DTc(Qbe`ItNtt5hr<)VijD0{U;T#bUEp381_y`%ZIav?kuYG{iyYdEBPW=*xNSc;Rlt6~F4M`5G+VtOjc z*0qGzCb@gME5udTjJA-9O<&TWd~}ysBd(eVT1-H82-doyH9RST)|+Pb{o*;$j9Tjs zhU!IlsPsj8=(x3bAKJTopW3^6AKROHR^7wZ185wJGVhA~hEc|LP;k7NEz-@4p5o}F z`AD6naG3(n=NF9HTH81=F+Q|JOz$7wm9I<+#BSmB@o_cLt2GkW9|?7mM;r!JZp89l zbo!Hp8=n!XH1{GwaDU+k)pGp`C|cXkCU5%vcH)+v@0eK>%7gWxmuMu9YLlChA|_D@ zi#5zovN_!a-0?~pUV-Rj*1P)KwdU-LguR>YM&*Nen+ln8Q$?WFCJg%DY%K}2!!1FE zDv-A%Cbwo^p(lzac&_TZ-l#9kq`mhLcY3h9ZTUVCM(Ad&=EriQY5{jJv<5K&g|*Lk zgV%ILnf1%8V2B0E&;Sp4sYbYOvvMebLwYwzkRQ#F8GpTQq#uv=J`uaSJ34OWITeSGo6+-8Xw znCk*n{kdDEi)Hi&u^)~cs@iyCkFWB2SWZU|Uc%^43ZIZQ-vWNExCCtDWjqHs;;tWf$v{}0{p0Rvxkq``)*>+Akq%|Na zA`@~-Vfe|+(AIlqru+7Ceh4nsVmO9p9jc8}HX^W&ViBDXT+uXbT#R#idPn&L>+#b6 zflC-4C5-X;kUnR~L>PSLh*gvL68}RBsu#2l`s_9KjUWRhiqF`j)`y`2`YU(>3bdBj z?>iyjEhe-~$^I5!nn%B6Wh+I`FvLNvauve~eX<+Ipl&04 zT}};W&1a3%W?dJ2=N#0t?e+aK+%t}5q%jSLvp3jZ%?&F}nOOWr>+{GFIa%wO_2`et z=JzoRR~}iKuuR+azPI8;Gf9)z3kyA4EIOSl!sRR$DlW}0>&?GbgPojmjmnln;cTqCt=ADbE zZ8GAnoM+S1(5$i8^O4t`ue;vO4i}z0wz-QEIVe5_u03;}-!G1NyY8;h^}y;tzY}i5 zqQr#Ur3Fy8sSa$Q0ys+f`!`+>9WbvU_I`Sj;$4{S>O3?#inLHCrtLy~!s#WXV=oVP zeE93*Nc`PBi4q@%Ao$x4lw9vLHM!6mn3-b_cebF|n-2vt-zYVF_&sDE--J-P;2WHo z+@n2areE0o$LjvjlV2X7ZU@j+`{*8zq`JR3gKF#EW|#+{nMyo-a>nFFTg&vhyT=b} zDa8+v0(Dgx0yRL@ZXOYIlVSZ0|MFizy0VPW8;AfA5|pe!#j zX}Py^8fl5SyS4g1WSKKtnyP+_PoOwMMwu`(i@Z)diJp~U54*-miOchy7Z35eL>^M z4p<-aIxH4VUZgS783@H%M7P9hX>t{|RU7$n4T(brCG#h9e9p! z+o`i;EGGq3&pF;~5V~eBD}lC)>if$w%Vf}AFxGqO88|ApfHf&Bvu+xdG)@vuF}Yvk z)o;~k-%+0K0g+L`Wala!$=ZV|z$e%>f0%XoLib%)!R^RoS+{!#X?h-6uu zF&&KxORdZU&EwQFITIRLo(7TA3W}y6X{?Y%y2j0It!ekU#<)$qghZtpcS>L3uh`Uj z7GY;6f$9qKynP#oS3$$a{p^{D+0oJQ71`1?OAn_m8)UGZmj3l*ZI)`V-a>MKGGFG< z&^jg#Ok%(hhm>hSrZ5;Qga4u(?^i>GiW_j9%_7M>j(^|Om$#{k+^*ULnEgzW_1gCICtAD^WpC`A z{9&DXkG#01Xo)U$OC(L5Y$DQ|Q4C6CjUKk1UkPj$nXH##J{c8e#K|&{mA*;b$r0E4 zUNo0jthwA(c&N1l=PEe8Rw_8cEl|-eya9z&H3#n`B$t#+aJ03RFMzrV@gowbe8v(c zIFM60^0&lCFO10NU4w@|61xiZ4CVXeaKjd;d?sv52XM*lS8XiVjgWpRB;&U_C0g+`6B5V&w|O6B*_q zsATxL!M}+$He)1eOWECce#eS@2n^xhlB4<_Nn?yCVEQWDs(r`|@2GqLe<#(|&P0U? z$7V5IgpWf09uIf_RazRwC?qEqRaHyL?iiS05UiGesJy%^>-C{{ypTBI&B0-iUYhk> zIk<5xpsuV@g|z(AZD+C-;A!fTG=df1=<%nxy(a(IS+U{ME4ZbDEBtcD_3V=icT6*_ z)>|J?>&6%nvHhZERBtjK+s4xnut*@>GAmA5m*OTp$!^CHTr}vM4n(X1Q*;{e-Rd2BCF-u@1ZGm z!S8hJ6L=Gl4T_SDa7Xx|-{4mxveJg=ctf`BJ*fy!yF6Dz&?w(Q_6B}WQVtNI!BVBC zKfX<>7vd6C96}XAQmF-Jd?1Q4eTfRB3q7hCh0f!(JkdWT5<{iAE#dKy*Jxq&3a1@~ z8C||Dn2mFNyrUV|<-)C^_y7@8c2Fz+2jrae9deBDu;U}tJ{^xAdxCD248(k;dCJ%o z`y3sADe>U%suxwwv~8A1+R$VB=Q?%U?4joI$um;aH+eCrBqpn- z%79D_7rb;R-;-9RTrwi9dPlg8&@tfWhhZ(Vx&1PQ+6(huX`;M9x~LrW~~#3{j0Bh2kDU$}@!fFQej4VGkJv?M4rU^x!RU zEwhu$!CA_iDjFjrJa`aocySDX16?~;+wgav;}Zut6Mg%C4>}8FL?8)Kgwc(Qlj{@#2Pt0?G`$h7P#M+qoXtlV@d}%c&OzO+QYKK`kyXaK{U(O^2DyIXCZlNQjt0^8~8JzNGrIxhj}}M z&~QZlbx%t;MJ(Vux;2tgNKGlAqphLq%pd}JG9uoVHUo?|hN{pLQ6Em%r*+7t^<);X zm~6=qChlNAVXNN*Sow->*4;}T;l;D1I-5T{Bif@4_}=>l`tK;qqDdt5zvisCKhMAH z#r}`)7VW?LZqfdmXQ%zo5bJ00{Xb9^YKrk0Nf|oIW*K@(=`o2Vndz}ZDyk{!u}PVx zzd--+_WC*U{~DH3{?GI64IB+@On&@9X>EUAo&L+G{L^dozaI4C3G#2wr~hseW@K&g zKWs{uHu-9Je!3;4pE>eBltKUXb^*hG8I&413)$J&{D4N%7PcloU6bn%jPxJyQL?g* z9g+YFFEDiE`8rW^laCNzQmi7CTnPfwyg3VDHRAl>h=In6jeaVOP@!-CP60j3+#vpL zEYmh_oP0{-gTe7Or`L6x)6w?77QVi~jD8lWN@3RHcm80iV%M1A!+Y6iHM)05iC64tb$X2lV_%Txk@0l^hZqi^%Z?#- zE;LE0uFx)R08_S-#(wC=dS&}vj6P4>5ZWjhthP=*Hht&TdLtKDR;rXEX4*z0h74FA zMCINqrh3Vq;s%3MC1YL`{WjIAPkVL#3rj^9Pj9Ss7>7duy!9H0vYF%>1jh)EPqvlr6h%R%CxDsk| z!BACz7E%j?bm=pH6Eaw{+suniuY7C9Ut~1cWfOX9KW9=H><&kQlinPV3h9R>3nJvK z4L9(DRM=x;R&d#a@oFY7mB|m8h4692U5eYfcw|QKwqRsshN(q^v$4$)HgPpAJDJ`I zkqjq(8Cd!K!+wCd=d@w%~e$=gdUgD&wj$LQ1r>-E=O@c ze+Z$x{>6(JA-fNVr)X;*)40Eym1TtUZI1Pwwx1hUi+G1Jlk~vCYeXMNYtr)1?qwyg zsX_e*$h?380O00ou?0R@7-Fc59o$UvyVs4cUbujHUA>sH!}L54>`e` zHUx#Q+Hn&Og#YVOuo*niy*GU3rH;%f``nk#NN5-xrZ34NeH$l`4@t);4(+0|Z#I>Y z)~Kzs#exIAaf--65L0UHT_SvV8O2WYeD>Mq^Y6L!Xu8%vnpofG@w!}R7M28?i1*T&zp3X4^OMCY6(Dg<-! zXmcGQrRgHXGYre7GfTJ)rhl|rs%abKT_Nt24_Q``XH{88NVPW+`x4ZdrMuO0iZ0g` z%p}y};~T5gbb9SeL8BSc`SO#ixC$@QhXxZ=B}L`tP}&k?1oSPS=4%{UOHe0<_XWln zwbl5cn(j-qK`)vGHY5B5C|QZd5)W7c@{bNVXqJ!!n$^ufc?N9C-BF2QK1(kv++h!>$QbAjq)_b$$PcJdV+F7hz0Hu@ zqj+}m0qn{t^tD3DfBb~0B36|Q`bs*xs|$i^G4uNUEBl4g;op-;Wl~iThgga?+dL7s zUP(8lMO?g{GcYpDS{NM!UA8Hco?#}eNEioRBHy4`mq!Pd-9@-97|k$hpEX>xoX+dY zDr$wfm^P&}Wu{!%?)U_(%Mn79$(ywvu*kJ9r4u|MyYLI_67U7%6Gd_vb##Nerf@>& z8W11z$$~xEZt$dPG}+*IZky+os5Ju2eRi;1=rUEeIn>t-AzC_IGM-IXWK3^6QNU+2pe=MBn4I*R@A%-iLDCOHTE-O^wo$sL_h{dcPl=^muAQb`_BRm};=cy{qSkui;`WSsj9%c^+bIDQ z0`_?KX0<-=o!t{u(Ln)v>%VGL z0pC=GB7*AQ?N7N{ut*a%MH-tdtNmNC+Yf$|KS)BW(gQJ*z$d{+{j?(e&hgTy^2|AR9vx1Xre2fagGv0YXWqtNkg*v%40v?BJBt|f9wX5 z{QTlCM}b-0{mV?IG>TW_BdviUKhtosrBqdfq&Frdz>cF~yK{P@(w{Vr7z2qKFwLhc zQuogKO@~YwyS9%+d-zD7mJG~@?EFJLSn!a&mhE5$_4xBl&6QHMzL?CdzEnC~C3$X@ zvY!{_GR06ep5;<#cKCSJ%srxX=+pn?ywDwtJ2{TV;0DKBO2t++B(tIO4)Wh`rD13P z4fE$#%zkd=UzOB74gi=-*CuID&Z3zI^-`4U^S?dHxK8fP*;fE|a(KYMgMUo`THIS1f!*6dOI2 zFjC3O=-AL`6=9pp;`CYPTdVX z8(*?V&%QoipuH0>WKlL8A*zTKckD!paN@~hh zmXzm~qZhMGVdQGd=AG8&20HW0RGV8X{$9LldFZYm zE?}`Q3i?xJRz43S?VFMmqRyvWaS#(~Lempg9nTM$EFDP(Gzx#$r)W&lpFKqcAoJh-AxEw$-bjW>`_+gEi z2w`99#UbFZGiQjS8kj~@PGqpsPX`T{YOj`CaEqTFag;$jY z8_{Wzz>HXx&G*Dx<5skhpETxIdhKH?DtY@b9l8$l?UkM#J-Snmts7bd7xayKTFJ(u zyAT&@6cAYcs{PBfpqZa%sxhJ5nSZBPji?Zlf&}#L?t)vC4X5VLp%~fz2Sx<*oN<7` z?ge=k<=X7r<~F7Tvp9#HB{!mA!QWBOf%EiSJ6KIF8QZNjg&x~-%e*tflL(ji_S^sO ztmib1rp09uon}RcsFi#k)oLs@$?vs(i>5k3YN%$T(5Or(TZ5JW9mA6mIMD08=749$ z!d+l*iu{Il7^Yu}H;lgw=En1sJpCKPSqTCHy4(f&NPelr31^*l%KHq^QE>z>Ks_bH zjbD?({~8Din7IvZeJ>8Ey=e;I?thpzD=zE5UHeO|neioJwG;IyLk?xOz(yO&0DTU~ z^#)xcs|s>Flgmp;SmYJ4g(|HMu3v7#;c*Aa8iF#UZo7CvDq4>8#qLJ|YdZ!AsH%^_7N1IQjCro

K7UpUK$>l@ zw`1S}(D?mUXu_C{wupRS-jiX~w=Uqqhf|Vb3Cm9L=T+w91Cu^ z*&Ty%sN?x*h~mJc4g~k{xD4ZmF%FXZNC;oVDwLZ_WvrnzY|{v8hc1nmx4^}Z;yriXsAf+Lp+OFLbR!&Ox?xABwl zu8w&|5pCxmu#$?Cv2_-Vghl2LZ6m7}VLEfR5o2Ou$x02uA-%QB2$c(c1rH3R9hesc zfpn#oqpbKuVsdfV#cv@5pV4^f_!WS+F>SV6N0JQ9E!T90EX((_{bSSFv9ld%I0&}9 zH&Jd4MEX1e0iqDtq~h?DBrxQX1iI0lIs<|kB$Yrh&cpeK0-^K%=FBsCBT46@h#yi!AyDq1V(#V}^;{{V*@T4WJ&U-NTq43w=|K>z8%pr_nC>%C(Wa_l78Ufib$r8Od)IIN=u>417 z`Hl{9A$mI5A(;+-Q&$F&h-@;NR>Z<2U;Y21>>Z;s@0V@SbkMQQj%_;~+qTuQ?c|AV zcWm3XZQHhP&R%QWarS%mJ!9R^&!_)*s(v+VR@I#QrAT}`17Y+l<`b-nvmDNW`De%y zrwTZ9EJrj1AFA>B`1jYDow}~*dfPs}IZMO3=a{Fy#IOILc8F0;JS4x(k-NSpbN@qM z`@aE_e}5{!$v3+qVs7u?sOV(y@1Os*Fgu`fCW9=G@F_#VQ%xf$hj0~wnnP0$hFI+@ zkQj~v#V>xn)u??YutKsX>pxKCl^p!C-o?+9;!Nug^ z{rP!|+KsP5%uF;ZCa5F;O^9TGac=M|=V z_H(PfkV1rz4jl?gJ(ArXMyWT4y(86d3`$iI4^l9`vLdZkzpznSd5Ikfrs8qcSy&>z zTIZgWZGXw0n9ibQxYWE@gI0(3#KA-dAdPcsL_|hg2@~C!VZDM}5;v_Nykfq!*@*Zf zE_wVgx82GMDryKO{U{D>vSzSc%B~|cjDQrt5BN=Ugpsf8H8f1lR4SGo#hCuXPL;QQ z#~b?C4MoepT3X`qdW2dNn& zo8)K}%Lpu>0tQei+{>*VGErz|qjbK#9 zvtd8rcHplw%YyQCKR{kyo6fgg!)6tHUYT(L>B7er5)41iG`j$qe*kSh$fY!PehLcD zWeKZHn<492B34*JUQh=CY1R~jT9Jt=k=jCU2=SL&&y5QI2uAG2?L8qd2U(^AW#{(x zThSy=C#>k+QMo^7caQcpU?Qn}j-`s?1vXuzG#j8(A+RUAY})F@=r&F(8nI&HspAy4 z4>(M>hI9c7?DCW8rw6|23?qQMSq?*Vx?v30U%luBo)B-k2mkL)Ljk5xUha3pK>EEj z@(;tH|M@xkuN?gsz;*bygizwYR!6=(Xgcg^>WlGtRYCozY<rFX2E>kaZo)O<^J7a`MX8Pf`gBd4vrtD|qKn&B)C&wp0O-x*@-|m*0egT=-t@%dD zgP2D+#WPptnc;_ugD6%zN}Z+X4=c61XNLb7L1gWd8;NHrBXwJ7s0ce#lWnnFUMTR& z1_R9Fin4!d17d4jpKcfh?MKRxxQk$@)*hradH2$3)nyXep5Z;B z?yX+-Bd=TqO2!11?MDtG0n(*T^!CIiF@ZQymqq1wPM_X$Iu9-P=^}v7npvvPBu!d$ z7K?@CsA8H38+zjA@{;{kG)#AHME>Ix<711_iQ@WWMObXyVO)a&^qE1GqpP47Q|_AG zP`(AD&r!V^MXQ^e+*n5~Lp9!B+#y3#f8J^5!iC@3Y@P`;FoUH{G*pj*q7MVV)29+j z>BC`a|1@U_v%%o9VH_HsSnM`jZ-&CDvbiqDg)tQEnV>b%Ptm)T|1?TrpIl)Y$LnG_ zzKi5j2Fx^K^PG1=*?GhK;$(UCF-tM~^=Z*+Wp{FSuy7iHt9#4n(sUuHK??@v+6*|10Csdnyg9hAsC5_OrSL;jVkLlf zHXIPukLqbhs~-*oa^gqgvtpgTk_7GypwH><53riYYL*M=Q@F-yEPLqQ&1Sc zZB%w}T~RO|#jFjMWcKMZccxm-SL)s_ig?OC?y_~gLFj{n8D$J_Kw%{r0oB8?@dWzn zB528d-wUBQzrrSSLq?fR!K%59Zv9J4yCQhhDGwhptpA5O5U?Hjqt>8nOD zi{)0CI|&Gu%zunGI*XFZh(ix)q${jT8wnnzbBMPYVJc4HX*9d^mz|21$=R$J$(y7V zo0dxdbX3N#=F$zjstTf*t8vL)2*{XH!+<2IJ1VVFa67|{?LP&P41h$2i2;?N~RA30LV`BsUcj zfO9#Pg1$t}7zpv#&)8`mis3~o+P(DxOMgz-V*(?wWaxi?R=NhtW}<#^Z?(BhSwyar zG|A#Q7wh4OfK<|DAcl9THc-W4*>J4nTevsD%dkj`U~wSUCh15?_N@uMdF^Kw+{agk zJ`im^wDqj`Ev)W3k3stasP`88-M0ZBs7;B6{-tSm3>I@_e-QfT?7|n0D~0RRqDb^G zyHb=is;IwuQ&ITzL4KsP@Z`b$d%B0Wuhioo1CWttW8yhsER1ZUZzA{F*K=wmi-sb#Ju+j z-l@In^IKnb{bQG}Ps>+Vu_W#grNKNGto+yjA)?>0?~X`4I3T@5G1)RqGUZuP^NJCq&^HykuYtMDD8qq+l8RcZNJsvN(10{ zQ1$XcGt}QH-U^WU!-wRR1d--{B$%vY{JLWIV%P4-KQuxxDeJaF#{eu&&r!3Qu{w}0f--8^H|KwE>)ORrcR+2Qf zb})DRcH>k0zWK8@{RX}NYvTF;E~phK{+F;MkIP$)T$93Ba2R2TvKc>`D??#mv9wg$ zd~|-`Qx5LwwsZ2hb*Rt4S9dsF%Cny5<1fscy~)d;0m2r$f=83<->c~!GNyb!U)PA; zq^!`@@)UaG)Ew(9V?5ZBq#c%dCWZrplmuM`o~TyHjAIMh0*#1{B>K4po-dx$Tk-Cq z=WZDkP5x2W&Os`N8KiYHRH#UY*n|nvd(U>yO=MFI-2BEp?x@=N<~CbLJBf6P)}vLS?xJXYJ2^<3KJUdrwKnJnTp{ zjIi|R=L7rn9b*D#Xxr4*R<3T5AuOS+#U8hNlfo&^9JO{VbH!v9^JbK=TCGR-5EWR@ zN8T-_I|&@A}(hKeL4_*eb!1G8p~&_Im8|wc>Cdir+gg90n1dw?QaXcx6Op_W1r=axRw>4;rM*UOpT#Eb9xU1IiWo@h?|5uP zka>-XW0Ikp@dIe;MN8B01a7+5V@h3WN{J=HJ*pe0uwQ3S&MyWFni47X32Q7SyCTNQ z+sR!_9IZa5!>f&V$`q!%H8ci!a|RMx5}5MA_kr+bhtQy{-^)(hCVa@I!^TV4RBi zAFa!Nsi3y37I5EK;0cqu|9MRj<^r&h1lF}u0KpKQD^5Y+LvFEwM zLU@@v4_Na#Axy6tn3P%sD^5P#<7F;sd$f4a7LBMk zGU^RZHBcxSA%kCx*eH&wgA?Qwazm8>9SCSz_!;MqY-QX<1@p$*T8lc?@`ikEqJ>#w zcG``^CoFMAhdEXT9qt47g0IZkaU)4R7wkGs^Ax}usqJ5HfDYAV$!=6?>J6+Ha1I<5 z|6=9soU4>E))tW$<#>F ziZ$6>KJf0bPfbx_)7-}tMINlc=}|H+$uX)mhC6-Hz+XZxsKd^b?RFB6et}O#+>Wmw9Ec9) z{q}XFWp{3@qmyK*Jvzpyqv57LIR;hPXKsrh{G?&dRjF%Zt5&m20Ll?OyfUYC3WRn{cgQ?^V~UAv+5 z&_m#&nIwffgX1*Z2#5^Kl4DbE#NrD&Hi4|7SPqZ}(>_+JMz=s|k77aEL}<=0Zfb)a z%F(*L3zCA<=xO)2U3B|pcTqDbBoFp>QyAEU(jMu8(jLA61-H!ucI804+B!$E^cQQa z)_ERrW3g!B9iLb3nn3dlkvD7KsY?sRvls3QC0qPi>o<)GHx%4Xb$5a3GBTJ(k@`e@ z$RUa^%S15^1oLEmA=sayrP5;9qtf!Z1*?e$ORVPsXpL{jL<6E)0sj&swP3}NPmR%FM?O>SQgN5XfHE< zo(4#Cv11(%Nnw_{_Ro}r6=gKd{k?NebJ~<~Kv0r(r0qe4n3LFx$5%x(BKvrz$m?LG zjLIc;hbj0FMdb9aH9Lpsof#yG$(0sG2%RL;d(n>;#jb!R_+dad+K;Ccw!|RY?uS(a zj~?=&M!4C(5LnlH6k%aYvz@7?xRa^2gml%vn&eKl$R_lJ+e|xsNfXzr#xuh(>`}9g zLHSyiFwK^-p!;p$yt7$F|3*IfO3Mlu9e>Dpx8O`37?fA`cj`C0B-m9uRhJjs^mRp# zWB;Aj6|G^1V6`jg7#7V9UFvnB4((nIwG?k%c7h`?0tS8J3Bn0t#pb#SA}N-|45$-j z$R>%7cc2ebAClXc(&0UtHX<>pd)akR3Kx_cK+n<}FhzmTx!8e9^u2e4%x{>T6pQ`6 zO182bh$-W5A3^wos0SV_TgPmF4WUP-+D25KjbC{y_6W_9I2_vNKwU(^qSdn&>^=*t z&uvp*@c8#2*paD!ZMCi3;K{Na;I4Q35zw$YrW5U@Kk~)&rw;G?d7Q&c9|x<Hg|CNMsxovmfth*|E*GHezPTWa^Hd^F4!B3sF;)? z(NaPyAhocu1jUe(!5Cy|dh|W2=!@fNmuNOzxi^tE_jAtzNJ0JR-avc_H|ve#KO}#S z#a(8secu|^Tx553d4r@3#6^MHbH)vmiBpn0X^29xEv!Vuh1n(Sr5I0V&`jA2;WS|Y zbf0e}X|)wA-Pf5gBZ>r4YX3Mav1kKY(ulAJ0Q*jB)YhviHK)w!TJsi3^dMa$L@^{` z_De`fF4;M87vM3Ph9SzCoCi$#Fsd38u!^0#*sPful^p5oI(xGU?yeYjn;Hq1!wzFk zG&2w}W3`AX4bxoVm03y>ts{KaDf!}b&7$(P4KAMP=vK5?1In^-YYNtx1f#}+2QK@h zeSeAI@E6Z8a?)>sZ`fbq9_snl6LCu6g>o)rO;ijp3|$vig+4t} zylEo7$SEW<_U+qgVcaVhk+4k+C9THI5V10qV*dOV6pPtAI$)QN{!JRBKh-D zk2^{j@bZ}yqW?<#VVuI_27*cI-V~sJiqQv&m07+10XF+#ZnIJdr8t`9s_EE;T2V;B z4UnQUH9EdX%zwh-5&wflY#ve!IWt0UE-My3?L#^Bh%kcgP1q{&26eXLn zTkjJ*w+(|_>Pq0v8{%nX$QZbf)tbJaLY$03;MO=Ic-uqYUmUCuXD>J>o6BCRF=xa% z3R4SK9#t1!K4I_d>tZgE>&+kZ?Q}1qo4&h%U$GfY058s%*=!kac{0Z+4Hwm!)pFLR zJ+5*OpgWUrm0FPI2ib4NPJ+Sk07j(`diti^i#kh&f}i>P4~|d?RFb#!JN)~D@)beox}bw?4VCf^y*`2{4`-@%SFTry2h z>9VBc9#JxEs1+0i2^LR@B1J`B9Ac=#FW=(?2;5;#U$0E0UNag_!jY$&2diQk_n)bT zl5Me_SUvqUjwCqmVcyb`igygB_4YUB*m$h5oeKv3uIF0sk}~es!{D>4r%PC*F~FN3owq5e0|YeUTSG#Vq%&Gk7uwW z0lDo#_wvflqHeRm*}l?}o;EILszBt|EW*zNPmq#?4A+&i0xx^?9obLyY4xx=Y9&^G;xYXYPxG)DOpPg!i_Ccl#3L}6xAAZzNhPK1XaC_~ z!A|mlo?Be*8Nn=a+FhgpOj@G7yYs(Qk(8&|h@_>w8Y^r&5nCqe0V60rRz?b5%J;GYeBqSAjo|K692GxD4` zRZyM2FdI+-jK2}WAZTZ()w_)V{n5tEb@>+JYluDozCb$fA4H)$bzg(Ux{*hXurjO^ zwAxc+UXu=&JV*E59}h3kzQPG4M)X8E*}#_&}w*KEgtX)cU{vm9b$atHa;s>| z+L6&cn8xUL*OSjx4YGjf6{Eq+Q3{!ZyhrL&^6Vz@jGbI%cAM9GkmFlamTbcQGvOlL zmJ?(FI)c86=JEs|*;?h~o)88>12nXlpMR4@yh%qdwFNpct;vMlc=;{FSo*apJ;p}! zAX~t;3tb~VuP|ZW;z$=IHf->F@Ml)&-&Bnb{iQyE#;GZ@C$PzEf6~q}4D>9jic@mTO5x76ulDz@+XAcm35!VSu zT*Gs>;f0b2TNpjU_BjHZ&S6Sqk6V1370+!eppV2H+FY!q*n=GHQ!9Rn6MjY!Jc77A zG7Y!lFp8?TIHN!LXO?gCnsYM-gQxsm=Ek**VmZu7vnuufD7K~GIxfxbsQ@qv2T zPa`tvHB$fFCyZl>3oYg?_wW)C>^_iDOc^B7klnTOoytQH18WkOk)L2BSD0r%xgRSW zQS9elF^?O=_@|58zKLK;(f77l-Zzu}4{fXed2saq!5k#UZAoDBqYQS{sn@j@Vtp|$ zG%gnZ$U|9@u#w1@11Sjl8ze^Co=)7yS(}=;68a3~g;NDe_X^}yJj;~s8xq9ahQ5_r zxAlTMnep*)w1e(TG%tWsjo3RR;yVGPEO4V{Zp?=a_0R#=V^ioQu4YL=BO4r0$$XTX zZfnw#_$V}sDAIDrezGQ+h?q24St0QNug_?{s-pI(^jg`#JRxM1YBV;a@@JQvH8*>> zIJvku74E0NlXkYe_624>znU0J@L<-c=G#F3k4A_)*;ky!C(^uZfj%WB3-*{*B$?9+ zDm$WFp=0(xnt6`vDQV3Jl5f&R(Mp};;q8d3I%Kn>Kx=^;uSVCw0L=gw53%Bp==8Sw zxtx=cs!^-_+i{2OK`Q;913+AXc_&Z5$@z3<)So0CU3;JAv=H?@Zpi~riQ{z-zLtVL z!oF<}@IgJp)Iyz1zVJ42!SPHSkjYNS4%ulVVIXdRuiZ@5Mx8LJS}J#qD^Zi_xQ@>DKDr-_e#>5h3dtje*NcwH_h;i{Sx7}dkdpuW z(yUCjckQsagv*QGMSi9u1`Z|V^}Wjf7B@q%j2DQXyd0nOyqg%m{CK_lAoKlJ7#8M} z%IvR?Vh$6aDWK2W!=i?*<77q&B8O&3?zP(Cs@kapc)&p7En?J;t-TX9abGT#H?TW? ztO5(lPKRuC7fs}zwcUKbRh=7E8wzTsa#Z{a`WR}?UZ%!HohN}d&xJ=JQhpO1PI#>X zHkb>pW04pU%Bj_mf~U}1F1=wxdBZu1790>3Dm44bQ#F=T4V3&HlOLsGH)+AK$cHk6 zia$=$kog?)07HCL*PI6}DRhpM^*%I*kHM<#1Se+AQ!!xyhcy6j7`iDX7Z-2i73_n# zas*?7LkxS-XSqv;YBa zW_n*32D(HTYQ0$feV_Fru1ZxW0g&iwqixPX3=9t4o)o|kOo79V$?$uh?#8Q8e>4e)V6;_(x&ViUVxma+i25qea;d-oK7ouuDsB^ab{ zu1qjQ%`n56VtxBE#0qAzb7lph`Eb-}TYpXB!H-}3Ykqyp`otprp7{VEuW*^IR2n$Fb99*nAtqT&oOFIf z@w*6>YvOGw@Ja?Pp1=whZqydzx@9X4n^2!n83C5{C?G@|E?&$?p*g68)kNvUTJ)I6 z1Q|(#UuP6pj78GUxq11m-GSszc+)X{C2eo-?8ud9sB=3(D47v?`JAa{V(IF zPZQ_0AY*9M97>Jf<o%#O_%Wq}8>YM=q0|tGY+hlXcpE=Z4Od z`NT7Hu2hnvRoqOw@g1f=bv`+nba{GwA$Ak0INlqI1k<9!x_!sL()h?hEWoWrdU3w` zZ%%)VR+Bc@_v!C#koM1p-3v_^L6)_Ktj4HE>aUh%2XZE@JFMOn)J~c`_7VWNb9c-N z2b|SZMR4Z@E7j&q&9(6H3yjEu6HV7{2!1t0lgizD;mZ9$r(r7W5G$ky@w(T_dFnOD z*p#+z$@pKE+>o@%eT(2-p_C}wbQ5s(%Sn_{$HDN@MB+Ev?t@3dPy`%TZ!z}AThZSu zN<1i$siJhXFdjV zP*y|V<`V8t=h#XTRUR~5`c`Z9^-`*BZf?WAehGdg)E2Je)hqFa!k{V(u+(hTf^Yq& zoruUh2(^3pe)2{bvt4&4Y9CY3js)PUHtd4rVG57}uFJL)D(JfSIo^{P=7liFXG zq5yqgof0V8paQcP!gy+;^pp-DA5pj=gbMN0eW=-eY+N8~y+G>t+x}oa!5r>tW$xhI zPQSv=pi;~653Gvf6~*JcQ%t1xOrH2l3Zy@8AoJ+wz@daW@m7?%LXkr!bw9GY@ns3e zSfuWF_gkWnesv?s3I`@}NgE2xwgs&rj?kH-FEy82=O8`+szN ziHch`vvS`zNfap14!&#i9H@wF7}yIPm=UB%(o(}F{wsZ(wA0nJ2aD^@B41>>o-_U6 zUqD~vdo48S8~FTb^+%#zcbQiiYoDKYcj&$#^;Smmb+Ljp(L=1Kt_J!;0s%1|JK}Wi z;={~oL!foo5n8=}rs6MmUW~R&;SIJO3TL4Ky?kh+b2rT9B1Jl4>#Uh-Bec z`Hsp<==#UEW6pGPhNk8H!!DUQR~#F9jEMI6T*OWfN^Ze&X(4nV$wa8QUJ>oTkruH# zm~O<`J7Wxseo@FqaZMl#Y(mrFW9AHM9Kb|XBMqaZ2a)DvJgYipkDD_VUF_PKd~dT7 z#02}bBfPn9a!X!O#83=lbJSK#E}K&yx-HI#T6ua)6o0{|={*HFusCkHzs|Fn&|C3H zBck1cmfcWVUN&i>X$YU^Sn6k2H;r3zuXbJFz)r5~3$d$tUj(l1?o={MM){kjgqXRO zc5R*#{;V7AQh|G|)jLM@wGAK&rm2~@{Pewv#06pHbKn#wL0P6F1!^qw9g&cW3Z=9} zj)POhOlwsh@eF=>z?#sIs*C-Nl(yU!#DaiaxhEs#iJqQ8w%(?+6lU02MYSeDkr!B- zPjMv+on6OLXgGnAtl(ao>|X2Y8*Hb}GRW5}-IzXnoo-d0!m4Vy$GS!XOLy>3_+UGs z2D|YcQx@M#M|}TDOetGi{9lGo9m-=0-^+nKE^*?$^uHkxZh}I{#UTQd;X!L+W@jm( zDg@N4+lUqI92o_rNk{3P>1gxAL=&O;x)ZT=q1mk0kLlE$WeWuY_$0`0jY-Kkt zP*|m3AF}Ubd=`<>(Xg0har*_@x2YH}bn0Wk*OZz3*e5;Zc;2uBdnl8?&XjupbkOeNZsNh6pvsq_ydmJI+*z**{I{0K)-;p1~k8cpJXL$^t!-`E}=*4G^-E8>H!LjTPxSx zcF+cS`ommfKMhNSbas^@YbTpH1*RFrBuATUR zt{oFWSk^$xU&kbFQ;MCX22RAN5F6eq9UfR$ut`Jw--p2YX)A*J69m^!oYfj2y7NYcH6&r+0~_sH^c^nzeN1AU4Ga7=FlR{S|Mm~MpzY0$Z+p2W(a={b-pR9EO1Rs zB%KY|@wLcAA@)KXi!d2_BxrkhDn`DT1=Dec}V!okd{$+wK z4E{n8R*xKyci1(CnNdhf$Dp2(Jpof0-0%-38X=Dd9PQgT+w%Lshx9+loPS~MOm%ZT zt%2B2iL_KU_ita%N>xjB!#71_3=3c}o zgeW~^U_ZTJQ2!PqXulQd=3b=XOQhwATK$y(9$#1jOQ4}4?~l#&nek)H(04f(Sr=s| zWv7Lu1=%WGk4FSw^;;!8&YPM)pQDCY9DhU`hMty1@sq1=Tj7bFsOOBZOFlpR`W>-J$-(kezWJj;`?x-v>ev{*8V z8p|KXJPV$HyQr1A(9LVrM47u-XpcrIyO`yWvx1pVYc&?154aneRpLqgx)EMvRaa#|9?Wwqs2+W8n5~79G z(}iCiLk;?enn}ew`HzhG+tu+Ru@T+K5juvZN)wY;x6HjvqD!&!)$$;1VAh~7fg0K| zEha#aN=Yv|3^~YFH}cc38ovVb%L|g@9W6fo(JtT6$fa?zf@Ct88e}m?i)b*Jgc{fl zExfdvw-BYDmH6>(4QMt#p0;FUIQqkhD}aH?a7)_%JtA~soqj{ppP_82yi9kaxuK>~ ze_)Zt>1?q=ZH*kF{1iq9sr*tVuy=u>Zev}!gEZx@O6-fjyu9X00gpIl-fS_pzjpqJ z1yqBmf9NF!jaF<+YxgH6oXBdK)sH(>VZ)1siyA$P<#KDt;8NT*l_0{xit~5j1P)FN zI8hhYKhQ)i z37^aP13B~u65?sg+_@2Kr^iWHN=U;EDSZ@2W2!5ALhGNWXnFBY%7W?1 z=HI9JzQ-pLKZDYTv<0-lt|6c-RwhxZ)mU2Os{bsX_i^@*fKUj8*aDO5pks=qn3Dv6 zwggpKLuyRCTVPwmw1r}B#AS}?X7b837UlXwp~E2|PJw2SGVueL7){Y&z!jL!XN=0i zU^Eig`S2`{+gU$68aRdWx?BZ{sU_f=8sn~>s~M?GU~`fH5kCc; z8ICp+INM3(3{#k32RZdv6b9MQYdZXNuk7ed8;G?S2nT+NZBG=Tar^KFl2SvhW$bGW#kdWL-I)s_IqVnCDDM9fm8g;P;8 z7t4yZn3^*NQfx7SwmkzP$=fwdC}bafQSEF@pd&P8@H#`swGy_rz;Z?Ty5mkS%>m#% zp_!m9e<()sfKiY(nF<1zBz&&`ZlJf6QLvLhl`_``%RW&{+O>Xhp;lwSsyRqGf=RWd zpftiR`={2(siiPAS|p}@q=NhVc0ELprt%=fMXO3B)4ryC2LT(o=sLM7hJC!}T1@)E zA3^J$3&1*M6Xq>03FX`R&w*NkrZE?FwU+Muut;>qNhj@bX17ZJxnOlPSZ=Zeiz~T_ zOu#yc3t6ONHB;?|r4w+pI)~KGN;HOGC)txxiUN8#mexj+W(cz%9a4sx|IRG=}ia zuEBuba3AHsV2feqw-3MvuL`I+2|`Ud4~7ZkN=JZ;L20|Oxna5vx1qbIh#k2O4$RQF zo`tL()zxaqibg^GbB+BS5#U{@K;WWQj~GcB1zb}zJkPwH|5hZ9iH2308!>_;%msji zJHSL~s)YHBR=Koa1mLEOHos*`gp=s8KA-C zu0aE+W!#iJ*0xqKm3A`fUGy#O+X+5W36myS>Uh2!R*s$aCU^`K&KKLCCDkejX2p=5 z%o7-fl03x`gaSNyr?3_JLv?2RLS3F*8ub>Jd@^Cc17)v8vYEK4aqo?OS@W9mt%ITJ z9=S2%R8M){CugT@k~~0x`}Vl!svYqX=E)c_oU6o}#Hb^%G1l3BudxA{F*tbjG;W_>=xV73pKY53v%>I)@D36I_@&p$h|Aw zonQS`07z_F#@T-%@-Tb|)7;;anoD_WH>9ewFy(ZcEOM$#Y)8>qi7rCnsH9GO-_7zF zu*C87{Df1P4TEOsnzZ@H%&lvV(3V@;Q!%+OYRp`g05PjY^gL$^$-t0Y>H*CDDs?FZly*oZ&dxvsxaUWF!{em4{A>n@vpXg$dwvt@_rgmHF z-MER`ABa8R-t_H*kv>}CzOpz;!>p^^9ztHMsHL|SRnS<-y5Z*r(_}c4=fXF`l^-i}>e7v!qs_jv zqvWhX^F=2sDNWA9c@P0?lUlr6ecrTKM%pNQ^?*Lq?p-0~?_j50xV%^(+H>sMul#Tw zeciF*1=?a7cI(}352%>LO96pD+?9!fNyl^9v3^v&Y4L)mNGK0FN43&Xf8jUlxW1Bw zyiu2;qW-aGNhs=zbuoxnxiwZ3{PFZM#Kw)9H@(hgX23h(`Wm~m4&TvoZoYp{plb^> z_#?vXcxd>r7K+1HKJvhed>gtK`TAbJUazUWQY6T~t2af%#<+Veyr%7-#*A#@&*;@g58{i|E%6yC_InGXCOd{L0;$)z#?n7M`re zh!kO{6=>7I?*}czyF7_frt#)s1CFJ_XE&VrDA?Dp3XbvF{qsEJgb&OLSNz_5g?HpK z9)8rsr4JN!Af3G9!#Qn(6zaUDqLN(g2g8*M)Djap?WMK9NKlkC)E2|-g|#-rp%!Gz zAHd%`iq|81efi93m3yTBw3g0j#;Yb2X{mhRAI?&KDmbGqou(2xiRNb^sV}%%Wu0?< z?($L>(#BO*)^)rSgyNRni$i`R4v;GhlCZ8$@e^ROX(p=2_v6Y!%^As zu022)fHdv_-~Yu_H6WVPLpHQx!W%^6j)cBhS`O3QBW#x(eX54d&I22op(N59b*&$v zFiSRY6rOc^(dgSV1>a7-5C;(5S5MvKcM2Jm-LD9TGqDpP097%52V+0>Xqq!! zq4e3vj53SE6i8J`XcQB|MZPP8j;PAOnpGnllH6#Ku~vS42xP*Nz@~y%db7Xi8s09P z1)e%8ys6&M8D=Dt6&t`iKG_4X=!kgRQoh%Z`dc&mlOUqXk-k`jKv9@(a^2-Upw>?< zt5*^DV~6Zedbec4NVl($2T{&b)zA@b#dUyd>`2JC0=xa_fIm8{5um zr-!ApXZhC8@=vC2WyxO|!@0Km)h8ep*`^he92$@YwP>VcdoS5OC^s38e#7RPsg4j+ zbVGG}WRSET&ZfrcR(x~k8n1rTP%CnfUNKUonD$P?FtNFF#cn!wEIab-;jU=B1dHK@ z(;(yAQJ`O$sMn>h;pf^8{JISW%d+@v6@CnXh9n5TXGC}?FI9i-D0OMaIg&mAg=0Kn zNJ7oz5*ReJukD55fUsMuaP+H4tDN&V9zfqF@ zr=#ecUk9wu{0;!+gl;3Bw=Vn^)z$ahVhhw)io!na&9}LmWurLb0zubxK=UEnU*{5P z+SP}&*(iBKSO4{alBHaY^)5Q=mZ+2OwIooJ7*Q5XJ+2|q`9#f?6myq!&oz?klihLq z4C)$XP!BNS0G_Z1&TM>?Jk{S~{F3n83ioli=IO6f%wkvCl(RFFw~j0tb{GvXTx>*sB0McY0s&SNvj4+^h`9nJ_wM>F!Uc>X}9PifQekn0sKI2SAJP!a4h z5cyGTuCj3ZBM^&{dRelIlT^9zcfaAuL5Y~bl!ppSf`wZbK$z#6U~rdclk``e+!qhe z6Qspo*%<)eu6?C;Bp<^VuW6JI|Ncvyn+LlSl;Mp22Bl7ARQ0Xc24%29(ZrdsIPw&-=yHQ7_Vle|5h>AST0 zUGX2Zk34vp?U~IHT|;$U86T+UUHl_NE4m|}>E~6q``7hccCaT^#y+?wD##Q%HwPd8 zV3x4L4|qqu`B$4(LXqDJngNy-{&@aFBvVsywt@X^}iH7P%>bR?ciC$I^U-4Foa`YKI^qDyGK7k%E%c_P=yzAi`YnxGA%DeNd++j3*h^ z=rn>oBd0|~lZ<6YvmkKY*ZJlJ;Im0tqgWu&E92eqt;+NYdxx`eS(4Hw_Jb5|yVvBg z*tbdY^!AN;luEyN4VRhS@-_DC{({ziH{&Z}iGElSV~qvT>L-8G%+yEL zX#MFOhj{InyKG=mvW-<1B@c-}x$vA(nU?>S>0*eN#!SLzQ)Ex7fvQ)S4D<8|I#N$3 zT5Ei`Z?cxBODHX8(Xp73v`IsAYC@9b;t}z0wxVuQSY1J^GRwDPN@qbM-ZF48T$GZ< z8WU+;Pqo?{ghI-KZ-i*ydXu`Ep0Xw^McH_KE9J0S7G;x8Fe`DVG?j3Pv=0YzJ}yZR z%2=oqHiUjvuk0~Ca>Kol4CFi0_xQT~;_F?=u+!kIDl-9g`#ZNZ9HCy17Ga1v^Jv9# z{T4Kb1-AzUxq*MutfOWWZgD*HnFfyYg0&e9f(5tZ>krPF6{VikNeHoc{linPPt#Si z&*g>(c54V8rT_AX!J&bNm-!umPvOR}vDai#`CX___J#=zeB*{4<&2WpaDncZsOkp* zsg<%@@rbrMkR_ux9?LsQxzoBa1s%$BBn6vk#{&&zUwcfzeCBJUwFYSF$08qDsB;gWQN*g!p8pxjofWbqNSZOEKOaTx@+* zwdt5*Q47@EOZ~EZL9s?1o?A%9TJT=Ob_13yyugvPg*e&ZU(r6^k4=2+D-@n=Hv5vu zSXG|hM(>h9^zn=eQ=$6`JO&70&2|%V5Lsx>)(%#;pcOfu>*nk_3HB_BNaH$`jM<^S zcSftDU1?nL;jy)+sfonQN}(}gUW?d_ikr*3=^{G)=tjBtEPe>TO|0ddVB zTklrSHiW+!#26frPXQQ(YN8DG$PZo?(po(QUCCf_OJC`pw*uey00%gmH!`WJkrKXj2!#6?`T25mTu9OJp2L8z3! z=arrL$ZqxuE{%yV)14Kd>k}j7pxZ6#$Dz8$@WV5p8kTqN<-7W)Q7Gt2{KoOPK_tZ| zf2WG~O5@{qPI+W<4f_;reuFVdO^5`ADC1!JQE|N`s3cq@(0WB!n0uh@*c{=LAd;~} zyGK@hbF-Oo+!nN)@i*O(`@FA#u?o=~e{`4O#5}z&=UkU*50fOrzi11D^&FOqe>wii z?*k+2|EcUs;Gx{!@KBT~>PAwLrIDT7Th=Utu?~?np@t^gFs?zgX=D${RwOY^WGh-+ z+#4$066ISh8eYW#FXWp~S`<*%O^ZuItL1Tyqt8#tZ zY120E;^VG`!lZn&3sPd$RkdHpU#|w+bYV)pJC|SH9g%|5IkxVTQcBA4CL0}$&}ef@ zW^Vtj%M;;_1xxP9x#ex17&4N*{ksO*_4O}xYu(p*JkL#yr}@7b)t5X?%CY<+s5_MJ zuiqt+N_;A(_)%lumoyRFixWa-M7qK_9s6<1X?JDa9fP!+_6u~~M$5L=ipB=7(j#f< zZ34J%=bs549%~_mA(|={uZNs_0?o7;-LBP(ZRnkd{-^|2|=4vUTmtByHL8 zEph`(LSEzQj68a+`d$V<45J7cyv^#|^|%fD#si1Nx!4NW*`l*{->HEWNh6-|g>-=r zXmQ|-i}Ku$ndUeHQ^&ieT!Lf}vf6GaqW9$DJ2NWrqwPY%%4nip$@vK$nRp*_C-v<| zuKz~ZyN&<%!NS26&x?jhy+@awJipMQ-8(X4#Ae5??U<1QMt1l9R=w9fAnEF}NYu$2 z>6}Vkc zIb*A?G*z8^IvibmBKn_u^5&T_1oey0gZS2~obf(#xk=erZGTEdQnt3DMGM+0oPwss zj5zXD;(oWhB_T@~Ig#9@v)AKtXu3>Inmgf@A|-lD-1U>cNyl3h?ADD9)GG4}zUGPk zZzaXe!~Kf?<~@$G?Uql3t8jy9{2!doq4=J}j9ktTxss{p6!9UdjyDERlA*xZ!=Q)KDs5O)phz>Vq3BNGoM(H|=1*Q4$^2fTZw z(%nq1P|5Rt81}SYJpEEzMPl5VJsV5&4e)ZWKDyoZ>1EwpkHx-AQVQc8%JMz;{H~p{=FXV>jIxvm4X*qv52e?Y-f%DJ zxEA165GikEASQ^fH6K#d!Tpu2HP{sFs%E=e$gYd$aj$+xue6N+Wc(rAz~wUsk2`(b z8Kvmyz%bKQxpP}~baG-rwYcYCvkHOi zlkR<=>ZBTU*8RF_d#Bl@zZsRIhx<%~Z@Z=ik z>adw3!DK(8R|q$vy{FTxw%#xliD~6qXmY^7_9kthVPTF~Xy1CfBqbU~?1QmxmU=+k z(ggxvEuA;0e&+ci-zQR{-f7aO{O(Pz_OsEjLh_K>MbvoZ4nxtk5u{g@nPv)cgW_R} z9}EA4K4@z0?7ue}Z(o~R(X&FjejUI2g~08PH1E4w>9o{)S(?1>Z0XMvTb|;&EuyOE zGvWNpYX)Nv<8|a^;1>bh#&znEcl-r!T#pn= z4$?Yudha6F%4b>*8@=BdtXXY4N+`U4Dmx$}>HeVJk-QdTG@t!tVT#0(LeV0gvqyyw z2sEp^9eY0N`u10Tm4n8No&A=)IeEC|gnmEXoNSzu!1<4R<%-9kY_8~5Ej?zRegMn78wuMs#;i&eUA0Zk_RXQ3b&TT} z;SCI=7-FUB@*&;8|n>(_g^HGf3@QODE3LpmX~ELnymQm{Sx9xrKS zK29p~?v@R$0=v6Dr5aW>-!{+h@?Q58|Kz8{{W`%J+lDAdb&M5VHrX_mDY;1-JLnf)ezmPau$)1;=`-FU=-r-83tX=C`S#}GZufju zQ>sXNT0Ny=k@nc%cFnvA_i4SC)?_ORXHq8B4D%el1uPX`c~uG#S1M7C+*MMqLw78E zhY2dI8@+N^qrMI1+;TUda(vGqGSRyU{Fnm`aqrr7bz42c5xsOO-~oZpkzorD1g}Y<6rk&3>PsSGy}W?MtqFky@A(X# zIuNZK0cK?^=;PUAu>j0#HtjbHCV*6?jzA&OoE$*Jlga*}LF`SF?WLhv1O|zqC<>*> zYB;#lsYKx0&kH@BFpW8n*yDcc6?;_zaJs<-jPSkCsSX-!aV=P5kUgF@Nu<{a%#K*F z134Q{9|YX7X(v$62_cY3^G%t~rD>Q0z@)1|zs)vjJ6Jq9;7#Ki`w+eS**En?7;n&7 zu==V3T&eFboN3ZiMx3D8qYc;VjFUk_H-WWCau(VFXSQf~viH0L$gwD$UfFHqNcgN`x}M+YQ6RnN<+@t>JUp#)9YOkqst-Ga?{FsDpEeX0(5v{0J~SEbWiL zXC2}M4?UH@u&|;%0y`eb33ldo4~z-x8zY!oVmV=c+f$m?RfDC35mdQ2E>Pze7KWP- z>!Bh<&57I+O_^s}9Tg^k)h7{xx@0a0IA~GAOt2yy!X%Q$1rt~LbTB6@Du!_0%HV>N zlf)QI1&gvERKwso23mJ!Ou6ZS#zCS5W`gxE5T>C#E|{i<1D35C222I33?Njaz`On7 zi<+VWFP6D{e-{yiN#M|Jgk<44u1TiMI78S5W`Sdb5f+{zu34s{CfWN7a3Cf^@L%!& zN$?|!!9j2c)j$~+R6n#891w-z8(!oBpL2K=+%a$r2|~8-(vQj5_XT`<0Ksf;oP+tz z9CObS!0m)Tgg`K#xBM8B(|Z)Wb&DYL{WTYv`;A=q6~Nnx2+!lTIXtj8J7dZE!P_{z z#f8w6F}^!?^KE#+ZDv+xd5O&3EmomZzsv?>E-~ygGum45fk!SBN&|eo1rKw^?aZJ4 E2O(~oYXATM literal 54329 zcmagFV|ZrKvM!pAZQHhO+qP}9lTNj?q^^Y^VFp)SH8qbSJ)2BQ2giqr}t zFG7D6)c?v~^Z#E_K}1nTQbJ9gQ9<%vVRAxVj)8FwL5_iTdUB>&m3fhE=kRWl;g`&m z!W5kh{WsV%fO*%je&j+Lv4xxK~zsEYQls$Q-p&dwID|A)!7uWtJF-=Tm1{V@#x*+kUI$=%KUuf2ka zjiZ{oiL1MXE2EjciJM!jrjFNwCh`~hL>iemrqwqnX?T*MX;U>>8yRcZb{Oy+VKZos zLiFKYPw=LcaaQt8tj=eoo3-@bG_342HQ%?jpgAE?KCLEHC+DmjxAfJ%Og^$dpC8Xw zAcp-)tfJm}BPNq_+6m4gBgBm3+CvmL>4|$2N$^Bz7W(}fz1?U-u;nE`+9`KCLuqg} zwNstNM!J4Uw|78&Y9~9>MLf56to!@qGkJw5Thx%zkzj%Ek9Nn1QA@8NBXbwyWC>9H z#EPwjMNYPigE>*Ofz)HfTF&%PFj$U6mCe-AFw$U%-L?~-+nSXHHKkdgC5KJRTF}`G zE_HNdrE}S0zf4j{r_f-V2imSqW?}3w-4=f@o@-q+cZgaAbZ((hn))@|eWWhcT2pLpTpL!;_5*vM=sRL8 zqU##{U#lJKuyqW^X$ETU5ETeEVzhU|1m1750#f}38_5N9)B_2|v@1hUu=Kt7-@dhA zq_`OMgW01n`%1dB*}C)qxC8q;?zPeF_r;>}%JYmlER_1CUbKa07+=TV45~symC*g8 zW-8(gag#cAOuM0B1xG8eTp5HGVLE}+gYTmK=`XVVV*U!>H`~j4+ROIQ+NkN$LY>h4 zqpwdeE_@AX@PL};e5vTn`Ro(EjHVf$;^oiA%@IBQq>R7_D>m2D4OwwEepkg}R_k*M zM-o;+P27087eb+%*+6vWFCo9UEGw>t&WI17Pe7QVuoAoGHdJ(TEQNlJOqnjZ8adCb zI`}op16D@v7UOEo%8E-~m?c8FL1utPYlg@m$q@q7%mQ4?OK1h%ODjTjFvqd!C z-PI?8qX8{a@6d&Lb_X+hKxCImb*3GFemm?W_du5_&EqRq!+H?5#xiX#w$eLti-?E$;Dhu`{R(o>LzM4CjO>ICf z&DMfES#FW7npnbcuqREgjPQM#gs6h>`av_oEWwOJZ2i2|D|0~pYd#WazE2Bbsa}X@ zu;(9fi~%!VcjK6)?_wMAW-YXJAR{QHxrD5g(ou9mR6LPSA4BRG1QSZT6A?kelP_g- zH(JQjLc!`H4N=oLw=f3{+WmPA*s8QEeEUf6Vg}@!xwnsnR0bl~^2GSa5vb!Yl&4!> zWb|KQUsC$lT=3A|7vM9+d;mq=@L%uWKwXiO9}a~gP4s_4Yohc!fKEgV7WbVo>2ITbE*i`a|V!^p@~^<={#?Gz57 zyPWeM2@p>D*FW#W5Q`1`#5NW62XduP1XNO(bhg&cX`-LYZa|m-**bu|>}S;3)eP8_ zpNTnTfm8 ze+7wDH3KJ95p)5tlwk`S7mbD`SqHnYD*6`;gpp8VdHDz%RR_~I_Ar>5)vE-Pgu7^Y z|9Px+>pi3!DV%E%4N;ii0U3VBd2ZJNUY1YC^-e+{DYq+l@cGtmu(H#Oh%ibUBOd?C z{y5jW3v=0eV0r@qMLgv1JjZC|cZ9l9Q)k1lLgm))UR@#FrJd>w^`+iy$c9F@ic-|q zVHe@S2UAnc5VY_U4253QJxm&Ip!XKP8WNcnx9^cQ;KH6PlW8%pSihSH2(@{2m_o+m zr((MvBja2ctg0d0&U5XTD;5?d?h%JcRJp{_1BQW1xu&BrA3(a4Fh9hon-ly$pyeHq zG&;6q?m%NJ36K1Sq_=fdP(4f{Hop;_G_(i?sPzvB zDM}>*(uOsY0I1j^{$yn3#U(;B*g4cy$-1DTOkh3P!LQ;lJlP%jY8}Nya=h8$XD~%Y zbV&HJ%eCD9nui-0cw!+n`V~p6VCRqh5fRX z8`GbdZ@73r7~myQLBW%db;+BI?c-a>Y)m-FW~M=1^|<21_Sh9RT3iGbO{o-hpN%d6 z7%++#WekoBOP^d0$$|5npPe>u3PLvX_gjH2x(?{&z{jJ2tAOWTznPxv-pAv<*V7r$ z6&glt>7CAClWz6FEi3bToz-soY^{ScrjwVPV51=>n->c(NJngMj6TyHty`bfkF1hc zkJS%A@cL~QV0-aK4>Id!9dh7>0IV;1J9(myDO+gv76L3NLMUm9XyPauvNu$S<)-|F zZS}(kK_WnB)Cl`U?jsdYfAV4nrgzIF@+%1U8$poW&h^c6>kCx3;||fS1_7JvQT~CV zQ8Js+!p)3oW>Df(-}uqC`Tcd%E7GdJ0p}kYj5j8NKMp(KUs9u7?jQ94C)}0rba($~ zqyBx$(1ae^HEDG`Zc@-rXk1cqc7v0wibOR4qpgRDt#>-*8N3P;uKV0CgJE2SP>#8h z=+;i_CGlv+B^+$5a}SicVaSeaNn29K`C&=}`=#Nj&WJP9Xhz4mVa<+yP6hkrq1vo= z1rX4qg8dc4pmEvq%NAkpMK>mf2g?tg_1k2%v}<3`$6~Wlq@ItJ*PhHPoEh1Yi>v57 z4k0JMO)*=S`tKvR5gb-(VTEo>5Y>DZJZzgR+j6{Y`kd|jCVrg!>2hVjz({kZR z`dLlKhoqT!aI8=S+fVp(5*Dn6RrbpyO~0+?fy;bm$0jmTN|t5i6rxqr4=O}dY+ROd zo9Et|x}!u*xi~>-y>!M^+f&jc;IAsGiM_^}+4|pHRn{LThFFpD{bZ|TA*wcGm}XV^ zr*C6~@^5X-*R%FrHIgo-hJTBcyQ|3QEj+cSqp#>&t`ZzB?cXM6S(lRQw$I2?m5=wd z78ki`R?%;o%VUhXH?Z#(uwAn9$m`npJ=cA+lHGk@T7qq_M6Zoy1Lm9E0UUysN)I_x zW__OAqvku^>`J&CB=ie@yNWsaFmem}#L3T(x?a`oZ+$;3O-icj2(5z72Hnj=9Z0w% z<2#q-R=>hig*(t0^v)eGq2DHC%GymE-_j1WwBVGoU=GORGjtaqr0BNigOCqyt;O(S zKG+DoBsZU~okF<7ahjS}bzwXxbAxFfQAk&O@>LsZMsZ`?N?|CDWM(vOm%B3CBPC3o z%2t@%H$fwur}SSnckUm0-k)mOtht`?nwsDz=2#v=RBPGg39i#%odKq{K^;bTD!6A9 zskz$}t)sU^=a#jLZP@I=bPo?f-L}wpMs{Tc!m7-bi!Ldqj3EA~V;4(dltJmTXqH0r z%HAWKGutEc9vOo3P6Q;JdC^YTnby->VZ6&X8f{obffZ??1(cm&L2h7q)*w**+sE6dG*;(H|_Q!WxU{g)CeoT z(KY&bv!Usc|m+Fqfmk;h&RNF|LWuNZ!+DdX*L=s-=_iH=@i` z?Z+Okq^cFO4}_n|G*!)Wl_i%qiMBaH8(WuXtgI7EO=M>=i_+;MDjf3aY~6S9w0K zUuDO7O5Ta6+k40~xh~)D{=L&?Y0?c$s9cw*Ufe18)zzk%#ZY>Tr^|e%8KPb0ht`b( zuP@8#Ox@nQIqz9}AbW0RzE`Cf>39bOWz5N3qzS}ocxI=o$W|(nD~@EhW13Rj5nAp; zu2obEJa=kGC*#3=MkdkWy_%RKcN=?g$7!AZ8vBYKr$ePY(8aIQ&yRPlQ=mudv#q$q z4%WzAx=B{i)UdLFx4os?rZp6poShD7Vc&mSD@RdBJ=_m^&OlkEE1DFU@csgKcBifJ zz4N7+XEJhYzzO=86 z#%eBQZ$Nsf2+X0XPHUNmg#(sNt^NW1Y0|M(${e<0kW6f2q5M!2YE|hSEQ*X-%qo(V zHaFwyGZ0on=I{=fhe<=zo{=Og-_(to3?cvL4m6PymtNsdDINsBh8m>a%!5o3s(en) z=1I z6O+YNertC|OFNqd6P=$gMyvmfa`w~p9*gKDESFqNBy(~Zw3TFDYh}$iudn)9HxPBi zdokK@o~nu?%imcURr5Y~?6oo_JBe}t|pU5qjai|#JDyG=i^V~7+a{dEnO<(y>ahND#_X_fcEBNiZ)uc&%1HVtx8Ts z*H_Btvx^IhkfOB#{szN*n6;y05A>3eARDXslaE>tnLa>+`V&cgho?ED+&vv5KJszf zG4@G;7i;4_bVvZ>!mli3j7~tPgybF5|J6=Lt`u$D%X0l}#iY9nOXH@(%FFJLtzb%p zzHfABnSs;v-9(&nzbZytLiqqDIWzn>JQDk#JULcE5CyPq_m#4QV!}3421haQ+LcfO*>r;rg6K|r#5Sh|y@h1ao%Cl)t*u`4 zMTP!deC?aL7uTxm5^nUv#q2vS-5QbBKP|drbDXS%erB>fYM84Kpk^au99-BQBZR z7CDynflrIAi&ahza+kUryju5LR_}-Z27g)jqOc(!Lx9y)e z{cYc&_r947s9pteaa4}dc|!$$N9+M38sUr7h(%@Ehq`4HJtTpA>B8CLNO__@%(F5d z`SmX5jbux6i#qc}xOhumzbAELh*Mfr2SW99=WNOZRZgoCU4A2|4i|ZVFQt6qEhH#B zK_9G;&h*LO6tB`5dXRSBF0hq0tk{2q__aCKXYkP#9n^)@cq}`&Lo)1KM{W+>5mSed zKp~=}$p7>~nK@va`vN{mYzWN1(tE=u2BZhga5(VtPKk(*TvE&zmn5vSbjo zZLVobTl%;t@6;4SsZ>5+U-XEGUZGG;+~|V(pE&qqrp_f~{_1h@5ZrNETqe{bt9ioZ z#Qn~gWCH!t#Ha^n&fT2?{`}D@s4?9kXj;E;lWV9Zw8_4yM0Qg-6YSsKgvQ*fF{#Pq z{=(nyV>#*`RloBVCs;Lp*R1PBIQOY=EK4CQa*BD0MsYcg=opP?8;xYQDSAJBeJpw5 zPBc_Ft9?;<0?pBhCmOtWU*pN*;CkjJ_}qVic`}V@$TwFi15!mF1*m2wVX+>5p%(+R zQ~JUW*zWkalde{90@2v+oVlkxOZFihE&ZJ){c?hX3L2@R7jk*xjYtHi=}qb+4B(XJ z$gYcNudR~4Kz_WRq8eS((>ALWCO)&R-MXE+YxDn9V#X{_H@j616<|P(8h(7z?q*r+ zmpqR#7+g$cT@e&(%_|ipI&A%9+47%30TLY(yuf&*knx1wNx|%*H^;YB%ftt%5>QM= z^i;*6_KTSRzQm%qz*>cK&EISvF^ovbS4|R%)zKhTH_2K>jP3mBGn5{95&G9^a#4|K zv+!>fIsR8z{^x4)FIr*cYT@Q4Z{y}};rLHL+atCgHbfX*;+k&37DIgENn&=k(*lKD zG;uL-KAdLn*JQ?@r6Q!0V$xXP=J2i~;_+i3|F;_En;oAMG|I-RX#FwnmU&G}w`7R{ z788CrR-g1DW4h_`&$Z`ctN~{A)Hv_-Bl!%+pfif8wN32rMD zJDs$eVWBYQx1&2sCdB0!vU5~uf)=vy*{}t{2VBpcz<+~h0wb7F3?V^44*&83Z2#F` z32!rd4>uc63rQP$3lTH3zb-47IGR}f)8kZ4JvX#toIpXH`L%NnPDE~$QI1)0)|HS4 zVcITo$$oWWwCN@E-5h>N?Hua!N9CYb6f8vTFd>h3q5Jg-lCI6y%vu{Z_Uf z$MU{{^o~;nD_@m2|E{J)q;|BK7rx%`m``+OqZAqAVj-Dy+pD4-S3xK?($>wn5bi90CFAQ+ACd;&m6DQB8_o zjAq^=eUYc1o{#+p+ zn;K<)Pn*4u742P!;H^E3^Qu%2dM{2slouc$AN_3V^M7H_KY3H)#n7qd5_p~Za7zAj|s9{l)RdbV9e||_67`#Tu*c<8!I=zb@ z(MSvQ9;Wrkq6d)!9afh+G`!f$Ip!F<4ADdc*OY-y7BZMsau%y?EN6*hW4mOF%Q~bw z2==Z3^~?q<1GTeS>xGN-?CHZ7a#M4kDL zQxQr~1ZMzCSKFK5+32C%+C1kE#(2L=15AR!er7GKbp?Xd1qkkGipx5Q~FI-6zt< z*PTpeVI)Ngnnyaz5noIIgNZtb4bQdKG{Bs~&tf)?nM$a;7>r36djllw%hQxeCXeW^ z(i6@TEIuxD<2ulwLTt|&gZP%Ei+l!(%p5Yij6U(H#HMkqM8U$@OKB|5@vUiuY^d6X zW}fP3;Kps6051OEO(|JzmVU6SX(8q>*yf*x5QoxDK={PH^F?!VCzES_Qs>()_y|jg6LJlJWp;L zKM*g5DK7>W_*uv}{0WUB0>MHZ#oJZmO!b3MjEc}VhsLD~;E-qNNd?x7Q6~v zR=0$u>Zc2Xr}>x_5$-s#l!oz6I>W?lw;m9Ae{Tf9eMX;TI-Wf_mZ6sVrMnY#F}cDd z%CV*}fDsXUF7Vbw>PuDaGhu631+3|{xp<@Kl|%WxU+vuLlcrklMC!Aq+7n~I3cmQ! z`e3cA!XUEGdEPSu``&lZEKD1IKO(-VGvcnSc153m(i!8ohi`)N2n>U_BemYJ`uY>8B*Epj!oXRLV}XK}>D*^DHQ7?NY*&LJ9VSo`Ogi9J zGa;clWI8vIQqkngv2>xKd91K>?0`Sw;E&TMg&6dcd20|FcTsnUT7Yn{oI5V4@Ow~m zz#k~8TM!A9L7T!|colrC0P2WKZW7PNj_X4MfESbt<-soq*0LzShZ}fyUx!(xIIDwx zRHt^_GAWe0-Vm~bDZ(}XG%E+`XhKpPlMBo*5q_z$BGxYef8O!ToS8aT8pmjbPq)nV z%x*PF5ZuSHRJqJ!`5<4xC*xb2vC?7u1iljB_*iUGl6+yPyjn?F?GOF2_KW&gOkJ?w z3e^qc-te;zez`H$rsUCE0<@7PKGW?7sT1SPYWId|FJ8H`uEdNu4YJjre`8F*D}6Wh z|FQ`xf7yiphHIAkU&OYCn}w^ilY@o4larl?^M7&8YI;hzBIsX|i3UrLsx{QDKwCX< zy;a>yjfJ6!sz`NcVi+a!Fqk^VE^{6G53L?@Tif|j!3QZ0fk9QeUq8CWI;OmO-Hs+F zuZ4sHLA3{}LR2Qlyo+{d@?;`tpp6YB^BMoJt?&MHFY!JQwoa0nTSD+#Ku^4b{5SZVFwU9<~APYbaLO zu~Z)nS#dxI-5lmS-Bnw!(u15by(80LlC@|ynj{TzW)XcspC*}z0~8VRZq>#Z49G`I zgl|C#H&=}n-ajxfo{=pxPV(L*7g}gHET9b*s=cGV7VFa<;Htgjk>KyW@S!|z`lR1( zGSYkEl&@-bZ*d2WQ~hw3NpP=YNHF^XC{TMG$Gn+{b6pZn+5=<()>C!N^jncl0w6BJ zdHdnmSEGK5BlMeZD!v4t5m7ct7{k~$1Ie3GLFoHjAH*b?++s<|=yTF+^I&jT#zuMx z)MLhU+;LFk8bse|_{j+d*a=&cm2}M?*arjBPnfPgLwv)86D$6L zLJ0wPul7IenMvVAK$z^q5<^!)7aI|<&GGEbOr=E;UmGOIa}yO~EIr5xWU_(ol$&fa zR5E(2vB?S3EvJglTXdU#@qfDbCYs#82Yo^aZN6`{Ex#M)easBTe_J8utXu(fY1j|R z9o(sQbj$bKU{IjyhosYahY{63>}$9_+hWxB3j}VQkJ@2$D@vpeRSldU?&7I;qd2MF zSYmJ>zA(@N_iK}m*AMPIJG#Y&1KR)6`LJ83qg~`Do3v^B0>fU&wUx(qefuTgzFED{sJ65!iw{F2}1fQ3= ziFIP{kezQxmlx-!yo+sC4PEtG#K=5VM9YIN0z9~c4XTX?*4e@m;hFM!zVo>A`#566 z>f&3g94lJ{r)QJ5m7Xe3SLau_lOpL;A($wsjHR`;xTXgIiZ#o&vt~ zGR6KdU$FFbLfZCC3AEu$b`tj!9XgOGLSV=QPIYW zjI!hSP#?8pn0@ezuenOzoka8!8~jXTbiJ6+ZuItsWW03uzASFyn*zV2kIgPFR$Yzm zE<$cZlF>R8?Nr2_i?KiripBc+TGgJvG@vRTY2o?(_Di}D30!k&CT`>+7ry2!!iC*X z<@=U0_C#16=PN7bB39w+zPwDOHX}h20Ap);dx}kjXX0-QkRk=cr};GYsjSvyLZa-t zzHONWddi*)RDUH@RTAsGB_#&O+QJaaL+H<<9LLSE+nB@eGF1fALwjVOl8X_sdOYme z0lk!X=S(@25=TZHR7LlPp}fY~yNeThMIjD}pd9+q=j<_inh0$>mIzWVY+Z9p<{D^#0Xk+b_@eNSiR8;KzSZ#7lUsk~NGMcB8C2c=m2l5paHPq`q{S(kdA7Z1a zyfk2Y;w?^t`?@yC5Pz9&pzo}Hc#}mLgDmhKV|PJ3lKOY(Km@Fi2AV~CuET*YfUi}u zfInZnqDX(<#vaS<^fszuR=l)AbqG{}9{rnyx?PbZz3Pyu!eSJK`uwkJU!ORQXy4x83r!PNgOyD33}}L=>xX_93l6njNTuqL8J{l%*3FVn3MG4&Fv*`lBXZ z?=;kn6HTT^#SrPX-N)4EZiIZI!0ByXTWy;;J-Tht{jq1mjh`DSy7yGjHxIaY%*sTx zuy9#9CqE#qi>1misx=KRWm=qx4rk|}vd+LMY3M`ow8)}m$3Ggv&)Ri*ON+}<^P%T5 z_7JPVPfdM=Pv-oH<tecoE}(0O7|YZc*d8`Uv_M*3Rzv7$yZnJE6N_W=AQ3_BgU_TjA_T?a)U1csCmJ&YqMp-lJe`y6>N zt++Bi;ZMOD%%1c&-Q;bKsYg!SmS^#J@8UFY|G3!rtyaTFb!5@e(@l?1t(87ln8rG? z--$1)YC~vWnXiW3GXm`FNSyzu!m$qT=Eldf$sMl#PEfGmzQs^oUd=GIQfj(X=}dw+ zT*oa0*oS%@cLgvB&PKIQ=Ok?>x#c#dC#sQifgMwtAG^l3D9nIg(Zqi;D%807TtUUCL3_;kjyte#cAg?S%e4S2W>9^A(uy8Ss0Tc++ZTjJw1 z&Em2g!3lo@LlDyri(P^I8BPpn$RE7n*q9Q-c^>rfOMM6Pd5671I=ZBjAvpj8oIi$! zl0exNl(>NIiQpX~FRS9UgK|0l#s@#)p4?^?XAz}Gjb1?4Qe4?j&cL$C8u}n)?A@YC zfmbSM`Hl5pQFwv$CQBF=_$Sq zxsV?BHI5bGZTk?B6B&KLdIN-40S426X3j_|ceLla*M3}3gx3(_7MVY1++4mzhH#7# zD>2gTHy*%i$~}mqc#gK83288SKp@y3wz1L_e8fF$Rb}ex+`(h)j}%~Ld^3DUZkgez zOUNy^%>>HHE|-y$V@B}-M|_{h!vXpk01xaD%{l{oQ|~+^>rR*rv9iQen5t?{BHg|% zR`;S|KtUb!X<22RTBA4AAUM6#M?=w5VY-hEV)b`!y1^mPNEoy2K)a>OyA?Q~Q*&(O zRzQI~y_W=IPi?-OJX*&&8dvY0zWM2%yXdFI!D-n@6FsG)pEYdJbuA`g4yy;qrgR?G z8Mj7gv1oiWq)+_$GqqQ$(ZM@#|0j7})=#$S&hZwdoijFI4aCFLVI3tMH5fLreZ;KD zqA`)0l~D2tuIBYOy+LGw&hJ5OyE+@cnZ0L5+;yo2pIMdt@4$r^5Y!x7nHs{@>|W(MzJjATyWGNwZ^4j+EPU0RpAl-oTM@u{lx*i0^yyWPfHt6QwPvYpk9xFMWfBFt!+Gu6TlAmr zeQ#PX71vzN*_-xh&__N`IXv6`>CgV#eA_%e@7wjgkj8jlKzO~Ic6g$cT`^W{R{606 zCDP~+NVZ6DMO$jhL~#+!g*$T!XW63#(ngDn#Qwy71yj^gazS{e;3jGRM0HedGD@pt z?(ln3pCUA(ekqAvvnKy0G@?-|-dh=eS%4Civ&c}s%wF@0K5Bltaq^2Os1n6Z3%?-Q zAlC4goQ&vK6TpgtzkHVt*1!tBYt-`|5HLV1V7*#45Vb+GACuU+QB&hZ=N_flPy0TY zR^HIrdskB#<$aU;HY(K{a3(OQa$0<9qH(oa)lg@Uf>M5g2W0U5 zk!JSlhrw8quBx9A>RJ6}=;W&wt@2E$7J=9SVHsdC?K(L(KACb#z)@C$xXD8^!7|uv zZh$6fkq)aoD}^79VqdJ!Nz-8$IrU(_-&^cHBI;4 z^$B+1aPe|LG)C55LjP;jab{dTf$0~xbXS9!!QdcmDYLbL^jvxu2y*qnx2%jbL%rB z{aP85qBJe#(&O~Prk%IJARcdEypZ)vah%ZZ%;Zk{eW(U)Bx7VlzgOi8)x z`rh4l`@l_Ada7z&yUK>ZF;i6YLGwI*Sg#Fk#Qr0Jg&VLax(nNN$u-XJ5=MsP3|(lEdIOJ7|(x3iY;ea)5#BW*mDV%^=8qOeYO&gIdJVuLLN3cFaN=xZtFB=b zH{l)PZl_j^u+qx@89}gAQW7ofb+k)QwX=aegihossZq*+@PlCpb$rpp>Cbk9UJO<~ zDjlXQ_Ig#W0zdD3&*ei(FwlN#3b%FSR%&M^ywF@Fr>d~do@-kIS$e%wkIVfJ|Ohh=zc zF&Rnic^|>@R%v?@jO}a9;nY3Qrg_!xC=ZWUcYiA5R+|2nsM*$+c$TOs6pm!}Z}dfM zGeBhMGWw3$6KZXav^>YNA=r6Es>p<6HRYcZY)z{>yasbC81A*G-le8~QoV;rtKnkx z;+os8BvEe?0A6W*a#dOudsv3aWs?d% z0oNngyVMjavLjtjiG`!007#?62ClTqqU$@kIY`=x^$2e>iqIy1>o|@Tw@)P)B8_1$r#6>DB_5 zmaOaoE~^9TolgDgooKFuEFB#klSF%9-~d2~_|kQ0Y{Ek=HH5yq9s zDq#1S551c`kSiWPZbweN^A4kWiP#Qg6er1}HcKv{fxb1*BULboD0fwfaNM_<55>qM zETZ8TJDO4V)=aPp_eQjX%||Ud<>wkIzvDlpNjqW>I}W!-j7M^TNe5JIFh#-}zAV!$ICOju8Kx)N z0vLtzDdy*rQN!7r>Xz7rLw8J-(GzQlYYVH$WK#F`i_i^qVlzTNAh>gBWKV@XC$T-` z3|kj#iCquDhiO7NKum07i|<-NuVsX}Q}mIP$jBJDMfUiaWR3c|F_kWBMw0_Sr|6h4 zk`_r5=0&rCR^*tOy$A8K;@|NqwncjZ>Y-75vlpxq%Cl3EgH`}^^~=u zoll6xxY@a>0f%Ddpi;=cY}fyG!K2N-dEyXXmUP5u){4VnyS^T4?pjN@Ot4zjL(Puw z_U#wMH2Z#8Pts{olG5Dy0tZj;N@;fHheu>YKYQU=4Bk|wcD9MbA`3O4bj$hNRHwzb zSLcG0SLV%zywdbuwl(^E_!@&)TdXge4O{MRWk2RKOt@!8E{$BU-AH(@4{gxs=YAz9LIob|Hzto0}9cWoz6Tp2x0&xi#$ zHh$dwO&UCR1Ob2w00-2eG7d4=cN(Y>0R#$q8?||q@iTi+7-w-xR%uMr&StFIthC<# zvK(aPduwuNB}oJUV8+Zl)%cnfsHI%4`;x6XW^UF^e4s3Z@S<&EV8?56Wya;HNs0E> z`$0dgRdiUz9RO9Au3RmYq>K#G=X%*_dUbSJHP`lSfBaN8t-~@F>)BL1RT*9I851A3 z<-+Gb#_QRX>~av#Ni<#zLswtu-c6{jGHR>wflhKLzC4P@b%8&~u)fosoNjk4r#GvC zlU#UU9&0Hv;d%g72Wq?Ym<&&vtA3AB##L}=ZjiTR4hh7J)e>ei} zt*u+>h%MwN`%3}b4wYpV=QwbY!jwfIj#{me)TDOG`?tI!%l=AwL2G@9I~}?_dA5g6 zCKgK(;6Q0&P&K21Tx~k=o6jwV{dI_G+Ba*Zts|Tl6q1zeC?iYJTb{hel*x>^wb|2RkHkU$!+S4OU4ZOKPZjV>9OVsqNnv5jK8TRAE$A&^yRwK zj-MJ3Pl?)KA~fq#*K~W0l4$0=8GRx^9+?w z!QT8*-)w|S^B0)ZeY5gZPI2G(QtQf?DjuK(s^$rMA!C%P22vynZY4SuOE=wX2f8$R z)A}mzJi4WJnZ`!bHG1=$lwaxm!GOnRbR15F$nRC-M*H<*VfF|pQw(;tbSfp({>9^5 zw_M1-SJ9eGF~m(0dvp*P8uaA0Yw+EkP-SWqu zqal$hK8SmM7#Mrs0@OD+%_J%H*bMyZiWAZdsIBj#lkZ!l2c&IpLu(5^T0Ge5PHzR} zn;TXs$+IQ_&;O~u=Jz+XE0wbOy`=6>m9JVG} zJ~Kp1e5m?K3x@@>!D)piw^eMIHjD4RebtR`|IlckplP1;r21wTi8v((KqNqn%2CB< zifaQc&T}*M&0i|LW^LgdjIaX|o~I$`owHolRqeH_CFrqCUCleN130&vH}dK|^kC>) z-r2P~mApHotL4dRX$25lIcRh_*kJaxi^%ZN5-GAAMOxfB!6flLPY-p&QzL9TE%ho( zRwftE3sy5<*^)qYzKkL|rE>n@hyr;xPqncY6QJ8125!MWr`UCWuC~A#G1AqF1@V$kv>@NBvN&2ygy*{QvxolkRRb%Ui zsmKROR%{*g*WjUUod@@cS^4eF^}yQ1>;WlGwOli z+Y$(8I`0(^d|w>{eaf!_BBM;NpCoeem2>J}82*!em=}}ymoXk>QEfJ>G(3LNA2-46 z5PGvjr)Xh9>aSe>vEzM*>xp{tJyZox1ZRl}QjcvX2TEgNc^(_-hir@Es>NySoa1g^ zFow_twnHdx(j?Q_3q51t3XI7YlJ4_q&(0#)&a+RUy{IcBq?)eaWo*=H2UUVIqtp&lW9JTJiP&u zw8+4vo~_IJXZIJb_U^&=GI1nSD%e;P!c{kZALNCm5c%%oF+I3DrA63_@4)(v4(t~JiddILp7jmoy+>cD~ivwoctFfEL zP*#2Rx?_&bCpX26MBgp^4G>@h`Hxc(lnqyj!*t>9sOBcXN(hTwEDpn^X{x!!gPX?1 z*uM$}cYRwHXuf+gYTB}gDTcw{TXSOUU$S?8BeP&sc!Lc{{pEv}x#ELX>6*ipI1#>8 zKes$bHjiJ1OygZge_ak^Hz#k;=od1wZ=o71ba7oClBMq>Uk6hVq|ePPt)@FM5bW$I z;d2Or@wBjbTyZj|;+iHp%Bo!Vy(X3YM-}lasMItEV_QrP-Kk_J4C>)L&I3Xxj=E?| zsAF(IfVQ4w+dRRnJ>)}o^3_012YYgFWE)5TT=l2657*L8_u1KC>Y-R{7w^S&A^X^U}h20jpS zQsdeaA#WIE*<8KG*oXc~$izYilTc#z{5xhpXmdT-YUnGh9v4c#lrHG6X82F2-t35} zB`jo$HjKe~E*W$=g|j&P>70_cI`GnOQ;Jp*JK#CT zuEGCn{8A@bC)~0%wsEv?O^hSZF*iqjO~_h|>xv>PO+?525Nw2472(yqS>(#R)D7O( zg)Zrj9n9$}=~b00=Wjf?E418qP-@8%MQ%PBiCTX=$B)e5cHFDu$LnOeJ~NC;xmOk# z>z&TbsK>Qzk)!88lNI8fOE2$Uxso^j*1fz>6Ot49y@=po)j4hbTIcVR`ePHpuJSfp zxaD^Dn3X}Na3@<_Pc>a;-|^Pon(>|ytG_+U^8j_JxP=_d>L$Hj?|0lz>_qQ#a|$+( z(x=Lipuc8p4^}1EQhI|TubffZvB~lu$zz9ao%T?%ZLyV5S9}cLeT?c} z>yCN9<04NRi~1oR)CiBakoNhY9BPnv)kw%*iv8vdr&&VgLGIs(-FbJ?d_gfbL2={- zBk4lkdPk~7+jIxd4{M(-W1AC_WcN&Oza@jZoj zaE*9Y;g83#m(OhA!w~LNfUJNUuRz*H-=$s*z+q+;snKPRm9EptejugC-@7-a-}Tz0 z@KHra#Y@OXK+KsaSN9WiGf?&jlZ!V7L||%KHP;SLksMFfjkeIMf<1e~t?!G3{n)H8 zQAlFY#QwfKuj;l@<$YDATAk;%PtD%B(0<|8>rXU< zJ66rkAVW_~Dj!7JGdGGi4NFuE?7ZafdMxIh65Sz7yQoA7fBZCE@WwysB=+`kT^LFX zz8#FlSA5)6FG9(qL3~A24mpzL@@2D#>0J7mMS1T*9UJ zvOq!!a(%IYY69+h45CE?(&v9H4FCr>gK0>mK~F}5RdOuH2{4|}k@5XpsX7+LZo^Qa4sH5`eUj>iffoBVm+ zz4Mtf`h?NW$*q1yr|}E&eNl)J``SZvTf6Qr*&S%tVv_OBpbjnA0&Vz#(;QmGiq-k! zgS0br4I&+^2mgA15*~Cd00cXLYOLA#Ep}_)eED>m+K@JTPr_|lSN}(OzFXQSBc6fM z@f-%2;1@BzhZa*LFV z-LrLmkmB%<<&jEURBEW>soaZ*rSIJNwaV%-RSaCZi4X)qYy^PxZ=oL?6N-5OGOMD2 z;q_JK?zkwQ@b3~ln&sDtT5SpW9a0q+5Gm|fpVY2|zqlNYBR}E5+ahgdj!CvK$Tlk0 z9g$5N;aar=CqMsudQV>yb4l@hN(9Jcc=1(|OHsqH6|g=K-WBd8GxZ`AkT?OO z-z_Ued-??Z*R4~L7jwJ%-`s~FK|qNAJ;EmIVDVpk{Lr7T4l{}vL)|GuUuswe9c5F| zv*5%u01hlv08?00Vpwyk*Q&&fY8k6MjOfpZfKa@F-^6d=Zv|0@&4_544RP5(s|4VPVP-f>%u(J@23BHqo2=zJ#v9g=F!cP((h zpt0|(s++ej?|$;2PE%+kc6JMmJjDW)3BXvBK!h!E`8Y&*7hS{c_Z?4SFP&Y<3evqf z9-ke+bSj$%Pk{CJlJbWwlBg^mEC^@%Ou?o>*|O)rl&`KIbHrjcpqsc$Zqt0^^F-gU2O=BusO+(Op}!jNzLMc zT;0YT%$@ClS%V+6lMTfhuzzxomoat=1H?1$5Ei7&M|gxo`~{UiV5w64Np6xV zVK^nL$)#^tjhCpTQMspXI({TW^U5h&Wi1Jl8g?P1YCV4=%ZYyjSo#5$SX&`r&1PyC zzc;uzCd)VTIih|8eNqFNeBMe#j_FS6rq81b>5?aXg+E#&$m++Gz9<+2)h=K(xtn}F ziV{rmu+Y>A)qvF}ms}4X^Isy!M&1%$E!rTO~5(p+8{U6#hWu>(Ll1}eD64Xa>~73A*538wry?v$vW z>^O#FRdbj(k0Nr&)U`Tl(4PI*%IV~;ZcI2z&rmq=(k^}zGOYZF3b2~Klpzd2eZJl> zB=MOLwI1{$RxQ7Y4e30&yOx?BvAvDkTBvWPpl4V8B7o>4SJn*+h1Ms&fHso%XLN5j z-zEwT%dTefp~)J_C8;Q6i$t!dnlh-!%haR1X_NuYUuP-)`IGWjwzAvp!9@h`kPZhf zwLwFk{m3arCdx8rD~K2`42mIN4}m%OQ|f)4kf%pL?Af5Ul<3M2fv>;nlhEPR8b)u} zIV*2-wyyD%%) zl$G@KrC#cUwoL?YdQyf9WH)@gWB{jd5w4evI& zOFF)p_D8>;3-N1z6mES!OPe>B^<;9xsh)){Cw$Vs-ez5nXS95NOr3s$IU;>VZSzKn zBvub8_J~I%(DozZW@{)Vp37-zevxMRZ8$8iRfwHmYvyjOxIOAF2FUngKj289!(uxY zaClWm!%x&teKmr^ABrvZ(ikx{{I-lEzw5&4t3P0eX%M~>$wG0ZjA4Mb&op+0$#SO_ z--R`>X!aqFu^F|a!{Up-iF(K+alKB{MNMs>e(i@Tpy+7Z-dK%IEjQFO(G+2mOb@BO zP>WHlS#fSQm0et)bG8^ZDScGnh-qRKIFz zfUdnk=m){ej0i(VBd@RLtRq3Ep=>&2zZ2%&vvf?Iex01hx1X!8U+?>ER;yJlR-2q4 z;Y@hzhEC=d+Le%=esE>OQ!Q|E%6yG3V_2*uh&_nguPcZ{q?DNq8h_2ahaP6=pP-+x zK!(ve(yfoYC+n(_+chiJ6N(ZaN+XSZ{|H{TR1J_s8x4jpis-Z-rlRvRK#U%SMJ(`C z?T2 zF(NNfO_&W%2roEC2j#v*(nRgl1X)V-USp-H|CwFNs?n@&vpRcj@W@xCJwR6@T!jt377?XjZ06=`d*MFyTdyvW!`mQm~t3luzYzvh^F zM|V}rO>IlBjZc}9Z zd$&!tthvr>5)m;5;96LWiAV0?t)7suqdh0cZis`^Pyg@?t>Ms~7{nCU;z`Xl+raSr zXpp=W1oHB*98s!Tpw=R5C)O{{Inl>9l7M*kq%#w9a$6N~v?BY2GKOVRkXYCgg*d

<5G2M1WZP5 zzqSuO91lJod(SBDDw<*sX(+F6Uq~YAeYV#2A;XQu_p=N5X+#cmu19Qk>QAnV=k!?wbk5I;tDWgFc}0NkvC*G=V+Yh1cyeJVq~9czZiDXe+S=VfL2g`LWo8om z$Y~FQc6MFjV-t1Y`^D9XMwY*U_re2R?&(O~68T&D4S{X`6JYU-pz=}ew-)V0AOUT1 zVOkHAB-8uBcRjLvz<9HS#a@X*Kc@|W)nyiSgi|u5$Md|P()%2(?olGg@ypoJwp6>m z*dnfjjWC>?_1p;%1brqZyDRR;8EntVA92EJ3ByOxj6a+bhPl z;a?m4rQAV1@QU^#M1HX)0+}A<7TCO`ZR_RzF}X9-M>cRLyN4C+lCk2)kT^3gN^`IT zNP~fAm(wyIoR+l^lQDA(e1Yv}&$I!n?&*p6?lZcQ+vGLLd~fM)qt}wsbf3r=tmVYe zl)ntf#E!P7wlakP9MXS7m0nsAmqxZ*)#j;M&0De`oNmFgi$ov#!`6^4)iQyxg5Iuj zjLAhzQ)r`^hf7`*1`Rh`X;LVBtDSz@0T?kkT1o!ijeyTGt5vc^Cd*tmNgiNo^EaWvaC8$e+nb_{W01j3%=1Y&92YacjCi>eNbwk%-gPQ@H-+4xskQ}f_c=jg^S-# zYFBDf)2?@5cy@^@FHK5$YdAK9cI;!?Jgd}25lOW%xbCJ>By3=HiK@1EM+I46A)Lsd zeT|ZH;KlCml=@;5+hfYf>QNOr^XNH%J-lvev)$Omy8MZ`!{`j>(J5cG&ZXXgv)TaF zg;cz99i$4CX_@3MIb?GL0s*8J=3`#P(jXF(_(6DXZjc@(@h&=M&JG)9&Te1?(^XMW zjjC_70|b=9hB6pKQi`S^Ls7JyJw^@P>Ko^&q8F&?>6i;#CbxUiLz1ZH4lNyd@QACd zu>{!sqjB!2Dg}pbAXD>d!3jW}=5aN0b;rw*W>*PAxm7D)aw(c*RX2@bTGEI|RRp}vw7;NR2wa;rXN{L{Q#=Fa z$x@ms6pqb>!8AuV(prv>|aU8oWV={C&$c zMa=p=CDNOC2tISZcd8~18GN5oTbKY+Vrq;3_obJlfSKRMk;Hdp1`y`&LNSOqeauR_ z^j*Ojl3Ohzb5-a49A8s|UnM*NM8tg}BJXdci5%h&;$afbmRpN0&~9rCnBA`#lG!p zc{(9Y?A0Y9yo?wSYn>iigf~KP$0*@bGZ>*YM4&D;@{<%Gg5^uUJGRrV4 z(aZOGB&{_0f*O=Oi0k{@8vN^BU>s3jJRS&CJOl3o|BE{FAA&a#2YYiX3pZz@|Go-F z|Fly;7eX2OTs>R}<`4RwpHFs9nwh)B28*o5qK1Ge=_^w0m`uJOv!=&!tzt#Save(C zgKU=Bsgql|`ui(e1KVxR`?>Dx>(rD1$iWp&m`v)3A!j5(6vBm*z|aKm*T*)mo(W;R zNGo2`KM!^SS7+*9YxTm6YMm_oSrLceqN*nDOAtagULuZl5Q<7mOnB@Hq&P|#9y{5B z!2x+2s<%Cv2Aa0+u{bjZXS);#IFPk(Ph-K7K?3i|4ro> zRbqJoiOEYo(Im^((r}U4b8nvo_>4<`)ut`24?ILnglT;Pd&U}$lV3U$F9#PD(O=yV zgNNA=GW|(E=&m_1;uaNmipQe?pon4{T=zK!N!2_CJL0E*R^XXIKf*wi!>@l}3_P9Z zF~JyMbW!+n-+>!u=A1ESxzkJy$DRuG+$oioG7(@Et|xVbJ#BCt;J43Nvj@MKvTxzy zMmjNuc#LXBxFAwIGZJk~^!q$*`FME}yKE8d1f5Mp}KHNq(@=Z8YxV}0@;YS~|SpGg$_jG7>_8WWYcVx#4SxpzlV9N4aO>K{c z$P?a_fyDzGX$Of3@ykvedGd<@-R;M^Shlj*SswJLD+j@hi_&_>6WZ}#AYLR0iWMK|A zH_NBeu(tMyG=6VO-=Pb>-Q#$F*or}KmEGg*-n?vWQREURdB#+6AvOj*I%!R-4E_2$ zU5n9m>RWs|Wr;h2DaO&mFBdDb-Z{APGQx$(L`if?C|njd*fC=rTS%{o69U|meRvu?N;Z|Y zbT|ojL>j;q*?xXmnHH#3R4O-59NV1j=uapkK7}6@Wo*^Nd#(;$iuGsb;H315xh3pl zHaJ>h-_$hdNl{+|Zb%DZH%ES;*P*v0#}g|vrKm9;j-9e1M4qX@zkl&5OiwnCz=tb6 zz<6HXD+rGIVpGtkb{Q^LIgExOm zz?I|oO9)!BOLW#krLmWvX5(k!h{i>ots*EhpvAE;06K|u_c~y{#b|UxQ*O@Ks=bca z^_F0a@61j3I(Ziv{xLb8AXQj3;R{f_l6a#H5ukg5rxwF9A$?Qp-Mo54`N-SKc}fWp z0T)-L@V$$&my;l#Ha{O@!fK4-FSA)L&3<${Hcwa7ue`=f&YsXY(NgeDU#sRlT3+9J z6;(^(sjSK@3?oMo$%L-nqy*E;3pb0nZLx6 z;h5)T$y8GXK1DS-F@bGun8|J(v-9o=42&nLJy#}M5D0T^5VWBNn$RpC zZzG6Bt66VY4_?W=PX$DMpKAI!d`INr) zkMB{XPQ<52rvWVQqgI0OL_NWxoe`xxw&X8yVftdODPj5|t}S6*VMqN$-h9)1MBe0N zYq?g0+e8fJCoAksr0af1)FYtz?Me!Cxn`gUx&|T;)695GG6HF7!Kg1zzRf_{VWv^bo81v4$?F6u2g|wxHc6eJQAg&V z#%0DnWm2Rmu71rPJ8#xFUNFC*V{+N_qqFH@gYRLZ6C?GAcVRi>^n3zQxORPG)$-B~ z%_oB?-%Zf7d*Fe;cf%tQwcGv2S?rD$Z&>QC2X^vwYjnr5pa5u#38cHCt4G3|efuci z@3z=#A13`+ztmp;%zjXwPY_aq-;isu*hecWWX_=Z8paSqq7;XYnUjK*T>c4~PR4W7 z#C*%_H&tfGx`Y$w7`dXvVhmovDnT>btmy~SLf>>~84jkoQ%cv=MMb+a{JV&t0+1`I z32g_Y@yDhKe|K^PevP~MiiVl{Ou7^Mt9{lOnXEQ`xY^6L8D$705GON{!1?1&YJEl#fTf5Z)da=yiEQ zGgtC-soFGOEBEB~ZF_{7b(76En>d}mI~XIwNw{e>=Fv)sgcw@qOsykWr?+qAOZSVrQfg}TNI ztKNG)1SRrAt6#Q?(me%)>&A_^DM`pL>J{2xu>xa$3d@90xR61TQDl@fu%_85DuUUA za9tn64?At;{`BAW6oykwntxHeDpXsV#{tmt5RqdN7LtcF4vR~_kZNT|wqyR#z^Xcd zFdymVRZvyLfTpBT>w9<)Ozv@;Yk@dOSVWbbtm^y@@C>?flP^EgQPAwsy75bveo=}T zFxl(f)s)j(0#N_>Or(xEuV(n$M+`#;Pc$1@OjXEJZumkaekVqgP_i}p`oTx;terTx zZpT+0dpUya2hqlf`SpXN{}>PfhajNk_J0`H|2<5E;U5Vh4F8er z;RxLSFgpGhkU>W?IwdW~NZTyOBrQ84H7_?gviIf71l`EETodG9a1!8e{jW?DpwjL? zGEM&eCzwoZt^P*8KHZ$B<%{I}>46IT%jJ3AnnB5P%D2E2Z_ z1M!vr#8r}1|KTqWA4%67ZdbMW2YJ81b(KF&SQ2L1Qn(y-=J${p?xLMx3W7*MK;LFQ z6Z`aU;;mTL4XrrE;HY*Rkh6N%?qviUGNAKiCB~!P}Z->IpO6E(gGd7I#eDuT7j|?nZ zK}I(EJ>$Kb&@338M~O+em9(L!+=0zBR;JAQesx|3?Ok90)D1aS9P?yTh6Poh8Cr4X zk3zc=f2rE7jj+aP7nUsr@~?^EGP>Q>h#NHS?F{Cn`g-gD<8F&dqOh-0sa%pfL`b+1 zUsF*4a~)KGb4te&K0}bE>z3yb8% zibb5Q%Sfiv7feb1r0tfmiMv z@^4XYwg@KZI=;`wC)`1jUA9Kv{HKe2t$WmRcR4y8)VAFjRi zaz&O7Y2tDmc5+SX(bj6yGHYk$dBkWc96u3u&F)2yEE~*i0F%t9Kg^L6MJSb&?wrXi zGSc;_rln$!^ybwYBeacEFRsVGq-&4uC{F)*Y;<0y7~USXswMo>j4?~5%Zm!m@i@-> zXzi82sa-vpU{6MFRktJy+E0j#w`f`>Lbog{zP|9~hg(r{RCa!uGe>Yl536cn$;ouH za#@8XMvS-kddc1`!1LVq;h57~zV`7IYR}pp3u!JtE6Q67 zq3H9ZUcWPm2V4IukS}MCHSdF0qg2@~ufNx9+VMjQP&exiG_u9TZAeAEj*jw($G)zL zq9%#v{wVyOAC4A~AF=dPX|M}MZV)s(qI9@aIK?Pe+~ch|>QYb+78lDF*Nxz2-vpRbtQ*F4$0fDbvNM#CCatgQ@z1+EZWrt z2dZfywXkiW=no5jus-92>gXn5rFQ-COvKyegmL=4+NPzw6o@a?wGE-1Bt;pCHe;34K%Z z-FnOb%!nH;)gX+!a3nCk?5(f1HaWZBMmmC@lc({dUah+E;NOros{?ui1zPC-Q0);w zEbJmdE$oU$AVGQPdm{?xxI_0CKNG$LbY*i?YRQ$(&;NiA#h@DCxC(U@AJ$Yt}}^xt-EC_ z4!;QlLkjvSOhdx!bR~W|Ezmuf6A#@T`2tsjkr>TvW*lFCMY>Na_v8+{Y|=MCu1P8y z89vPiH5+CKcG-5lzk0oY>~aJC_0+4rS@c@ZVKLAp`G-sJB$$)^4*A!B zmcf}lIw|VxV9NSoJ8Ag3CwN&d7`|@>&B|l9G8tXT^BDHOUPrtC70NgwN4${$k~d_4 zJ@eo6%YQnOgq$th?0{h`KnqYa$Nz@vlHw<%!C5du6<*j1nwquk=uY}B8r7f|lY+v7 zm|JU$US08ugor8E$h3wH$c&i~;guC|3-tqJy#T;v(g( zBZtPMSyv%jzf->435yM(-UfyHq_D=6;ouL4!ZoD+xI5uCM5ay2m)RPmm$I}h>()hS zO!0gzMxc`BPkUZ)WXaXam%1;)gedA7SM8~8yIy@6TPg!hR0=T>4$Zxd)j&P-pXeSF z9W`lg6@~YDhd19B9ETv(%er^Xp8Yj@AuFVR_8t*KS;6VHkEDKI#!@l!l3v6`W1`1~ zP{C@keuV4Q`Rjc08lx?zmT$e$!3esc9&$XZf4nRL(Z*@keUbk!GZi(2Bmyq*saOD? z3Q$V<*P-X1p2}aQmuMw9nSMbOzuASsxten7DKd6A@ftZ=NhJ(0IM|Jr<91uAul4JR zADqY^AOVT3a(NIxg|U;fyc#ZnSzw2cr}#a5lZ38>nP{05D)7~ad7JPhw!LqOwATXtRhK!w0X4HgS1i<%AxbFmGJx9?sEURV+S{k~g zGYF$IWSlQonq6}e;B(X(sIH|;52+(LYW}v_gBcp|x%rEAVB`5LXg_d5{Q5tMDu0_2 z|LOm$@K2?lrLNF=mr%YP|U-t)~9bqd+wHb4KuPmNK<}PK6e@aosGZK57=Zt+kcszVOSbe;`E^dN! ze7`ha3WUUU7(nS0{?@!}{0+-VO4A{7+nL~UOPW9_P(6^GL0h${SLtqG!} zKl~Ng5#@Sy?65wk9z*3SA`Dpd4b4T^@C8Fhd8O)k_4%0RZL5?#b~jmgU+0|DB%0Z) zql-cPC>A9HPjdOTpPC` zQwvF}uB5kG$Xr4XnaH#ruSjM*xG?_hT7y3G+8Ox`flzU^QIgb_>2&-f+XB6MDr-na zSi#S+c!ToK84<&m6sCiGTd^8pNdXo+$3^l3FL_E`0 z>8it5YIDxtTp2Tm(?}FX^w{fbfgh7>^8mtvN>9fWgFN_*a1P`Gz*dyOZF{OV7BC#j zQV=FQM5m>47xXgapI$WbPM5V`V<7J9tD)oz@d~MDoM`R^Y6-Na(lO~uvZlpu?;zw6 zVO1faor3dg#JEb5Q*gz4<W8tgC3nE2BG2jeIQs1)<{In&7hJ39x=;ih;CJDy)>0S1at*7n?Wr0ahYCpFjZ|@u91Zl7( zv;CSBRC65-6f+*JPf4p1UZ)k=XivKTX6_bWT~7V#rq0Xjas6hMO!HJN8GdpBKg_$B zwDHJF6;z?h<;GXFZan8W{XFNPpOj!(&I1`&kWO86p?Xz`a$`7qV7Xqev|7nn_lQuX ziGpU1MMYt&5dE2A62iX3;*0WzNB9*nSTzI%62A+N?f?;S>N@8M=|ef3gtQTIA*=yq zQAAjOqa!CkHOQo4?TsqrrsJLclXcP?dlAVv?v`}YUjo1Htt;6djP@NPFH+&p1I+f_ z)Y279{7OWomY8baT(4TAOlz1OyD{4P?(DGv3XyJTA2IXe=kqD)^h(@*E3{I~w;ws8 z)ZWv7E)pbEM zd3MOXRH3mQhks9 zv6{s;k0y5vrcjXaVfw8^>YyPo=oIqd5IGI{)+TZq5Z5O&hXAw%ZlL}^6FugH;-%vP zAaKFtt3i^ag226=f0YjzdPn6|4(C2sC5wHFX{7QF!tG1E-JFA`>eZ`}$ymcRJK?0c zN363o{&ir)QySOFY0vcu6)kX#;l??|7o{HBDVJN+17rt|w3;(C_1b>d;g9Gp=8YVl zYTtA52@!7AUEkTm@P&h#eg+F*lR zQ7iotZTcMR1frJ0*V@Hw__~CL>_~2H2cCtuzYIUD24=Cv!1j6s{QS!v=PzwQ(a0HS zBKx04KA}-Ue+%9d`?PG*hIij@54RDSQpA7|>qYVIrK_G6%6;#ZkR}NjUgmGju)2F`>|WJoljo)DJgZr4eo1k1i1+o z1D{>^RlpIY8OUaOEf5EBu%a&~c5aWnqM zxBpJq98f=%M^{4mm~5`CWl%)nFR64U{(chmST&2jp+-r z3675V<;Qi-kJud%oWnCLdaU-)xTnMM%rx%Jw6v@=J|Ir=4n-1Z23r-EVf91CGMGNz zb~wyv4V{H-hkr3j3WbGnComiqmS0vn?n?5v2`Vi>{Ip3OZUEPN7N8XeUtF)Ry6>y> zvn0BTLCiqGroFu|m2zG-;Xb6;W`UyLw)@v}H&(M}XCEVXZQoWF=Ykr5lX3XWwyNyF z#jHv)A*L~2BZ4lX?AlN3X#axMwOC)PoVy^6lCGse9bkGjb=qz%kDa6}MOmSwK`cVO zt(e*MW-x}XtU?GY5}9{MKhRhYOlLhJE5=ca+-RmO04^ z66z{40J=s=ey9OCdc(RCzy zd7Zr1%!y3}MG(D=wM_ebhXnJ@MLi7cImDkhm0y{d-Vm81j`0mbi4lF=eirlr)oW~a zCd?26&j^m4AeXEsIUXiTal)+SPM4)HX%%YWF1?(FV47BaA`h9m67S9x>hWMVHx~Hg z1meUYoLL(p@b3?x|9DgWeI|AJ`Ia84*P{Mb%H$ZRROouR4wZhOPX15=KiBMHl!^JnCt$Az`KiH^_d>cev&f zaG2>cWf$=A@&GP~DubsgYb|L~o)cn5h%2`i^!2)bzOTw2UR!>q5^r&2Vy}JaWFUQE04v>2;Z@ZPwXr?y&G(B^@&y zsd6kC=hHdKV>!NDLIj+3rgZJ|dF`%N$DNd;B)9BbiT9Ju^Wt%%u}SvfM^=|q-nxDG zuWCQG9e#~Q5cyf8@y76#kkR^}{c<_KnZ0QsZcAT|YLRo~&tU|N@BjxOuy`#>`X~Q< z?R?-Gsk$$!oo(BveQLlUrcL#eirhgBLh`qHEMg`+sR1`A=1QX7)ZLMRT+GBy?&mM8 zQG^z-!Oa&J-k7I(3_2#Q6Bg=NX<|@X&+YMIOzfEO2$6Mnh}YV!m!e^__{W@-CTprr zbdh3f=BeCD$gHwCrmwgM3LAv3!Mh$wM)~KWzp^w)Cu6roO7uUG5z*}i0_0j47}pK; ztN530`ScGatLOL06~zO)Qmuv`h!gq5l#wx(EliKe&rz-5qH(hb1*fB#B+q`9=jLp@ zOa2)>JTl7ovxMbrif`Xe9;+fqB1K#l=Dv!iT;xF zdkCvS>C5q|O;}ns3AgoE({Ua-zNT-9_5|P0iANmC6O76Sq_(AN?UeEQJ>#b54fi3k zFmh+P%b1x3^)0M;QxXLP!BZ^h|AhOde*{9A=f3|Xq*JAs^Y{eViF|=EBfS6L%k4ip zk+7M$gEKI3?bQg?H3zaE@;cyv9kv;cqK$VxQbFEsy^iM{XXW0@2|DOu$!-k zSFl}Y=jt-VaT>Cx*KQnHTyXt}f9XswFB9ibYh+k2J!ofO+nD?1iw@mwtrqI4_i?nE zhLkPp41ED62me}J<`3RN80#vjW;wt`pP?%oQ!oqy7`miL>d-35a=qotK$p{IzeSk# ze_$CFYp_zIkrPFVaW^s#U4xT1lI^A0IBe~Y<4uS%zSV=wcuLr%gQT=&5$&K*bwqx| zWzCMiz>7t^Et@9CRUm9E+@hy~sBpm9fri$sE1zgLU((1?Yg{N1Sars=DiW&~Zw=3I zi7y)&oTC?UWD2w97xQ&5vx zRXEBGeJ(I?Y}eR0_O{$~)bMJRTsNUPIfR!xU9PE7A>AMNr_wbrFK>&vVw=Y;RH zO$mlpmMsQ}-FQ2cSj7s7GpC+~^Q~dC?y>M}%!-3kq(F3hGWo9B-Gn02AwUgJ>Z-pKOaj zysJBQx{1>Va=*e@sLb2z&RmQ7ira;aBijM-xQ&cpR>X3wP^foXM~u1>sv9xOjzZpX z0K;EGouSYD~oQ&lAafj3~EaXfFShC+>VsRlEMa9cg9i zFxhCKO}K0ax6g4@DEA?dg{mo>s+~RPI^ybb^u--^nTF>**0l5R9pocwB?_K)BG_)S zyLb&k%XZhBVr7U$wlhMqwL)_r&&n%*N$}~qijbkfM|dIWP{MyLx}X&}ES?}7i;9bW zmTVK@zR)7kE2+L42Q`n4m0VVg5l5(W`SC9HsfrLZ=v%lpef=Gj)W59VTLe+Z$8T8i z4V%5+T0t8LnM&H>Rsm5C%qpWBFqgTwL{=_4mE{S3EnBXknM&u8n}A^IIM4$s3m(Rd z>zq=CP-!9p9es2C*)_hoL@tDYABn+o#*l;6@7;knWIyDrt5EuakO99S$}n((Fj4y} zD!VvuRzghcE{!s;jC*<_H$y6!6QpePo2A3ZbX*ZzRnQq*b%KK^NF^z96CHaWmzU@f z#j;y?X=UP&+YS3kZx7;{ zDA{9(wfz7GF`1A6iB6fnXu0?&d|^p|6)%3$aG0Uor~8o? z*e}u#qz7Ri?8Uxp4m_u{a@%bztvz-BzewR6bh*1Xp+G=tQGpcy|4V_&*aOqu|32CM zz3r*E8o8SNea2hYJpLQ-_}R&M9^%@AMx&`1H8aDx4j%-gE+baf2+9zI*+Pmt+v{39 zDZ3Ix_vPYSc;Y;yn68kW4CG>PE5RoaV0n@#eVmk?p$u&Fy&KDTy!f^Hy6&^-H*)#u zdrSCTJPJw?(hLf56%2;_3n|ujUSJOU8VPOTlDULwt0jS@j^t1WS z!n7dZIoT+|O9hFUUMbID4Ec$!cc($DuQWkocVRcYSikFeM&RZ=?BW)mG4?fh#)KVG zcJ!<=-8{&MdE)+}?C8s{k@l49I|Zwswy^ZN3;E!FKyglY~Aq?4m74P-0)sMTGXqd5(S<-(DjjM z&7dL-Mr8jhUCAG$5^mI<|%`;JI5FVUnNj!VO2?Jiqa|c2;4^n!R z`5KK0hyB*F4w%cJ@Un6GC{mY&r%g`OX|1w2$B7wxu97%<@~9>NlXYd9RMF2UM>(z0 zouu4*+u+1*k;+nFPk%ly!nuMBgH4sL5Z`@Rok&?Ef=JrTmvBAS1h?C0)ty5+yEFRz zY$G=coQtNmT@1O5uk#_MQM1&bPPnspy5#>=_7%WcEL*n$;sSAZcXxMpcXxLe;_mLA z5F_paad+bGZV*oh@8h0(|D2P!q# zTHjmiphJ=AazSeKQPkGOR-D8``LjzToyx{lfK-1CDD6M7?pMZOdLKFtjZaZMPk4}k zW)97Fh(Z+_Fqv(Q_CMH-YYi?fR5fBnz7KOt0*t^cxmDoIokc=+`o# zrud|^h_?KW=Gv%byo~(Ln@({?3gnd?DUf-j2J}|$Mk>mOB+1{ZQ8HgY#SA8END(Zw z3T+W)a&;OO54~m}ffemh^oZ!Vv;!O&yhL0~hs(p^(Yv=(3c+PzPXlS5W79Er8B1o* z`c`NyS{Zj_mKChj+q=w)B}K za*zzPhs?c^`EQ;keH{-OXdXJet1EsQ)7;{3eF!-t^4_Srg4(Ot7M*E~91gwnfhqaM zNR7dFaWm7MlDYWS*m}CH${o?+YgHiPC|4?X?`vV+ws&Hf1ZO-w@OGG^o4|`b{bLZj z&9l=aA-Y(L11!EvRjc3Zpxk7lc@yH1e$a}8$_-r$)5++`_eUr1+dTb@ zU~2P1HM#W8qiNN3b*=f+FfG1!rFxnNlGx{15}BTIHgxO>Cq4 z;#9H9YjH%>Z2frJDJ8=xq>Z@H%GxXosS@Z>cY9ppF+)e~t_hWXYlrO6)0p7NBMa`+ z^L>-#GTh;k_XnE)Cgy|0Dw;(c0* zSzW14ZXozu)|I@5mRFF1eO%JM=f~R1dkNpZM+Jh(?&Zje3NgM{2ezg1N`AQg5%+3Y z64PZ0rPq6;_)Pj-hyIOgH_Gh`1$j1!jhml7ksHA1`CH3FDKiHLz+~=^u@kUM{ilI5 z^FPiJ7mSrzBs9{HXi2{sFhl5AyqwUnU{sPcUD{3+l-ZHAQ)C;c$=g1bdoxeG(5N01 zZy=t8i{*w9m?Y>V;uE&Uy~iY{pY4AV3_N;RL_jT_QtLFx^KjcUy~q9KcLE3$QJ{!)@$@En{UGG7&}lc*5Kuc^780;7Bj;)X?1CSy*^^ zPP^M)Pr5R>mvp3_hmCtS?5;W^e@5BjE>Cs<`lHDxj<|gtOK4De?Sf0YuK5GX9G93i zMYB{8X|hw|T6HqCf7Cv&r8A$S@AcgG1cF&iJ5=%+x;3yB`!lQ}2Hr(DE8=LuNb~Vs z=FO&2pdc16nD$1QL7j+!U^XWTI?2qQKt3H8=beVTdHHa9=MiJ&tM1RRQ-=+vy!~iz zj3O{pyRhCQ+b(>jC*H)J)%Wq}p>;?@W*Eut@P&?VU+Sdw^4kE8lvX|6czf{l*~L;J zFm*V~UC;3oQY(ytD|D*%*uVrBB}BbAfjK&%S;z;7$w68(8PV_whC~yvkZmX)xD^s6 z{$1Q}q;99W?*YkD2*;)tRCS{q2s@JzlO~<8x9}X<0?hCD5vpydvOw#Z$2;$@cZkYrp83J0PsS~!CFtY%BP=yxG?<@#{7%2sy zOc&^FJxsUYN36kSY)d7W=*1-{7ghPAQAXwT7z+NlESlkUH&8ODlpc8iC*iQ^MAe(B z?*xO4i{zFz^G=^G#9MsLKIN64rRJykiuIVX5~0#vAyDWc9-=6BDNT_aggS2G{B>dD ze-B%d3b6iCfc5{@yz$>=@1kdK^tX9qh0=ocv@9$ai``a_ofxT=>X7_Y0`X}a^M?d# z%EG)4@`^Ej_=%0_J-{ga!gFtji_byY&Vk@T1c|ucNAr(JNr@)nCWj?QnCyvXg&?FW;S-VOmNL6^km_dqiVjJuIASVGSFEos@EVF7St$WE&Z%)`Q##+0 zjaZ=JI1G@0!?l|^+-ZrNd$WrHBi)DA0-Eke>dp=_XpV<%CO_Wf5kQx}5e<90dt>8k zAi00d0rQ821nA>B4JHN7U8Zz=0;9&U6LOTKOaC1FC8GgO&kc=_wHIOGycL@c*$`ce703t%>S}mvxEnD-V!;6c`2(p74V7D0No1Xxt`urE66$0(ThaAZ1YVG#QP$ zy~NN%kB*zhZ2Y!kjn826pw4bh)75*e!dse+2Db(;bN34Uq7bLpr47XTX{8UEeC?2i z*{$`3dP}32${8pF$!$2Vq^gY|#w+VA_|o(oWmQX8^iw#n_crb(K3{69*iU?<%C-%H zuKi)3M1BhJ@3VW>JA`M>L~5*_bxH@Euy@niFrI$82C1}fwR$p2E&ZYnu?jlS}u7W9AyfdXh2pM>78bIt3 z)JBh&XE@zA!kyCDfvZ1qN^np20c1u#%P6;6tU&dx0phT1l=(mw7`u!-0e=PxEjDds z9E}{E!7f9>jaCQhw)&2TtG-qiD)lD(4jQ!q{`x|8l&nmtHkdul# zy+CIF8lKbp9_w{;oR+jSLtTfE+B@tOd6h=QePP>rh4@~!8c;Hlg9m%%&?e`*Z?qz5-zLEWfi>`ord5uHF-s{^bexKAoMEV@9nU z^5nA{f{dW&g$)BAGfkq@r5D)jr%!Ven~Q58c!Kr;*Li#`4Bu_?BU0`Y`nVQGhNZk@ z!>Yr$+nB=`z#o2nR0)V3M7-eVLuY`z@6CT#OTUXKnxZn$fNLPv7w1y7eGE=Qv@Hey`n;`U=xEl|q@CCV^#l)s0ZfT+mUf z^(j5r4)L5i2jnHW4+!6Si3q_LdOLQi<^fu?6WdohIkn79=jf%Fs3JkeXwF(?_tcF? z?z#j6iXEd(wJy4|p6v?xNk-)iIf2oX5^^Y3q3ziw16p9C6B;{COXul%)`>nuUoM*q zzmr|NJ5n)+sF$!yH5zwp=iM1#ZR`O%L83tyog-qh1I z0%dcj{NUs?{myT~33H^(%0QOM>-$hGFeP;U$puxoJ>>o-%Lk*8X^rx1>j|LtH$*)>1C!Pv&gd16%`qw5LdOIUbkNhaBBTo}5iuE%K&ZV^ zAr_)kkeNKNYJRgjsR%vexa~&8qMrQYY}+RbZ)egRg9_$vkoyV|Nc&MH@8L)`&rpqd zXnVaI@~A;Z^c3+{x=xgdhnocA&OP6^rr@rTvCnhG6^tMox$ulw2U7NgUtW%|-5VeH z_qyd47}1?IbuKtqNbNx$HR`*+9o=8`%vM8&SIKbkX9&%TS++x z5|&6P<%=F$C?owUI`%uvUq^yW0>`>yz!|WjzsoB9dT;2Dx8iSuK%%_XPgy0dTD4kd zDXF@&O_vBVVKQq(9YTClUPM30Sk7B!v7nOyV`XC!BA;BIVwphh+c)?5VJ^(C;GoQ$ zvBxr7_p*k$T%I1ke}`U&)$uf}I_T~#3XTi53OX)PoXVgxEcLJgZG^i47U&>LY(l%_ z;9vVDEtuMCyu2fqZeez|RbbIE7@)UtJvgAcVwVZNLccswxm+*L&w`&t=ttT=sv6Aq z!HouSc-24Y9;0q$>jX<1DnnGmAsP))- z^F~o99gHZw`S&Aw7e4id6Lg7kMk-e)B~=tZ!kE7sGTOJ)8@q}np@j7&7Sy{2`D^FH zI7aX%06vKsfJ168QnCM2=l|i>{I{%@gcr>ExM0Dw{PX6ozEuqFYEt z087%MKC;wVsMV}kIiuu9Zz9~H!21d!;Cu#b;hMDIP7nw3xSX~#?5#SSjyyg+Y@xh| z%(~fv3`0j#5CA2D8!M2TrG=8{%>YFr(j)I0DYlcz(2~92?G*?DeuoadkcjmZszH5& zKI@Lis%;RPJ8mNsbrxH@?J8Y2LaVjUIhRUiO-oqjy<&{2X~*f|)YxnUc6OU&5iac= z*^0qwD~L%FKiPmlzi&~a*9sk2$u<7Al=_`Ox^o2*kEv?p`#G(p(&i|ot8}T;8KLk- zPVf_4A9R`5^e`Om2LV*cK59EshYXse&IoByj}4WZaBomoHAPKqxRKbPcD`lMBI)g- zeMRY{gFaUuecSD6q!+b5(?vAnf>c`Z(8@RJy%Ulf?W~xB1dFAjw?CjSn$ph>st5bc zUac1aD_m6{l|$#g_v6;=32(mwpveQDWhmjR7{|B=$oBhz`7_g7qNp)n20|^^op3 zSfTdWV#Q>cb{CMKlWk91^;mHap{mk)o?udk$^Q^^u@&jd zfZ;)saW6{e*yoL6#0}oVPb2!}r{pAUYtn4{P~ES9tTfC5hXZnM{HrC8^=Pof{G4%Bh#8 ze~?C9m*|fd8MK;{L^!+wMy>=f^8b&y?yr6KnTq28$pFMBW9Oy7!oV5z|VM$s-cZ{I|Xf@}-)1=$V&x7e;9v81eiTi4O5-vs?^5pCKy2l>q);!MA zS!}M48l$scB~+Umz}7NbwyTn=rqt@`YtuwiQSMvCMFk2$83k50Q>OK5&fe*xCddIm)3D0I6vBU<+!3=6?(OhkO|b4fE_-j zimOzyfBB_*7*p8AmZi~X2bgVhyPy>KyGLAnOpou~sx9)S9%r)5dE%ADs4v%fFybDa_w*0?+>PsEHTbhKK^G=pFz z@IxLTCROWiKy*)cV3y%0FwrDvf53Ob_XuA1#tHbyn%Ko!1D#sdhBo`;VC*e1YlhrC z?*y3rp86m#qI|qeo8)_xH*G4q@70aXN|SP+6MQ!fJQqo1kwO_v7zqvUfU=Gwx`CR@ zRFb*O8+54%_8tS(ADh}-hUJzE`s*8wLI>1c4b@$al)l}^%GuIXjzBK!EWFO8W`>F^ ze7y#qPS0NI7*aU)g$_ziF(1ft;2<}6Hfz10cR8P}67FD=+}MfhrpOkF3hFhQu;Q1y zu%=jJHTr;0;oC94Hi@LAF5quAQ(rJG(uo%BiRQ@8U;nhX)j0i?0SL2g-A*YeAqF>RVCBOTrn{0R27vu}_S zS>tX4!#&U4W;ikTE!eFH+PKw%p+B(MR2I%n#+m0{#?qRP_tR@zpgCb=4rcrL!F=;A zh%EIF8m6%JG+qb&mEfuFTLHSxUAZEvC-+kvZKyX~SA3Umt`k}}c!5dy?-sLIM{h@> z!2=C)@nx>`;c9DdwZ&zeUc(7t<21D7qBj!|1^Mp1eZ6)PuvHx+poKSDCSBMFF{bKy z;9*&EyKitD99N}%mK8431rvbT+^%|O|HV23{;RhmS{$5tf!bIPoH9RKps`-EtoW5h zo6H_!s)Dl}2gCeGF6>aZtah9iLuGd19^z0*OryPNt{70RvJSM<#Ox9?HxGg04}b^f zrVEPceD%)#0)v5$YDE?f`73bQ6TA6wV;b^x*u2Ofe|S}+q{s5gr&m~4qGd!wOu|cZ||#h_u=k*fB;R6&k?FoM+c&J;ISg70h!J7*xGus)ta4veTdW)S^@sU@ z4$OBS=a~@F*V0ECic;ht4@?Jw<9kpjBgHfr2FDPykCCz|v2)`JxTH55?b3IM={@DU z!^|9nVO-R#s{`VHypWyH0%cs;0GO3E;It6W@0gX6wZ%W|Dzz&O%m17pa19db(er}C zUId1a4#I+Ou8E1MU$g=zo%g7K(=0Pn$)Rk z<4T2u<0rD)*j+tcy2XvY+0 z0d2pqm4)4lDewsAGThQi{2Kc3&C=|OQF!vOd#WB_`4gG3@inh-4>BoL!&#ij8bw7? zqjFRDaQz!J-YGitV4}$*$hg`vv%N)@#UdzHFI2E<&_@0Uw@h_ZHf}7)G;_NUD3@18 zH5;EtugNT0*RXVK*by>WS>jaDDfe!A61Da=VpIK?mcp^W?!1S2oah^wowRnrYjl~`lgP-mv$?yb6{{S55CCu{R z$9;`dyf0Y>uM1=XSl_$01Lc1Iy68IosWN8Q9Op=~I(F<0+_kKfgC*JggjxNgK6 z-3gQm6;sm?J&;bYe&(dx4BEjvq}b`OT^RqF$J4enP1YkeBK#>l1@-K`ajbn05`0J?0daOtnzh@l3^=BkedW1EahZlRp;`j*CaT;-21&f2wU z+Nh-gc4I36Cw+;3UAc<%ySb`#+c@5y ze~en&bYV|kn?Cn|@fqmGxgfz}U!98$=drjAkMi`43I4R%&H0GKEgx-=7PF}y`+j>r zg&JF`jomnu2G{%QV~Gf_-1gx<3Ky=Md9Q3VnK=;;u0lyTBCuf^aUi?+1+`4lLE6ZK zT#(Bf`5rmr(tgTbIt?yA@y`(Ar=f>-aZ}T~>G32EM%XyFvhn&@PWCm#-<&ApLDCXT zD#(9m|V(OOo7PmE@`vD4$S5;+9IQm19dd zvMEU`)E1_F+0o0-z>YCWqg0u8ciIknU#{q02{~YX)gc_u;8;i233D66pf(IkTDxeN zL=4z2)?S$TV9=ORVr&AkZMl<4tTh(v;Ix1{`pPVqI3n2ci&4Dg+W|N8TBUfZ*WeLF zqCH_1Q0W&f9T$lx3CFJ$o@Lz$99 zW!G&@zFHxTaP!o#z^~xgF|(vrHz8R_r9eo;TX9}2ZyjslrtH=%6O)?1?cL&BT(Amp zTGFU1%%#xl&6sH-UIJk_PGk_McFn7=%yd6tAjm|lnmr8bE2le3I~L{0(ffo}TQjyo zHZZI{-}{E4ohYTlZaS$blB!h$Jq^Rf#(ch}@S+Ww&$b);8+>g84IJcLU%B-W?+IY& zslcZIR>+U4v3O9RFEW;8NpCM0w1ROG84=WpKxQ^R`{=0MZCubg3st z48AyJNEvyxn-jCPTlTwp4EKvyEwD3e%kpdY?^BH0!3n6Eb57_L%J1=a*3>|k68A}v zaW`*4YitylfD}ua8V)vb79)N_Ixw_mpp}yJGbNu+5YYOP9K-7nf*jA1#<^rb4#AcS zKg%zCI)7cotx}L&J8Bqo8O1b0q;B1J#B5N5Z$Zq=wX~nQFgUfAE{@u0+EnmK{1hg> zC{vMfFLD;L8b4L+B51&LCm|scVLPe6h02rws@kGv@R+#IqE8>Xn8i|vRq_Z`V;x6F zNeot$1Zsu`lLS92QlLWF54za6vOEKGYQMdX($0JN*cjG7HP&qZ#3+bEN$8O_PfeAb z0R5;=zXac2IZ?fxu59?Nka;1lKm|;0)6|#RxkD05P5qz;*AL@ig!+f=lW5^Jbag%2 z%9@iM0ph$WFlxS!`p31t92z~TB}P-*CS+1Oo_g;7`6k(Jyj8m8U|Q3Sh7o-Icp4kV zK}%qri5>?%IPfamXIZ8pXbm-#{ytiam<{a5A+3dVP^xz!Pvirsq7Btv?*d7eYgx7q zWFxrzb3-%^lDgMc=Vl7^={=VDEKabTG?VWqOngE`Kt7hs236QKidsoeeUQ_^FzsXjprCDd@pW25rNx#6x&L6ZEpoX9Ffzv@olnH3rGOSW( zG-D|cV0Q~qJ>-L}NIyT?T-+x+wU%;+_GY{>t(l9dI%Ximm+Kmwhee;FK$%{dnF;C% zFjM2&$W68Sz#d*wtfX?*WIOXwT;P6NUw}IHdk|)fw*YnGa0rHx#paG!m=Y6GkS4VX zX`T$4eW9k1W!=q8!(#8A9h67fw))k_G)Q9~Q1e3f`aV@kbcSv7!priDUN}gX(iXTy zr$|kU0Vn%*ylmyDCO&G0Z3g>%JeEPFAW!5*H2Ydl>39w3W+gEUjL&vrRs(xGP{(ze zy7EMWF14@Qh>X>st8_029||TP0>7SG9on_xxeR2Iam3G~Em$}aGsNt$iES9zFa<3W zxtOF*!G@=PhfHO!=9pVPXMUVi30WmkPoy$02w}&6A7mF)G6-`~EVq5CwD2`9Zu`kd)52``#V zNSb`9dG~8(dooi1*-aSMf!fun7Sc`-C$-E(3BoSC$2kKrVcI!&yC*+ff2+C-@!AT_ zsvlAIV+%bRDfd{R*TMF><1&_a%@yZ0G0lg2K;F>7b+7A6pv3-S7qWIgx+Z?dt8}|S z>Qbb6x(+^aoV7FQ!Ph8|RUA6vXWQH*1$GJC+wXLXizNIc9p2yLzw9 z0=MdQ!{NnOwIICJc8!+Jp!zG}**r#E!<}&Te&}|B4q;U57$+pQI^}{qj669zMMe_I z&z0uUCqG%YwtUc8HVN7?0GHpu=bL7&{C>hcd5d(iFV{I5c~jpX&!(a{yS*4MEoYXh z*X4|Y@RVfn;piRm-C%b@{0R;aXrjBtvx^HO;6(>i*RnoG0Rtcd25BT6edxTNOgUAOjn zJ2)l{ipj8IP$KID2}*#F=M%^n&=bA0tY98@+2I+7~A&T-tw%W#3GV>GTmkHaqftl)#+E zMU*P(Rjo>8%P@_@#UNq(_L{}j(&-@1iY0TRizhiATJrnvwSH0v>lYfCI2ex^><3$q znzZgpW0JlQx?JB#0^^s-Js1}}wKh6f>(e%NrMwS`Q(FhazkZb|uyB@d%_9)_xb$6T zS*#-Bn)9gmobhAtvBmL+9H-+0_0US?g6^TOvE8f3v=z3o%NcPjOaf{5EMRnn(_z8- z$|m0D$FTU zDy;21v-#0i)9%_bZ7eo6B9@Q@&XprR&oKl4m>zIj-fiRy4Dqy@VVVs?rscG| zmzaDQ%>AQTi<^vYCmv#KOTd@l7#2VIpsj?nm_WfRZzJako`^uU%Nt3e;cU*y*|$7W zLm%fX#i_*HoUXu!NI$ey>BA<5HQB=|nRAwK!$L#n-Qz;~`zACig0PhAq#^5QS<8L2 zS3A+8%vbVMa7LOtTEM?55apt(DcWh#L}R^P2AY*c8B}Cx=6OFAdMPj1f>k3#^#+Hk z6uW1WJW&RlBRh*1DLb7mJ+KO>!t^t8hX1#_Wk`gjDio9)9IGbyCAGI4DJ~orK+YRv znjxRMtshZQHc$#Y-<-JOV6g^Cr@odj&Xw5B(FmI)*qJ9NHmIz_r{t)TxyB`L-%q5l ztzHgD;S6cw?7Atg*6E1!c6*gPRCb%t7D%z<(xm+K{%EJNiI2N0l8ud0Ch@_av_RW? zIr!nO4dL5466WslE6MsfMss7<)-S!e)2@r2o=7_W)OO`~CwklRWzHTfpB)_HYwgz=BzLhgZ9S<{nLBOwOIgJU=94uj6r!m>Xyn9>&xP+=5!zG_*yEoRgM0`aYts z^)&8(>z5C-QQ*o_s(8E4*?AX#S^0)aqB)OTyX>4BMy8h(cHjA8ji1PRlox@jB*1n? zDIfyDjzeg91Ao(;Q;KE@zei$}>EnrF6I}q&Xd=~&$WdDsyH0H7fJX|E+O~%LS*7^Q zYzZ4`pBdY{b7u72gZm6^5~O-57HwzwAz{)NvVaowo`X02tL3PpgLjwA`^i9F^vSpN zAqH3mRjG8VeJNHZ(1{%!XqC+)Z%D}58Qel{_weSEHoygT9pN@i zi=G;!Vj6XQk2tuJC>lza%ywz|`f7TIz*EN2Gdt!s199Dr4Tfd_%~fu8gXo~|ogt5Q zlEy_CXEe^BgsYM^o@L?s33WM14}7^T(kqohOX_iN@U?u;$l|rAvn{rwy>!yfZw13U zB@X9)qt&4;(C6dP?yRsoTMI!j-f1KC!<%~i1}u7yLXYn)(#a;Z6~r>hp~kfP));mi zcG%kdaB9H)z9M=H!f>kM->fTjRVOELNwh1amgKQT=I8J66kI)u_?0@$$~5f`u%;zl zC?pkr^p2Fe=J~WK%4ItSzKA+QHqJ@~m|Cduv=Q&-P8I5rQ-#G@bYH}YJr zUS(~(w|vKyU(T(*py}jTUp%I%{2!W!K(i$uvotcPjVddW z8_5HKY!oBCwGZcs-q`4Yt`Zk~>K?mcxg51wkZlX5e#B08I75F7#dgn5yf&Hrp`*%$ zQ;_Qg>TYRzBe$x=T(@WI9SC!ReSas9vDm(yslQjBJZde5z8GDU``r|N(MHcxNopGr z_}u39W_zwWDL*XYYt>#Xo!9kL#97|EAGyGBcRXtLTd59x%m=3i zL^9joWYA)HfL15l9%H?q`$mY27!<9$7GH(kxb%MV>`}hR4a?+*LH6aR{dzrX@?6X4 z3e`9L;cjqYb`cJmophbm(OX0b)!AFG?5`c#zLagzMW~o)?-!@e80lvk!p#&CD8u5_r&wp4O0zQ>y!k5U$h_K;rWGk=U)zX!#@Q%|9g*A zWx)qS1?fq6X<$mQTB$#3g;;5tHOYuAh;YKSBz%il3Ui6fPRv#v62SsrCdMRTav)Sg zTq1WOu&@v$Ey;@^+_!)cf|w_X<@RC>!=~+A1-65O0bOFYiH-)abINwZvFB;hJjL_$ z(9iScmUdMp2O$WW!520Hd0Q^Yj?DK%YgJD^ez$Z^?@9@Ab-=KgW@n8nC&88)TDC+E zlJM)L3r+ZJfZW_T$;Imq*#2<(j+FIk8ls7)WJ6CjUu#r5PoXxQs4b)mZza<8=v{o)VlLRM<9yw^0En#tXAj`Sylxvki{<1DPe^ zhjHwx^;c8tb?Vr$6ZB;$Ff$+3(*oinbwpN-#F)bTsXq@Sm?43MC#jQ~`F|twI=7oC zH4TJtu#;ngRA|Y~w5N=UfMZi?s0%ZmKUFTAye&6Y*y-%c1oD3yQ%IF2q2385Zl+=> zfz=o`Bedy|U;oxbyb^rB9ixG{Gb-{h$U0hVe`J;{ql!s_OJ_>>eoQn(G6h7+b^P48 zG<=Wg2;xGD-+d@UMZ!c;0>#3nws$9kIDkK13IfloGT@s14AY>&>>^#>`PT7GV$2Hp zN<{bN*ztlZu_%W=&3+=#3bE(mka6VoHEs~0BjZ$+=0`a@R$iaW)6>wp2w)=v2@|2d z%?34!+iOc5S@;AAC4hELWLH56RGxo4jw8MDMU0Wk2k_G}=Vo(>eRFo(g3@HjG|`H3 zm8b*dK=moM*oB<)*A$M9!!5o~4U``e)wxavm@O_R(`P|u%9^LGi(_%IF<6o;NLp*0 zKsfZ0#24GT8(G`i4UvoMh$^;kOhl?`0yNiyrC#HJH=tqOH^T_d<2Z+ zeN>Y9Zn!X4*DMCK^o75Zk2621bdmV7Rx@AX^alBG4%~;G_vUoxhfhFRlR&+3WwF^T zaL)8xPq|wCZoNT^>3J0K?e{J-kl+hu2rZI>CUv#-z&u@`hjeb+bBZ>bcciQVZ{SbW zez04s9oFEgc8Z+Kp{XFX`MVf-s&w9*dx7wLen(_@y34}Qz@&`$2+osqfxz4&d}{Ql z*g1ag00Gu+$C`0avds{Q65BfGsu9`_`dML*rX~hyWIe$T>CsPRoLIr%MTk3pJ^2zH1qub1MBzPG}PO;Wmav9w%F7?%l=xIf#LlP`! z_Nw;xBQY9anH5-c8A4mME}?{iewjz(Sq-29r{fV;Fc>fv%0!W@(+{={Xl-sJ6aMoc z)9Q+$bchoTGTyWU_oI19!)bD=IG&OImfy;VxNXoIO2hYEfO~MkE#IXTK(~?Z&!ae! zl8z{D&2PC$Q*OBC(rS~-*-GHNJ6AC$@eve>LB@Iq;jbBZj`wk4|LGogE||Ie=M5g= z9d`uYQ1^Sr_q2wmZE>w2WG)!F%^KiqyaDtIAct?}D~JP4shTJy5Bg+-(EA8aXaxbd~BKMtTf2iQ69jD1o* zZF9*S3!v-TdqwK$%&?91Sh2=e63;X0Lci@n7y3XOu2ofyL9^-I767eHESAq{m+@*r zbVDx!FQ|AjT;!bYsXv8ilQjy~Chiu&HNhFXt3R_6kMC8~ChEFqG@MWu#1Q1#=~#ix zrkHpJre_?#r=N0wv`-7cHHqU`phJX2M_^{H0~{VP79Dv{6YP)oA1&TSfKPEPZn2)G z9o{U1huZBLL;Tp_0OYw@+9z(jkrwIGdUrOhKJUbwy?WBt zlIK)*K0lQCY0qZ!$%1?3A#-S70F#YyUnmJF*`xx?aH5;gE5pe-15w)EB#nuf6B*c~ z8Z25NtY%6Wlb)bUA$w%HKs5$!Z*W?YKV-lE0@w^{4vw;J>=rn?u!rv$&eM+rpU6rc=j9>N2Op+C{D^mospMCjF2ZGhe4eADA#skp2EA26%p3Ex9wHW8l&Y@HX z$Qv)mHM}4*@M*#*ll5^hE9M^=q~eyWEai*P;4z<9ZYy!SlNE5nlc7gm;M&Q zKhKE4d*%A>^m0R?{N}y|i6i^k>^n4(wzKvlQeHq{l&JuFD~sTsdhs`(?lFK@Q{pU~ zb!M3c@*3IwN1RUOVjY5>uT+s-2QLWY z4T2>fiSn>>Fob+%B868-v9D@AfWr#M8eM6w#eAlhc#zk6jkLxGBGk`E3$!A@*am!R zy>29&ptYK6>cvP`b!syNp)Q$0UOW|-O@)8!?94GOYF_}+zlW%fCEl|Tep_zx05g6q z>tp47e-&R*hSNe{6{H!mL?+j$c^TXT{C&@T-xIaesNCl05 z9SLb@q&mSb)I{VXMaiWa3PWj=Ed!>*GwUe;^|uk=Pz$njNnfFY^MM>E?zqhf6^{}0 zx&~~dA5#}1ig~7HvOQ#;d9JZBeEQ+}-~v$at`m!(ai z$w(H&mWCC~;PQ1$%iuz3`>dWeb3_p}X>L2LK%2l59Tyc}4m0>9A!8rhoU3m>i2+hl zx?*qs*c^j}+WPs>&v1%1Ko8_ivAGIn@QK7A`hDz-Emkcgv2@wTbYhkiwX2l=xz*XG zaiNg+j4F-I>9v+LjosI-QECrtKjp&0T@xIMKVr+&)gyb4@b3y?2CA?=ooN zT#;rU86WLh(e@#mF*rk(NV-qSIZyr z$6!ZUmzD)%yO-ot`rw3rp6?*_l*@Z*IB0xn4|BGPWHNc-1ZUnNSMWmDh=EzWJRP`) zl%d%J613oXzh5;VY^XWJi{lB`f#u+ThvtP7 zq(HK<4>tw(=yzSBWtYO}XI`S1pMBe3!jFxBHIuwJ(@%zdQFi1Q_hU2eDuHqXte7Ki zOV55H2D6u#4oTfr7|u*3p75KF&jaLEDpxk!4*bhPc%mpfj)Us3XIG3 zIKMX^s^1wt8YK7Ky^UOG=w!o5e7W-<&c|fw2{;Q11vm@J{)@N3-p1U>!0~sKWHaL= zWV(0}1IIyt1p%=_-Fe5Kfzc71wg}`RDDntVZv;4!=&XXF-$48jS0Sc;eDy@Sg;+{A zFStc{dXT}kcIjMXb4F7MbX~2%i;UrBxm%qmLKb|2=?uPr00-$MEUIGR5+JG2l2Nq` zkM{{1RO_R)+8oQ6x&-^kCj)W8Z}TJjS*Wm4>hf+4#VJP)OBaDF%3pms7DclusBUw} z{ND#!*I6h85g6DzNvdAmnwWY{&+!KZM4DGzeHI?MR@+~|su0{y-5-nICz_MIT_#FE zm<5f3zlaKq!XyvY3H`9s&T};z!cK}G%;~!rpzk9-6L}4Rg7vXtKFsl}@sT#U#7)x- z7UWue5sa$R>N&b{J61&gvKcKlozH*;OjoDR+elkh|4bJ!_3AZNMOu?n9&|L>OTD78 z^i->ah_Mqc|Ev)KNDzfu1P3grBIM#%`QZqj5W{qu(HocQhjyS;UINoP`{J+DvV?|1 z_sw6Yr3z6%e7JKVDY<$P=M)dbk@~Yw9|2!Cw!io3%j92wTD!c^e9Vj+7VqXo3>u#= zv#M{HHJ=e$X5vQ>>ML?E8#UlmvJgTnb73{PSPTf*0)mcj6C z{KsfUbDK|F$E(k;ER%8HMdDi`=BfpZzP3cl5yJHu;v^o2FkHNk;cXc17tL8T!CsYI zfeZ6sw@;8ia|mY_AXjCS?kUfxdjDB28)~Tz1dGE|{VfBS9`0m2!m1yG?hR})er^pl4c@9Aq+|}ZlDaHL)K$O| z%9Jp-imI-Id0|(d5{v~w6mx)tUKfbuVD`xNt04Mry%M+jXzE>4(TBsx#&=@wT2Vh) z1yeEY&~17>0%P(eHP0HB^|7C+WJxQBTG$uyOWY@iDloRIb-Cf!p<{WQHR!422#F34 zG`v|#CJ^G}y9U*7jgTlD{D&y$Iv{6&PYG>{Ixg$pGk?lWrE#PJ8KunQC@}^6OP!|< zS;}p3to{S|uZz%kKe|;A0bL0XxPB&Q{J(9PyX`+Kr`k~r2}yP^ND{8!v7Q1&vtk& z2Y}l@J@{|2`oA%sxvM9i0V+8IXrZ4;tey)d;LZI70Kbim<4=WoTPZy=Yd|34v#$Kh zx|#YJ8s`J>W&jt#GcMpx84w2Z3ur-rK7gf-p5cE)=w1R2*|0mj12hvapuUWM0b~dG zMg9p8FmAZI@i{q~0@QuY44&mMUNXd7z>U58shA3o`p5eVLpq>+{(<3->DWuSFVZwC zxd50Uz(w~LxC4}bgag#q#NNokK@yNc+Q|Ap!u>Ddy+df>v;j@I12CDNN9do+0^n8p zMQs7X#+FVF0C5muGfN{r0|Nkql%BQT|K(DDNdR2pzM=_ea5+GO|J67`05AV92t@4l z0Qno0078PIHdaQGHZ~Scw!dzgqjK~3B7kf>BcP__&lLyU(cu3B^uLo%{j|Mb0NR)tkeT7Hcwp4O# z)yzu>cvG(d9~0a^)eZ;;%3ksk@F&1eEBje~ zW+-_s)&RgiweQc!otF>4%vbXKaOU41{!hw?|2`Ld3I8$&#WOsq>EG)1ANb!{N4z9@ zsU!bPG-~-bqCeIDzo^Q;gnucB{tRzm{ZH^Orphm2U+REA!*<*J6YQV83@&xoDl%#wnl5qcBqCcAF-vX5{30}(oJrnSH z{RY85hylK2dMOh2%oO1J8%)0?8TOL%rS8)+CsDv}aQ>4D)Jv+DLK)9gI^n-T^$)Tc zFPUD75qJm!Y-KBqj;JP4dV4 z`X{lGmn<)1IGz330}s}Jrjtf{(lnuuNHe5(ezA(pYa=1|Ff-LhPFK8 zyJh_b{yzu0yll6ZkpRzRjezyYivjyjW7QwO;@6X`m;2Apn2EK2!~7S}-*=;5*7K$B z`x(=!^?zgj(-`&ApZJXI09aDLXaT@<;CH=?fBOY5d|b~wBA@@p^K#nxr`)?i?SqTupI_PJ(A3cx`z~9mX_*)>L F{|7XC?P&l2 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 669386b8..0eebfdcf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Mon Aug 22 21:08:00 CEST 2022 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index cccdd3d5..4f906e0c 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -66,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -109,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -138,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f9553162..107acd32 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/settings.gradle b/settings.gradle index f4f85485..faddd618 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,10 +1,25 @@ -include ':Bcore' -include ':app' -include ':Bcore:black-fake' -include ':Bcore:black-hook' -include ':Bcore:pine-xposed' -include ':Bcore:pine-core' -include ':Bcore:pine-xposed-res' -include ':android-mirror' +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + jcenter() + maven { + url 'https://jitpack.io' + } + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + jcenter() + maven { + url 'https://jitpack.io' + } + } +} rootProject.name = "BlackBox" +include ':Bcore', ':app', ':Bcore:black-fake', ':Bcore:black-hook', ':Bcore:pine-xposed', ':Bcore:pine-core', ':Bcore:pine-xposed-res', ':android-mirror' From 54167bf1f4164e48de0145083a92ed6e85a80293 Mon Sep 17 00:00:00 2001 From: Arcy Date: Wed, 24 Aug 2022 20:47:48 +0200 Subject: [PATCH 2/2] Fix IActivityManagerProxy Added SetServiceForeground in IActivityManagerProxy + a few minor changes --- Bcore/black-fake/build.gradle | 2 +- Bcore/black-hook/build.gradle | 2 +- Bcore/build.gradle | 2 +- .../system/am/IBActivityManagerService.aidl | 2 + .../core/system/am/ActiveServices.java | 7 ++ .../system/am/BActivityManagerService.java | 9 ++ .../fake/frameworks/BActivityManager.java | 9 ++ .../service/ActivityManagerCommonProxy.java | 2 +- .../fake/service/IActivityManagerProxy.java | 108 +++--------------- app/build.gradle | 5 +- 10 files changed, 53 insertions(+), 95 deletions(-) diff --git a/Bcore/black-fake/build.gradle b/Bcore/black-fake/build.gradle index 57cfbc7c..8ce9fd9b 100644 --- a/Bcore/black-fake/build.gradle +++ b/Bcore/black-fake/build.gradle @@ -28,6 +28,7 @@ android { testOptions { unitTests.returnDefaultValues = true } + lint { abortOnError false checkOnly 'NewApi', 'InlinedApi' @@ -37,7 +38,6 @@ android { textReport false warningsAsErrors false } - } tasks.withType(Javadoc) { diff --git a/Bcore/black-hook/build.gradle b/Bcore/black-hook/build.gradle index be95aa76..8cce8b16 100644 --- a/Bcore/black-hook/build.gradle +++ b/Bcore/black-hook/build.gradle @@ -29,6 +29,7 @@ android { testOptions { unitTests.returnDefaultValues = true } + lint { abortOnError false checkOnly 'NewApi', 'InlinedApi' @@ -38,7 +39,6 @@ android { textReport false warningsAsErrors false } - } tasks.withType(Javadoc) { diff --git a/Bcore/build.gradle b/Bcore/build.gradle index db52a55c..0fe92141 100644 --- a/Bcore/build.gradle +++ b/Bcore/build.gradle @@ -42,6 +42,7 @@ android { testOptions { unitTests.returnDefaultValues = true } + lint { abortOnError false checkOnly 'NewApi', 'InlinedApi' @@ -58,7 +59,6 @@ android { } } - buildFeatures { prefab true } diff --git a/Bcore/src/main/aidl/top/niunaijun/blackbox/core/system/am/IBActivityManagerService.aidl b/Bcore/src/main/aidl/top/niunaijun/blackbox/core/system/am/IBActivityManagerService.aidl index 75600cd0..4159dff8 100644 --- a/Bcore/src/main/aidl/top/niunaijun/blackbox/core/system/am/IBActivityManagerService.aidl +++ b/Bcore/src/main/aidl/top/niunaijun/blackbox/core/system/am/IBActivityManagerService.aidl @@ -57,4 +57,6 @@ interface IBActivityManagerService { void getIntentSender(in IBinder target, String packageName, int uid, int userId); String getPackageForIntentSender(in IBinder target, int userId); int getUidForIntentSender(in IBinder target, int userId); + + void setServiceForeground(in ComponentName className, in IBinder token, int id, in Notification notification, int flags, int foregroundServiceType, int userId); } diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/core/system/am/ActiveServices.java b/Bcore/src/main/java/top/niunaijun/blackbox/core/system/am/ActiveServices.java index 354383b9..2d049383 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/core/system/am/ActiveServices.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/core/system/am/ActiveServices.java @@ -2,6 +2,7 @@ import android.annotation.SuppressLint; import android.app.ActivityManager; +import android.app.Notification; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -18,11 +19,13 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; +import black.android.os.UserHandle; import top.niunaijun.blackbox.BlackBoxCore; import top.niunaijun.blackbox.core.IEmpty; import top.niunaijun.blackbox.core.system.BProcessManagerService; import top.niunaijun.blackbox.core.system.ProcessRecord; import top.niunaijun.blackbox.core.system.pm.BPackageManagerService; +import top.niunaijun.blackbox.entity.ServiceRecord; import top.niunaijun.blackbox.entity.UnbindRecord; import top.niunaijun.blackbox.entity.am.RunningServiceInfo; import top.niunaijun.blackbox.proxy.ProxyManifest; @@ -168,6 +171,10 @@ public void stopServiceToken(ComponentName className, IBinder token, int userId) } } + public void setServiceForeground(ComponentName className, IBinder token, int id, Notification notification, int flags, int foregroundServiceType, int userId) { + + } + public void onStartCommand(Intent proxyIntent, int userId) { } diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/core/system/am/BActivityManagerService.java b/Bcore/src/main/java/top/niunaijun/blackbox/core/system/am/BActivityManagerService.java index 14413ff1..8971813c 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/core/system/am/BActivityManagerService.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/core/system/am/BActivityManagerService.java @@ -1,6 +1,7 @@ package top.niunaijun.blackbox.core.system.am; import android.app.ActivityManager; +import android.app.Notification; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -272,6 +273,14 @@ public int getUidForIntentSender(IBinder target, int userId) { return -1; } + @Override + public void setServiceForeground(ComponentName className, IBinder token, int id, Notification notification, int flags, int foregroundServiceType, int userId) { + UserSpace userSpace = getOrCreateSpaceLocked(userId); + synchronized (userSpace.mActiveServices) { + userSpace.mActiveServices.setServiceForeground(className, token, id, notification, flags, foregroundServiceType, userId); + } + } + @Override public void onStartCommand(Intent intent, int userId) { UserSpace userSpace = getOrCreateSpaceLocked(userId); diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/frameworks/BActivityManager.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/frameworks/BActivityManager.java index a75e2ec0..7a5f2227 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/frameworks/BActivityManager.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/frameworks/BActivityManager.java @@ -1,6 +1,7 @@ package top.niunaijun.blackbox.fake.frameworks; import android.app.Activity; +import android.app.Notification; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ProviderInfo; @@ -290,4 +291,12 @@ public int getUidForIntentSender(IBinder target) { } return -1; } + + public void setServiceForeground(ComponentName className, IBinder token, int id, Notification notification, int flags, int foregroundServiceType) { + try { + getService().setServiceForeground(className, token, id, notification, flags, foregroundServiceType, BActivityThread.getUserId()); + } catch (RemoteException e) { + e.printStackTrace(); + } + } } diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/ActivityManagerCommonProxy.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/ActivityManagerCommonProxy.java index 22ba0be9..e53d9cda 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/ActivityManagerCommonProxy.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/ActivityManagerCommonProxy.java @@ -123,7 +123,7 @@ protected Object hook(Object who, Method method, Object[] args) throws Throwable String[] resolvedTypes = (String[]) args[index++]; IBinder resultTo = (IBinder) args[index++]; Bundle options = (Bundle) args[index]; - // todo ?? + if (!ComponentUtils.isSelf(intents)) { return method.invoke(who, args); } diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IActivityManagerProxy.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IActivityManagerProxy.java index 03e55146..bb7c54e9 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IActivityManagerProxy.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/IActivityManagerProxy.java @@ -1,8 +1,12 @@ package top.niunaijun.blackbox.fake.service; +import static android.content.pm.PackageManager.GET_META_DATA; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + import android.Manifest; import android.app.ActivityManager; import android.app.IServiceConnection; +import android.app.Notification; import android.content.ComponentName; import android.content.IIntentReceiver; import android.content.Intent; @@ -30,6 +34,7 @@ import top.niunaijun.blackbox.BlackBoxCore; import top.niunaijun.blackbox.app.BActivityThread; import top.niunaijun.blackbox.core.env.AppSystemEnv; +import top.niunaijun.blackbox.core.system.DaemonService; import top.niunaijun.blackbox.entity.AppConfig; import top.niunaijun.blackbox.entity.am.RunningAppProcessInfo; import top.niunaijun.blackbox.entity.am.RunningServiceInfo; @@ -54,9 +59,6 @@ import top.niunaijun.blackbox.utils.compat.ParceledListSliceCompat; import top.niunaijun.blackbox.utils.compat.TaskDescriptionCompat; -import static android.content.pm.PackageManager.GET_META_DATA; -import static android.content.pm.PackageManager.PERMISSION_GRANTED; - /** * Created by Milk on 3/30/21. * * ∧_∧ @@ -395,18 +397,6 @@ protected Object hook(Object who, Method method, Object[] args) throws Throwable } } - @ProxyMethod("getIntentSenderWithSourceToken") - public static class GetIntentSenderWithSourceToken extends GetIntentSender { - } - - @ProxyMethod("getIntentSenderWithFeature") - public static class GetIntentSenderWithFeature extends GetIntentSender { - } - - @ProxyMethod("broadcastIntentWithFeature") - public static class BroadcastIntentWithFeature extends BroadcastIntent { - } - @ProxyMethod("broadcastIntent") public static class BroadcastIntent extends MethodHook { @Override @@ -441,30 +431,6 @@ int getIntentIndex(Object[] args) { } } - @ProxyMethod("unregisterReceiver") - public static class unregisterReceiver extends MethodHook { - @Override - protected Object hook(Object who, Method method, Object[] args) throws Throwable { - return method.invoke(who, args); - } - } - - @ProxyMethod("finishReceiver") - public static class finishReceiver extends MethodHook { - @Override - protected Object hook(Object who, Method method, Object[] args) throws Throwable { - return method.invoke(who, args); - } - } - - @ProxyMethod("publishService") - public static class PublishService extends MethodHook { - @Override - protected Object hook(Object who, Method method, Object[] args) throws Throwable { - return method.invoke(who, args); - } - } - @ProxyMethod("peekService") public static class PeekService extends MethodHook { @Override @@ -472,8 +438,7 @@ protected Object hook(Object who, Method method, Object[] args) throws Throwable MethodParameterUtils.replaceLastAppPkg(args); Intent intent = (Intent) args[0]; String resolvedType = (String) args[1]; - IBinder peek = BlackBoxCore.getBActivityManager().peekService(intent, resolvedType, BActivityThread.getUserId()); - return peek; + return BlackBoxCore.getBActivityManager().peekService(intent, resolvedType, BActivityThread.getUserId()); } } @@ -486,11 +451,6 @@ protected Object hook(Object who, Method method, Object[] args) throws Throwable } } - // android 10 - @ProxyMethod("registerReceiverWithFeature") - public static class RegisterReceiverWithFeature extends RegisterReceiver { - } - @ProxyMethod("registerReceiver") public static class RegisterReceiver extends MethodHook { @Override @@ -544,14 +504,22 @@ protected Object hook(Object who, Method method, Object[] args) throws Throwable } @ProxyMethod("setServiceForeground") - public static class setServiceForeground extends MethodHook { + public static class SetServiceForeground extends MethodHook { @Override protected Object hook(Object who, Method method, Object[] args) throws Throwable { - /*if (args[0] instanceof ComponentName) { - args[0] = new ComponentName(BlackBoxCore.getHostPkg(), ProxyManifest.getProxyService(BActivityThread.getAppPid())); + Notification notification = (Notification) args[3]; + + Intent intent = new Intent(BlackBoxCore.getContext(), DaemonService.class); + if (notification != null) { + if (BuildCompat.isOreo()) { + BlackBoxCore.getContext().startForegroundService(intent); + } else { + BlackBoxCore.getContext().startService(intent); + } + } else { + BlackBoxCore.getContext().stopService(intent); } - return method.invoke(who, args);*/ - return 0; + return method.invoke(who, args); } } @@ -603,42 +571,4 @@ protected Object hook(Object who, Method method, Object[] args) throws Throwable return method.invoke(who, args); } } - - @ProxyMethod("setRequestedOrientation") - public static class setRequestedOrientation extends MethodHook { - - @Override - protected Object hook(Object who, Method method, Object[] args) throws Throwable { - try { - return method.invoke(who, args); - } catch (Throwable e) { - e.printStackTrace(); - } - return 0; - } - } - - @ProxyMethod("registerUidObserver") - public static class registerUidObserver extends MethodHook { - @Override - protected Object hook(Object who, Method method, Object[] args) throws Throwable { - return 0; - } - } - - @ProxyMethod("unregisterUidObserver") - public static class unregisterUidObserver extends MethodHook { - @Override - protected Object hook(Object who, Method method, Object[] args) throws Throwable { - return 0; - } - } - - @ProxyMethod("updateConfiguration") - public static class updateConfiguration extends MethodHook { - @Override - protected Object hook(Object who, Method method, Object[] args) throws Throwable { - return 0; - } - } } diff --git a/app/build.gradle b/app/build.gradle index 8a39fb43..bdfc4c60 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "top.niunaijun.blackbox" minSdk rootProject.ext.minSdkVersion targetSdk rootProject.ext.targetSdk - versionCode 12 - versionName "2.1.4" + versionCode 7 + versionName "1.6.0" flavorDimensions "BlackBox32" multiDexEnabled true @@ -96,6 +96,7 @@ android { jniLibs.srcDirs = ['libs'] } } + lint { abortOnError false checkOnly 'NewApi', 'InlinedApi'