Repository: android-hacker/VirtualXposed Branch: vxp Commit: 122beb371519 Files: 801 Total size: 2.7 MB Directory structure: gitextract_16jertsa/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── bug_report_cn.md │ │ ├── feature_request.md │ │ └── feature_request_cn.md │ ├── issue-close-app.yml │ ├── stale.yml │ └── workflows/ │ └── android.yml ├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHINESE.md ├── LICENSE.txt ├── README.md └── VirtualApp/ ├── .gitignore ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── aosp/ │ │ └── java/ │ │ └── io/ │ │ └── virtualapp/ │ │ └── delegate/ │ │ ├── MyCrashHandler.java │ │ └── MyVirtualInitializer.java │ ├── fdroid/ │ │ └── java/ │ │ └── io/ │ │ └── virtualapp/ │ │ └── delegate/ │ │ └── MyVirtualInitializer.java │ └── main/ │ ├── AndroidManifest.xml │ ├── assets/ │ │ └── XposedInstaller_3.1.5.apk_ │ ├── java/ │ │ └── io/ │ │ └── virtualapp/ │ │ ├── VCommends.java │ │ ├── XApp.java │ │ ├── abs/ │ │ │ ├── BasePresenter.java │ │ │ ├── BaseView.java │ │ │ ├── Callback.java │ │ │ └── ui/ │ │ │ ├── VActivity.java │ │ │ ├── VFragment.java │ │ │ └── VUiKit.java │ │ ├── delegate/ │ │ │ ├── BaseCrashHandler.java │ │ │ ├── BaseVirtualInitializer.java │ │ │ ├── MyAppRequestListener.java │ │ │ ├── MyComponentDelegate.java │ │ │ ├── MyPhoneInfoDelegate.java │ │ │ └── MyTaskDescDelegate.java │ │ ├── dev/ │ │ │ └── CmdReceiver.java │ │ ├── glide/ │ │ │ ├── GlideUtils.java │ │ │ ├── MyGlideModule.java │ │ │ ├── PackageIconResourceDataFetcher.java │ │ │ ├── PackageIconResourceLoader.java │ │ │ └── PackageIconResourceLoaderFactory.java │ │ ├── gms/ │ │ │ └── FakeGms.java │ │ ├── home/ │ │ │ ├── ListAppActivity.java │ │ │ ├── ListAppContract.java │ │ │ ├── ListAppFragment.java │ │ │ ├── ListAppPresenterImpl.java │ │ │ ├── LoadingActivity.java │ │ │ ├── NewHomeActivity.java │ │ │ ├── adapters/ │ │ │ │ ├── AppPagerAdapter.java │ │ │ │ └── CloneAppListAdapter.java │ │ │ ├── models/ │ │ │ │ ├── AppData.java │ │ │ │ ├── AppInfo.java │ │ │ │ ├── AppInfoLite.java │ │ │ │ ├── MultiplePackageAppData.java │ │ │ │ └── PackageAppData.java │ │ │ └── repo/ │ │ │ ├── AppDataSource.java │ │ │ ├── AppRepository.java │ │ │ └── PackageAppDataStorage.java │ │ ├── settings/ │ │ │ ├── AboutActivity.java │ │ │ ├── AppManageActivity.java │ │ │ ├── NougatPolicy.java │ │ │ ├── OnlinePlugin.java │ │ │ ├── RecommendPluginActivity.java │ │ │ ├── SettingsActivity.java │ │ │ └── TaskManageActivity.java │ │ ├── splash/ │ │ │ └── SplashActivity.java │ │ ├── sys/ │ │ │ ├── Installd.java │ │ │ ├── InstallerActivity.java │ │ │ └── ShareBridgeActivity.java │ │ ├── update/ │ │ │ └── VAVersionService.java │ │ ├── utils/ │ │ │ ├── DialogUtil.java │ │ │ ├── HanziToPinyin.java │ │ │ └── Misc.java │ │ └── widgets/ │ │ ├── BaseView.java │ │ ├── CardStackAdapter.java │ │ ├── CardStackLayout.java │ │ ├── DragSelectRecyclerView.java │ │ ├── DragSelectRecyclerViewAdapter.java │ │ ├── EatBeansView.java │ │ ├── Indicator.java │ │ ├── LabelView.java │ │ ├── MarqueeTextView.java │ │ └── fittext/ │ │ ├── BaseTextView.java │ │ ├── FitTextHelper.java │ │ └── FitTextView.java │ └── res/ │ ├── drawable/ │ │ ├── blue_circle.xml │ │ ├── fab_bg.xml │ │ ├── home_bg.xml │ │ ├── icon_bg.xml │ │ ├── sel_clone_app_btn.xml │ │ ├── sel_guide_btn.xml │ │ ├── shape_clone_app_btn.xml │ │ └── shape_clone_app_btn_pressed.xml │ ├── drawable-nodpi/ │ │ ├── about_icon_copy_right.xml │ │ └── ic_more.xml │ ├── layout/ │ │ ├── activity_clone_app.xml │ │ ├── activity_install.xml │ │ ├── activity_list.xml │ │ ├── activity_loading.xml │ │ ├── activity_location_settings.xml │ │ ├── activity_marker.xml │ │ ├── activity_splash.xml │ │ ├── activity_users.xml │ │ ├── content_toolbar.xml │ │ ├── fragment_list_app.xml │ │ ├── item_app.xml │ │ ├── item_app_manage.xml │ │ ├── item_clone_app.xml │ │ ├── item_location_app.xml │ │ ├── item_plugin_recommend.xml │ │ ├── item_share.xml │ │ ├── item_task_manage.xml │ │ └── item_user.xml │ ├── menu/ │ │ ├── app_manage_menu.xml │ │ ├── main_menu.xml │ │ └── marktet_map.xml │ ├── values/ │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── fitTextView.xml │ │ ├── ids.xml │ │ ├── integers.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── values-en/ │ │ └── strings.xml │ ├── values-es/ │ │ └── strings.xml │ ├── values-fr/ │ │ └── strings.xml │ ├── values-ja/ │ │ └── strings.xml │ ├── values-pt-rBR/ │ │ └── strings.xml │ ├── values-ru/ │ │ └── strings.xml │ ├── values-uk/ │ │ └── strings.xml │ ├── values-zh-rCN/ │ │ └── strings.xml │ ├── values-zh-rTW/ │ │ └── strings.xml │ └── xml/ │ └── settings_preferences.xml ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── lib/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── aidl/ │ │ ├── android/ │ │ │ ├── accounts/ │ │ │ │ ├── IAccountAuthenticator.aidl │ │ │ │ ├── IAccountAuthenticatorResponse.aidl │ │ │ │ └── IAccountManagerResponse.aidl │ │ │ ├── app/ │ │ │ │ ├── IActivityManager/ │ │ │ │ │ └── ContentProviderHolder.aidl │ │ │ │ ├── IServiceConnection.aidl │ │ │ │ ├── IStopUserCallback.aidl │ │ │ │ └── job/ │ │ │ │ ├── IJobCallback.aidl │ │ │ │ └── IJobService.aidl │ │ │ ├── content/ │ │ │ │ ├── IIntentReceiver.aidl │ │ │ │ ├── ISyncAdapter.aidl │ │ │ │ ├── ISyncContext.aidl │ │ │ │ ├── ISyncStatusObserver.aidl │ │ │ │ └── pm/ │ │ │ │ ├── IPackageDataObserver.aidl │ │ │ │ ├── IPackageDeleteObserver2.aidl │ │ │ │ ├── IPackageInstallObserver.aidl │ │ │ │ ├── IPackageInstallObserver2.aidl │ │ │ │ ├── IPackageInstallerCallback.aidl │ │ │ │ └── IPackageInstallerSession.aidl │ │ │ ├── location/ │ │ │ │ └── ILocationListener.aidl │ │ │ └── net/ │ │ │ ├── IConnectivityManager.aidl │ │ │ └── wifi/ │ │ │ └── IWifiScanner.aidl │ │ └── com/ │ │ └── lody/ │ │ └── virtual/ │ │ ├── client/ │ │ │ └── IVClient.aidl │ │ ├── os/ │ │ │ └── VUserInfo.aidl │ │ ├── remote/ │ │ │ ├── AppTaskInfo.aidl │ │ │ ├── BadgerInfo.aidl │ │ │ ├── InstallResult.aidl │ │ │ ├── InstalledAppInfo.aidl │ │ │ ├── PendingIntentData.aidl │ │ │ ├── PendingResultData.aidl │ │ │ ├── Problem.aidl │ │ │ ├── ReceiverInfo.aidl │ │ │ ├── VDeviceInfo.aidl │ │ │ ├── VParceledListSlice.aidl │ │ │ └── vloc/ │ │ │ ├── VCell.aidl │ │ │ ├── VLocation.aidl │ │ │ └── VWifi.aidl │ │ └── server/ │ │ ├── IAccountManager.aidl │ │ ├── IActivityManager.aidl │ │ ├── IAppManager.aidl │ │ ├── IBinderDelegateService.aidl │ │ ├── IDeviceInfoManager.aidl │ │ ├── IJobScheduler.aidl │ │ ├── INotificationManager.aidl │ │ ├── IPackageInstaller.aidl │ │ ├── IPackageInstallerSession.aidl │ │ ├── IPackageManager.aidl │ │ ├── IUserManager.aidl │ │ ├── IVirtualLocationManager.aidl │ │ ├── IVirtualStorageService.aidl │ │ ├── interfaces/ │ │ │ ├── IAppRequestListener.aidl │ │ │ ├── IIntentFilterObserver.aidl │ │ │ ├── IPackageObserver.aidl │ │ │ ├── IProcessObserver.aidl │ │ │ ├── IServiceFetcher.aidl │ │ │ └── IUiCallback.aidl │ │ └── pm/ │ │ └── installer/ │ │ ├── SessionInfo.aidl │ │ └── SessionParams.aidl │ ├── java/ │ │ ├── android/ │ │ │ ├── app/ │ │ │ │ ├── ActivityOptions.java │ │ │ │ ├── ActivityThread.java │ │ │ │ ├── ClientTransactionHandler.java │ │ │ │ ├── LoadedApk.java │ │ │ │ ├── TransactionHandlerProxy.java │ │ │ │ └── servertransaction/ │ │ │ │ ├── ClientTransaction.java │ │ │ │ ├── PendingTransactionActions.java │ │ │ │ └── TransactionExecutor.java │ │ │ ├── content/ │ │ │ │ ├── SyncStatusInfo.java │ │ │ │ ├── pm/ │ │ │ │ │ └── PackageParser.java │ │ │ │ └── res/ │ │ │ │ └── CompatibilityInfo.java │ │ │ ├── location/ │ │ │ │ └── LocationRequest.java │ │ │ ├── util/ │ │ │ │ └── MergedConfiguration.java │ │ │ └── view/ │ │ │ └── DisplayAdjustments.java │ │ ├── com/ │ │ │ └── lody/ │ │ │ └── virtual/ │ │ │ ├── Build.java │ │ │ ├── DelegateApplication64Bit.java │ │ │ ├── GmsSupport.java │ │ │ ├── client/ │ │ │ │ ├── NativeEngine.java │ │ │ │ ├── VClientImpl.java │ │ │ │ ├── badger/ │ │ │ │ │ ├── BadgerManager.java │ │ │ │ │ ├── BroadcastBadger1.java │ │ │ │ │ ├── BroadcastBadger2.java │ │ │ │ │ └── IBadger.java │ │ │ │ ├── core/ │ │ │ │ │ ├── CrashHandler.java │ │ │ │ │ ├── InstallStrategy.java │ │ │ │ │ ├── InvocationStubManager.java │ │ │ │ │ └── VirtualCore.java │ │ │ │ ├── env/ │ │ │ │ │ ├── Constants.java │ │ │ │ │ ├── DeadServerException.java │ │ │ │ │ ├── GPSStateline.java │ │ │ │ │ ├── SpecialComponentList.java │ │ │ │ │ ├── VirtualGPSSatalines.java │ │ │ │ │ └── VirtualRuntime.java │ │ │ │ ├── fixer/ │ │ │ │ │ ├── ActivityFixer.java │ │ │ │ │ ├── ComponentFixer.java │ │ │ │ │ └── ContextFixer.java │ │ │ │ ├── hook/ │ │ │ │ │ ├── base/ │ │ │ │ │ │ ├── BinderInvocationProxy.java │ │ │ │ │ │ ├── BinderInvocationStub.java │ │ │ │ │ │ ├── Inject.java │ │ │ │ │ │ ├── LogInvocation.java │ │ │ │ │ │ ├── MethodBox.java │ │ │ │ │ │ ├── MethodInvocationProxy.java │ │ │ │ │ │ ├── MethodInvocationStub.java │ │ │ │ │ │ ├── MethodProxy.java │ │ │ │ │ │ ├── ReplaceCallingPkgMethodProxy.java │ │ │ │ │ │ ├── ReplaceLastPkgMethodProxy.java │ │ │ │ │ │ ├── ReplaceLastUidMethodProxy.java │ │ │ │ │ │ ├── ReplaceSequencePkgMethodProxy.java │ │ │ │ │ │ ├── ReplaceSpecPkgMethodProxy.java │ │ │ │ │ │ ├── ReplaceUidMethodProxy.java │ │ │ │ │ │ ├── ResultStaticMethodProxy.java │ │ │ │ │ │ ├── SkipInject.java │ │ │ │ │ │ └── StaticMethodProxy.java │ │ │ │ │ ├── delegate/ │ │ │ │ │ │ ├── AppInstrumentation.java │ │ │ │ │ │ ├── ComponentDelegate.java │ │ │ │ │ │ ├── InstrumentationDelegate.java │ │ │ │ │ │ ├── PhoneInfoDelegate.java │ │ │ │ │ │ └── TaskDescriptionDelegate.java │ │ │ │ │ ├── providers/ │ │ │ │ │ │ ├── DownloadProviderHook.java │ │ │ │ │ │ ├── ExternalProviderHook.java │ │ │ │ │ │ ├── InternalProviderHook.java │ │ │ │ │ │ ├── MediaProviderHook.java │ │ │ │ │ │ ├── ProviderHook.java │ │ │ │ │ │ ├── QueryRedirectCursor.java │ │ │ │ │ │ └── SettingsProviderHook.java │ │ │ │ │ ├── proxies/ │ │ │ │ │ │ ├── account/ │ │ │ │ │ │ │ └── AccountManagerStub.java │ │ │ │ │ │ ├── alarm/ │ │ │ │ │ │ │ └── AlarmManagerStub.java │ │ │ │ │ │ ├── am/ │ │ │ │ │ │ │ ├── ActivityManagerStub.java │ │ │ │ │ │ │ ├── ActivityTaskManagerStub.java │ │ │ │ │ │ │ ├── HCallbackStub.java │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ └── TransactionHandlerStub.java │ │ │ │ │ │ ├── appops/ │ │ │ │ │ │ │ └── AppOpsManagerStub.java │ │ │ │ │ │ ├── appwidget/ │ │ │ │ │ │ │ └── AppWidgetManagerStub.java │ │ │ │ │ │ ├── audio/ │ │ │ │ │ │ │ └── AudioManagerStub.java │ │ │ │ │ │ ├── backup/ │ │ │ │ │ │ │ └── BackupManagerStub.java │ │ │ │ │ │ ├── battery/ │ │ │ │ │ │ │ └── BatteryStatsStub.java │ │ │ │ │ │ ├── bluetooth/ │ │ │ │ │ │ │ └── BluetoothStub.java │ │ │ │ │ │ ├── clipboard/ │ │ │ │ │ │ │ └── ClipBoardStub.java │ │ │ │ │ │ ├── connectivity/ │ │ │ │ │ │ │ └── ConnectivityStub.java │ │ │ │ │ │ ├── content/ │ │ │ │ │ │ │ ├── ContentServiceStub.java │ │ │ │ │ │ │ └── MethodProxies.java │ │ │ │ │ │ ├── context_hub/ │ │ │ │ │ │ │ └── ContextHubServiceStub.java │ │ │ │ │ │ ├── devicepolicy/ │ │ │ │ │ │ │ └── DevicePolicyManagerStub.java │ │ │ │ │ │ ├── display/ │ │ │ │ │ │ │ └── DisplayStub.java │ │ │ │ │ │ ├── dropbox/ │ │ │ │ │ │ │ └── DropBoxManagerStub.java │ │ │ │ │ │ ├── fingerprint/ │ │ │ │ │ │ │ └── FingerprintManagerStub.java │ │ │ │ │ │ ├── graphics/ │ │ │ │ │ │ │ └── GraphicsStatsStub.java │ │ │ │ │ │ ├── imms/ │ │ │ │ │ │ │ └── MmsStub.java │ │ │ │ │ │ ├── input/ │ │ │ │ │ │ │ ├── InputMethodManagerStub.java │ │ │ │ │ │ │ └── MethodProxies.java │ │ │ │ │ │ ├── isms/ │ │ │ │ │ │ │ └── ISmsStub.java │ │ │ │ │ │ ├── isub/ │ │ │ │ │ │ │ └── ISubStub.java │ │ │ │ │ │ ├── job/ │ │ │ │ │ │ │ └── JobServiceStub.java │ │ │ │ │ │ ├── libcore/ │ │ │ │ │ │ │ ├── LibCoreStub.java │ │ │ │ │ │ │ └── MethodProxies.java │ │ │ │ │ │ ├── location/ │ │ │ │ │ │ │ ├── GPSListenerThread.java │ │ │ │ │ │ │ ├── GPSStatusListenerThread.java │ │ │ │ │ │ │ ├── LocationManagerStub.java │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ └── MockLocationHelper.java │ │ │ │ │ │ ├── media/ │ │ │ │ │ │ │ ├── router/ │ │ │ │ │ │ │ │ └── MediaRouterServiceStub.java │ │ │ │ │ │ │ └── session/ │ │ │ │ │ │ │ └── SessionManagerStub.java │ │ │ │ │ │ ├── mount/ │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ └── MountServiceStub.java │ │ │ │ │ │ ├── network/ │ │ │ │ │ │ │ └── NetworkManagementStub.java │ │ │ │ │ │ ├── notification/ │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ └── NotificationManagerStub.java │ │ │ │ │ │ ├── os/ │ │ │ │ │ │ │ └── DeviceIdentifiersPolicyServiceStub.java │ │ │ │ │ │ ├── persistent_data_block/ │ │ │ │ │ │ │ └── PersistentDataBlockServiceStub.java │ │ │ │ │ │ ├── phonesubinfo/ │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ └── PhoneSubInfoStub.java │ │ │ │ │ │ ├── pm/ │ │ │ │ │ │ │ ├── LauncherAppsStub.java │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ └── PackageManagerStub.java │ │ │ │ │ │ ├── power/ │ │ │ │ │ │ │ └── PowerManagerStub.java │ │ │ │ │ │ ├── restriction/ │ │ │ │ │ │ │ └── RestrictionStub.java │ │ │ │ │ │ ├── search/ │ │ │ │ │ │ │ └── SearchManagerStub.java │ │ │ │ │ │ ├── shortcut/ │ │ │ │ │ │ │ └── ShortcutServiceStub.java │ │ │ │ │ │ ├── telephony/ │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ ├── TelephonyRegistryStub.java │ │ │ │ │ │ │ └── TelephonyStub.java │ │ │ │ │ │ ├── usage/ │ │ │ │ │ │ │ └── UsageStatsManagerStub.java │ │ │ │ │ │ ├── user/ │ │ │ │ │ │ │ └── UserManagerStub.java │ │ │ │ │ │ ├── vibrator/ │ │ │ │ │ │ │ └── VibratorStub.java │ │ │ │ │ │ ├── view/ │ │ │ │ │ │ │ └── AutoFillManagerStub.java │ │ │ │ │ │ ├── wifi/ │ │ │ │ │ │ │ └── WifiManagerStub.java │ │ │ │ │ │ ├── wifi_scanner/ │ │ │ │ │ │ │ ├── GhostWifiScannerImpl.java │ │ │ │ │ │ │ └── WifiScannerStub.java │ │ │ │ │ │ └── window/ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ ├── WindowManagerStub.java │ │ │ │ │ │ └── session/ │ │ │ │ │ │ ├── BaseMethodProxy.java │ │ │ │ │ │ └── WindowSessionPatch.java │ │ │ │ │ ├── secondary/ │ │ │ │ │ │ ├── HackAppUtils.java │ │ │ │ │ │ ├── ProxyServiceFactory.java │ │ │ │ │ │ ├── ServiceConnectionDelegate.java │ │ │ │ │ │ └── StubBinder.java │ │ │ │ │ └── utils/ │ │ │ │ │ └── MethodParameterUtils.java │ │ │ │ ├── interfaces/ │ │ │ │ │ └── IInjector.java │ │ │ │ ├── ipc/ │ │ │ │ │ ├── ActivityClientRecord.java │ │ │ │ │ ├── LocalProxyUtils.java │ │ │ │ │ ├── ProviderCall.java │ │ │ │ │ ├── ServiceManagerNative.java │ │ │ │ │ ├── VAccountManager.java │ │ │ │ │ ├── VActivityManager.java │ │ │ │ │ ├── VDeviceManager.java │ │ │ │ │ ├── VJobScheduler.java │ │ │ │ │ ├── VNotificationManager.java │ │ │ │ │ ├── VPackageManager.java │ │ │ │ │ ├── VirtualLocationManager.java │ │ │ │ │ └── VirtualStorageManager.java │ │ │ │ ├── natives/ │ │ │ │ │ └── NativeMethods.java │ │ │ │ └── stub/ │ │ │ │ ├── AmsTask.java │ │ │ │ ├── ChooseAccountTypeActivity.java │ │ │ │ ├── ChooseTypeAndAccountActivity.java │ │ │ │ ├── ChooserActivity.java │ │ │ │ ├── DaemonJobService.java │ │ │ │ ├── DaemonService.java │ │ │ │ ├── ResolverActivity.java │ │ │ │ ├── ShortcutHandleActivity.java │ │ │ │ ├── StubActivity.java │ │ │ │ ├── StubCP.java │ │ │ │ ├── StubDialog.java │ │ │ │ ├── StubExcludeFromRecentActivity.java │ │ │ │ ├── StubJob.java │ │ │ │ ├── StubPendingActivity.java │ │ │ │ ├── StubPendingReceiver.java │ │ │ │ ├── StubPendingService.java │ │ │ │ └── VASettings.java │ │ │ ├── helper/ │ │ │ │ ├── ArtDexOptimizer.java │ │ │ │ ├── ParcelHelper.java │ │ │ │ ├── PersistenceLayer.java │ │ │ │ ├── collection/ │ │ │ │ │ ├── ArrayMap.java │ │ │ │ │ ├── ArraySet.java │ │ │ │ │ ├── ContainerHelpers.java │ │ │ │ │ ├── IntArray.java │ │ │ │ │ ├── MapCollections.java │ │ │ │ │ ├── SimpleArrayMap.java │ │ │ │ │ └── SparseArray.java │ │ │ │ ├── compat/ │ │ │ │ │ ├── AccountManagerCompat.java │ │ │ │ │ ├── ActivityManagerCompat.java │ │ │ │ │ ├── ApplicationThreadCompat.java │ │ │ │ │ ├── BuildCompat.java │ │ │ │ │ ├── BundleCompat.java │ │ │ │ │ ├── ContentProviderCompat.java │ │ │ │ │ ├── ContentResolverCompat.java │ │ │ │ │ ├── IApplicationThreadCompat.java │ │ │ │ │ ├── NativeLibraryHelperCompat.java │ │ │ │ │ ├── ObjectsCompat.java │ │ │ │ │ ├── PackageParserCompat.java │ │ │ │ │ ├── ParceledListSliceCompat.java │ │ │ │ │ ├── StorageManagerCompat.java │ │ │ │ │ └── SystemPropertiesCompat.java │ │ │ │ └── utils/ │ │ │ │ ├── ArrayUtils.java │ │ │ │ ├── AtomicFile.java │ │ │ │ ├── BitmapUtils.java │ │ │ │ ├── ClassUtils.java │ │ │ │ ├── ComponentUtils.java │ │ │ │ ├── DeviceUtil.java │ │ │ │ ├── DrawableUtils.java │ │ │ │ ├── EncodeUtils.java │ │ │ │ ├── FastXmlSerializer.java │ │ │ │ ├── FileUtils.java │ │ │ │ ├── MD5Utils.java │ │ │ │ ├── OSUtils.java │ │ │ │ ├── Reflect.java │ │ │ │ ├── ReflectException.java │ │ │ │ ├── SchedulerTask.java │ │ │ │ ├── Singleton.java │ │ │ │ ├── VLog.java │ │ │ │ ├── XmlSerializerAndParser.java │ │ │ │ └── marks/ │ │ │ │ ├── FakeDeviceMark.java │ │ │ │ └── FakeLocMark.java │ │ │ ├── os/ │ │ │ │ ├── VBinder.java │ │ │ │ ├── VEnvironment.java │ │ │ │ ├── VUserHandle.java │ │ │ │ ├── VUserInfo.java │ │ │ │ └── VUserManager.java │ │ │ ├── remote/ │ │ │ │ ├── AppTaskInfo.java │ │ │ │ ├── BadgerInfo.java │ │ │ │ ├── InstallResult.java │ │ │ │ ├── InstalledAppInfo.java │ │ │ │ ├── PendingIntentData.java │ │ │ │ ├── PendingResultData.java │ │ │ │ ├── Problem.java │ │ │ │ ├── ReceiverInfo.java │ │ │ │ ├── StubActivityRecord.java │ │ │ │ ├── SyncInfo.java │ │ │ │ ├── VDeviceInfo.java │ │ │ │ ├── VParceledListSlice.java │ │ │ │ └── vloc/ │ │ │ │ ├── VCell.java │ │ │ │ ├── VLocation.java │ │ │ │ └── VWifi.java │ │ │ └── server/ │ │ │ ├── BinderProvider.java │ │ │ ├── IJobScheduler.java │ │ │ ├── ServiceCache.java │ │ │ ├── accounts/ │ │ │ │ ├── RegisteredServicesParser.java │ │ │ │ ├── VAccount.java │ │ │ │ ├── VAccountManagerService.java │ │ │ │ ├── VContentService.java │ │ │ │ └── VSyncRecord.java │ │ │ ├── am/ │ │ │ │ ├── ActivityRecord.java │ │ │ │ ├── ActivityStack.java │ │ │ │ ├── AppBindRecord.java │ │ │ │ ├── AttributeCache.java │ │ │ │ ├── BroadcastSystem.java │ │ │ │ ├── ConnectionRecord.java │ │ │ │ ├── PendingIntents.java │ │ │ │ ├── ProcessMap.java │ │ │ │ ├── ProcessRecord.java │ │ │ │ ├── ServiceRecord.java │ │ │ │ ├── TaskRecord.java │ │ │ │ ├── UidSystem.java │ │ │ │ └── VActivityManagerService.java │ │ │ ├── device/ │ │ │ │ ├── DeviceInfoPersistenceLayer.java │ │ │ │ └── VDeviceManagerService.java │ │ │ ├── job/ │ │ │ │ └── VJobSchedulerService.java │ │ │ ├── location/ │ │ │ │ └── VirtualLocationService.java │ │ │ ├── notification/ │ │ │ │ ├── NotificationCompat.java │ │ │ │ ├── NotificationCompatCompatV14.java │ │ │ │ ├── NotificationCompatCompatV21.java │ │ │ │ ├── NotificationFixer.java │ │ │ │ ├── PendIntentCompat.java │ │ │ │ ├── ReflectionActionCompat.java │ │ │ │ ├── RemoteViewsFixer.java │ │ │ │ ├── VNotificationManagerService.java │ │ │ │ └── WidthCompat.java │ │ │ ├── pm/ │ │ │ │ ├── FastImmutableArraySet.java │ │ │ │ ├── IntentResolver.java │ │ │ │ ├── PackageCacheManager.java │ │ │ │ ├── PackagePersistenceLayer.java │ │ │ │ ├── PackageSetting.java │ │ │ │ ├── PackageUserState.java │ │ │ │ ├── PrivilegeAppOptimizer.java │ │ │ │ ├── ProviderIntentResolver.java │ │ │ │ ├── VAppManagerService.java │ │ │ │ ├── VPackageManagerService.java │ │ │ │ ├── VUserManagerService.java │ │ │ │ ├── installer/ │ │ │ │ │ ├── FileBridge.java │ │ │ │ │ ├── PackageHelper.java │ │ │ │ │ ├── PackageInstallInfo.java │ │ │ │ │ ├── PackageInstallObserver.java │ │ │ │ │ ├── PackageInstallerSession.java │ │ │ │ │ ├── SessionInfo.java │ │ │ │ │ ├── SessionParams.java │ │ │ │ │ └── VPackageInstallerService.java │ │ │ │ └── parser/ │ │ │ │ ├── PackageParserEx.java │ │ │ │ └── VPackage.java │ │ │ ├── secondary/ │ │ │ │ ├── BinderDelegateService.java │ │ │ │ └── FakeIdentityBinder.java │ │ │ └── vs/ │ │ │ ├── VSConfig.java │ │ │ ├── VSPersistenceLayer.java │ │ │ └── VirtualStorageService.java │ │ └── mirror/ │ │ ├── MethodParams.java │ │ ├── MethodReflectParams.java │ │ ├── RefBoolean.java │ │ ├── RefClass.java │ │ ├── RefConstructor.java │ │ ├── RefDouble.java │ │ ├── RefFloat.java │ │ ├── RefInt.java │ │ ├── RefLong.java │ │ ├── RefMethod.java │ │ ├── RefObject.java │ │ ├── RefStaticInt.java │ │ ├── RefStaticMethod.java │ │ ├── RefStaticObject.java │ │ ├── android/ │ │ │ ├── accounts/ │ │ │ │ └── IAccountManager.java │ │ │ ├── app/ │ │ │ │ ├── Activity.java │ │ │ │ ├── ActivityManagerNative.java │ │ │ │ ├── ActivityManagerOreo.java │ │ │ │ ├── ActivityThread.java │ │ │ │ ├── ActivityThreadNMR1.java │ │ │ │ ├── ApplicationThreadNative.java │ │ │ │ ├── ContextImpl.java │ │ │ │ ├── ContextImplICS.java │ │ │ │ ├── ContextImplKitkat.java │ │ │ │ ├── IActivityManager.java │ │ │ │ ├── IActivityManagerICS.java │ │ │ │ ├── IActivityManagerL.java │ │ │ │ ├── IActivityManagerN.java │ │ │ │ ├── IActivityTaskManager.java │ │ │ │ ├── IAlarmManager.java │ │ │ │ ├── IApplicationThread.java │ │ │ │ ├── IApplicationThreadICSMR1.java │ │ │ │ ├── IApplicationThreadJBMR1.java │ │ │ │ ├── IApplicationThreadKitkat.java │ │ │ │ ├── IApplicationThreadOreo.java │ │ │ │ ├── ISearchManager.java │ │ │ │ ├── IServiceConnectionO.java │ │ │ │ ├── IUsageStatsManager.java │ │ │ │ ├── LoadedApk.java │ │ │ │ ├── LoadedApkHuaWei.java │ │ │ │ ├── Notification.java │ │ │ │ ├── NotificationL.java │ │ │ │ ├── NotificationM.java │ │ │ │ ├── NotificationManager.java │ │ │ │ ├── PendingIntentJBMR2.java │ │ │ │ ├── ServiceStartArgs.java │ │ │ │ ├── admin/ │ │ │ │ │ └── IDevicePolicyManager.java │ │ │ │ ├── backup/ │ │ │ │ │ └── IBackupManager.java │ │ │ │ └── job/ │ │ │ │ ├── IJobScheduler.java │ │ │ │ ├── JobInfo.java │ │ │ │ ├── JobParameters.java │ │ │ │ └── JobWorkItem.java │ │ │ ├── bluetooth/ │ │ │ │ └── IBluetooth.java │ │ │ ├── content/ │ │ │ │ ├── BroadcastReceiver.java │ │ │ │ ├── ClipboardManager.java │ │ │ │ ├── ClipboardManagerOreo.java │ │ │ │ ├── ContentProviderClient.java │ │ │ │ ├── ContentProviderHolderOreo.java │ │ │ │ ├── ContentProviderNative.java │ │ │ │ ├── ContentResolver.java │ │ │ │ ├── ContentResolverJBMR2.java │ │ │ │ ├── IClipboard.java │ │ │ │ ├── IContentProvider.java │ │ │ │ ├── IContentService.java │ │ │ │ ├── IIntentReceiver.java │ │ │ │ ├── IIntentReceiverJB.java │ │ │ │ ├── IRestrictionsManager.java │ │ │ │ ├── IntentFilter.java │ │ │ │ ├── SyncAdapterType.java │ │ │ │ ├── SyncAdapterTypeN.java │ │ │ │ ├── SyncInfo.java │ │ │ │ ├── SyncRequest.java │ │ │ │ ├── pm/ │ │ │ │ │ ├── ApplicationInfoL.java │ │ │ │ │ ├── ApplicationInfoN.java │ │ │ │ │ ├── ILauncherApps.java │ │ │ │ │ ├── IShortcutService.java │ │ │ │ │ ├── LauncherApps.java │ │ │ │ │ ├── PackageInstaller.java │ │ │ │ │ ├── PackageParser.java │ │ │ │ │ ├── PackageParserJellyBean.java │ │ │ │ │ ├── PackageParserJellyBean17.java │ │ │ │ │ ├── PackageParserLollipop.java │ │ │ │ │ ├── PackageParserLollipop22.java │ │ │ │ │ ├── PackageParserMarshmallow.java │ │ │ │ │ ├── PackageParserNougat.java │ │ │ │ │ ├── PackageParserP28.java │ │ │ │ │ ├── PackageUserState.java │ │ │ │ │ ├── ParceledListSlice.java │ │ │ │ │ ├── ParceledListSliceJBMR2.java │ │ │ │ │ ├── ShortcutInfo.java │ │ │ │ │ └── UserInfo.java │ │ │ │ └── res/ │ │ │ │ ├── AssetManager.java │ │ │ │ └── CompatibilityInfo.java │ │ │ ├── ddm/ │ │ │ │ ├── DdmHandleAppName.java │ │ │ │ └── DdmHandleAppNameJBMR1.java │ │ │ ├── graphics/ │ │ │ │ └── drawable/ │ │ │ │ └── Icon.java │ │ │ ├── hardware/ │ │ │ │ ├── display/ │ │ │ │ │ ├── DisplayManagerGlobal.java │ │ │ │ │ └── IDisplayManager.java │ │ │ │ ├── fingerprint/ │ │ │ │ │ └── IFingerprintService.java │ │ │ │ └── location/ │ │ │ │ └── IContextHubService.java │ │ │ ├── location/ │ │ │ │ ├── ILocationListener.java │ │ │ │ ├── ILocationManager.java │ │ │ │ ├── LocationManager.java │ │ │ │ └── LocationRequestL.java │ │ │ ├── media/ │ │ │ │ ├── AudioManager.java │ │ │ │ ├── IAudioService.java │ │ │ │ ├── IMediaRouterService.java │ │ │ │ ├── MediaRouter.java │ │ │ │ └── session/ │ │ │ │ └── ISessionManager.java │ │ │ ├── net/ │ │ │ │ ├── IConnectivityManager.java │ │ │ │ ├── NetworkInfo.java │ │ │ │ └── wifi/ │ │ │ │ ├── IWifiManager.java │ │ │ │ ├── WifiInfo.java │ │ │ │ ├── WifiScanner.java │ │ │ │ └── WifiSsid.java │ │ │ ├── os/ │ │ │ │ ├── BaseBundle.java │ │ │ │ ├── Build.java │ │ │ │ ├── Bundle.java │ │ │ │ ├── BundleICS.java │ │ │ │ ├── Handler.java │ │ │ │ ├── IDeviceIdentifiersPolicyService.java │ │ │ │ ├── INetworkManagementService.java │ │ │ │ ├── IPowerManager.java │ │ │ │ ├── IUserManager.java │ │ │ │ ├── Message.java │ │ │ │ ├── Process.java │ │ │ │ ├── ServiceManager.java │ │ │ │ ├── StrictMode.java │ │ │ │ ├── UserHandle.java │ │ │ │ ├── mount/ │ │ │ │ │ └── IMountService.java │ │ │ │ └── storage/ │ │ │ │ └── IStorageManager.java │ │ │ ├── providers/ │ │ │ │ └── Settings.java │ │ │ ├── renderscript/ │ │ │ │ └── RenderScriptCacheDir.java │ │ │ ├── rms/ │ │ │ │ └── resource/ │ │ │ │ ├── ReceiverResourceLP.java │ │ │ │ ├── ReceiverResourceM.java │ │ │ │ └── ReceiverResourceN.java │ │ │ ├── service/ │ │ │ │ └── persistentdata/ │ │ │ │ └── IPersistentDataBlockService.java │ │ │ ├── telephony/ │ │ │ │ ├── CellIdentityCdma.java │ │ │ │ ├── CellIdentityGsm.java │ │ │ │ ├── CellInfoCdma.java │ │ │ │ ├── CellInfoGsm.java │ │ │ │ ├── CellSignalStrengthCdma.java │ │ │ │ ├── CellSignalStrengthGsm.java │ │ │ │ └── NeighboringCellInfo.java │ │ │ ├── util/ │ │ │ │ └── Singleton.java │ │ │ ├── view/ │ │ │ │ ├── Display.java │ │ │ │ ├── HardwareRenderer.java │ │ │ │ ├── IAutoFillManager.java │ │ │ │ ├── IGraphicsStats.java │ │ │ │ ├── IWindowManager.java │ │ │ │ ├── RenderScript.java │ │ │ │ ├── SurfaceControl.java │ │ │ │ ├── ThreadedRenderer.java │ │ │ │ └── WindowManagerGlobal.java │ │ │ ├── webkit/ │ │ │ │ ├── IWebViewUpdateService.java │ │ │ │ └── WebViewFactory.java │ │ │ └── widget/ │ │ │ ├── RemoteViews.java │ │ │ └── Toast.java │ │ ├── com/ │ │ │ └── android/ │ │ │ └── internal/ │ │ │ ├── R_Hide.java │ │ │ ├── app/ │ │ │ │ ├── IAppOpsService.java │ │ │ │ └── IBatteryStats.java │ │ │ ├── appwidget/ │ │ │ │ └── IAppWidgetService.java │ │ │ ├── content/ │ │ │ │ ├── NativeLibraryHelper.java │ │ │ │ └── ReferrerIntent.java │ │ │ ├── os/ │ │ │ │ ├── IDropBoxManagerService.java │ │ │ │ ├── IVibratorService.java │ │ │ │ ├── UserManager.java │ │ │ │ └── health/ │ │ │ │ └── SystemHealthManager.java │ │ │ ├── policy/ │ │ │ │ └── PhoneWindow.java │ │ │ ├── telephony/ │ │ │ │ ├── IMms.java │ │ │ │ ├── IPhoneSubInfo.java │ │ │ │ ├── ISms.java │ │ │ │ ├── ISub.java │ │ │ │ ├── ITelephony.java │ │ │ │ ├── ITelephonyRegistry.java │ │ │ │ └── PhoneConstantsMtk.java │ │ │ └── view/ │ │ │ ├── IInputMethodManager.java │ │ │ └── inputmethod/ │ │ │ └── InputMethodManager.java │ │ ├── dalvik/ │ │ │ └── system/ │ │ │ └── VMRuntime.java │ │ ├── java/ │ │ │ └── lang/ │ │ │ ├── ThreadGroup.java │ │ │ └── ThreadGroupN.java │ │ ├── libcore/ │ │ │ └── io/ │ │ │ ├── ForwardingOs.java │ │ │ ├── Libcore.java │ │ │ └── Os.java │ │ └── mirror/ │ │ └── android/ │ │ └── security/ │ │ └── net/ │ │ └── config/ │ │ └── ApplicationConfig.java │ ├── jni/ │ │ ├── A64Inlinehook/ │ │ │ ├── And64InlineHook.cpp │ │ │ └── And64InlineHook.hpp │ │ ├── Android.mk │ │ ├── Application.mk │ │ ├── Foundation/ │ │ │ ├── IOUniformer.cpp │ │ │ ├── IOUniformer.h │ │ │ ├── Path.cpp │ │ │ ├── Path.h │ │ │ ├── SandboxFs.cpp │ │ │ ├── SandboxFs.h │ │ │ ├── SymbolFinder.cpp │ │ │ ├── SymbolFinder.h │ │ │ ├── VMPatch.cpp │ │ │ ├── VMPatch.h │ │ │ ├── fake_dlfcn.cpp │ │ │ └── fake_dlfcn.h │ │ ├── Jni/ │ │ │ ├── Helper.h │ │ │ ├── VAJni.cpp │ │ │ └── VAJni.h │ │ ├── Substrate/ │ │ │ ├── Buffer.hpp │ │ │ ├── CydiaSubstrate.h │ │ │ ├── SubstrateARM.hpp │ │ │ ├── SubstrateDebug.cpp │ │ │ ├── SubstrateDebug.hpp │ │ │ ├── SubstrateHook.cpp │ │ │ ├── SubstrateHook.h │ │ │ ├── SubstrateLog.hpp │ │ │ ├── SubstratePosixMemory.cpp │ │ │ ├── SubstrateX86.hpp │ │ │ ├── hde64.c │ │ │ ├── hde64.h │ │ │ └── table64.h │ │ └── fb/ │ │ ├── Android.mk │ │ ├── BUCK │ │ ├── Doxyfile │ │ ├── assert.cpp │ │ ├── include/ │ │ │ ├── fb/ │ │ │ │ ├── ALog.h │ │ │ │ ├── Build.h │ │ │ │ ├── Countable.h │ │ │ │ ├── Doxyfile │ │ │ │ ├── Environment.h │ │ │ │ ├── ProgramLocation.h │ │ │ │ ├── RefPtr.h │ │ │ │ ├── StaticInitialized.h │ │ │ │ ├── ThreadLocal.h │ │ │ │ ├── assert.h │ │ │ │ ├── fbjni/ │ │ │ │ │ ├── Boxed.h │ │ │ │ │ ├── ByteBuffer.h │ │ │ │ │ ├── Common.h │ │ │ │ │ ├── Context.h │ │ │ │ │ ├── CoreClasses-inl.h │ │ │ │ │ ├── CoreClasses.h │ │ │ │ │ ├── Exceptions.h │ │ │ │ │ ├── File.h │ │ │ │ │ ├── Hybrid.h │ │ │ │ │ ├── Iterator-inl.h │ │ │ │ │ ├── Iterator.h │ │ │ │ │ ├── JThread.h │ │ │ │ │ ├── JWeakReference.h │ │ │ │ │ ├── Meta-forward.h │ │ │ │ │ ├── Meta-inl.h │ │ │ │ │ ├── Meta.h │ │ │ │ │ ├── MetaConvert.h │ │ │ │ │ ├── NativeRunnable.h │ │ │ │ │ ├── ReferenceAllocators-inl.h │ │ │ │ │ ├── ReferenceAllocators.h │ │ │ │ │ ├── References-forward.h │ │ │ │ │ ├── References-inl.h │ │ │ │ │ ├── References.h │ │ │ │ │ ├── Registration-inl.h │ │ │ │ │ ├── Registration.h │ │ │ │ │ └── TypeTraits.h │ │ │ │ ├── fbjni.h │ │ │ │ ├── log.h │ │ │ │ ├── lyra.h │ │ │ │ ├── noncopyable.h │ │ │ │ ├── nonmovable.h │ │ │ │ └── visibility.h │ │ │ └── jni/ │ │ │ ├── Countable.h │ │ │ ├── GlobalReference.h │ │ │ ├── JniTerminateHandler.h │ │ │ ├── LocalReference.h │ │ │ ├── LocalString.h │ │ │ ├── Registration.h │ │ │ ├── WeakReference.h │ │ │ └── jni_helpers.h │ │ ├── jni/ │ │ │ ├── ByteBuffer.cpp │ │ │ ├── Countable.cpp │ │ │ ├── Environment.cpp │ │ │ ├── Exceptions.cpp │ │ │ ├── Hybrid.cpp │ │ │ ├── LocalString.cpp │ │ │ ├── OnLoad.cpp │ │ │ ├── References.cpp │ │ │ ├── WeakReference.cpp │ │ │ ├── android/ │ │ │ │ ├── CpuCapabilities.cpp │ │ │ │ └── ReferenceChecking.cpp │ │ │ ├── fbjni.cpp │ │ │ ├── java/ │ │ │ │ ├── BUCK │ │ │ │ ├── CppException.java │ │ │ │ ├── CppSystemErrorException.java │ │ │ │ └── UnknownCppException.java │ │ │ └── jni_helpers.cpp │ │ ├── log.cpp │ │ ├── lyra/ │ │ │ └── lyra.cpp │ │ └── onload.cpp │ └── res/ │ ├── layout/ │ │ ├── app_not_authorized.xml │ │ ├── choose_account_row.xml │ │ ├── choose_account_type.xml │ │ ├── choose_type_and_account.xml │ │ ├── custom_notification.xml │ │ ├── custom_notification_lite.xml │ │ └── resolve_list_item.xml │ ├── values/ │ │ ├── dimens.xml │ │ ├── integer.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── values-ru/ │ │ └── strings.xml │ └── values-uk/ │ └── strings.xml └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # replace patreon: weishu open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: http://paypal.me/virtualxposed ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report_cn.md ================================================ --- name: BUG反馈 about: 中文BUG反馈 --- **反馈BUG之前,先issue里面搜看看有没有别人已经反馈过,重复的不予处理!!** ## 问题描述 (请尽量详细地描述你遇到的问题) ## 复现步骤 (请分步骤描述如何复现这个BUG,非毕现BUG请给出如何能大概率复现的步骤) ## 环境 机型: 系统版本: ROM版本:(请区分内测版和开发版稳定版,除稳定版本外不予修复) Xposed 插件以及插件版本: VirtualXposed版本: ## 补充 (别的需要描述的内容) **写完之后,请自己再读一遍自己写的,如果你自己都读不懂,就不用说修复了** ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request_cn.md ================================================ --- name: 意见和建议 about: Feature中文版 --- **BUG反馈请不要用这个模版,否则直接关闭!!** ## 场景描述 (请详细和精确地表述你的使用场景) ## 希望的解决方案 (你希望如何解决这个问题?) ## 其他信息 (其他你认为有用的信息) ================================================ FILE: .github/issue-close-app.yml ================================================ # Comment that will be sent if an issue is judged to be closed comment: "This issue is closed because it does not meet our issue template/为方便解决问题,请使用 issue 模版提交问题。" issueConfigs: # There can be several configs for different kind of issues. - content: # Example 1: bug report - "Expected behavior" - "To Reproduce" - "Describe the bug" - content: # Example 2: feature request - "Describe the solution you'd like" - content: - "问题描述" - "复现步骤" - "环境" - content: - "场景描述" - "希望的解决方案" ================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an issue becomes stale daysUntilStale: 20 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - bug - security # Label to use when marking an issue as stale staleLabel: wontfix # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false ================================================ FILE: .github/workflows/android.yml ================================================ name: Android CI on: [pull_request, push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Checkout submodules uses: srt32/git-actions@v0.0.3 with: args: git submodule update --init --recursive - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: Build with Gradle run: cd VirtualApp && ./gradlew assembleRelease - name: ls run: ls - name: Archive production artifacts uses: actions/upload-artifact@v1 with: name: compiled path: VirtualApp/app/build/ ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.ap_ # Files for the ART/Dalvik VM *.dex .idea # Java class files *.class # Generated files bin/ gen/ out/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # Intellij *.iml .idea/workspace.xml # Keystore files *.jks ================================================ FILE: .gitmodules ================================================ [submodule "VirtualApp/launcher"] path = VirtualApp/launcher url = https://github.com/android-hacker/Launcher3.git ================================================ FILE: .travis.yml ================================================ language: android android: components: - tools - build-tools-28.0.3 - android-27 - android-28 - extra-android-m2repository - extra-android-support install: - echo y | sdkmanager 'ndk-bundle' - echo y | sdkmanager 'cmake;3.6.4111459' - echo y | sdkmanager 'lldb;3.0' before_install: - chmod +x ./VirtualApp/gradlew script: - cd VirtualApp - ./gradlew assembleRelease ================================================ FILE: CHINESE.md ================================================ [![Build Status](https://travis-ci.org/android-hacker/VirtualXposed.svg?branch=exposed)](https://travis-ci.org/android-hacker/VirtualXposed) 简介 ----- **VirtualXposed** 是基于[VirtualApp](https://github.com/asLody/VirtualApp) 和 [epic](https://github.com/tiann/epic) 在**非ROOT**环境下运行Xposed模块的实现(支持5.0~10.0)。 与 Xposed 相比,目前 VirtualXposed 有两个限制: 1. 不支持修改系统(可以修改普通APP中对系统API的调用),因此重力工具箱,应用控制器等无法使用。 2. 暂不支持资源HOOK,因此资源钩子不会起任何作用;使用资源HOOK的模块,相应的功能不会生效。 警告 ------- 本项目使用的 VirtualApp 不允许用于商业用途,并且其内部的 VirtualApp 版本已经过时,如果有这个需求,为了贵公司的长期稳定发展,请使用商业授权,联系 Lody (imlody@foxmail.com)即可。 使用 ---------- ## 准备 首先在 [发布页面](https://github.com/android-hacker/VirtualXposed/releases) 下载最新的VAExposed安装包安装到手机。 ## 安装模块 打开 VirtualXposed,在里面安装要使用的APP,以及相应的Xposed模块即可。 注意:**所有的工作(安装Xposed模块,安装APP)必须在 VirtualXposed中**进行,否则Xposed模块不会有任何作用!比如,将微信直接安装在系统上(而非VirtualXposed中),防撤回安装在VirtualXposed中;或者把微信安装在VirtualXposed上,防撤回插件直接安装在系统上;或者两者都直接安装在系统上,**均不会起任何作用**。 在VirtualXposed中安装App有两种方式: 1. 直接复制已经在系统中安装好的APP,比如如果你系统中装了微信,那么可以直接复制一份。 2. 通过外置存储直接安装APK文件;点主界面的底部按钮-添加应用,然后选择后面两个TAB即可。 在VirtualXposed中安装Xposed模块,可以跟安装正常的APK一样,以上两种安装App的方式也适用于安装Xposed模块。不过,你也可以通过VirtualXposed中内置的XposedInstaller来安装和管理模块,跟通常的XposedInstaller使用方式一样;去下载页面,下载安装即可。 ## 亲测可用的模块 - [XPrivacyLua][xpl]: Really simple to use privacy manager for Android 6.0 Marshmallow and later. - [XInsta][xinsta]: Instagram module(Feed downing, stories downloading, etc). - [Minminguard][minminguard]: Completely remove both the ads inside apps and the empty space caused by those ads. - [YouTube AdAway][yta]: Get rid of ads on the official YouTube App. - [微X模块][wx]: 微信模块,功能强大。 - [畅玩微信][cwwx]: 微信模块新秀,功能丰富。 - [微信巫师][wxws]: 微信模块,项目开源,代码优秀。 - [MDWechat][mdwechat]: 微信美化模块,可以把微信整成MD风格。 - [应用变量][yybl]: 可以用来进行机型修改,比如王者荣耀高帧率;QQ空间修改小尾巴等。 - [音量增强器][ylzqq]: 网易云音乐模块,非常好用,低调。 - [微信学英语][wxxyy]: 自动把微信消息翻译为英语,非常实用。 - [情迁抢包][qqqb]: 微信QQ抢红包模块。 - [微信跳一跳助手][ttzs]: 微信跳一跳游戏辅助模块。 - [步数修改器][bsxg]: 运动步数修改模块。 - [模拟位置][mnwz]: 虚拟定位模块,稳定好用。 - [指纹支付][zwzf]: 对不支持指纹支付但系统本身有指纹的手机开启指纹支付的模块。 - [QQ精简模块 2.0][qqjj]: QQ模块,不仅可以精简QQ,还能防撤回,防闪照。 - [微信增强插件][wxzqcj]: 微信模块,VXP内最稳定的微信模块;如无特殊需求建议用这个。 - [QX模块][qx]: QQ模块,防撤回抢红包斗图一应俱全。 - [QQ斗图神器][qqdtsq]: 各种表情,斗图神器。 - [微信斗图神器][wxdtsq]: 斗图神器,微信用的。 - [大圣净化][dsjh]: 去广告神器,推荐使用。 真正能用的模块远不止这么多,要用的话可以自己测试;如果你发现某些模块可以用但不在上面的列表中,欢迎给我发个PR。 其他 ------- ### GameGuardian VirtualXposed也支持GG修改器,如果你需要用GG,那么请使用GG专版(可以在发布页面下载,带 For_GameGuardian后缀)。 [GG修改器使用视频教程](https://gameguardian.net/forum/gallery/image/437-no-root-via-virtualxposed-without-error-105-gameguardian/) ### VirusTotal VirusTotal 还有一些其他的杀毒引擎检测到VirtualXposed有病毒,这一点我该不承认,而且我觉得这些愚蠢的杀毒引擎是在胡扯。请看[我的说明](https://github.com/android-hacker/VirtualXposed/issues/10). 而且,VirtualXposed是开源的,你可以直接查看代码;我可以打包票,VirtualXposed本身没有做任何有害的事情(但是它确实有这个能力,所以请不要下载不明来源的Xposed插件)。 如果你还是不放心,那么你可以使用 [0.8.7版本](https://github.com/android-hacker/VirtualXposed/releases/tag/0.8.7), 这个版本杀毒引擎的检测结果是安全的(简直就是扯淡)。 支持和加入 ------------ 目前VirtualXposed 还不完善,如果你对非ROOT下实现Xposed感兴趣;欢迎加入!你可以通过如下方式来支持: 1. 直接贡献代码,提供Feature,修复BUG! 2. 使用你拥有的手机,安装你常用的Xposed模块,反馈不可用情况;协助帮忙解决兼容性问题! 3. 提出体验上,功能上的建议,帮助完善VirtualXposed! 致谢 ------ 1. [VirtualApp](https://github.com/asLody/VirtualApp) 2. [Xposed](https://github.com/rovo89/Xposed) [wx]: http://repo.xposed.info/module/com.fkzhang.wechatxposed [qx]: http://repo.xposed.info/module/com.fkzhang.qqxposed [wxws]: https://github.com/Gh0u1L5/WechatMagician/releases [yybl]: https://www.coolapk.com/apk/com.sollyu.xposed.hook.model [ylzqq]: https://github.com/bin456789/Unblock163MusicClient-Xposed/releases [wxxyy]: https://www.coolapk.com/apk/com.hiwechart.translate [qqqb]: http://repo.xposed.info/module/cn.qssq666.redpacket [ttzs]: http://repo.xposed.info/module/com.emily.mmjumphelper [mnwz]: https://www.coolapk.com/apk/com.rong.xposed.fakelocation [zwzf]: https://github.com/android-hacker/Xposed-Fingerprint-pay/releases [bsxg]: https://www.coolapk.com/apk/com.specher.sm [mdwechat]: https://github.com/Blankeer/MDWechat [wxzqcj]:https://github.com/firesunCN/WechatEnhancement [qqjj]: https://www.coolapk.com/apk/me.zpp0196.qqsimple [qqdtsq]: https://www.coolapk.com/apk/x.hook.qqemoji [wxdtsq]: https://www.coolapk.com/apk/x.hook.emojihook [dsjh]: https://wiki.ad-gone.com/archives/32 [xpl]: https://github.com/android-hacker/VirtualXposed/wiki/Privacy-control(XPrivacyLua) [minminguard]: http://repo.xposed.info/module/tw.fatminmin.xposed.minminguard [yta]: http://repo.xposed.info/module/ma.wanam.youtubeadaway [xinsta]: http://repo.xposed.info/module/com.ihelp101.instagram [cwwx]: http://repo.xposed.info/module/com.example.wx_plug_in3 ================================================ FILE: LICENSE.txt ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ [![Build Status](https://travis-ci.org/android-hacker/VirtualXposed.svg?branch=exposed)](https://travis-ci.org/android-hacker/VirtualXposed) [中文文档](CHINESE.md "中文") Introduction ------------ **VirtualXposed** is a simple App based on [VirtualApp](https://github.com/asLody/VirtualApp) and [epic](https://github.com/tiann/epic) that allows you to use an Xposed Module without needing to root, unlock the bootloader, or flash a custom system image. (Supports Android 5.0~10.0) The only two restriction of VirtualXposed are: 1. Unable to modify system, so any Module which modifies system won't be able to work properly. 2. Currently resource hooks are not supported. (Theming modules use Resource Hooks). Warning ----------- Usage for Commercial Purposes are not allowed!!! Please refer to VirtualApp's [declaration](https://github.com/asLody/VirtualApp). Usage ------- ### Preparation Download the latest APK from the [release page](https://github.com/android-hacker/VirtualXposed/releases), and install it on your Android device. ### Install APP and Xposed Module Open VirtualXposed, Click on the **Drawer Button** at the bottom of home page(Or long click the screen), add your desired APP and Xposed Module to VirtualXposed's virtual environment. Note: **All operations(installation of Xposed Module, APP)must be done in VirtualXposed**, otherwise the Xposed Module installed won't take effect. For example, if you install the YouTube app on your system (Your phone's original system, not in VirtualXposed), and then install YouTube AdAway (A YouTube Xposed Module) in VirtualXposed; or you install YouTube in VirtualXposed, and install YouTube AdAway on original system; or both of them are installed on original system, **neither of these three cases will work!** ![How to install](https://raw.githubusercontent.com/tiann/arts/master/vxp_install.gif) There are three ways to install an APP or Xposed Module to VirtualXposed: 1. **Clone an installed app from your original system.** (Click Button at bottom of home page, then click Add App, the first page shows a list of installed apps.) 2. **Install via an APK file.** (Click Button at bottom of home page, then click Add App, the second page shows APKs found in your sdcard) 3. **Install via an external file chooser.** (Click Button at bottom of home page, then click Add App, use the floating action button to choose an APK file to install) For Xposed Module, You can install it from Xposed Installer, too. ### Activate the Xposed Module Open Xposed Installer in VirtualXposed, go to the module fragment, check the module you want to use: ![How to activate module](https://raw.githubusercontent.com/tiann/arts/master/vxp_activate.gif) ### Reboot You only need to reboot VirtualXposed, **There's no need to reboot your phone**; Just click Settings in home page of VirtualXposed, click `Reboot` button, and VirtualXposed will reboot in a blink. ![How to reboot](https://raw.githubusercontent.com/tiann/arts/master/vxp_reboot.gif) Supported Modules ------------------------- Almost all modules except system-relevant are supported, please try it by yourself :) Others ------- ### GameGuardian VirtualXposed also supports GameGuardian, **you should use the separate version for GameGuardian**.(Download it in release page). [Video Tutorial](https://gameguardian.net/forum/gallery/image/437-no-root-via-virtualxposed-without-error-105-gameguardian/) ### VirusTotal VirusTotal might report VirtualXposed as a malware, it is stupid, you can refer to my [explanation](https://github.com/android-hacker/VirtualXposed/issues/10). And obviously, VirtualXposed is open source, so you can refer to the source code. I am sure that it is safe to use. If you still couldn't believe in me, you can install version [0.8.7](https://github.com/android-hacker/VirtualXposed/releases/tag/0.8.7); VirusTotal reports this version as safe. Support ----------- Contributions to VirtualXposed are always welcomed!! For Developers -------------- - [File a bug](https://github.com/android-hacker/exposed/issues) - [Wiki](https://github.com/android-hacker/VirtualXposed/wiki) - [Telegram](https://t.me/vxp_devs) Credits ------- 1. [VirtualApp](https://github.com/asLody/VirtualApp) 2. [Xposed](https://github.com/rovo89/Xposed) 3. [And64InlineHook](https://github.com/Rprop/And64InlineHook) ================================================ FILE: VirtualApp/.gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .idea .DS_Store /build /captures ================================================ FILE: VirtualApp/app/.gitignore ================================================ /build ================================================ FILE: VirtualApp/app/build.gradle ================================================ apply plugin: 'com.android.application' Properties properties = new Properties() def localProp = file(project.rootProject.file('local.properties')) if (localProp.exists()) { properties.load(localProp.newDataInputStream()) } def keyFile = file(properties.getProperty("keystore.path") ?: "/tmp/does_not_exist") android { signingConfigs { config { keyAlias properties.getProperty("keystore.alias") keyPassword properties.getProperty("keystore.pwd") storeFile keyFile storePassword properties.getProperty("keystore.alias_pwd") } } compileSdkVersion 28 buildToolsVersion '28.0.3' defaultConfig { applicationId "io.va.exposed64" minSdkVersion 21 targetSdkVersion 23 versionCode 220 versionName "0.22.0" multiDexEnabled false android { defaultConfig { ndk { abiFilters "arm64-v8a", "x86_64" } } } } // https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html?utm_source=android-studio flavorDimensions 'main' productFlavors { aosp { dimension 'main' //matchingFallbacks ['aosp'] if (keyFile.exists()) { signingConfig signingConfigs.config } } fdroid { dimension 'main' } } sourceSets { main { jniLibs.srcDirs = ['libs'] } } buildTypes { release { minifyEnabled false debuggable false } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { checkReleaseBuilds false // Or, if you prefer, you can continue to check for errors in release builds, // but continue the build even when errors are found: abortOnError false } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation project(':lib') implementation project(':launcher') //Android Lib implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:appcompat-v7:27.1.1' implementation 'com.android.support:recyclerview-v7:27.1.1' implementation 'com.android.support:design:27.1.1' //Promise Support implementation 'org.jdeferred:jdeferred-android-aar:1.2.4' // ThirdParty implementation 'com.jonathanfinerty.once:once:1.0.3' def appCenterSdkVersion = '3.0.0' aospImplementation("com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}") aospImplementation("com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}") implementation 'com.kyleduo.switchbutton:library:1.4.6' implementation 'com.allenliu.versionchecklib:library:1.8.3' implementation 'com.github.medyo:android-about-page:1.2.2' implementation 'moe.feng:AlipayZeroSdk:1.1' //Glide implementation ('com.github.bumptech.glide:glide:4.8.0') { exclude(group: "com.android.support") } annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0' } ================================================ FILE: VirtualApp/app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/lody/Desktop/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} -keep class com.amap.api.maps.**{*;} -keep class com.autonavi.**{*;} -keep class com.amap.api.trace.**{*;} #定位 -keep class com.amap.api.location.**{*;} -keep class com.amap.api.fence.**{*;} -keep class com.autonavi.aps.amapapi.model.**{*;} #搜索 -keep class com.amap.api.services.**{*;} #2D地图 -keep class com.amap.api.maps2d.**{*;} -keep class com.amap.api.mapcore2d.**{*;} #导航 -keep class com.amap.api.navi.**{*;} -keep class com.autonavi.**{*;} ##--Glide-- -keep class com.bumptech.glide.**{*;} -keep public class * implements com.bumptech.glide.module.GlideModule -keep public class * extends com.bumptech.glide.module.AppGlideModule -keep public enum com.bumptech.glide.load.ImageHeaderParser$** { **[] $VALUES; public *; } ================================================ FILE: VirtualApp/app/src/aosp/java/io/virtualapp/delegate/MyCrashHandler.java ================================================ package io.virtualapp.delegate; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.util.Log; import com.lody.virtual.client.VClientImpl; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.os.VUserHandle; import com.lody.virtual.remote.InstalledAppInfo; import com.microsoft.appcenter.crashes.Crashes; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; /** * @author weishu * @date 2019/2/25. */ public class MyCrashHandler extends BaseCrashHandler { private static final String CRASH_SP = "vxp_crash"; private static final String KEY_LAST_CRASH_TIME = "last_crash_time"; private static final String KEY_LAST_CRASH_TYPE = "last_crash_type"; @Override public void handleUncaughtException(Thread t, Throwable e) { SharedPreferences sp = VirtualCore.get().getContext().getSharedPreferences(CRASH_SP, Context.MODE_MULTI_PROCESS); Map properties = new HashMap<>(); try { ApplicationInfo currentApplicationInfo = VClientImpl.get().getCurrentApplicationInfo(); if (currentApplicationInfo != null) { String packageName = currentApplicationInfo.packageName; String processName = currentApplicationInfo.processName; properties.put("process", processName); properties.put("package", packageName); int userId = VUserHandle.myUserId(); properties.put("uid", String.valueOf(userId)); InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(packageName, 0); if (installedAppInfo != null) { PackageInfo packageInfo = installedAppInfo.getPackageInfo(userId); if (packageInfo != null) { String versionName = packageInfo.versionName; int versionCode = packageInfo.versionCode; properties.put("versionName", versionName); properties.put("versionCode", String.valueOf(versionCode)); } } } } catch (Throwable ignored) { } final String exceptionType = e.getClass().getName(); final long now = System.currentTimeMillis(); final long lastCrash = sp.getLong(KEY_LAST_CRASH_TIME, 0); final String lastCrashType = sp.getString(KEY_LAST_CRASH_TYPE, null); if (exceptionType.equals(lastCrashType) && (now - lastCrash) < TimeUnit.MINUTES.toMillis(1)) { // continues crash, do not upload } else { Crashes.trackError(e, properties, null); } Log.i(TAG, "uncaught :" + t, e); // must commit. sp.edit().putLong(KEY_LAST_CRASH_TIME, now).putString(KEY_LAST_CRASH_TYPE, exceptionType).commit(); super.handleUncaughtException(t, e); } } ================================================ FILE: VirtualApp/app/src/aosp/java/io/virtualapp/delegate/MyVirtualInitializer.java ================================================ package io.virtualapp.delegate; import android.app.Application; import com.lody.virtual.client.core.VirtualCore; import com.microsoft.appcenter.AppCenter; import com.microsoft.appcenter.analytics.Analytics; import com.microsoft.appcenter.crashes.Crashes; /** * @author weishu * @date 2019/2/25. */ public class MyVirtualInitializer extends BaseVirtualInitializer { public MyVirtualInitializer(Application application, VirtualCore core) { super(application, core); } @Override public void onMainProcess() { AppCenter.start(application, "bf5e74bd-3795-49bd-95c8-327db494dd11", Analytics.class, Crashes.class); super.onMainProcess(); } @Override public void onVirtualProcess() { // For Crash statics AppCenter.start(application, "bf5e74bd-3795-49bd-95c8-327db494dd11", Analytics.class, Crashes.class); super.onVirtualProcess(); // Override virtualCore.setCrashHandler(new MyCrashHandler()); } } ================================================ FILE: VirtualApp/app/src/fdroid/java/io/virtualapp/delegate/MyVirtualInitializer.java ================================================ package io.virtualapp.delegate; import android.app.Application; import com.lody.virtual.client.core.VirtualCore; /** * @author weishu * @date 2019/2/25. */ public class MyVirtualInitializer extends BaseVirtualInitializer { public MyVirtualInitializer(Application application, VirtualCore core) { super(application, core); } } ================================================ FILE: VirtualApp/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/VCommends.java ================================================ package io.virtualapp; /** * @author Lody */ public class VCommends { public static final String TAG_NEW_VERSION = "First launch new Version"; public static final String TAG_SHOW_ADD_APP_GUIDE = "Should show add app guide"; public static final int REQUEST_SELECT_APP = 5; public static final String EXTRA_APP_INFO_LIST = "va.extra.APP_INFO_LIST"; public static final String TAG_ASK_INSTALL_GMS = "va.extra.ASK_INSTALL_GMS"; } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/XApp.java ================================================ package io.virtualapp; import android.app.Application; import android.content.Context; import android.os.Build; import com.lody.virtual.client.NativeEngine; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.client.stub.VASettings; import io.virtualapp.delegate.MyVirtualInitializer; /** * @author Lody */ public class XApp extends Application { private static final String TAG = "XApp"; public static final String XPOSED_INSTALLER_PACKAGE = "de.robv.android.xposed.installer"; private static XApp gApp; public static XApp getApp() { return gApp; } @Override protected void attachBaseContext(Context base) { gApp = this; super.attachBaseContext(base); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { NativeEngine.disableJit(Build.VERSION.SDK_INT); } VASettings.ENABLE_IO_REDIRECT = true; VASettings.ENABLE_INNER_SHORTCUT = false; try { VirtualCore.get().startup(base); } catch (Throwable e) { e.printStackTrace(); } } @Override public void onCreate() { super.onCreate(); VirtualCore virtualCore = VirtualCore.get(); virtualCore.initialize(new MyVirtualInitializer(this, virtualCore)); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/abs/BasePresenter.java ================================================ package io.virtualapp.abs; /** * @author Lody */ public interface BasePresenter { void start(); } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/abs/BaseView.java ================================================ package io.virtualapp.abs; import android.app.Activity; import android.content.Context; /** * @author Lody */ public interface BaseView { Activity getActivity(); Context getContext(); void setPresenter(T presenter); } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/abs/Callback.java ================================================ package io.virtualapp.abs; /** * @author Lody */ public interface Callback { void callback(T result); } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/abs/ui/VActivity.java ================================================ package io.virtualapp.abs.ui; import android.app.Activity; import android.content.Context; import android.support.annotation.IdRes; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import org.jdeferred.android.AndroidDeferredManager; import io.virtualapp.abs.BaseView; /** * @author Lody */ public class VActivity extends AppCompatActivity { /** * Implement of {@link BaseView#getActivity()} */ public Activity getActivity() { return this; } /** * Implement of {@link BaseView#getContext()} ()} */ public Context getContext() { return this; } protected AndroidDeferredManager defer() { return VUiKit.defer(); } public Fragment findFragmentById(@IdRes int id) { return getSupportFragmentManager().findFragmentById(id); } public void replaceFragment(@IdRes int id, Fragment fragment) { getSupportFragmentManager().beginTransaction().replace(id, fragment).commit(); } @Override protected void onStart() { super.onStart(); } @Override protected void onStop() { super.onStop(); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/abs/ui/VFragment.java ================================================ package io.virtualapp.abs.ui; import org.jdeferred.android.AndroidDeferredManager; import android.app.Activity; import android.support.v4.app.Fragment; import io.virtualapp.abs.BasePresenter; /** * @author Lody */ public class VFragment extends Fragment { protected T mPresenter; public T getPresenter() { return mPresenter; } public void setPresenter(T presenter) { this.mPresenter = presenter; } protected AndroidDeferredManager defer() { return VUiKit.defer(); } public void finishActivity() { Activity activity = getActivity(); if (activity != null) { activity.finish(); } } public void destroy() { finishActivity(); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/abs/ui/VUiKit.java ================================================ package io.virtualapp.abs.ui; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.util.TypedValue; import org.jdeferred.android.AndroidDeferredManager; /** * @author Lody *

* A set of tools for UI. */ public class VUiKit { private static final AndroidDeferredManager gDM = new AndroidDeferredManager(); private static final Handler gUiHandler = new Handler(Looper.getMainLooper()); public static AndroidDeferredManager defer() { return gDM; } public static int dpToPx(Context context, int dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); } public static void post(Runnable r) { gUiHandler.post(r); } public static void postDelayed(long delay, Runnable r) { gUiHandler.postDelayed(r, delay); } public static void sleep(long time) { try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/delegate/BaseCrashHandler.java ================================================ package io.virtualapp.delegate; import android.annotation.SuppressLint; import android.os.Looper; import android.util.Log; import com.lody.virtual.client.core.CrashHandler; /** * author: weishu on 18/3/10. */ public class BaseCrashHandler implements CrashHandler { protected static final String TAG = "XApp"; @SuppressLint("ApplySharedPref") @Override public void handleUncaughtException(Thread t, Throwable e) { if (t == Looper.getMainLooper().getThread()) { System.exit(0); } else { Log.e(TAG, "ignore uncaught exception of sub thread: " + t); } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/delegate/BaseVirtualInitializer.java ================================================ package io.virtualapp.delegate; import android.app.Application; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.os.VEnvironment; import jonathanfinerty.once.Once; import me.weishu.exposed.LogcatService; import static io.virtualapp.XApp.XPOSED_INSTALLER_PACKAGE; /** * @author weishu * @date 2019/2/25. */ public class BaseVirtualInitializer extends VirtualCore.VirtualInitializer { protected Application application; protected VirtualCore virtualCore; public BaseVirtualInitializer(Application application, VirtualCore core) { this.application = application; this.virtualCore = core; } @Override public void onMainProcess() { Once.initialise(application); } @Override public void onVirtualProcess() { virtualCore.setCrashHandler(new BaseCrashHandler()); //listener components virtualCore.setComponentDelegate(new MyComponentDelegate()); //fake phone imei,macAddress,BluetoothAddress virtualCore.setPhoneInfoDelegate(new MyPhoneInfoDelegate()); //fake task description's icon and title virtualCore.setTaskDescriptionDelegate(new MyTaskDescDelegate()); // ensure the logcat service alive when every virtual process start. LogcatService.start(application, VEnvironment.getDataUserPackageDirectory(0, XPOSED_INSTALLER_PACKAGE)); } @Override public void onServerProcess() { virtualCore.setAppRequestListener(new MyAppRequestListener(application)); virtualCore.addVisibleOutsidePackage("com.tencent.mobileqq"); virtualCore.addVisibleOutsidePackage("com.tencent.mobileqqi"); virtualCore.addVisibleOutsidePackage("com.tencent.minihd.qq"); virtualCore.addVisibleOutsidePackage("com.tencent.qqlite"); virtualCore.addVisibleOutsidePackage("com.facebook.katana"); virtualCore.addVisibleOutsidePackage("com.whatsapp"); virtualCore.addVisibleOutsidePackage("com.tencent.mm"); virtualCore.addVisibleOutsidePackage("com.immomo.momo"); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/delegate/MyAppRequestListener.java ================================================ package io.virtualapp.delegate; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.widget.Toast; import com.lody.virtual.client.core.VirtualCore; import java.io.File; import io.virtualapp.sys.InstallerActivity; /** * @author Lody */ public class MyAppRequestListener implements VirtualCore.AppRequestListener { private final Context context; public MyAppRequestListener(Context context) { this.context = context; } @Override public void onRequestInstall(String path) { try { Intent t = new Intent(context, InstallerActivity.class); t.setDataAndType(Uri.fromFile(new File(path)), "application/vnd.android.package-archive"); t.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(t); } catch (Throwable e) { e.printStackTrace(); } } @Override public void onRequestUninstall(String pkg) { Toast.makeText(context, "Uninstall: " + pkg, Toast.LENGTH_SHORT).show(); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/delegate/MyComponentDelegate.java ================================================ package io.virtualapp.delegate; import android.app.Activity; import android.app.Application; import android.content.Intent; import com.lody.virtual.client.hook.delegate.ComponentDelegate; import com.lody.virtual.helper.utils.Reflect; import java.io.File; public class MyComponentDelegate implements ComponentDelegate { @Override public void beforeApplicationCreate(Application application) { } @Override public void afterApplicationCreate(Application application) { } @Override public void beforeActivityCreate(Activity activity) { } @Override public void beforeActivityResume(Activity activity) { } @Override public void beforeActivityPause(Activity activity) { } @Override public void beforeActivityDestroy(Activity activity) { } @Override public void afterActivityCreate(Activity activity) { } @Override public void afterActivityResume(Activity activity) { } @Override public void afterActivityPause(Activity activity) { } @Override public void afterActivityDestroy(Activity activity) { } @Override public void onSendBroadcast(Intent intent) { } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/delegate/MyPhoneInfoDelegate.java ================================================ package io.virtualapp.delegate; import com.lody.virtual.client.hook.delegate.PhoneInfoDelegate; /** * Fake the Device ID. */ public class MyPhoneInfoDelegate implements PhoneInfoDelegate { @Override public String getDeviceId(String oldDeviceId, int userId) { return oldDeviceId; } @Override public String getBluetoothAddress(String oldAddress, int userId) { return oldAddress; } @Override public String getMacAddress(String oldAddress, int userId) { return oldAddress; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/delegate/MyTaskDescDelegate.java ================================================ package io.virtualapp.delegate; import android.annotation.TargetApi; import android.app.ActivityManager; import android.os.Build; import com.lody.virtual.client.hook.delegate.TaskDescriptionDelegate; import com.lody.virtual.os.VUserManager; /** * Patch the task description with the (Virtual) user name */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public class MyTaskDescDelegate implements TaskDescriptionDelegate { @Override public ActivityManager.TaskDescription getTaskDescription(ActivityManager.TaskDescription oldTaskDescription) { if (oldTaskDescription == null) { return null; } String labelPrefix = "[" + VUserManager.get().getUserName() + "] "; String oldLabel = oldTaskDescription.getLabel() != null ? oldTaskDescription.getLabel() : ""; if (!oldLabel.startsWith(labelPrefix)) { // Is it really necessary? return new ActivityManager.TaskDescription(labelPrefix + oldTaskDescription.getLabel(), oldTaskDescription.getIcon(), oldTaskDescription.getPrimaryColor()); } else { return oldTaskDescription; } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/dev/CmdReceiver.java ================================================ package io.virtualapp.dev; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.text.TextUtils; import android.widget.Toast; import com.lody.virtual.client.core.InstallStrategy; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.remote.InstallResult; import io.virtualapp.BuildConfig; import io.virtualapp.home.LoadingActivity; /** * author: weishu on 18/2/23. */ public class CmdReceiver extends BroadcastReceiver { private static final String ACTION = BuildConfig.APPLICATION_ID + ".CMD"; private static final String KEY_CMD = "cmd"; private static final String KEY_PKG = "pkg"; private static final String KEY_UID = "uid"; private static final String CMD_UPDATE = "update"; private static final String CMD_REBOOT = "reboot"; private static final String CMD_LAUNCH = "launch"; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (!ACTION.equalsIgnoreCase(action)) { return; } String cmd = intent.getStringExtra(KEY_CMD); if (TextUtils.isEmpty(cmd)) { showTips(context, "No cmd found!"); return; } if (CMD_REBOOT.equalsIgnoreCase(cmd)) { VirtualCore.get().killAllApps(); showTips(context, "Reboot Success!!"); return; } if (CMD_UPDATE.equalsIgnoreCase(cmd)) { String pkg = intent.getStringExtra(KEY_PKG); if (TextUtils.isEmpty(pkg)) { showTips(context, "Please tell me the update package!!"); return; } PackageManager packageManager = context.getPackageManager(); if (packageManager == null) { showTips(context, "system error, update failed!"); return; } try { ApplicationInfo applicationInfo = packageManager.getApplicationInfo(pkg, 0); String apkPath = applicationInfo.sourceDir; InstallResult installResult = VirtualCore.get().installPackage(apkPath, InstallStrategy.UPDATE_IF_EXIST); if (installResult.isSuccess) { if (installResult.isUpdate) { showTips(context, "Update " + pkg + " Success!!"); } } else { showTips(context, "Update " + pkg + " failed: " + installResult.error); } } catch (PackageManager.NameNotFoundException e) { showTips(context, "Can not found " + pkg + " outside!"); } } else if (CMD_LAUNCH.equalsIgnoreCase(cmd)) { String pkg = intent.getStringExtra(KEY_PKG); if (TextUtils.isEmpty(pkg)) { showTips(context, "Please tell me the launch package!!"); return; } String uid = intent.getStringExtra(KEY_UID); int userId = 0; if (!TextUtils.isEmpty(uid)){ try { userId = Integer.parseInt(uid); }catch (NumberFormatException e){ e.printStackTrace(); } } LoadingActivity.launch(context, pkg, userId); } } private void showTips(Context context, String tips) { Toast.makeText(context, tips, Toast.LENGTH_SHORT).show(); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/glide/GlideUtils.java ================================================ package io.virtualapp.glide; import android.content.Context; import android.support.annotation.DrawableRes; import android.widget.ImageView; import com.bumptech.glide.load.engine.DiskCacheStrategy; import static io.virtualapp.glide.PackageIconResourceLoader.DATA_PACKAGE_FILE_PATH_PREFIX; import static io.virtualapp.glide.PackageIconResourceLoader.DATA_PACKAGE_PREFIX; /** * Created by Windy on 2018/10/25 */ public class GlideUtils { public static void loadInstalledPackageIcon(Context context, String packageName, ImageView target, @DrawableRes int placeHolder) { GlideApp.with(context) .load(DATA_PACKAGE_PREFIX + packageName) .placeholder(placeHolder) .diskCacheStrategy(DiskCacheStrategy.NONE) .into(target); } public static void loadPackageIconFromApkFile(Context context, String apkFilePath, ImageView target, @DrawableRes int placeHolder) { GlideApp.with(context) .load(DATA_PACKAGE_FILE_PATH_PREFIX + apkFilePath) .placeholder(placeHolder) .diskCacheStrategy(DiskCacheStrategy.NONE) .into(target); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/glide/MyGlideModule.java ================================================ package io.virtualapp.glide; import android.content.Context; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.load.engine.cache.LruResourceCache; import com.bumptech.glide.load.engine.cache.MemorySizeCalculator; import com.bumptech.glide.module.AppGlideModule; import com.lody.virtual.helper.utils.VLog; import java.io.InputStream; /** * Created by Windy on 2018/10/25 */ @GlideModule public class MyGlideModule extends AppGlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { MemorySizeCalculator calculator = new MemorySizeCalculator.Builder(context) .build(); builder.setMemoryCache(new LruResourceCache(calculator.getMemoryCacheSize() / 2)); VLog.i("MyGlideModule", "applyOptions"); } @Override public boolean isManifestParsingEnabled() { return false; } @Override public void registerComponents(Context context, Glide glide, Registry registry) { super.registerComponents(context, glide, registry); registry.prepend(String.class, InputStream.class, new PackageIconResourceLoaderFactory(context)); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/glide/PackageIconResourceDataFetcher.java ================================================ package io.virtualapp.glide; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.DataFetcher; import com.lody.virtual.helper.utils.VLog; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import static io.virtualapp.glide.PackageIconResourceLoader.DATA_PACKAGE_FILE_PATH_PREFIX; import static io.virtualapp.glide.PackageIconResourceLoader.DATA_PACKAGE_PREFIX; /** * Created by Windy on 2018/10/25 */ public class PackageIconResourceDataFetcher implements DataFetcher { private static final String TAG = PackageIconResourceDataFetcher.class.getSimpleName(); private Context context; private String packageModel; private InputStream data; public PackageIconResourceDataFetcher(Context context, String packageName) { this.context = context.getApplicationContext(); this.packageModel = packageName; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { try { data = loadResource(); } catch (Exception e) { VLog.e(TAG, "Failed to load data from asset manager", e); callback.onLoadFailed(e); return; } callback.onDataReady(data); } @Override public void cleanup() { if (data == null) { return; } try { data.close(); } catch (IOException e) { // Ignored. } } @Override public void cancel() { } @NonNull @Override public Class getDataClass() { return InputStream.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } //load icon res accord to package name, or apk path private InputStream loadResource() { PackageInfo packageInfo = null; Drawable drawable = null; try { packageInfo = getPackageInfo(); if (packageInfo == null) { return null; } drawable = packageInfo.applicationInfo.loadIcon(context.getPackageManager()); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } if (drawable == null) { return null; } return drawableToInputStream(drawable); } private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException { if (packageModel.startsWith(DATA_PACKAGE_PREFIX)) { return context.getPackageManager().getPackageInfo(getPackageTrueModel(DATA_PACKAGE_PREFIX), 0); } else if (packageModel.startsWith(DATA_PACKAGE_FILE_PATH_PREFIX)) { return context.getPackageManager().getPackageArchiveInfo(getPackageTrueModel(DATA_PACKAGE_FILE_PATH_PREFIX), 0); } return null; } private String getPackageTrueModel(String prefix) { return packageModel.replaceAll(prefix, ""); } private InputStream drawableToInputStream(Drawable drawable) { Bitmap bitmap = drawableToBitmap(drawable); ByteArrayOutputStream stream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); //use the compression format of your need return new ByteArrayInputStream(stream.toByteArray()); } private static Bitmap drawableToBitmap(Drawable drawable) { if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } int width = drawable.getIntrinsicWidth(); width = width > 0 ? width : 1; int height = drawable.getIntrinsicHeight(); height = height > 0 ? height : 1; Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/glide/PackageIconResourceLoader.java ================================================ package io.virtualapp.glide; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.signature.ObjectKey; import java.io.InputStream; /** * Created by Windy on 2018/10/25 */ public class PackageIconResourceLoader implements ModelLoader { public static final String DATA_PACKAGE_PREFIX = "data:packageName/"; public static final String DATA_PACKAGE_FILE_PATH_PREFIX = "data:packageFilePath/"; private Context context; public PackageIconResourceLoader(Context context) { this.context = context; } @Nullable @Override public LoadData buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) { return new LoadData<>(new ObjectKey(model), new PackageIconResourceDataFetcher(context, model)); } @Override public boolean handles(@NonNull String model) { return model.startsWith(DATA_PACKAGE_PREFIX) || model.startsWith(DATA_PACKAGE_FILE_PATH_PREFIX); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/glide/PackageIconResourceLoaderFactory.java ================================================ package io.virtualapp.glide; import android.content.Context; import android.support.annotation.NonNull; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import java.io.InputStream; /** * Created by Windy on 2018/10/25 */ public class PackageIconResourceLoaderFactory implements ModelLoaderFactory { private Context context; public PackageIconResourceLoaderFactory(Context context) { this.context = context; } @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new PackageIconResourceLoader(context); } @Override public void teardown() { } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/gms/FakeGms.java ================================================ package io.virtualapp.gms; import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.util.Log; import com.lody.virtual.client.core.InstallStrategy; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.os.VEnvironment; import com.lody.virtual.remote.InstallResult; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import io.virtualapp.R; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.utils.DialogUtil; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; /** * @author weishu * @date 2018/6/9. */ public class FakeGms { private static final String TAG = "FakeGms"; private static final String GMS_CONFIG_URL = "http://vaexposed.weishu.me/gms.json"; private static final String GMS_PKG = "com.google.android.gms"; private static final String GSF_PKG = "com.google.android.gsf"; private static final String STORE_PKG = "com.android.vending"; private static final String FAKE_GAPPS_PKG = "com.thermatk.android.xf.fakegapps"; private static ExecutorService executorService = Executors.newSingleThreadExecutor(); public static void uninstallGms(Activity activity) { if (activity == null) { return; } AlertDialog failDialog = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_DayNight_Dialog_Alert) .setTitle(R.string.uninstall_gms_title) .setMessage(R.string.uninstall_gms_content) .setPositiveButton(R.string.uninstall_gms_ok, ((dialog1, which1) -> { ProgressDialog dialog = new ProgressDialog(activity); dialog.show(); VUiKit.defer().when(() -> { VirtualCore.get().uninstallPackage(GMS_PKG); VirtualCore.get().uninstallPackage(GSF_PKG); VirtualCore.get().uninstallPackage(STORE_PKG); VirtualCore.get().uninstallPackage(FAKE_GAPPS_PKG); }).then((v) -> { dialog.dismiss(); AlertDialog hits = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_DayNight_Dialog_Alert) .setTitle(R.string.uninstall_gms_title) .setMessage(R.string.uninstall_gms_success) .setPositiveButton(android.R.string.ok, null) .create(); DialogUtil.showDialog(hits); }).fail((v) -> { dialog.dismiss(); }); })) .setNegativeButton(android.R.string.cancel, null) .create(); DialogUtil.showDialog(failDialog); } public static boolean isAlreadyInstalled(Context context) { if (context == null) { return false; } boolean alreadyInstalled = true; if (!VirtualCore.get().isAppInstalled(GMS_PKG)) { alreadyInstalled = false; } if (!VirtualCore.get().isAppInstalled(GSF_PKG)) { alreadyInstalled = false; } if (!VirtualCore.get().isAppInstalled(STORE_PKG)) { alreadyInstalled = false; } if (!VirtualCore.get().isAppInstalled(FAKE_GAPPS_PKG)) { alreadyInstalled = false; } return alreadyInstalled; } public static void installGms(Activity activity) { if (activity == null) { return; } AlertDialog alertDialog = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_DayNight_Dialog_Alert) .setTitle(R.string.install_gms_title) .setMessage(R.string.install_gms_content) .setPositiveButton(android.R.string.ok, ((dialog, which) -> { // show a loading dialog and start install gms. ProgressDialog progressDialog = new ProgressDialog(activity); progressDialog.setCancelable(false); progressDialog.show(); executorService.submit(() -> { String failMsg = installGmsInternal(activity, progressDialog); Log.i(TAG, "install gms result: " + failMsg); try { progressDialog.dismiss(); } catch (Throwable e) { e.printStackTrace(); } if (failMsg == null) { activity.runOnUiThread(() -> { AlertDialog failDialog = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_DayNight_Dialog_Alert) .setTitle(R.string.install_gms_title) .setMessage(R.string.install_gms_success) .setPositiveButton(android.R.string.ok, null) .create(); DialogUtil.showDialog(failDialog); }); } else { activity.runOnUiThread(() -> { AlertDialog failDialog = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_DayNight_Dialog_Alert) .setTitle(R.string.install_gms_fail_title) .setMessage(R.string.install_gms_fail_content) .setPositiveButton(R.string.install_gms_fail_ok, ((dialog1, which1) -> { try { Intent t = new Intent(Intent.ACTION_VIEW); t.setData(Uri.parse("https://github.com/android-hacker/VirtualXposed/wiki/Google-service-support")); activity.startActivity(t); } catch (Throwable ignored) { ignored.printStackTrace(); } })) .setNegativeButton(android.R.string.cancel, null) .create(); DialogUtil.showDialog(failDialog); }); } }); })) .setNegativeButton(android.R.string.cancel, null) .create(); DialogUtil.showDialog(alertDialog); } private static String installGmsInternal(Activity activity, ProgressDialog dialog) { File cacheDir = activity.getCacheDir(); // 下载配置文件,得到各自的URL OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build(); Request request = new Request.Builder() .url(GMS_CONFIG_URL) .build(); updateMessage(activity, dialog, "Fetching gms config..."); Response response; try { response = client.newCall(request).execute(); } catch (IOException e) { return "Download gms config failed, please check your network, error: 0"; } if (!response.isSuccessful()) { return "Download gms config failed, please check your network, error: 1"; } Log.i(TAG, "response success: " + response.code()); if (200 != response.code()) { return "Download gms config failed, please check your network, error: 2"; } updateMessage(activity, dialog, "Parsing gms config..."); ResponseBody body = response.body(); if (body == null) { return "Download gms config failed, please check your network, error: 3"; } String string = null; try { string = body.string(); } catch (IOException e) { return "Download gms config failed, please check your network, error: 4"; } JSONObject jsonObject = null; try { jsonObject = new JSONObject(string); } catch (JSONException e) { return "Download gms config failed, please check your network, error: 5"; } String gmsCoreUrl = null; try { gmsCoreUrl = jsonObject.getString("gms"); } catch (JSONException e) { return "Download gms config failed, please check your network, error: 6"; } String gmsServiceUrl = null; try { gmsServiceUrl = jsonObject.getString("gsf"); } catch (JSONException e) { return "Download gms config failed, please check your network, error: 7"; } String storeUrl = null; try { storeUrl = jsonObject.getString("store"); } catch (JSONException e) { return "Download gms config failed, please check your network, error: 8"; } String fakeGappsUrl = null; try { fakeGappsUrl = jsonObject.getString("fakegapps"); } catch (JSONException e) { return "Download gms config failed, please check your network, error: 9"; } String yalpStoreUrl = null; try { yalpStoreUrl = jsonObject.getString("yalp"); } catch (JSONException e) { // ignore. Log.i(TAG, "Download gms config failed, please check your network"); } updateMessage(activity, dialog, "config parse success!"); File gmsCoreFile = new File(cacheDir, "gms.apk"); File gmsServiceFile = new File(cacheDir, "gsf.apk"); File storeFile = new File(cacheDir, "store.apk"); File fakeGappsFile = new File(cacheDir, "fakegapps.apk"); File yalpStoreFile = new File(cacheDir, "yalpStore.apk"); // clear old files. if (gmsCoreFile.exists()) { gmsCoreFile.delete(); } if (gmsServiceFile.exists()) { gmsServiceFile.delete(); } if (storeFile.exists()) { storeFile.delete(); } if (fakeGappsFile.exists()) { fakeGappsFile.delete(); } boolean downloadResult = downloadFile(gmsCoreUrl, gmsCoreFile, (progress) -> updateMessage(activity, dialog, "download gms core..." + progress + "%")); if (!downloadResult) { return "Download gms config failed, please check your network, error: 10"; } downloadResult = downloadFile(gmsServiceUrl, gmsServiceFile, (progress -> updateMessage(activity, dialog, "download gms service framework proxy.." + progress + "%"))); if (!downloadResult) { return "Download gms config failed, please check your network, error: 11"; } updateMessage(activity, dialog, "download gms store..."); downloadResult = downloadFile(storeUrl, storeFile, (progress -> updateMessage(activity, dialog, "download gms store.." + progress + "%"))); if (!downloadResult) { return "Download gms config failed, please check your network, error: 12"; } downloadResult = downloadFile(fakeGappsUrl, fakeGappsFile, (progress -> updateMessage(activity, dialog, "download gms Xposed module.." + progress + "%"))); if (!downloadResult) { return "Download gms config failed, please check your network, error: 13"; } if (yalpStoreUrl != null) { downloadFile(yalpStoreUrl,yalpStoreFile, (progress -> updateMessage(activity, dialog, "download yalp store.." + progress + "%"))); } updateMessage(activity, dialog, "installing gms core"); InstallResult installResult = VirtualCore.get().installPackage(gmsCoreFile.getAbsolutePath(), InstallStrategy.UPDATE_IF_EXIST); if (!installResult.isSuccess) { return "install gms core failed: " + installResult.error; } updateMessage(activity, dialog, "installing gms service framework..."); installResult = VirtualCore.get().installPackage(gmsServiceFile.getAbsolutePath(), InstallStrategy.UPDATE_IF_EXIST); if (!installResult.isSuccess) { return "install gms service framework failed: " + installResult.error; } updateMessage(activity, dialog, "installing gms store..."); installResult = VirtualCore.get().installPackage(storeFile.getAbsolutePath(), InstallStrategy.UPDATE_IF_EXIST); if (!installResult.isSuccess) { return "install gms store failed: " + installResult.error; } updateMessage(activity, dialog, "installing gms Xposed module..."); installResult = VirtualCore.get().installPackage(fakeGappsFile.getAbsolutePath(), InstallStrategy.UPDATE_IF_EXIST); if (!installResult.isSuccess) { return "install gms xposed module failed: " + installResult.error; } if (yalpStoreFile.exists()) { updateMessage(activity, dialog, "installing yalp store..."); VirtualCore.get().installPackage(yalpStoreFile.getAbsolutePath(), InstallStrategy.UPDATE_IF_EXIST); } // Enable the Xposed module. File dataDir = VEnvironment.getDataUserPackageDirectory(0, "de.robv.android.xposed.installer"); File modulePath = VEnvironment.getPackageResourcePath(FAKE_GAPPS_PKG); File configDir = new File(dataDir, "exposed_conf" + File.separator + "modules.list"); FileWriter writer = null; try { writer = new FileWriter(configDir, true); writer.append(modulePath.getAbsolutePath()); writer.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if (writer != null) { try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } } // success!!! return null; } private static void updateMessage(Activity activity, ProgressDialog dialog, String msg) { if (activity == null || dialog == null || TextUtils.isEmpty(msg)) { return; } Log.i(TAG, "update dialog message: " + msg); activity.runOnUiThread(() -> { dialog.setMessage(msg); }); } public interface DownloadListener { void onProgress(int progress); } public static boolean downloadFile(String url, File outFile, DownloadListener listener) { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder().url(url).build(); FileOutputStream fos = null; try { Response response = client.newCall(request).execute(); if (response.code() != 200) { return false; } ResponseBody body = response.body(); if (body == null) { return false; } long toal = body.contentLength(); long sum = 0; InputStream inputStream = body.byteStream(); fos = new FileOutputStream(outFile); byte[] buffer = new byte[1024]; int count = 0; while ((count = inputStream.read(buffer)) >= 0) { fos.write(buffer, 0, count); sum += count; int progress = (int) ((sum * 1.0) / toal * 100); if (listener != null) { listener.onProgress(progress); } } fos.flush(); return true; } catch (IOException e) { return false; } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/ListAppActivity.java ================================================ package io.virtualapp.home; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.TabLayout; import android.support.v4.view.ViewPager; import android.support.v7.widget.Toolbar; import android.view.MenuItem; import io.virtualapp.R; import io.virtualapp.VCommends; import io.virtualapp.abs.ui.VActivity; import io.virtualapp.home.adapters.AppPagerAdapter; /** * @author Lody */ public class ListAppActivity extends VActivity { private Toolbar mToolBar; private TabLayout mTabLayout; private ViewPager mViewPager; public static void gotoListApp(Activity activity) { Intent intent = new Intent(activity, ListAppActivity.class); activity.startActivityForResult(intent, VCommends.REQUEST_SELECT_APP); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_clone_app); mToolBar = findViewById(R.id.clone_app_tool_bar); mTabLayout = mToolBar.findViewById(R.id.clone_app_tab_layout); mViewPager = findViewById(R.id.clone_app_view_pager); mViewPager.setAdapter(new AppPagerAdapter(getSupportFragmentManager())); mTabLayout.setupWithViewPager(mViewPager); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/ListAppContract.java ================================================ package io.virtualapp.home; import java.util.List; import io.virtualapp.abs.BasePresenter; import io.virtualapp.abs.BaseView; import io.virtualapp.home.models.AppInfo; /** * @author Lody * @version 1.0 */ /*package*/ class ListAppContract { interface ListAppView extends BaseView { void startLoading(); void loadFinish(List infoList); } interface ListAppPresenter extends BasePresenter { } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/ListAppFragment.java ================================================ package io.virtualapp.home; import android.app.Activity; import android.app.AlertDialog; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.OrientationHelper; import android.support.v7.widget.StaggeredGridLayoutManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ProgressBar; import android.widget.Toast; import com.lody.virtual.helper.compat.NativeLibraryHelperCompat; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Locale; import io.virtualapp.R; import io.virtualapp.XApp; import io.virtualapp.abs.ui.VFragment; import io.virtualapp.home.adapters.CloneAppListAdapter; import io.virtualapp.home.models.AppInfo; import io.virtualapp.home.models.AppInfoLite; import io.virtualapp.sys.Installd; import io.virtualapp.widgets.DragSelectRecyclerView; /** * @author Lody */ public class ListAppFragment extends VFragment implements ListAppContract.ListAppView { private static final String KEY_SELECT_FROM = "key_select_from"; private static final int REQUEST_GET_FILE = 1; private DragSelectRecyclerView mRecyclerView; private ProgressBar mProgressBar; private Button mInstallButton; private CloneAppListAdapter mAdapter; private View mSelectFromExternal; public static ListAppFragment newInstance(File selectFrom) { Bundle args = new Bundle(); if (selectFrom != null) args.putString(KEY_SELECT_FROM, selectFrom.getPath()); ListAppFragment fragment = new ListAppFragment(); fragment.setArguments(args); return fragment; } private File getSelectFrom() { Bundle bundle = getArguments(); if (bundle != null) { String selectFrom = bundle.getString(KEY_SELECT_FROM); if (selectFrom != null) { return new File(selectFrom); } } return null; } @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_list_app, null); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mAdapter.saveInstanceState(outState); } private void chooseInstallWay(Runnable runnable, String path) { AlertDialog alertDialog = new AlertDialog.Builder(getContext()) .setTitle(R.string.install_choose_way) .setMessage(R.string.install_choose_content) .setPositiveButton(R.string.install_choose_taichi, (dialog, which) -> { PackageManager packageManager = getActivity().getPackageManager(); try { packageManager.getPackageInfo("me.weishu.exp", 0); Intent intent = new Intent(); intent.setComponent(new ComponentName("me.weishu.exp", "me.weishu.exp.ui.MainActivity")); intent.putExtra("path", path); startActivity(intent); } catch (PackageManager.NameNotFoundException e) { AlertDialog showInstallDialog = new AlertDialog.Builder(getContext()) .setTitle(android.R.string.dialog_alert_title) .setMessage(R.string.install_taichi_not_exist) .setPositiveButton(R.string.install_go_to_install_exp, (dialog1, which1) -> { Intent t = new Intent(Intent.ACTION_VIEW); t.setData(Uri.parse("https://www.coolapk.com/apk/me.weishu.exp")); startActivity(t); }) .create(); showInstallDialog.show(); } catch (Throwable e) { AlertDialog showInstallDialog = new AlertDialog.Builder(getContext()) .setTitle(android.R.string.dialog_alert_title) .setMessage(R.string.install_taichi_while_old_version) .setPositiveButton(R.string.install_go_latest_exp, (dialog1, which1) -> { Intent t = new Intent(Intent.ACTION_VIEW); t.setData(Uri.parse("https://taichi.cool")); startActivity(t); }) .create(); showInstallDialog.show(); } finishActivity(); }).setNegativeButton("VirtualXposed", (dialog, which) -> { if (runnable != null) { runnable.run(); } finishActivity(); }).setNeutralButton(R.string.what_is_exp, ((dialog, which) -> { Intent t = new Intent(Intent.ACTION_VIEW); t.setData(Uri.parse("https://taichi.cool")); startActivity(t); })) .create(); try { alertDialog.show(); } catch (Throwable ignored) { } } @Override public void onViewCreated(View view, Bundle savedInstanceState) { mRecyclerView = (DragSelectRecyclerView) view.findViewById(R.id.select_app_recycler_view); mProgressBar = (ProgressBar) view.findViewById(R.id.select_app_progress_bar); mInstallButton = (Button) view.findViewById(R.id.select_app_install_btn); mSelectFromExternal = view.findViewById(R.id.select_app_from_external); mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(1, OrientationHelper.VERTICAL)); DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL); dividerItemDecoration.setDrawable(new ColorDrawable(0x1f000000)); mRecyclerView.addItemDecoration(dividerItemDecoration); mAdapter = new CloneAppListAdapter(getActivity(), getSelectFrom()); mRecyclerView.setAdapter(mAdapter); mAdapter.setOnItemClickListener(new CloneAppListAdapter.ItemEventListener() { @Override public void onItemClick(AppInfo info, int position) { if (!NativeLibraryHelperCompat.isApk64(info.path)) { Toast.makeText(getContext(), R.string.unsupported_for_32bit_app, Toast.LENGTH_SHORT).show(); return; } int count = mAdapter.getSelectedCount(); if (!mAdapter.isIndexSelected(position)) { if (count >= 9) { Toast.makeText(getContext(), R.string.install_too_much_once_time, Toast.LENGTH_SHORT).show(); return; } } mAdapter.toggleSelected(position); } @Override public boolean isSelectable(int position) { return mAdapter.isIndexSelected(position) || mAdapter.getSelectedCount() < 9; } }); mAdapter.setSelectionListener(count -> { mInstallButton.setEnabled(count > 0); mInstallButton.setText(String.format(Locale.ENGLISH, XApp.getApp().getResources().getString(R.string.install_d), count)); }); mInstallButton.setOnClickListener(v -> { Integer[] selectedIndices = mAdapter.getSelectedIndices(); ArrayList dataList = new ArrayList(selectedIndices.length); for (int index : selectedIndices) { AppInfo info = mAdapter.getItem(index); dataList.add(new AppInfoLite(info.packageName, info.path, info.fastOpen, info.disableMultiVersion)); } if (dataList.size() > 0) { String path = dataList.get(0).path; chooseInstallWay(() -> { Activity activity = getActivity(); if (activity == null) { return; } Installd.startInstallerActivity(activity, dataList); activity.setResult(Activity.RESULT_OK); }, path); } }); mSelectFromExternal.setOnClickListener(v -> { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("application/vnd.android.package-archive"); // apk file intent.addCategory(Intent.CATEGORY_OPENABLE); try { startActivityForResult(intent, REQUEST_GET_FILE); } catch (Throwable ignored) { Toast.makeText(getActivity(), "Error", Toast.LENGTH_SHORT).show(); } }); new ListAppPresenterImpl(getActivity(), this, getSelectFrom()).start(); } @Override public void startLoading() { mProgressBar.setVisibility(View.VISIBLE); mRecyclerView.setVisibility(View.GONE); } @Override public void loadFinish(List infoList) { mAdapter.setList(infoList); mRecyclerView.setDragSelectActive(false, 0); mAdapter.setSelected(0, false); mProgressBar.setVisibility(View.GONE); mRecyclerView.setVisibility(View.VISIBLE); } @Override public void setPresenter(ListAppContract.ListAppPresenter presenter) { this.mPresenter = presenter; } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (!(requestCode == REQUEST_GET_FILE && resultCode == Activity.RESULT_OK)) { return; } Activity current = getActivity(); if (current == null) { return; } Uri uri = data.getData(); String path = getPath(getActivity(), uri); if (path == null) { return; } chooseInstallWay(() -> { Installd.handleRequestFromFile(getActivity(), path); getActivity().setResult(Activity.RESULT_OK); }, path); } public static String getPath(Context context, Uri uri) { if (uri == null) { return null; } if ("content".equalsIgnoreCase(uri.getScheme())) { String[] projection = {"_data"}; Cursor cursor = null; try { cursor = context.getContentResolver().query(uri, projection, null, null, null); int column_index = cursor.getColumnIndexOrThrow("_data"); if (cursor.moveToFirst()) { return cursor.getString(column_index); } } catch (Exception e) { // Eat it Or Log it. } finally { if (cursor != null) { cursor.close(); } } } else if ("file".equalsIgnoreCase(uri.getScheme())) { return uri.getPath(); } return null; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/ListAppPresenterImpl.java ================================================ package io.virtualapp.home; import android.app.Activity; import java.io.File; import io.virtualapp.home.repo.AppDataSource; import io.virtualapp.home.repo.AppRepository; /** * @author Lody */ class ListAppPresenterImpl implements ListAppContract.ListAppPresenter { private Activity mActivity; private ListAppContract.ListAppView mView; private AppDataSource mRepository; private File from; ListAppPresenterImpl(Activity activity, ListAppContract.ListAppView view, File fromWhere) { mActivity = activity; mView = view; mRepository = new AppRepository(activity); mView.setPresenter(this); this.from = fromWhere; } @Override public void start() { mView.setPresenter(this); mView.startLoading(); if (from == null) { mRepository.getInstalledApps(mActivity).done(mView::loadFinish); } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/LoadingActivity.java ================================================ package io.virtualapp.home; import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.RemoteException; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.util.Log; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.client.env.Constants; import com.lody.virtual.client.ipc.VActivityManager; import com.lody.virtual.client.ipc.VPackageManager; import com.lody.virtual.helper.utils.VLog; import com.lody.virtual.server.pm.parser.VPackage; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import io.virtualapp.R; import io.virtualapp.abs.ui.VActivity; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.models.PackageAppData; import io.virtualapp.home.repo.PackageAppDataStorage; import io.virtualapp.widgets.EatBeansView; import jonathanfinerty.once.Once; /** * @author Lody */ public class LoadingActivity extends VActivity { private static final String TAG = "LoadingActivity"; private PackageAppData appModel; private EatBeansView loadingView; private static final int REQUEST_PERMISSION_CODE = 100; private Intent intentToLaunch; private int userToLaunch; private long start; public static boolean launch(Context context, String packageName, int userId) { Intent intent = VirtualCore.get().getLaunchIntent(packageName, userId); if (intent != null) { Intent loadingPageIntent = new Intent(context, LoadingActivity.class); loadingPageIntent.putExtra(Constants.PASS_PKG_NAME_ARGUMENT, packageName); loadingPageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); loadingPageIntent.putExtra(Constants.PASS_KEY_INTENT, intent); loadingPageIntent.putExtra(Constants.PASS_KEY_USER, userId); context.startActivity(loadingPageIntent); return true; } else { return false; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); start = SystemClock.elapsedRealtime(); setContentView(R.layout.activity_loading); loadingView = (EatBeansView) findViewById(R.id.loading_anim); int userId = getIntent().getIntExtra(Constants.PASS_KEY_USER, -1); String pkg = getIntent().getStringExtra(Constants.PASS_PKG_NAME_ARGUMENT); appModel = PackageAppDataStorage.get().acquire(pkg); if (appModel == null) { Toast.makeText(getApplicationContext(), "Open App:" + pkg + " failed.", Toast.LENGTH_SHORT).show(); finish(); return; } ImageView iconView = (ImageView) findViewById(R.id.app_icon); iconView.setImageDrawable(appModel.icon); TextView nameView = (TextView) findViewById(R.id.app_name); nameView.setText(String.format(Locale.ENGLISH, "Opening %s...", appModel.name)); Intent intent = getIntent().getParcelableExtra(Constants.PASS_KEY_INTENT); if (intent == null) { finish(); return; } VirtualCore.get().setUiCallback(intent, mUiCallback); try { // 如果已经在运行了,那么直接拉起,不做任何检测。 boolean uiRunning = false; ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE); if (am != null) { List runningAppProcesses = am.getRunningAppProcesses(); for (ActivityManager.RunningAppProcessInfo runningAppProcess : runningAppProcesses) { String appProcessName = VActivityManager.get().getAppProcessName(runningAppProcess.pid); if (TextUtils.equals(appProcessName, pkg)) { uiRunning = true; break; } } } VLog.i(TAG, pkg + "is running: " + uiRunning); if (uiRunning) { launchActivity(intent, userId); return; } } catch (Throwable ignored) { ignored.printStackTrace(); } checkAndLaunch(intent, userId); } private void checkAndLaunch(Intent intent, int userId) { final int RUNTIME_PERMISSION_API_LEVEL = android.os.Build.VERSION_CODES.M; if (android.os.Build.VERSION.SDK_INT < RUNTIME_PERMISSION_API_LEVEL) { // the device is below Android M, the permissions are granted when install, start directly Log.i(TAG, "device's api level below Android M, do not need runtime permission."); launchActivityWithDelay(intent, userId); return; } // The device is above android M, support runtime permission. String packageName = appModel.packageName; String name = appModel.name; // analyze permission try { ApplicationInfo applicationInfo = VPackageManager.get().getApplicationInfo(packageName, 0, 0); int targetSdkVersion = applicationInfo.targetSdkVersion; Log.i(TAG, "target package: " + packageName + " targetSdkVersion: " + targetSdkVersion); if (targetSdkVersion >= RUNTIME_PERMISSION_API_LEVEL) { Log.i(TAG, "target package support runtime permission, launch directly."); launchActivityWithDelay(intent, userId); } else { intentToLaunch = intent; userToLaunch = userId; PackageInfo packageInfo = VPackageManager.get().getPackageInfo(packageName, PackageManager.GET_PERMISSIONS, 0); String[] requestedPermissions = packageInfo.requestedPermissions; Set dangerousPermissions = new HashSet<>(); for (String requestedPermission : requestedPermissions) { if (VPackage.PermissionComponent.DANGEROUS_PERMISSION.contains(requestedPermission)) { // dangerous permission, check it if (ContextCompat.checkSelfPermission(this, requestedPermission) != PackageManager.PERMISSION_GRANTED) { dangerousPermissions.add(requestedPermission); } else { Log.i(TAG, "permission: " + requestedPermission + " is granted, ignore."); } } } if (dangerousPermissions.isEmpty()) { Log.i(TAG, "all permission are granted, launch directly."); // all permission are granted, launch directly. launchActivityWithDelay(intent, userId); } else { // tell user that this app need that permission Log.i(TAG, "request permission: " + dangerousPermissions); AlertDialog alertDialog = new AlertDialog.Builder(this, R.style.Theme_AppCompat_DayNight_Dialog_Alert) .setTitle(R.string.permission_tip_title) .setMessage(getResources().getString(R.string.permission_tips_content, name)) .setPositiveButton(R.string.permission_tips_confirm, (dialog, which) -> { String[] permissionToRequest = dangerousPermissions.toArray(new String[dangerousPermissions.size()]); try { ActivityCompat.requestPermissions(this, permissionToRequest, REQUEST_PERMISSION_CODE); } catch (Throwable ignored) { } }) .create(); try { alertDialog.show(); } catch (Throwable ignored) { // BadTokenException. finish(); Toast.makeText(this, getResources().getString(R.string.start_app_failed, appModel.name), Toast.LENGTH_SHORT).show(); } } } } catch (Throwable e) { Log.e(TAG, "check permission failed: ", e); launchActivityWithDelay(intent, userId); } } private void launchActivityWithDelay(Intent intent, int userId) { final int MAX_WAIT = 1000; long delta = SystemClock.elapsedRealtime() - start; long waitTime = MAX_WAIT - delta; if (waitTime <= 0) { launchActivity(intent, userId); } else { loadingView.postDelayed(() -> launchActivity(intent, userId), waitTime); } } private void launchActivity(Intent intent, int userId) { try { VActivityManager.get().startActivity(intent, userId); } catch (Throwable e) { VLog.e(TAG, "start activity failed:", e); Toast.makeText(getApplicationContext(), getResources().getString(R.string.start_app_failed, appModel.name), Toast.LENGTH_SHORT).show(); finish(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_PERMISSION_CODE) { boolean allGranted = true; for (int grantResult : grantResults) { if (grantResult == PackageManager.PERMISSION_DENIED) { allGranted = false; break; } } if (allGranted) { if (intentToLaunch == null) { Toast.makeText(this, getResources().getString(R.string.start_app_failed, appModel.name), Toast.LENGTH_SHORT).show(); finish(); } else { launchActivityWithDelay(intentToLaunch, userToLaunch); } } else { // 提示用户,targetSdkVersion < 23 无法使用运行时权限 Log.i(TAG, "can not use runtime permission, you must grant all permission, otherwise the app may not work!"); final String tag = "permission_tips_" + appModel.packageName.replaceAll("\\.", "_"); // TODO find a device figuring out why some permissions are not detected. if (!Once.beenDone(tag)) { AlertDialog alertDialog = new AlertDialog.Builder(this, R.style.Theme_AppCompat_DayNight_Dialog_Alert) .setTitle(android.R.string.dialog_alert_title) .setMessage(getResources().getString(R.string.permission_denied_tips_content, appModel.name)) .setPositiveButton(R.string.permission_tips_confirm, (dialog, which) -> { finish(); Once.markDone(tag); launchActivityWithDelay(intentToLaunch, userToLaunch); }) .create(); try { alertDialog.show(); } catch (Throwable ignored) { // BadTokenException. Toast.makeText(this, getResources().getString(R.string.start_app_failed, appModel.name), Toast.LENGTH_SHORT).show(); } } else { launchActivityWithDelay(intentToLaunch, userToLaunch); finish(); } } } } private final VirtualCore.UiCallback mUiCallback = new VirtualCore.UiCallback() { @Override public void onAppOpened(String packageName, int userId) throws RemoteException { finish(); } @Override public void onOpenFailed(String packageName, int userId) throws RemoteException { VUiKit.defer().when(() -> { }).done((v) -> { if (!isFinishing()) { Toast.makeText(getApplicationContext(), getResources().getString(R.string.start_app_failed, packageName), Toast.LENGTH_SHORT).show(); } }); finish(); } }; @Override protected void onResume() { super.onResume(); startAnim(); } private void startAnim() { if (loadingView != null) { loadingView.startAnim(); } } @Override protected void onStop() { super.onStop(); if (loadingView != null) { loadingView.stopAnim(); } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/NewHomeActivity.java ================================================ package io.virtualapp.home; import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.PowerManager; import android.os.SystemClock; import android.preference.PreferenceManager; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.widget.Toast; import com.android.launcher3.LauncherFiles; import com.google.android.apps.nexuslauncher.NexusLauncherActivity; import com.lody.virtual.client.core.InstallStrategy; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.helper.utils.DeviceUtil; import com.lody.virtual.helper.utils.FileUtils; import com.lody.virtual.helper.utils.MD5Utils; import com.lody.virtual.helper.utils.VLog; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Method; import java.util.concurrent.atomic.AtomicBoolean; import io.virtualapp.R; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.settings.SettingsActivity; import io.virtualapp.update.VAVersionService; import io.virtualapp.utils.Misc; import jonathanfinerty.once.Once; import static io.virtualapp.XApp.XPOSED_INSTALLER_PACKAGE; /** * @author weishu * @date 18/2/9. */ public class NewHomeActivity extends NexusLauncherActivity { private static final String SHOW_DOZE_ALERT_KEY = "SHOW_DOZE_ALERT_KEY"; private static final String WALLPAPER_FILE_NAME = "wallpaper.png"; private Handler mUiHandler; private boolean mDirectlyBack = false; private final AtomicBoolean checkXposedInstaller = new AtomicBoolean(true); public static void goHome(Context context) { Intent intent = new Intent(context, NewHomeActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } @Override public void onCreate(Bundle savedInstanceState) { SharedPreferences sharedPreferences = getSharedPreferences(LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); super.onCreate(savedInstanceState); showMenuKey(); mUiHandler = new Handler(getMainLooper()); alertForMeizu(); alertForDonate(); mDirectlyBack = sharedPreferences.getBoolean(SettingsActivity.DIRECTLY_BACK_KEY, false); } private void installXposed() { if (!VirtualCore.get().isXposedEnabled()) { return; } boolean isXposedInstalled = false; try { isXposedInstalled = VirtualCore.get().isAppInstalled(XPOSED_INSTALLER_PACKAGE); File oldXposedInstallerApk = getFileStreamPath("XposedInstaller_1_31.apk"); if (oldXposedInstallerApk.exists()) { VirtualCore.get().uninstallPackage(XPOSED_INSTALLER_PACKAGE); oldXposedInstallerApk.delete(); isXposedInstalled = false; Log.d(TAG, "remove xposed installer success!"); } } catch (Throwable e) { VLog.d(TAG, "remove xposed install failed.", e); } if (!isXposedInstalled) { ProgressDialog dialog = new ProgressDialog(this); dialog.setCancelable(false); dialog.setMessage(getResources().getString(R.string.prepare_xposed_installer)); dialog.show(); VUiKit.defer().when(() -> { File xposedInstallerApk = getFileStreamPath("XposedInstaller_5_8.apk"); if (!xposedInstallerApk.exists()) { InputStream input = null; OutputStream output = null; try { input = getApplicationContext().getAssets().open("XposedInstaller_3.1.5.apk_"); output = new FileOutputStream(xposedInstallerApk); byte[] buffer = new byte[1024]; int length; while ((length = input.read(buffer)) > 0) { output.write(buffer, 0, length); } } catch (Throwable e) { VLog.e(TAG, "copy file error", e); } finally { FileUtils.closeQuietly(input); FileUtils.closeQuietly(output); } } if (xposedInstallerApk.isFile() && !DeviceUtil.isMeizuBelowN()) { try { if ("8537fb219128ead3436cc19ff35cfb2e".equals(MD5Utils.getFileMD5String(xposedInstallerApk))) { VirtualCore.get().installPackage(xposedInstallerApk.getPath(), InstallStrategy.TERMINATE_IF_EXIST); } else { VLog.w(TAG, "unknown Xposed installer, ignore!"); } } catch (Throwable ignored) { } } }).then((v) -> { dismissDialog(dialog); }).fail((err) -> { dismissDialog(dialog); }); } } private static void dismissDialog(ProgressDialog dialog) { if (dialog == null) { return; } try { dialog.dismiss(); } catch (Throwable e) { e.printStackTrace(); } } @Override protected void onResume() { super.onResume(); if (checkXposedInstaller.compareAndSet(true, false)) { installXposed(); } // check for update new Handler().postDelayed(() -> VAVersionService.checkUpdate(getApplicationContext(), false), 1000); // check for wallpaper setWallpaper(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_MENU) { onSettingsClicked(); return true; } return super.onKeyDown(keyCode, event); } public Activity getActivity() { return this; } public Context getContext() { return this; } @Override public void onClickAddWidgetButton(View view) { onAddAppClicked(); } private void onAddAppClicked() { ListAppActivity.gotoListApp(this); } private void onSettingsClicked() { startActivity(new Intent(NewHomeActivity.this, SettingsActivity.class)); } @Override public void onClickSettingsButton(View v) { onSettingsClicked(); } @Override protected void onClickAllAppsButton(View v) { onSettingsClicked(); } @Override public void startVirtualActivity(Intent intent, Bundle options, int usedId) { String packageName = intent.getPackage(); if (TextUtils.isEmpty(packageName)) { ComponentName component = intent.getComponent(); if (component != null) { packageName = component.getPackageName(); } } if (packageName == null) { try { startActivity(intent); return; } catch (Throwable ignored) { // ignore } } boolean result = LoadingActivity.launch(this, packageName, usedId); if (!result) { throw new ActivityNotFoundException("can not launch activity for :" + intent); } if (mDirectlyBack) { finish(); } } private void alertForDonate() { final String TAG = "show_donate"; if (Once.beenDone(Once.THIS_APP_VERSION, TAG)) { alertForDoze(); return; } AlertDialog alertDialog = new AlertDialog.Builder(getContext()) .setTitle(R.string.about_donate) .setMessage(R.string.donate_dialog_content) .setPositiveButton(android.R.string.yes, (dialog, which) -> { Misc.showDonate(this); Once.markDone(TAG); }) .create(); try { alertDialog.show(); } catch (Throwable ignored) { } } private void alertForMeizu() { if (!DeviceUtil.isMeizuBelowN()) { return; } boolean isXposedInstalled = VirtualCore.get().isAppInstalled(XPOSED_INSTALLER_PACKAGE); if (isXposedInstalled) { return; } mUiHandler.postDelayed(() -> { AlertDialog alertDialog = new AlertDialog.Builder(getContext()) .setTitle(R.string.meizu_device_tips_title) .setMessage(R.string.meizu_device_tips_content) .setPositiveButton(android.R.string.yes, (dialog, which) -> { }) .create(); try { alertDialog.show(); } catch (Throwable ignored) { } }, 2000); } private void alertForDoze() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return; } PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); if (powerManager == null) { return; } boolean showAlert = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(SHOW_DOZE_ALERT_KEY, true); if (!showAlert) { return; } String packageName = getPackageName(); boolean ignoringBatteryOptimizations = powerManager.isIgnoringBatteryOptimizations(packageName); if (!ignoringBatteryOptimizations) { mUiHandler.postDelayed(() -> { AlertDialog alertDialog = new AlertDialog.Builder(getContext()) .setTitle(R.string.alert_for_doze_mode_title) .setMessage(R.string.alert_for_doze_mode_content) .setPositiveButton(R.string.alert_for_doze_mode_yes, (dialog, which) -> { try { startActivity(new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Uri.parse("package:" + getPackageName()))); } catch (ActivityNotFoundException ignored) { // ActivityNotFoundException on some devices. try { startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)); } catch (Throwable e) { PreferenceManager.getDefaultSharedPreferences(getActivity()) .edit().putBoolean(SHOW_DOZE_ALERT_KEY, false).apply(); } } catch (Throwable e) { PreferenceManager.getDefaultSharedPreferences(getActivity()) .edit().putBoolean(SHOW_DOZE_ALERT_KEY, false).apply(); } }) .setNegativeButton(R.string.alert_for_doze_mode_no, (dialog, which) -> PreferenceManager.getDefaultSharedPreferences(getActivity()) .edit().putBoolean(SHOW_DOZE_ALERT_KEY, false).apply()) .create(); try { alertDialog.show(); } catch (Throwable ignored) { ignored.printStackTrace(); } }, 1000); } } private void setWallpaper() { File wallpaper = getFileStreamPath(WALLPAPER_FILE_NAME); if (wallpaper == null || !wallpaper.exists() || wallpaper.isDirectory()) { setOurWallpaper(getResources().getDrawable(R.drawable.home_bg)); } else { long start = SystemClock.elapsedRealtime(); Drawable d; try { d = BitmapDrawable.createFromPath(wallpaper.getPath()); } catch (Throwable e) { Toast.makeText(getApplicationContext(), R.string.wallpaper_too_big_tips, Toast.LENGTH_SHORT).show(); return; } long cost = SystemClock.elapsedRealtime() - start; if (cost > 200) { Toast.makeText(getApplicationContext(), R.string.wallpaper_too_big_tips, Toast.LENGTH_SHORT).show(); } if (d == null) { setOurWallpaper(getResources().getDrawable(R.drawable.home_bg)); } else { setOurWallpaper(d); } } } private void showMenuKey() { try { Method setNeedsMenuKey = Window.class.getDeclaredMethod("setNeedsMenuKey", int.class); setNeedsMenuKey.setAccessible(true); int value = WindowManager.LayoutParams.class.getField("NEEDS_MENU_SET_TRUE").getInt(null); setNeedsMenuKey.invoke(getWindow(), value); } catch (Exception e) { e.printStackTrace(); } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/adapters/AppPagerAdapter.java ================================================ package io.virtualapp.home.adapters; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import java.io.File; import java.util.ArrayList; import java.util.List; import io.virtualapp.R; import io.virtualapp.XApp; import io.virtualapp.home.ListAppFragment; /** * @author Lody */ public class AppPagerAdapter extends FragmentPagerAdapter { private List titles = new ArrayList<>(); private List dirs = new ArrayList<>(); public AppPagerAdapter(FragmentManager fm) { super(fm); titles.add(XApp.getApp().getResources().getString(R.string.clone_apps)); dirs.add(null); } @Override public Fragment getItem(int position) { return ListAppFragment.newInstance(dirs.get(position)); } @Override public int getCount() { return titles.size(); } @Override public CharSequence getPageTitle(int position) { return titles.get(position); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/adapters/CloneAppListAdapter.java ================================================ package io.virtualapp.home.adapters; import android.content.Context; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import java.io.File; import java.util.List; import io.virtualapp.R; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.glide.GlideUtils; import io.virtualapp.home.models.AppInfo; import io.virtualapp.widgets.DragSelectRecyclerViewAdapter; import io.virtualapp.widgets.LabelView; /** * @author Lody */ public class CloneAppListAdapter extends DragSelectRecyclerViewAdapter { private static final int TYPE_FOOTER = -2; private final View mFooterView; private LayoutInflater mInflater; private List mAppList; private ItemEventListener mItemEventListener; private Context mContext; private File mFrom; public CloneAppListAdapter(Context context, @Nullable File from) { mContext = context; mFrom = from; this.mInflater = LayoutInflater.from(context); mFooterView = new View(context); StaggeredGridLayoutManager.LayoutParams params = new StaggeredGridLayoutManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, VUiKit.dpToPx(context, 60) ); params.setFullSpan(true); mFooterView.setLayoutParams(params); } public void setOnItemClickListener(ItemEventListener mItemEventListener) { this.mItemEventListener = mItemEventListener; } public List getList() { return mAppList; } public void setList(List models) { this.mAppList = models; notifyDataSetChanged(); } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == TYPE_FOOTER) { return new ViewHolder(mFooterView); } return new ViewHolder(mInflater.inflate(R.layout.item_clone_app, null)); } @Override public void onBindViewHolder(ViewHolder holder, int position) { if (getItemViewType(position) == TYPE_FOOTER) { return; } super.onBindViewHolder(holder, position); AppInfo info = mAppList.get(position); if (mFrom == null) { GlideUtils.loadInstalledPackageIcon(mContext, info.packageName, holder.iconView, android.R.drawable.sym_def_app_icon); } else { GlideUtils.loadPackageIconFromApkFile(mContext, info.path, holder.iconView, android.R.drawable.sym_def_app_icon); } holder.nameView.setText(String.format("%s: %s%s", info.name, info.version, info.splitApk ? " [S]" : "")); if (isIndexSelected(position)) { holder.iconView.setAlpha(1f); holder.appCheckView.setImageResource(R.drawable.ic_check); } else { holder.iconView.setAlpha(0.65f); holder.appCheckView.setImageResource(R.drawable.ic_no_check); } if (info.cloneCount > 0) { holder.labelView.setVisibility(View.VISIBLE); holder.labelView.setText(info.cloneCount + 1 + ""); } else { holder.labelView.setVisibility(View.INVISIBLE); } holder.itemView.setOnClickListener(v -> { mItemEventListener.onItemClick(info, position); }); } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); } @Override protected boolean isIndexSelectable(int index) { return mItemEventListener.isSelectable(index); } @Override public int getItemCount() { return mAppList == null ? 1 : mAppList.size() + 1; } @Override public int getItemViewType(int position) { if (position == getItemCount() - 1) { return TYPE_FOOTER; } return super.getItemViewType(position); } public AppInfo getItem(int index) { return mAppList.get(index); } public interface ItemEventListener { void onItemClick(AppInfo appData, int position); boolean isSelectable(int position); } class ViewHolder extends RecyclerView.ViewHolder { private ImageView iconView; private TextView nameView; private ImageView appCheckView; private LabelView labelView; ViewHolder(View itemView) { super(itemView); if (itemView != mFooterView) { iconView = (ImageView) itemView.findViewById(R.id.item_app_icon); nameView = (TextView) itemView.findViewById(R.id.item_app_name); appCheckView = (ImageView) itemView.findViewById(R.id.item_app_checked); labelView = (LabelView) itemView.findViewById(R.id.item_app_clone_count); } } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/models/AppData.java ================================================ package io.virtualapp.home.models; import android.graphics.drawable.Drawable; /** * @author Lody */ public interface AppData { boolean isInstalling(); boolean isLoading(); boolean isFirstOpen(); Drawable getIcon(); String getName(); boolean canReorder(); boolean canLaunch(); boolean canDelete(); boolean canCreateShortcut(); } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/models/AppInfo.java ================================================ package io.virtualapp.home.models; import android.graphics.drawable.Drawable; /** * @author Lody */ public class AppInfo { public String packageName; public String path; public boolean fastOpen; public Drawable icon; public CharSequence name; public CharSequence version; public int cloneCount; public boolean disableMultiVersion; public boolean splitApk; } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/models/AppInfoLite.java ================================================ package io.virtualapp.home.models; import android.os.Parcel; import android.os.Parcelable; /** * @author Lody */ public class AppInfoLite implements Parcelable { public static final Creator CREATOR = new Creator() { @Override public AppInfoLite createFromParcel(Parcel source) { return new AppInfoLite(source); } @Override public AppInfoLite[] newArray(int size) { return new AppInfoLite[size]; } }; public String packageName; public String path; public boolean fastOpen; public boolean disableMultiVersion; public AppInfoLite(String packageName, String path, boolean fastOpen, boolean disableMultiVersion) { this.packageName = packageName; this.path = path; this.fastOpen = fastOpen; this.disableMultiVersion = disableMultiVersion; } protected AppInfoLite(Parcel in) { this.packageName = in.readString(); this.path = in.readString(); this.fastOpen = in.readByte() != 0; this.disableMultiVersion = in.readByte() != 0; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.packageName); dest.writeString(this.path); dest.writeByte(this.fastOpen ? (byte) 1 : (byte) 0); dest.writeByte(this.disableMultiVersion ? (byte) 1 : (byte) 0); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/models/MultiplePackageAppData.java ================================================ package io.virtualapp.home.models; import android.graphics.drawable.Drawable; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.remote.InstalledAppInfo; /** * @author Lody */ public class MultiplePackageAppData implements AppData { public InstalledAppInfo appInfo; public int userId; public boolean isFirstOpen; public boolean isInstalling; public boolean isLoading; public Drawable icon; public String name; public MultiplePackageAppData(PackageAppData target, int userId) { this.userId = userId; this.appInfo = VirtualCore.get().getInstalledAppInfo(target.packageName, 0); this.isFirstOpen = !appInfo.isLaunched(userId); if (target.icon != null) { Drawable.ConstantState state = target.icon.getConstantState(); if (state != null) { icon = state.newDrawable(); } } name = target.name; } @Override public boolean isInstalling() { return isInstalling; } @Override public boolean isLoading() { return isLoading; } @Override public boolean isFirstOpen() { return isFirstOpen; } @Override public Drawable getIcon() { return icon; } @Override public String getName() { return name; } @Override public boolean canReorder() { return true; } @Override public boolean canLaunch() { return true; } @Override public boolean canDelete() { return true; } @Override public boolean canCreateShortcut() { return true; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/models/PackageAppData.java ================================================ package io.virtualapp.home.models; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import com.lody.virtual.remote.InstalledAppInfo; /** * @author Lody */ public class PackageAppData implements AppData { public String packageName; public String name; public Drawable icon; public boolean fastOpen; public boolean isFirstOpen; public boolean isLoading; public boolean isInstalling; public PackageAppData(Context context, InstalledAppInfo installedAppInfo) { this.packageName = installedAppInfo.packageName; this.isFirstOpen = !installedAppInfo.isLaunched(0); loadData(context, installedAppInfo.getApplicationInfo(installedAppInfo.getInstalledUsers()[0])); } public PackageAppData(Context context, ApplicationInfo appInfo) { this.packageName = appInfo.packageName; loadData(context, appInfo); } private void loadData(Context context, ApplicationInfo appInfo) { if (appInfo == null) { return; } PackageManager pm = context.getPackageManager(); try { CharSequence sequence = appInfo.loadLabel(pm); if (sequence != null) { name = sequence.toString(); } icon = appInfo.loadIcon(pm); } catch (Throwable e) { e.printStackTrace(); } } @Override public boolean isInstalling() { return isInstalling; } @Override public boolean isLoading() { return isLoading; } @Override public boolean isFirstOpen() { return isFirstOpen; } @Override public Drawable getIcon() { return icon; } @Override public String getName() { return name; } @Override public boolean canReorder() { return true; } @Override public boolean canLaunch() { return true; } @Override public boolean canDelete() { return true; } @Override public boolean canCreateShortcut() { return true; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/repo/AppDataSource.java ================================================ package io.virtualapp.home.repo; import android.content.Context; import com.lody.virtual.remote.InstallResult; import org.jdeferred.Promise; import java.io.File; import java.util.List; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.AppInfo; import io.virtualapp.home.models.AppInfoLite; /** * @author Lody * @version 1.0 */ public interface AppDataSource { /** * @return All the Applications we Virtual. */ Promise, Throwable, Void> getVirtualApps(); /** * @param context Context * @return All the Applications we Installed. */ Promise, Throwable, Void> getInstalledApps(Context context); Promise, Throwable, Void> getStorageApps(Context context, File rootDir); InstallResult addVirtualApp(AppInfoLite info); boolean removeVirtualApp(String packageName, int userId); } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/repo/AppRepository.java ================================================ package io.virtualapp.home.repo; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import com.lody.virtual.GmsSupport; import com.lody.virtual.client.core.InstallStrategy; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.helper.utils.DeviceUtil; import com.lody.virtual.remote.InstallResult; import com.lody.virtual.remote.InstalledAppInfo; import org.jdeferred.Promise; import java.io.File; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.AppInfo; import io.virtualapp.home.models.AppInfoLite; import io.virtualapp.home.models.MultiplePackageAppData; import io.virtualapp.home.models.PackageAppData; import io.virtualapp.utils.HanziToPinyin; /** * @author Lody */ public class AppRepository implements AppDataSource { private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA); private static final List SCAN_PATH_LIST = Arrays.asList( ".", "wandoujia/app", "tencent/tassistant/apk", "BaiduAsa9103056", "360Download", "pp/downloader", "pp/downloader/apk", "pp/downloader/silent/apk"); private static final int MAX_SCAN_DEPTH = 2; private Context mContext; public AppRepository(Context context) { mContext = context; } private static boolean isSystemApplication(PackageInfo packageInfo) { return (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 && !GmsSupport.isGmsFamilyPackage(packageInfo.packageName); } @Override public Promise, Throwable, Void> getVirtualApps() { return VUiKit.defer().when(() -> { List infos = VirtualCore.get().getInstalledApps(0); List models = new ArrayList<>(); for (InstalledAppInfo info : infos) { if (!VirtualCore.get().isPackageLaunchable(info.packageName)) { continue; } PackageAppData data = new PackageAppData(mContext, info); if (VirtualCore.get().isAppInstalledAsUser(0, info.packageName)) { models.add(data); } int[] userIds = info.getInstalledUsers(); for (int userId : userIds) { if (userId != 0) { models.add(new MultiplePackageAppData(data, userId)); } } } return models; }); } @Override public Promise, Throwable, Void> getInstalledApps(Context context) { return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, context.getPackageManager().getInstalledPackages(PackageManager.GET_META_DATA), true)); } @Override public Promise, Throwable, Void> getStorageApps(Context context, File rootDir) { // return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, findAndParseAPKs(context, rootDir, SCAN_PATH_LIST), false)); return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, findAndParseApkRecursively(context, rootDir,null, 0), false)); } private List findAndParseApkRecursively(Context context, File rootDir, List result, int depth) { if (result == null) { result = new ArrayList<>(); } if (depth > MAX_SCAN_DEPTH) { return result; } File[] dirFiles = rootDir.listFiles(); if (dirFiles == null) { return Collections.emptyList(); } for (File f: dirFiles) { if (f.isDirectory()) { List andParseApkRecursively = findAndParseApkRecursively(context, f, new ArrayList<>(), depth + 1); result.addAll(andParseApkRecursively); } if (!(f.isFile() && f.getName().toLowerCase().endsWith(".apk"))) { continue; } PackageInfo pkgInfo = null; try { pkgInfo = context.getPackageManager().getPackageArchiveInfo(f.getAbsolutePath(), PackageManager.GET_META_DATA); pkgInfo.applicationInfo.sourceDir = f.getAbsolutePath(); pkgInfo.applicationInfo.publicSourceDir = f.getAbsolutePath(); } catch (Exception e) { // Ignore } if (pkgInfo != null) { result.add(pkgInfo); } } return result; } private List findAndParseAPKs(Context context, File rootDir, List paths) { List packageList = new ArrayList<>(); if (paths == null) return packageList; for (String path : paths) { File[] dirFiles = new File(rootDir, path).listFiles(); if (dirFiles == null) continue; for (File f : dirFiles) { if (!f.getName().toLowerCase().endsWith(".apk")) continue; PackageInfo pkgInfo = null; try { pkgInfo = context.getPackageManager().getPackageArchiveInfo(f.getAbsolutePath(), 0); pkgInfo.applicationInfo.sourceDir = f.getAbsolutePath(); pkgInfo.applicationInfo.publicSourceDir = f.getAbsolutePath(); } catch (Exception e) { // Ignore } if (pkgInfo != null) packageList.add(pkgInfo); } } return packageList; } private List convertPackageInfoToAppData(Context context, List pkgList, boolean fastOpen) { PackageManager pm = context.getPackageManager(); List list = new ArrayList<>(pkgList.size()); String hostPkg = VirtualCore.get().getHostPkg(); for (PackageInfo pkg : pkgList) { // ignore the host package if (hostPkg.equals(pkg.packageName)) { continue; } // ignore taichi package if (VirtualCore.TAICHI_PACKAGE.equals(pkg.packageName)) { continue; } // ignore the System package if (isSystemApplication(pkg)) { continue; } ApplicationInfo ai = pkg.applicationInfo; String path = ai.publicSourceDir != null ? ai.publicSourceDir : ai.sourceDir; boolean splitApk = false; if (ai.splitPublicSourceDirs != null || ai.splitSourceDirs != null) { splitApk = true; path = new File(path).getParent(); } if (path == null) { continue; } AppInfo info = new AppInfo(); info.packageName = pkg.packageName; info.fastOpen = fastOpen; info.path = path; info.icon = null; // Use Glide to load the icon async info.name = ai.loadLabel(pm); info.version = pkg.versionName; info.splitApk = splitApk; InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(pkg.packageName, 0); if (installedAppInfo != null) { info.cloneCount = installedAppInfo.getInstalledUsers().length; } if (ai.metaData != null && ai.metaData.containsKey("xposedmodule")) { info.disableMultiVersion = true; info.cloneCount = 0; } list.add(info); } // sort by name Collections.sort(list, (o1, o2) -> { HanziToPinyin hanziToPinyin = HanziToPinyin.getInstance(); String pinyin1 = hanziToPinyin.toPinyinString(o1.name.toString().trim()); String pinyin2 = hanziToPinyin.toPinyinString(o2.name.toString().trim()); return pinyin1.compareTo(pinyin2); }); return list; } @Override public InstallResult addVirtualApp(AppInfoLite info) { int flags = InstallStrategy.COMPARE_VERSION | InstallStrategy.SKIP_DEX_OPT; info.fastOpen = false; // disable fast open for compile. if (DeviceUtil.isMeizuBelowN()) { info.fastOpen = true; } if (info.fastOpen) { flags |= InstallStrategy.DEPEND_SYSTEM_IF_EXIST; } if (info.disableMultiVersion) { flags |= InstallStrategy.UPDATE_IF_EXIST; } return VirtualCore.get().installPackage(info.path, flags); } @Override public boolean removeVirtualApp(String packageName, int userId) { return VirtualCore.get().uninstallPackageAsUser(packageName, userId); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/repo/PackageAppDataStorage.java ================================================ package io.virtualapp.home.repo; import android.content.pm.ApplicationInfo; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.remote.InstalledAppInfo; import java.util.HashMap; import java.util.Map; import io.virtualapp.XApp; import io.virtualapp.abs.Callback; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.models.PackageAppData; /** * @author Lody *

* Cache the loaded PackageAppData. */ public class PackageAppDataStorage { private static final PackageAppDataStorage STORAGE = new PackageAppDataStorage(); private final Map packageDataMap = new HashMap<>(); public static PackageAppDataStorage get() { return STORAGE; } public PackageAppData acquire(String packageName) { PackageAppData data; synchronized (packageDataMap) { data = packageDataMap.get(packageName); if (data == null) { data = loadAppData(packageName); } } return data; } public void acquire(String packageName, Callback callback) { VUiKit.defer() .when(() -> acquire(packageName)) .done(callback::callback); } private PackageAppData loadAppData(String packageName) { InstalledAppInfo setting = VirtualCore.get().getInstalledAppInfo(packageName, 0); if (setting != null) { PackageAppData data = new PackageAppData(XApp.getApp(), setting); synchronized (packageDataMap) { packageDataMap.put(packageName, data); } return data; } return null; } public PackageAppData acquire(ApplicationInfo appInfo) { PackageAppData data; synchronized (packageDataMap) { data = packageDataMap.get(appInfo.packageName); if (data == null) { data = loadAppData(appInfo); } } return data; } public void acquire(ApplicationInfo appInfo, Callback callback) { VUiKit.defer() .when(() -> acquire(appInfo)) .done(callback::callback); } private PackageAppData loadAppData(ApplicationInfo appInfo) { PackageAppData data = new PackageAppData(XApp.getApp(), appInfo); synchronized (packageDataMap) { packageDataMap.put(appInfo.packageName, data); } return data; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/settings/AboutActivity.java ================================================ package io.virtualapp.settings; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; import android.view.Gravity; import android.view.View; import android.widget.Toast; import java.util.Calendar; import io.virtualapp.R; import io.virtualapp.abs.ui.VActivity; import io.virtualapp.update.VAVersionService; import mehdi.sakout.aboutpage.AboutPage; import mehdi.sakout.aboutpage.Element; /** * author: weishu on 18/1/12. */ public class AboutActivity extends VActivity { private AboutPage mPage; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPage = new AboutPage(this) .isRTL(false) .setImage(R.mipmap.ic_launcher) .addItem(getCopyRightsElement()) .addItem(getVersionElement()) .addItem(getCheckUpdateElement()) .addItem(getFeedbackEmailElement()) .addItem(getThanksElement()) .addItem(getFeedbacTelegramElement()) .addItem(getWebsiteElement()) .addGitHub("tiann"); View aboutPage = mPage.create(); setContentView(aboutPage); } Element getCopyRightsElement() { Element copyRightsElement = new Element(); final String copyrights = String.format(getString(R.string.copy_right), Calendar.getInstance().get(Calendar.YEAR)); copyRightsElement.setTitle(copyrights); copyRightsElement.setGravity(Gravity.START); return copyRightsElement; } Element getVersionElement() { Element version = new Element(); String versionName = "unknown"; try { PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0); versionName = packageInfo.versionName; } catch (PackageManager.NameNotFoundException ignored) { } version.setTitle(getResources().getString(R.string.about_version_title, versionName)); final int[] clickCount = {0}; version.setOnClickListener(v -> { clickCount[0]++; if (clickCount[0] == 3) { mPage.addItem(getFeedbackQQElement()); mPage.addItem(getFeedbackWechatElement()); } }); return version; } Element getFeedbackQQElement() { Element feedback = new Element(); final String qqGroup = "597478474"; feedback.setTitle(getResources().getString(R.string.about_feedback_qq_title)); feedback.setOnClickListener(v -> { ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); if (clipboardManager != null) { clipboardManager.setPrimaryClip(ClipData.newPlainText(null, qqGroup)); } Toast.makeText(v.getContext(), getResources().getString(R.string.about_feedback_tips), Toast.LENGTH_SHORT).show(); }); return feedback; } Element getFeedbackEmailElement() { Element emailElement = new Element(); final String email = "virtualxposed@gmail.com"; String title = getResources().getString(R.string.about_feedback_title); emailElement.setTitle(title); Uri uri = Uri.parse("mailto:" + email); Intent intent = new Intent(Intent.ACTION_SENDTO, uri); intent.putExtra(Intent.EXTRA_SUBJECT, title); // 主题 String hint = getResources().getString(R.string.about_feedback_hint); intent.putExtra(Intent.EXTRA_TEXT, hint); emailElement.setIntent(intent); return emailElement; } Element getFeedbackWechatElement() { Element feedback = new Element(); // final String weChatGroup = "CSYJZF"; feedback.setTitle(getResources().getString(R.string.about_feedback_wechat_title)); feedback.setOnClickListener(v -> { ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); if (clipboardManager != null) { clipboardManager.setPrimaryClip(ClipData.newPlainText(null, "VirtualXposed")); } Toast.makeText(v.getContext(), getResources().getString(R.string.about_feedback_tips), Toast.LENGTH_SHORT).show(); }); return feedback; } Element getFeedbacTelegramElement() { Element feedback = new Element(); final String weChatGroup = "VirtualXposed"; feedback.setTitle(getResources().getString(R.string.about_feedback_tel_title, weChatGroup)); feedback.setOnClickListener(v -> { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("https://t.me/joinchat/Gtti8Usj1JD4TchHQmy-ew")); try { startActivity(intent); } catch (Throwable ignored) { } }); return feedback; } Element getWebsiteElement() { Element feedback = new Element(); feedback.setTitle(getResources().getString(R.string.about_website_title)); feedback.setOnClickListener(v -> { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("http://vxposed.com")); try { startActivity(intent); } catch (Throwable ignored) { } }); return feedback; } Element getThanksElement() { Element thanks = new Element(); thanks.setTitle(getResources().getString(R.string.about_thanks)); thanks.setOnClickListener(v -> { AlertDialog alertDialog = new AlertDialog.Builder(this, R.style.Theme_AppCompat_DayNight_Dialog_Alert) .setTitle(R.string.thanks_dialog_title) .setMessage(R.string.thanks_dialog_content) .setPositiveButton(R.string.about_icon_yes, null) .create(); try { alertDialog.show(); } catch (Throwable ignored) { // BadTokenException. } }); return thanks; } Element getCheckUpdateElement() { Element checkUpdate = new Element(); checkUpdate.setTitle(getResources().getString(R.string.check_update)); checkUpdate.setOnClickListener(v -> { VAVersionService.checkUpdateImmediately(getApplicationContext(), true); }); return checkUpdate; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/settings/AppManageActivity.java ================================================ package io.virtualapp.settings; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.provider.Settings; import android.support.annotation.Nullable; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.client.ipc.VirtualStorageManager; import com.lody.virtual.helper.ArtDexOptimizer; import com.lody.virtual.os.VEnvironment; import com.lody.virtual.remote.InstalledAppInfo; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import io.virtualapp.R; import io.virtualapp.abs.ui.VActivity; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.glide.GlideUtils; /** * @author weishu * @date 18/2/15. */ public class AppManageActivity extends VActivity { private ListView mListView; private List mInstalledApps = new ArrayList<>(); private AppManageAdapter mAdapter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list); mListView = (ListView) findViewById(R.id.list); mAdapter = new AppManageAdapter(); mListView.setAdapter(mAdapter); mListView.setOnItemClickListener((parent, view, position, id) -> { AppManageInfo appManageInfo = mInstalledApps.get(position); showContextMenu(appManageInfo, view); }); loadAsync(); } private void loadAsync() { VUiKit.defer().when(this::loadApp).done((v) -> mAdapter.notifyDataSetChanged()); } private void loadApp() { List ret = new ArrayList<>(); List installedApps = VirtualCore.get().getInstalledApps(0); PackageManager packageManager = getPackageManager(); for (InstalledAppInfo installedApp : installedApps) { int[] installedUsers = installedApp.getInstalledUsers(); for (int installedUser : installedUsers) { AppManageInfo info = new AppManageInfo(); info.userId = installedUser; ApplicationInfo applicationInfo = installedApp.getApplicationInfo(installedUser); info.name = applicationInfo.loadLabel(packageManager); // info.icon = applicationInfo.loadIcon(packageManager); //Use Glide to load icon async info.pkgName = installedApp.packageName; info.path = applicationInfo.sourceDir; ret.add(info); } } mInstalledApps.clear(); mInstalledApps.addAll(ret); } class AppManageAdapter extends BaseAdapter { @Override public int getCount() { return mInstalledApps.size(); } @Override public AppManageInfo getItem(int position) { return mInstalledApps.get(position); } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(AppManageActivity.this, parent); convertView = holder.root; convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } AppManageInfo item = getItem(position); holder.label.setText(item.getName()); if (VirtualCore.get().isOutsideInstalled(item.pkgName)) { GlideUtils.loadInstalledPackageIcon(getContext(), item.pkgName, holder.icon, android.R.drawable.sym_def_app_icon); } else { GlideUtils.loadPackageIconFromApkFile(getContext(), item.path, holder.icon, android.R.drawable.sym_def_app_icon); } holder.button.setOnClickListener(v -> showContextMenu(item, v)); return convertView; } } private void showContextMenu(AppManageInfo appManageInfo, View anchor) { if (appManageInfo == null) { return; } PopupMenu popupMenu = new PopupMenu(this, anchor); popupMenu.inflate(R.menu.app_manage_menu); MenuItem redirectMenu = popupMenu.getMenu().findItem(R.id.action_redirect); try { final String packageName = appManageInfo.pkgName; final int userId = appManageInfo.userId; boolean virtualStorageEnable = VirtualStorageManager.get().isVirtualStorageEnable(packageName, userId); redirectMenu.setTitle(virtualStorageEnable ? R.string.app_manage_redirect_off : R.string.app_manage_redirect_on); } catch (Throwable e) { redirectMenu.setVisible(false); } popupMenu.setOnMenuItemClickListener(item -> { switch (item.getItemId()) { case R.id.action_uninstall: showUninstallDialog(appManageInfo, appManageInfo.getName()); break; case R.id.action_repair: showRepairDialog(appManageInfo); break; case R.id.action_redirect: showStorageRedirectDialog(appManageInfo); break; } return false; }); try { popupMenu.show(); } catch (Throwable ignored) { } } private void showRepairDialog(AppManageInfo item) { ProgressDialog dialog = new ProgressDialog(this); dialog.setTitle(getResources().getString(R.string.app_manage_repairing)); try { dialog.setCancelable(false); dialog.show(); } catch (Throwable e) { return; } VUiKit.defer().when(() -> { NougatPolicy.fullCompile(getApplicationContext()); String packageName = item.pkgName; String apkPath = item.path; if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(apkPath)) { return; } // 1. kill package VirtualCore.get().killApp(packageName, item.userId); // 2. backup the odex file File odexFile = VEnvironment.getOdexFile(packageName); if (odexFile.delete()) { try { ArtDexOptimizer.compileDex2Oat(apkPath, odexFile.getPath()); } catch (IOException e) { throw new RuntimeException("compile failed."); } } }).done((v) -> { dismiss(dialog); showAppDetailDialog(); }).fail((v) -> { dismiss(dialog); Toast.makeText(this, R.string.app_manage_repair_failed_tips, Toast.LENGTH_SHORT).show(); }); } private void showAppDetailDialog() { AlertDialog alertDialog = new AlertDialog.Builder(AppManageActivity.this) .setTitle(R.string.app_manage_repair_success_title) .setMessage(getResources().getString(R.string.app_manage_repair_success_content)) .setPositiveButton(R.string.app_manage_repair_reboot_now, (dialog, which) -> { String packageName = getPackageName(); Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", packageName, null)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(intent); }) .create(); alertDialog.setCancelable(false); try { alertDialog.show(); } catch (Throwable ignored) { } } private void showUninstallDialog(AppManageInfo item, CharSequence name) { AlertDialog alertDialog = new AlertDialog.Builder(AppManageActivity.this) .setTitle(com.android.launcher3.R.string.home_menu_delete_title) .setMessage(getResources().getString(com.android.launcher3.R.string.home_menu_delete_content, name)) .setPositiveButton(android.R.string.yes, (dialog, which) -> { VirtualCore.get().uninstallPackageAsUser(item.pkgName, item.userId); loadAsync(); }) .setNegativeButton(android.R.string.no, null) .create(); try { alertDialog.show(); } catch (Throwable ignored) { } } private void showStorageRedirectDialog(AppManageInfo item) { final String packageName = item.pkgName; final int userId = item.userId; boolean virtualStorageEnable; try { virtualStorageEnable = VirtualStorageManager.get().isVirtualStorageEnable(packageName, userId); } catch (Throwable e) { return; } AlertDialog alertDialog = new AlertDialog.Builder(AppManageActivity.this) .setTitle(virtualStorageEnable ? R.string.app_manage_redirect_off : R.string.app_manage_redirect_on) .setMessage(getResources().getString(R.string.app_manage_redirect_desc)) .setPositiveButton(virtualStorageEnable ? R.string.app_manage_redirect_off_confirm : R.string.app_manage_redirect_on_confirm, (dialog, which) -> { try { VirtualStorageManager.get().setVirtualStorageState(packageName, userId, !virtualStorageEnable); } catch (Throwable ignored) { } }) .setNegativeButton(android.R.string.no, null) .create(); try { alertDialog.show(); } catch (Throwable ignored) { } } static class ViewHolder { ImageView icon; TextView label; ImageView button; View root; ViewHolder(Context context, ViewGroup parent) { root = LayoutInflater.from(context).inflate(R.layout.item_app_manage, parent, false); icon = root.findViewById(R.id.item_app_icon); label = root.findViewById(R.id.item_app_name); button = root.findViewById(R.id.item_app_button); } } static class AppManageInfo { CharSequence name; int userId; Drawable icon; String pkgName; String path; CharSequence getName() { if (userId == 0) { return name; } else { return name + "[" + (userId + 1) + "]"; } } } private static void dismiss(Dialog dialog) { if (dialog == null) { return; } try { dialog.dismiss(); } catch (Throwable ignored) { } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/settings/NougatPolicy.java ================================================ package io.virtualapp.settings; import android.content.Context; import android.os.Build; import java.lang.reflect.Method; /** * Android 7.0 全量编译策略 * Created by weishu on 17/6/12. */ public class NougatPolicy { static boolean fullCompile(Context context) { if (Build.VERSION.SDK_INT < 24) { return true; } try { Object pm = getPackageManagerBinderProxy(); if (pm == null) { return false; } /* @Override public boolean performDexOptMode(String packageName, boolean checkProfiles, String targetCompilerFilter, boolean force) { int dexOptStatus = performDexOptTraced(packageName, checkProfiles, targetCompilerFilter, force); return dexOptStatus != PackageDexOptimizer.DEX_OPT_FAILED; */ final Method performDexOptMode = pm.getClass().getDeclaredMethod("performDexOptMode", String.class, boolean.class, String.class, boolean.class); return (boolean) performDexOptMode.invoke(pm, context.getPackageName(), false, "speed", true); } catch (Throwable e) { return false; } } public static boolean clearCompileData(Context context) { boolean ret; try { Object pm = getPackageManagerBinderProxy(); final Method performDexOpt = pm.getClass().getDeclaredMethod("performDexOpt", String.class, boolean.class, int.class, boolean.class); ret = (Boolean) performDexOpt.invoke(pm, context.getPackageName(), false, 2 /*install*/, true); } catch (Throwable e) { ret = false; } return ret; } private static Object getPackageManagerBinderProxy() throws Exception { Class activityThread = Class.forName("android.app.ActivityThread"); final Method getPackageManager = activityThread.getDeclaredMethod("getPackageManager"); return getPackageManager.invoke(null); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/settings/OnlinePlugin.java ================================================ package io.virtualapp.settings; import android.app.Activity; import android.app.ProgressDialog; import android.os.SystemClock; import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; import com.lody.virtual.client.core.InstallStrategy; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.os.VEnvironment; import com.lody.virtual.remote.InstallResult; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import io.virtualapp.R; import io.virtualapp.gms.FakeGms; import io.virtualapp.home.LoadingActivity; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; import static io.virtualapp.utils.DialogUtil.showDialog; /** * @author weishu * @date 2018/7/5. */ public class OnlinePlugin { private static final String TAG = "OnlinePlugin"; public static final String FILE_MANAGE_PACKAGE = "com.amaze.filemanager"; public static final String FILE_MANAGE_URL = "http://vaexposed.weishu.me/amaze.json"; public static final String PERMISSION_MANAGE_PACKAGE = "eu.faircode.xlua"; public static final String PERMISSION_MANAGE_URL = "http://vaexposed.weishu.me/xlua.json"; public static void openOrDownload(Activity context, String packageName, String url, String tips) { if (context == null || packageName == null) { return; } if (VirtualCore.get().isAppInstalled(packageName)) { LoadingActivity.launch(context, packageName, 0); return; } AlertDialog failDialog = new AlertDialog.Builder(context, R.style.Theme_AppCompat_DayNight_Dialog_Alert) .setTitle(android.R.string.dialog_alert_title) .setMessage(tips) .setPositiveButton(android.R.string.ok, ((dialog1, which1) -> { ProgressDialog progressDialog = new ProgressDialog(context); progressDialog.setCancelable(false); progressDialog.show(); Executors.newSingleThreadExecutor().submit(() -> { String error = downloadAndInstall(context, progressDialog, url, packageName); try { progressDialog.dismiss(); } catch (Throwable e) { e.printStackTrace(); } if (error == null) { context.runOnUiThread(() -> { LoadingActivity.launch(context, packageName, 0); }); } else { context.runOnUiThread(() -> Toast.makeText(context, error, Toast.LENGTH_SHORT).show()); } }); })) .setNegativeButton(android.R.string.cancel, null) .create(); showDialog(failDialog); } private static String downloadAndInstall(Activity activity, ProgressDialog dialog, String url, String packageName) { OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build(); Request request = new Request.Builder() .url(url) .build(); updateMessage(activity, dialog, "Prepare download..."); Response response; try { response = client.newCall(request).execute(); } catch (IOException e) { return "Download failed, please check your network, error: 0"; } if (!response.isSuccessful()) { return "Download failed, please check your network, error: 1"; } Log.i(TAG, "response success: " + response.code()); if (200 != response.code()) { return "Download failed, please check your network, error: 2"; } updateMessage(activity, dialog, "Parsing config..."); ResponseBody body = response.body(); if (body == null) { return "Download failed, please check your network, error: 3"; } String string; try { string = body.string(); } catch (IOException e) { return "Download failed, please check your network, error: 4"; } JSONObject jsonObject; try { jsonObject = new JSONObject(string); } catch (JSONException e) { return "Download failed, please check your network, error: 5"; } String downloadLink; boolean isXposed; try { downloadLink = jsonObject.getString("url"); isXposed = jsonObject.optBoolean("xposed", false); } catch (JSONException e) { return "Download failed, please check your network, error: 6"; } File outFile = new File(activity.getCacheDir(), packageName + ".apk"); FakeGms.downloadFile(downloadLink, outFile, progress -> updateMessage(activity, dialog, "download " + packageName + "..." + progress + "%")); updateMessage(activity, dialog, "installing " + packageName); InstallResult installResult = VirtualCore.get().installPackage(outFile.getAbsolutePath(), InstallStrategy.UPDATE_IF_EXIST); if (!installResult.isSuccess) { return "install " + packageName + " failed: " + installResult.error; } if (isXposed) { // Enable the Xposed module. updateMessage(activity, dialog, "enable " + packageName + " in Xposed Installer"); File dataDir = VEnvironment.getDataUserPackageDirectory(0, "de.robv.android.xposed.installer"); File modulePath = VEnvironment.getPackageResourcePath(packageName); File configDir = new File(dataDir, "exposed_conf" + File.separator + "modules.list"); FileWriter writer = null; try { writer = new FileWriter(configDir, true); writer.append(modulePath.getAbsolutePath()); writer.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if (writer != null) { try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } } } updateMessage(activity, dialog, " install success!!"); SystemClock.sleep(300); return null; } private static void updateMessage(Activity activity, ProgressDialog dialog, String msg) { if (activity == null || dialog == null || TextUtils.isEmpty(msg)) { return; } Log.i(TAG, "update dialog message: " + msg); activity.runOnUiThread(() -> { dialog.setMessage(msg); }); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/settings/RecommendPluginActivity.java ================================================ package io.virtualapp.settings; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ListView; import android.widget.TextView; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.List; import io.virtualapp.R; import io.virtualapp.abs.ui.VActivity; /** * author: weishu on 2018/5/9. */ public class RecommendPluginActivity extends VActivity { private List mData = new ArrayList<>(); private PluginAdapter mAdapter; private ProgressDialog mLoadingDialog; private static final String ADDRESS = "http://vaexposed.weishu.me/plugin.json"; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list); mLoadingDialog = new ProgressDialog(this); mLoadingDialog.setTitle("Loading"); ListView mListView = findViewById(R.id.list); mAdapter = new PluginAdapter(); mListView.setAdapter(mAdapter); mListView.setEmptyView(findViewById(R.id.empty_view)); mListView.setOnItemClickListener((parent, view, position, id) -> { try { PluginInfo item = mAdapter.getItem(position); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(item.link)); startActivity(intent); } catch (Throwable ignored) { ignored.printStackTrace(); } }); loadRecommend(); } private void loadRecommend() { try { mLoadingDialog.show(); } catch (Throwable ignored) { } defer().when(() -> { JSONArray jsonArray = null; URL url = new URL(ADDRESS); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setConnectTimeout(30000); connection.setReadTimeout(30000); if(connection.getResponseCode() == 200){ BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8")); String line; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null) { sb.append(line); } jsonArray = new JSONArray(sb.toString()); br.close(); } connection.disconnect(); return jsonArray; }).done(jsonArray -> { mLoadingDialog.dismiss(); if (jsonArray == null) { return; } mData.clear(); int length = jsonArray.length(); for (int i = 0; i < length; i++) { PluginInfo info = new PluginInfo(); try { JSONObject jsonObject = jsonArray.getJSONObject(i); info.name = jsonObject.getString("name"); info.desc = jsonObject.getString("desc"); info.link = jsonObject.getString("link"); } catch (JSONException e) { continue; } mData.add(info); } mAdapter.notifyDataSetChanged(); }).fail((v) -> { mLoadingDialog.dismiss(); }); } static class PluginInfo { String name; String desc; String link; } static class ViewHolder { TextView title; TextView summary; View root; public ViewHolder(Context context, ViewGroup parent) { root = LayoutInflater.from(context).inflate(R.layout.item_plugin_recommend, parent, false); title = root.findViewById(R.id.item_plugin_name); summary = root.findViewById(R.id.item_plugin_summary); } } class PluginAdapter extends BaseAdapter { @Override public int getCount() { return mData.size(); } @Override public PluginInfo getItem(int position) { return mData.get(position); } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(RecommendPluginActivity.this, parent); convertView = holder.root; convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } PluginInfo info = getItem(position); holder.title.setText(info.name); holder.summary.setText(info.desc); return convertView; } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/settings/SettingsActivity.java ================================================ package io.virtualapp.settings; import android.app.Activity; import android.app.ProgressDialog; import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; import android.preference.SwitchPreference; import android.widget.Toast; import com.android.launcher3.LauncherFiles; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.client.env.Constants; import com.lody.virtual.client.ipc.VActivityManager; import java.io.File; import java.io.IOException; import io.virtualapp.R; import io.virtualapp.VCommends; import io.virtualapp.gms.FakeGms; import io.virtualapp.home.ListAppActivity; import io.virtualapp.utils.Misc; /** * Settings activity for Launcher. Currently implements the following setting: Allow rotation */ public class SettingsActivity extends Activity { private static final String ADVANCE_SETTINGS_KEY = "settings_advance"; private static final String ADD_APP_KEY = "settings_add_app"; private static final String MODULE_MANAGE_KEY = "settings_module_manage"; private static final String APP_MANAGE_KEY = "settings_app_manage"; private static final String TASK_MANAGE_KEY = "settings_task_manage"; private static final String DESKTOP_SETTINGS_KEY = "settings_desktop"; private static final String FAQ_SETTINGS_KEY = "settings_faq"; private static final String DONATE_KEY = "settings_donate"; private static final String ABOUT_KEY = "settings_about"; private static final String REBOOT_KEY = "settings_reboot"; private static final String HIDE_SETTINGS_KEY = "advance_settings_hide_settings"; private static final String DISABLE_INSTALLER_KEY = "advance_settings_disable_installer"; public static final String ENABLE_LAUNCHER = "advance_settings_enable_launcher"; private static final String INSTALL_GMS_KEY = "advance_settings_install_gms"; public static final String DIRECTLY_BACK_KEY = "advance_settings_directly_back"; private static final String RECOMMEND_PLUGIN = "settings_plugin_recommend"; private static final String DISABLE_RESIDENT_NOTIFICATION = "advance_settings_disable_resident_notification"; private static final String ALLOW_FAKE_SIGNATURE = "advance_settings_allow_fake_signature"; private static final String DISABLE_XPOSED = "advance_settings_disable_xposed"; private static final String FILE_MANAGE = "settings_file_manage"; private static final String PERMISSION_MANAGE = "settings_permission_manage"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { // Display the fragment as the main content. getFragmentManager().beginTransaction() .replace(android.R.id.content, new SettingsFragment()) .commit(); } } /** * This fragment shows the launcher preferences. */ public static class SettingsFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getPreferenceManager().setSharedPreferencesName(LauncherFiles.SHARED_PREFERENCES_KEY); addPreferencesFromResource(R.xml.settings_preferences); // Setup allow rotation preference Preference addApp = findPreference(ADD_APP_KEY); Preference moduleManage = findPreference(MODULE_MANAGE_KEY); Preference recommend = findPreference(RECOMMEND_PLUGIN); Preference appManage = findPreference(APP_MANAGE_KEY); Preference taskManage = findPreference(TASK_MANAGE_KEY); Preference desktop = findPreference(DESKTOP_SETTINGS_KEY); Preference faq = findPreference(FAQ_SETTINGS_KEY); Preference donate = findPreference(DONATE_KEY); Preference about = findPreference(ABOUT_KEY); Preference reboot = findPreference(REBOOT_KEY); Preference fileMange = findPreference(FILE_MANAGE); Preference permissionManage = findPreference(PERMISSION_MANAGE); SwitchPreference disableInstaller = (SwitchPreference) findPreference(DISABLE_INSTALLER_KEY); SwitchPreference enableLauncher = (SwitchPreference) findPreference(ENABLE_LAUNCHER); SwitchPreference disableResidentNotification = (SwitchPreference) findPreference(DISABLE_RESIDENT_NOTIFICATION); SwitchPreference allowFakeSignature = (SwitchPreference) findPreference(ALLOW_FAKE_SIGNATURE); SwitchPreference disableXposed = (SwitchPreference) findPreference(DISABLE_XPOSED); addApp.setOnPreferenceClickListener(preference -> { ListAppActivity.gotoListApp(getActivity()); return false; }); moduleManage.setOnPreferenceClickListener(preference -> { try { Intent t = new Intent(); t.setComponent(new ComponentName("de.robv.android.xposed.installer", "de.robv.android.xposed.installer.WelcomeActivity")); t.putExtra("fragment", 1); int ret = VActivityManager.get().startActivity(t, 0); if (ret < 0) { Toast.makeText(getActivity(), R.string.xposed_installer_not_found, Toast.LENGTH_SHORT).show(); } } catch (Throwable ignored) { ignored.printStackTrace(); } return false; }); recommend.setOnPreferenceClickListener(preference -> { startActivity(new Intent(getActivity(), RecommendPluginActivity.class)); return false; }); boolean xposedEnabled = VirtualCore.get().isXposedEnabled(); if (!xposedEnabled) { getPreferenceScreen().removePreference(moduleManage); getPreferenceScreen().removePreference(recommend); } appManage.setOnPreferenceClickListener(preference -> { startActivity(new Intent(getActivity(), AppManageActivity.class)); return false; }); taskManage.setOnPreferenceClickListener(preference -> { startActivity(new Intent(getActivity(), TaskManageActivity.class)); return false; }); faq.setOnPreferenceClickListener(preference -> { Uri uri = Uri.parse("https://github.com/android-hacker/VAExposed/wiki/FAQ"); Intent t = new Intent(Intent.ACTION_VIEW, uri); startActivity(t); return false; }); desktop.setOnPreferenceClickListener(preference -> { startActivity(new Intent(getActivity(), com.google.android.apps.nexuslauncher.SettingsActivity.class)); return false; }); donate.setOnPreferenceClickListener(preference -> { Misc.showDonate(getActivity()); return false; }); about.setOnPreferenceClickListener(preference -> { startActivity(new Intent(getActivity(), AboutActivity.class)); return false; }); reboot.setOnPreferenceClickListener(preference -> { android.app.AlertDialog alertDialog = new android.app.AlertDialog.Builder(getActivity()) .setTitle(R.string.settings_reboot_title) .setMessage(getResources().getString(R.string.settings_reboot_content)) .setPositiveButton(android.R.string.yes, (dialog, which) -> { VirtualCore.get().killAllApps(); Toast.makeText(getActivity(), R.string.reboot_tips_1, Toast.LENGTH_SHORT).show(); }) .setNegativeButton(android.R.string.no, null) .create(); try { alertDialog.show(); } catch (Throwable ignored) { } return false; }); disableInstaller.setOnPreferenceChangeListener((preference, newValue) -> { if (!(newValue instanceof Boolean)) { return false; } try { boolean disable = (boolean) newValue; PackageManager packageManager = getActivity().getPackageManager(); packageManager.setComponentEnabledSetting(new ComponentName(getActivity().getPackageName(), "vxp.installer"), !disable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); return true; } catch (Throwable ignored) { return false; } }); enableLauncher.setOnPreferenceChangeListener((preference, newValue) -> { if (!(newValue instanceof Boolean)) { return false; } try { boolean enable = (boolean) newValue; PackageManager packageManager = getActivity().getPackageManager(); packageManager.setComponentEnabledSetting(new ComponentName(getActivity().getPackageName(), "vxp.launcher"), enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); return true; } catch (Throwable ignored) { return false; } }); Preference installGms = findPreference(INSTALL_GMS_KEY); installGms.setOnPreferenceClickListener(preference -> { boolean alreadyInstalled = FakeGms.isAlreadyInstalled(getActivity()); if (alreadyInstalled) { FakeGms.uninstallGms(getActivity()); } else { FakeGms.installGms(getActivity()); } return true; }); fileMange.setOnPreferenceClickListener(preference -> { OnlinePlugin.openOrDownload(getActivity(), OnlinePlugin.FILE_MANAGE_PACKAGE, OnlinePlugin.FILE_MANAGE_URL, getString(R.string.install_file_manager_tips)); return false; }); permissionManage.setOnPreferenceClickListener(preference -> { OnlinePlugin.openOrDownload(getActivity(), OnlinePlugin.PERMISSION_MANAGE_PACKAGE, OnlinePlugin.PERMISSION_MANAGE_URL, getString(R.string.install_permission_manager_tips)); return false; }); disableXposed.setOnPreferenceChangeListener((preference, newValue) -> { if (!(newValue instanceof Boolean)) { return false; } boolean on = (boolean) newValue; File disableXposedFile = getActivity().getFileStreamPath(".disable_xposed"); // 文件不存在代表是保守模式 if (on) { boolean success; try { success = disableXposedFile.createNewFile(); } catch (IOException e) { success = false; } return success; } else { return !disableXposedFile.exists() || disableXposedFile.delete(); } }); disableResidentNotification.setOnPreferenceChangeListener(((preference, newValue) -> { if (!(newValue instanceof Boolean)) { return false; } boolean on = (boolean) newValue; File flag = getActivity().getFileStreamPath(Constants.NO_NOTIFICATION_FLAG); if (on) { boolean success; try { success = flag.createNewFile(); } catch (IOException e) { success = false; } return success; } else { return !flag.exists() || flag.delete(); } })); if (android.os.Build.VERSION.SDK_INT < 25) { // Android NR1 below do not need this. PreferenceScreen advance = (PreferenceScreen) findPreference(ADVANCE_SETTINGS_KEY); advance.removePreference(disableResidentNotification); } allowFakeSignature.setOnPreferenceChangeListener((preference, newValue) -> { if (!(newValue instanceof Boolean)) { return false; } boolean on = (boolean) newValue; File flag = getActivity().getFileStreamPath(Constants.FAKE_SIGNATURE_FLAG); if (on) { boolean success; try { success = flag.createNewFile(); } catch (IOException e) { success = false; } return success; } else { return !flag.exists() || flag.delete(); } }); } private static void dismiss(ProgressDialog dialog) { try { dialog.dismiss(); } catch (Throwable ignored) { } } protected int dp2px(float dp) { final float scale = getResources().getDisplayMetrics().density; return (int) (dp * scale + 0.5f); } @Override public void startActivity(Intent intent) { try { super.startActivity(intent); } catch (Throwable ignored) { Toast.makeText(getActivity(), "startActivity failed.", Toast.LENGTH_SHORT).show(); ignored.printStackTrace(); } } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == VCommends.REQUEST_SELECT_APP) { if (resultCode == RESULT_OK) { finish(); } } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/settings/TaskManageActivity.java ================================================ package io.virtualapp.settings; import android.app.ActivityManager; import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.client.ipc.VActivityManager; import com.lody.virtual.os.VUserHandle; import com.lody.virtual.remote.InstalledAppInfo; import java.util.ArrayList; import java.util.List; import io.virtualapp.R; import io.virtualapp.abs.ui.VActivity; import io.virtualapp.glide.GlideUtils; /** * @author weishu * @date 18/2/15. */ public class TaskManageActivity extends VActivity { private ListView mListView; private List mInstalledApps = new ArrayList<>(); private AppManageAdapter mAdapter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list); mListView = (ListView) findViewById(R.id.list); mAdapter = new AppManageAdapter(); mListView.setAdapter(mAdapter); loadAsync(); } private void loadAsync() { defer().when(this::loadApp).done((v) -> mAdapter.notifyDataSetChanged()); } private void loadApp() { ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE); if (am == null) { return; } List ret = new ArrayList<>(); List infoList = am.getRunningAppProcesses(); if (infoList == null) { return; } List retList = new ArrayList<>(); String hostPkg = VirtualCore.get().getHostPkg(); for (ActivityManager.RunningAppProcessInfo info : infoList) { if (VActivityManager.get().isAppPid(info.pid)) { List pkgList = VActivityManager.get().getProcessPkgList(info.pid); if (pkgList.contains(hostPkg)) { continue; } String processName = VActivityManager.get().getAppProcessName(info.pid); if (processName != null) { info.processName = processName; } info.pkgList = pkgList.toArray(new String[pkgList.size()]); info.uid = VUserHandle.getAppId(VActivityManager.get().getUidByPid(info.pid)); retList.add(info); } } for (ActivityManager.RunningAppProcessInfo runningAppProcessInfo : retList) { TaskManageInfo info = new TaskManageInfo(); info.name = runningAppProcessInfo.processName; info.pid = runningAppProcessInfo.pid; info.uid = runningAppProcessInfo.uid; if (runningAppProcessInfo.pkgList != null) { for (String pkg : runningAppProcessInfo.pkgList) { InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(pkg, 0); if (installedAppInfo != null) { info.pkgName = installedAppInfo.packageName; info.path = installedAppInfo.apkPath; } } } ret.add(info); } mInstalledApps.clear(); mInstalledApps.addAll(ret); } class AppManageAdapter extends BaseAdapter { @Override public int getCount() { return mInstalledApps.size(); } @Override public TaskManageInfo getItem(int position) { return mInstalledApps.get(position); } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(TaskManageActivity.this, parent); convertView = holder.root; convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } TaskManageInfo item = getItem(position); holder.button.setText(R.string.task_manage_uninstall); holder.label.setText(item.name); holder.icon.setImageDrawable(item.icon); if (VirtualCore.get().isOutsideInstalled(item.name.toString())) { GlideUtils.loadInstalledPackageIcon(getContext(), item.pkgName, holder.icon, android.R.drawable.sym_def_app_icon); } else { GlideUtils.loadPackageIconFromApkFile(getContext(), item.path, holder.icon, android.R.drawable.sym_def_app_icon); } holder.button.setOnClickListener(v -> { VActivityManager.get().killApplicationProcess(item.name.toString(), item.uid); holder.button.postDelayed(TaskManageActivity.this::loadAsync, 300); }); return convertView; } } static class ViewHolder { ImageView icon; TextView label; Button button; View root; ViewHolder(Context context, ViewGroup parent) { root = LayoutInflater.from(context).inflate(R.layout.item_task_manage, parent, false); icon = root.findViewById(R.id.item_app_icon); label = root.findViewById(R.id.item_app_name); button = root.findViewById(R.id.item_app_button); } } static class TaskManageInfo { public String pkgName; public String path; CharSequence name; int uid; int pid; Drawable icon; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/splash/SplashActivity.java ================================================ package io.virtualapp.splash; import android.os.Bundle; import android.view.WindowManager; import com.lody.virtual.client.core.VirtualCore; import io.virtualapp.R; import io.virtualapp.VCommends; import io.virtualapp.abs.ui.VActivity; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.NewHomeActivity; import jonathanfinerty.once.Once; public class SplashActivity extends VActivity { @Override protected void onCreate(Bundle savedInstanceState) { @SuppressWarnings("unused") boolean enterGuide = !Once.beenDone(Once.THIS_APP_INSTALL, VCommends.TAG_NEW_VERSION); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); VUiKit.defer().when(() -> { long time = System.currentTimeMillis(); doActionInThread(); time = System.currentTimeMillis() - time; long delta = 100L - time; if (delta > 0) { VUiKit.sleep(delta); } }).done((res) -> { NewHomeActivity.goHome(this); finish(); }); } private void doActionInThread() { if (!VirtualCore.get().isEngineLaunched()) { VirtualCore.get().waitForEngine(); } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/sys/Installd.java ================================================ package io.virtualapp.sys; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.text.TextUtils; import android.widget.Toast; import com.lody.virtual.GmsSupport; import com.lody.virtual.client.core.InstallStrategy; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.helper.utils.DeviceUtil; import com.lody.virtual.os.VUserInfo; import com.lody.virtual.os.VUserManager; import com.lody.virtual.remote.InstallResult; import com.lody.virtual.remote.InstalledAppInfo; import java.io.IOException; import java.util.ArrayList; import java.util.List; import io.virtualapp.R; import io.virtualapp.VCommends; import io.virtualapp.XApp; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.AppInfoLite; import io.virtualapp.home.models.MultiplePackageAppData; import io.virtualapp.home.models.PackageAppData; import io.virtualapp.home.repo.PackageAppDataStorage; /** * author: weishu on 18/3/19. */ public class Installd { public interface UpdateListener { void update(AppData model); void fail(String msg); } public static void addApp(AppInfoLite info, UpdateListener refreshListener) { class AddResult { private PackageAppData appData; private int userId; private boolean justEnableHidden; } AddResult addResult = new AddResult(); VUiKit.defer().when(() -> { InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(info.packageName, 0); addResult.justEnableHidden = installedAppInfo != null; if (info.disableMultiVersion) { addResult.justEnableHidden = false; } if (addResult.justEnableHidden) { int[] userIds = installedAppInfo.getInstalledUsers(); int nextUserId = userIds.length; /* Input : userIds = {0, 1, 3} Output: nextUserId = 2 */ for (int i = 0; i < userIds.length; i++) { if (userIds[i] != i) { nextUserId = i; break; } } addResult.userId = nextUserId; if (VUserManager.get().getUserInfo(nextUserId) == null) { // user not exist, create it automatically. String nextUserName = "Space " + (nextUserId + 1); VUserInfo newUserInfo = VUserManager.get().createUser(nextUserName, VUserInfo.FLAG_ADMIN); if (newUserInfo == null) { throw new IllegalStateException(); } } boolean success = VirtualCore.get().installPackageAsUser(nextUserId, info.packageName); if (!success) { throw new IllegalStateException(); } } else { PackageInfo pkgInfo = null; try { pkgInfo = XApp.getApp().getPackageManager().getPackageArchiveInfo(info.path, 0); pkgInfo.applicationInfo.sourceDir = info.path; pkgInfo.applicationInfo.publicSourceDir = info.path; } catch (Exception e) { } if(pkgInfo != null) { PackageAppData data = PackageAppDataStorage.get().acquire(pkgInfo.applicationInfo); addResult.appData = data; data.isInstalling = true; data.isFirstOpen = false; if (refreshListener != null) { refreshListener.update(data); } } InstallResult res = addVirtualApp(info); if (!res.isSuccess) { if (addResult.appData != null) { // mView.removeAppToLauncher(addResult.appData); } throw new IllegalStateException(res.error); } } }).then((res) -> { if (addResult.appData == null) { addResult.appData = PackageAppDataStorage.get().acquire(info.packageName); } }).done(res -> { boolean multipleVersion = addResult.justEnableHidden && addResult.userId != 0; if (!multipleVersion) { PackageAppData data = addResult.appData; data.isInstalling = false; data.isLoading = true; if (refreshListener != null) { refreshListener.update(data); } handleOptApp(data, info.packageName, true, refreshListener); } else { MultiplePackageAppData data = new MultiplePackageAppData(addResult.appData, addResult.userId); data.isInstalling = false; data.isLoading = true; if (refreshListener != null) { refreshListener.update(data); } handleOptApp(data, info.packageName, false, refreshListener); } }).fail(result -> { if (refreshListener != null) { refreshListener.fail(result.getMessage()); } }); } private static void handleOptApp(AppData data, String packageName, boolean needOpt, UpdateListener refreshListener) { VUiKit.defer().when(() -> { long time = System.currentTimeMillis(); if (needOpt) { try { VirtualCore.get().preOpt(packageName); } catch (IOException e) { e.printStackTrace(); } } time = System.currentTimeMillis() - time; if (time < 1500L) { try { Thread.sleep(1500L - time); } catch (InterruptedException e) { e.printStackTrace(); } } }).done((res) -> { if (data instanceof PackageAppData) { ((PackageAppData) data).isLoading = false; ((PackageAppData) data).isFirstOpen = true; } else if (data instanceof MultiplePackageAppData) { ((MultiplePackageAppData) data).isLoading = false; ((MultiplePackageAppData) data).isFirstOpen = true; } if (refreshListener != null) { refreshListener.update(data); } }); } public static InstallResult addVirtualApp(AppInfoLite info) { int flags = InstallStrategy.COMPARE_VERSION | InstallStrategy.SKIP_DEX_OPT; info.fastOpen = false; // disable fast open for compile. if (DeviceUtil.isMeizuBelowN()) { info.fastOpen = true; } if (info.fastOpen) { flags |= InstallStrategy.DEPEND_SYSTEM_IF_EXIST; } if (info.disableMultiVersion) { flags |= InstallStrategy.UPDATE_IF_EXIST; } return VirtualCore.get().installPackage(info.path, flags); } private static ArrayList getAppInfoLiteFromPath(Context context, String path) { if (context == null) { return null; } PackageInfo pkgInfo = null; try { pkgInfo = context.getPackageManager().getPackageArchiveInfo(path, PackageManager.GET_META_DATA); pkgInfo.applicationInfo.sourceDir = path; pkgInfo.applicationInfo.publicSourceDir = path; } catch (Exception e) { // Ignore } if (pkgInfo == null) { return null; } if (TextUtils.equals(VirtualCore.TAICHI_PACKAGE, pkgInfo.packageName)) { return null; } if (VirtualCore.get().getHostPkg().equals(pkgInfo.packageName)) { Toast.makeText(VirtualCore.get().getContext(), R.string.install_self_eggs, Toast.LENGTH_SHORT).show(); return null; } boolean isXposed = pkgInfo.applicationInfo.metaData != null && pkgInfo.applicationInfo.metaData.containsKey("xposedmodule"); AppInfoLite appInfoLite = new AppInfoLite(pkgInfo.packageName, path, false, isXposed); ArrayList dataList = new ArrayList<>(); dataList.add(appInfoLite); return dataList; } public static void handleRequestFromFile(Context context, String path) { ArrayList dataList = getAppInfoLiteFromPath(context, path); if (dataList == null) { return; } startInstallerActivity(context, dataList); } public static void startInstallerActivity(Context context, ArrayList data) { if (context == null) { return; } Intent intent = new Intent(context, InstallerActivity.class); intent.putParcelableArrayListExtra(VCommends.EXTRA_APP_INFO_LIST, data); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } public static void addGmsSupport() { List gApps = new ArrayList<>(); gApps.addAll(GmsSupport.GOOGLE_APP); gApps.addAll(GmsSupport.GOOGLE_SERVICE); VirtualCore core = VirtualCore.get(); final int userId = 0; ArrayList toInstalled = new ArrayList<>(); for (String packageName : gApps) { if (core.isAppInstalledAsUser(userId, packageName)) { continue; } ApplicationInfo info = null; try { info = VirtualCore.get().getUnHookPackageManager().getApplicationInfo(packageName, 0); } catch (PackageManager.NameNotFoundException e) { // Ignore } if (info == null || info.sourceDir == null) { continue; } AppInfoLite lite = new AppInfoLite(info.packageName, info.sourceDir, false, true); toInstalled.add(lite); } startInstallerActivity(VirtualCore.get().getContext(), toInstalled); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/sys/InstallerActivity.java ================================================ package io.virtualapp.sys; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.lody.virtual.client.core.InstallStrategy; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.helper.utils.EncodeUtils; import com.lody.virtual.helper.utils.FileUtils; import com.lody.virtual.remote.InstalledAppInfo; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import io.virtualapp.R; import io.virtualapp.VCommends; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.LoadingActivity; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.AppInfoLite; /** * author: weishu on 18/3/19. */ public class InstallerActivity extends AppCompatActivity { private TextView mTips; private Button mLeft; private Button mRight; private ProgressBar mProgressBar; private TextView mProgressText; private int mInstallCount = 0; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_install); mTips = (TextView) findViewById(R.id.installer_text); mLeft = (Button) findViewById(R.id.installer_left_button); mRight = (Button) findViewById(R.id.installer_right_button); mProgressBar = (ProgressBar) findViewById(R.id.installer_loading); mProgressText = (TextView) findViewById(R.id.installer_progress_text); handleIntent(getIntent()); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); handleIntent(intent); } @Override public void onBackPressed() { // do nothing. if (mInstallCount > 0) { } } private void handleIntent(Intent intent) { if (intent == null) { finish(); return; } ArrayList dataList = intent.getParcelableArrayListExtra(VCommends.EXTRA_APP_INFO_LIST); if (dataList == null) { handleSystemIntent(intent); } else { handleSelfIntent(dataList); } } private void handleSelfIntent(ArrayList appList) { if (appList != null) { boolean showTip = false; int size = appList.size(); mInstallCount = size; if (dealUpdate(appList)) { return; } for (int i = 0; i < size; i++) { AppInfoLite info = appList.get(i); if (new File(info.path).length() > 1024 * 1024 * 24) { showTip = true; } addApp(info); } if (showTip) { Toast.makeText(this, R.string.large_app_install_tips, Toast.LENGTH_SHORT).show(); } } } private void addApp(AppInfoLite appInfoLite) { Installd.addApp(appInfoLite, new Installd.UpdateListener() { @Override public void update(AppData model) { runOnUiThread(() -> { if (model.isInstalling()) { mProgressText.setVisibility(View.VISIBLE); mProgressBar.setVisibility(View.VISIBLE); mProgressText.setText(getResources().getString(R.string.add_app_installing_tips, model.getName())); } else if (model.isLoading()) { mProgressText.setVisibility(View.VISIBLE); mProgressBar.setVisibility(View.VISIBLE); mProgressText.setText(getResources().getString(R.string.add_app_loading_tips, model.getName())); } else { mInstallCount--; if (mInstallCount <= 0) { mInstallCount = 0; // only dismiss when the app is the last to install. mProgressText.setText(getResources().getString(R.string.add_app_loading_complete, model.getName())); mProgressText.postDelayed(() -> { mProgressBar.setVisibility(View.GONE); mLeft.setVisibility(View.VISIBLE); mLeft.setText(R.string.install_complete); mLeft.setOnClickListener((vv) -> finish()); mRight.setVisibility(View.VISIBLE); mRight.setText(R.string.install_complete_and_open); mRight.setOnClickListener((vv) -> { LoadingActivity.launch(getApplicationContext(), appInfoLite.packageName, 0); finish(); }); }, 500); } } } ); } @Override public void fail(String msg) { if (msg == null) { msg = "Unknown"; } mProgressText.setText(getResources().getString(R.string.install_fail, msg)); mProgressText.postDelayed(() -> { mProgressBar.setVisibility(View.GONE); mRight.setVisibility(View.VISIBLE); mRight.setText(R.string.install_complete); mRight.setOnClickListener((vv) -> finish()); }, 500); } }); } private boolean dealUpdate(List appList) { if (appList == null || appList.size() != 1) { return false; } AppInfoLite appInfoLite = appList.get(0); if (appInfoLite == null) { return false; } List magicApps = Arrays.asList(EncodeUtils.decode("Y29tLmxiZS5wYXJhbGxlbA=="), // com.lbe.parallel EncodeUtils.decode("aW8udmlydHVhbGFwcC5zYW5kdnhwb3NlZA=="), // io.virtualapp.sandvxposed EncodeUtils.decode("Y29tLnNrLnNwYXRjaA=="), // com.sk.spatch EncodeUtils.decode("Y29tLnFpaG9vLm1hZ2lj"), // com.qihoo.magic EncodeUtils.decode("Y29tLmRvdWJsZW9wZW4=")); // com.doubleopen if (magicApps.contains(appInfoLite.packageName)) { Toast.makeText(VirtualCore.get().getContext(), R.string.install_self_eggs, Toast.LENGTH_SHORT).show(); } if (appInfoLite.disableMultiVersion) { return false; } InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(appInfoLite.packageName, 0); if (installedAppInfo == null) { return false; } String currentVersion; String toInstalledVersion; int currentVersionCode; int toInstalledVersionCode; PackageManager packageManager = getPackageManager(); if (packageManager == null) { return false; } try { PackageInfo applicationInfo = installedAppInfo.getPackageInfo(0); currentVersion = applicationInfo.versionName; currentVersionCode = applicationInfo.versionCode; PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(appInfoLite.path, 0); toInstalledVersion = packageArchiveInfo.versionName; toInstalledVersionCode = packageArchiveInfo.versionCode; String multiVersionUpdate = getResources().getString(currentVersionCode == toInstalledVersionCode ? R.string.multi_version_cover : ( currentVersionCode < toInstalledVersionCode ? R.string.multi_version_upgrade : R.string.multi_version_downgrade )); AlertDialog alertDialog = new AlertDialog.Builder(this) .setTitle(R.string.multi_version_tip_title) .setMessage(getResources().getString(R.string.multi_version_tips_content, currentVersion, toInstalledVersion)) .setPositiveButton(R.string.multi_version_multi, (dialog, which) -> { addApp(appInfoLite); }) .setNegativeButton(multiVersionUpdate, ((dialog, which) -> { appInfoLite.disableMultiVersion = true; addApp(appInfoLite); })) .create(); alertDialog.show(); } catch (Throwable ignored) { return false; } return true; } private void handleSystemIntent(Intent intent) { Context context = VirtualCore.get().getContext(); String path; try { path = FileUtils.getFileFromUri(context, intent.getData()); } catch (Throwable e) { e.printStackTrace(); return; } PackageInfo pkgInfo = null; try { pkgInfo = context.getPackageManager().getPackageArchiveInfo(path, PackageManager.GET_META_DATA); pkgInfo.applicationInfo.sourceDir = path; pkgInfo.applicationInfo.publicSourceDir = path; } catch (Exception e) { // Ignore } if (pkgInfo == null) { finish(); return; } boolean isXposed = pkgInfo.applicationInfo.metaData != null && pkgInfo.applicationInfo.metaData.containsKey("xposedmodule"); InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(pkgInfo.packageName, 0); String tipsText; String rightString; String leftString = getResources().getString(android.R.string.cancel); PackageManager packageManager = getPackageManager(); if (packageManager == null) { finish(); return; } final String packageName = pkgInfo.packageName; String toInstalledVersion = pkgInfo.versionName; int toInstalledVersionCode = pkgInfo.versionCode; CharSequence label = packageName; if (installedAppInfo != null) { String currentVersion; int currentVersionCode; PackageInfo applicationInfo = installedAppInfo.getPackageInfo(0); if (applicationInfo == null) { finish(); return; } currentVersion = applicationInfo.versionName; currentVersionCode = applicationInfo.versionCode; label = applicationInfo.applicationInfo.loadLabel(packageManager); String multiVersionUpdate = getResources().getString(currentVersionCode == toInstalledVersionCode ? R.string.multi_version_cover : ( currentVersionCode < toInstalledVersionCode ? R.string.multi_version_upgrade : R.string.multi_version_downgrade )); tipsText = getResources().getString(R.string.install_package_version_tips, currentVersion, toInstalledVersion); rightString = multiVersionUpdate; } else { tipsText = getResources().getString(R.string.install_package, label); rightString = getResources().getString(R.string.install); } final CharSequence apkName = label; mTips.setText(tipsText); mLeft.setText(leftString); mRight.setText(rightString); mLeft.setOnClickListener(v -> finish()); mRight.setOnClickListener(v -> { mProgressBar.setVisibility(View.VISIBLE); mTips.setVisibility(View.GONE); mLeft.setVisibility(View.GONE); mRight.setEnabled(false); VUiKit.defer().when(() -> { return VirtualCore.get().installPackage(path, InstallStrategy.UPDATE_IF_EXIST); }).done((res) -> { // install success mTips.setVisibility(View.GONE); mProgressText.setVisibility(View.VISIBLE); mProgressText.setText(getResources().getString(R.string.add_app_loading_complete, apkName)); mProgressBar.setVisibility(View.GONE); mRight.setVisibility(View.VISIBLE); mRight.setEnabled(true); mRight.setText(R.string.install_complete_and_open); mRight.setOnClickListener(vv -> { LoadingActivity.launch(this, packageName, 0); finish(); }); mLeft.setVisibility(View.VISIBLE); mLeft.setEnabled(true); mLeft.setText(res.isSuccess ? getResources().getString(R.string.install_complete) : getResources().getString(R.string.install_fail, res.error)); mLeft.setOnClickListener((vv) -> finish()); }).fail((res) -> { String msg = res.getMessage(); if (msg == null) { msg = "Unknown"; } mProgressText.setVisibility(View.VISIBLE); mProgressText.setText(getResources().getString(R.string.install_fail, msg)); mRight.setEnabled(true); mProgressBar.setVisibility(View.GONE); mRight.setText(android.R.string.ok); mRight.setOnClickListener((vv) -> finish()); }); }); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/sys/ShareBridgeActivity.java ================================================ package io.virtualapp.sys; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.lody.virtual.client.ipc.VActivityManager; import com.lody.virtual.client.ipc.VPackageManager; import java.util.List; import io.virtualapp.R; /** * author: weishu on 18/3/16. */ public class ShareBridgeActivity extends AppCompatActivity { private SharedAdapter mAdapter; private List mShareComponents; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); String action = intent.getAction(); String type = intent.getType(); intent.setComponent(null); if (!Intent.ACTION_SEND.equals(action)) { finish(); return; } try { mShareComponents = VPackageManager.get(). queryIntentActivities(new Intent(Intent.ACTION_SEND), type, 0, 0); // multi-user? } catch (Throwable ignored) { } if (mShareComponents == null || mShareComponents.size() == 0) { finish(); return; } setContentView(R.layout.activity_list); ListView mListView = (ListView) findViewById(R.id.list); mAdapter = new SharedAdapter(); mListView.setAdapter(mAdapter); mListView.setOnItemClickListener((parent, view, position, id) -> { try { ResolveInfo item = mAdapter.getItem(position); Intent t = new Intent(intent); t.setComponent(new ComponentName(item.activityInfo.packageName, item.activityInfo.name)); VActivityManager.get().startActivity(t, 0); } catch (Throwable e) { Toast.makeText(getApplicationContext(), R.string.shared_to_vxp_failed, Toast.LENGTH_SHORT).show(); } finish(); }); } private class SharedAdapter extends BaseAdapter { @Override public int getCount() { return mShareComponents.size(); } @Override public ResolveInfo getItem(int position) { return mShareComponents.get(position); } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(getActivity(), parent); convertView = holder.root; convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } ResolveInfo item = getItem(position); PackageManager packageManager = getPackageManager(); try { holder.label.setText(item.loadLabel(packageManager)); } catch (Throwable e) { holder.label.setText(R.string.package_state_unknown); } try { holder.icon.setImageDrawable(item.loadIcon(packageManager)); } catch (Throwable e) { holder.icon.setImageDrawable(getResources().getDrawable(android.R.drawable.sym_def_app_icon)); } return convertView; } } static class ViewHolder { ImageView icon; TextView label; View root; ViewHolder(Context context, ViewGroup parent) { root = LayoutInflater.from(context).inflate(R.layout.item_share, parent, false); icon = root.findViewById(R.id.item_share_icon); label = root.findViewById(R.id.item_share_name); } } private Activity getActivity() { return this; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/update/VAVersionService.java ================================================ package io.virtualapp.update; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.SystemClock; import android.util.Log; import android.widget.Toast; import com.allenliu.versionchecklib.core.AVersionService; import com.allenliu.versionchecklib.core.AllenChecker; import com.allenliu.versionchecklib.core.VersionParams; import org.json.JSONException; import org.json.JSONObject; import java.util.concurrent.TimeUnit; import io.virtualapp.R; /** * @author weishu * @date 18/1/4. */ public class VAVersionService extends AVersionService { private static final String TAG = "VAVersionService"; private static final long CHECK_INTERVAL = TimeUnit.HOURS.toMillis(1); private static final String KEY_SHOW_TIP = "show_tips"; private static long sLastCheckTime; static { AllenChecker.init(false); } public static final String CHECK_VERION_URL = "http://vaexposed.weishu.me/update.json"; @Override public void onResponses(AVersionService service, String response) { try { JSONObject versionInfo = new JSONObject(response); // { // url: "download url", // versionCode: 3, // updateMessage: "Android 7.0" // } String url = versionInfo.getString("url"); int versionCode = versionInfo.getInt("versionCode"); String updateMessage = versionInfo.getString("updateMessage"); int currentVersion = getCurrentVersionCode(this); if (currentVersion < versionCode) { showVersionDialog(url, getResources().getString(R.string.new_version_detected), updateMessage); } else { boolean showTip = versionParams != null && versionParams.getParamBundle() != null && versionParams.getParamBundle().getBoolean(KEY_SHOW_TIP, false); if (showTip) { Toast.makeText(getApplicationContext(), R.string.version_is_latest, Toast.LENGTH_SHORT).show(); } } } catch (JSONException e) { Log.e(TAG, "version info parse error!!", e); } catch (Throwable e) { Log.e(TAG, "check version failed:", e); } finally { stopSelf(); } } public static void checkUpdateImmediately(Context context, boolean showTip) { Bundle bundle = new Bundle(); bundle.putBoolean(KEY_SHOW_TIP, showTip); VersionParams.Builder builder = new VersionParams.Builder() .setRequestUrl(CHECK_VERION_URL) .setShowDownloadingDialog(false) .setParamBundle(bundle) .setService(VAVersionService.class); AllenChecker.startVersionCheck(context, builder.build()); } public static void checkUpdate(Context context, boolean showTip) { long now = SystemClock.elapsedRealtime(); if (now - sLastCheckTime > CHECK_INTERVAL) { checkUpdateImmediately(context, showTip); sLastCheckTime = now; } } private static int getCurrentVersionCode(Context context) { try { // ---get the package info--- PackageManager pm = context.getPackageManager(); PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0); return pi.versionCode; } catch (Exception e) { Log.e("VersionInfo", "Exception", e); } return -1; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/utils/DialogUtil.java ================================================ package io.virtualapp.utils; import android.support.v7.app.AlertDialog; /** * @author weishu * @date 2018/7/5. */ public class DialogUtil { public static void showDialog(AlertDialog dialog) { if (dialog == null) { return; } try { dialog.show(); } catch (Throwable e) { e.printStackTrace(); } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/utils/HanziToPinyin.java ================================================ package io.virtualapp.utils; /* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.text.TextUtils; import android.util.Log; import java.text.Collator; import java.util.ArrayList; import java.util.Locale; /** * An object to convert Chinese character to its corresponding pinyin string. For characters with * multiple possible pinyin string, only one is selected according to collator. Polyphone is not * supported in this implementation. This class is implemented to achieve the best runtime * performance and minimum runtime resources with tolerable sacrifice of accuracy. This * implementation highly depends on zh_CN ICU collation data and must be always synchronized with * ICU. *

* Currently this file is aligned to zh.txt in ICU 4.6 */ public class HanziToPinyin { private static final String TAG = "HanziToPinyin"; // Turn on this flag when we want to check internal data structure. private static final boolean DEBUG = false; /** * Unihans array. *

* Each unihans is the first one within same pinyin when collator is zh_CN. */ public static final char[] UNIHANS = { '\u963f', '\u54ce', '\u5b89', '\u80ae', '\u51f9', '\u516b', '\u6300', '\u6273', '\u90a6', '\u52f9', '\u9642', '\u5954', '\u4f3b', '\u5c44', '\u8fb9', '\u706c', '\u618b', '\u6c43', '\u51ab', '\u7676', '\u5cec', '\u5693', '\u5072', '\u53c2', '\u4ed3', '\u64a1', '\u518a', '\u5d7e', '\u66fd', '\u66fe', '\u5c64', '\u53c9', '\u8286', '\u8fbf', '\u4f25', '\u6284', '\u8f66', '\u62bb', '\u6c88', '\u6c89', '\u9637', '\u5403', '\u5145', '\u62bd', '\u51fa', '\u6b3b', '\u63e3', '\u5ddb', '\u5205', '\u5439', '\u65fe', '\u9034', '\u5472', '\u5306', '\u51d1', '\u7c97', '\u6c46', '\u5d14', '\u90a8', '\u6413', '\u5491', '\u5446', '\u4e39', '\u5f53', '\u5200', '\u561a', '\u6265', '\u706f', '\u6c10', '\u55f2', '\u7538', '\u5201', '\u7239', '\u4e01', '\u4e1f', '\u4e1c', '\u543a', '\u53be', '\u8011', '\u8968', '\u5428', '\u591a', '\u59b8', '\u8bf6', '\u5940', '\u97a5', '\u513f', '\u53d1', '\u5e06', '\u531a', '\u98de', '\u5206', '\u4e30', '\u8985', '\u4ecf', '\u7d11', '\u4f15', '\u65ee', '\u4f85', '\u7518', '\u5188', '\u768b', '\u6208', '\u7ed9', '\u6839', '\u522f', '\u5de5', '\u52fe', '\u4f30', '\u74dc', '\u4e56', '\u5173', '\u5149', '\u5f52', '\u4e28', '\u5459', '\u54c8', '\u548d', '\u4f44', '\u592f', '\u8320', '\u8bc3', '\u9ed2', '\u62eb', '\u4ea8', '\u5677', '\u53ff', '\u9f41', '\u4e6f', '\u82b1', '\u6000', '\u72bf', '\u5ddf', '\u7070', '\u660f', '\u5419', '\u4e0c', '\u52a0', '\u620b', '\u6c5f', '\u827d', '\u9636', '\u5dfe', '\u5755', '\u5182', '\u4e29', '\u51e5', '\u59e2', '\u5658', '\u519b', '\u5494', '\u5f00', '\u520a', '\u5ffc', '\u5c3b', '\u533c', '\u808e', '\u52a5', '\u7a7a', '\u62a0', '\u625d', '\u5938', '\u84af', '\u5bbd', '\u5321', '\u4e8f', '\u5764', '\u6269', '\u5783', '\u6765', '\u5170', '\u5577', '\u635e', '\u808b', '\u52d2', '\u5d1a', '\u5215', '\u4fe9', '\u5941', '\u826f', '\u64a9', '\u5217', '\u62ce', '\u5222', '\u6e9c', '\u56d6', '\u9f99', '\u779c', '\u565c', '\u5a08', '\u7567', '\u62a1', '\u7f57', '\u5463', '\u5988', '\u57cb', '\u5ada', '\u7264', '\u732b', '\u4e48', '\u5445', '\u95e8', '\u753f', '\u54aa', '\u5b80', '\u55b5', '\u4e5c', '\u6c11', '\u540d', '\u8c2c', '\u6478', '\u54de', '\u6bea', '\u55ef', '\u62cf', '\u8149', '\u56e1', '\u56d4', '\u5b6c', '\u7592', '\u5a1e', '\u6041', '\u80fd', '\u59ae', '\u62c8', '\u5b22', '\u9e1f', '\u634f', '\u56dc', '\u5b81', '\u599e', '\u519c', '\u7fba', '\u5974', '\u597b', '\u759f', '\u9ec1', '\u90cd', '\u5594', '\u8bb4', '\u5991', '\u62cd', '\u7705', '\u4e53', '\u629b', '\u5478', '\u55b7', '\u5309', '\u4e15', '\u56e8', '\u527d', '\u6c15', '\u59d8', '\u4e52', '\u948b', '\u5256', '\u4ec6', '\u4e03', '\u6390', '\u5343', '\u545b', '\u6084', '\u767f', '\u4eb2', '\u72c5', '\u828e', '\u4e18', '\u533a', '\u5cd1', '\u7f3a', '\u590b', '\u5465', '\u7a63', '\u5a06', '\u60f9', '\u4eba', '\u6254', '\u65e5', '\u8338', '\u53b9', '\u909a', '\u633c', '\u5827', '\u5a51', '\u77a4', '\u637c', '\u4ee8', '\u6be2', '\u4e09', '\u6852', '\u63bb', '\u95aa', '\u68ee', '\u50e7', '\u6740', '\u7b5b', '\u5c71', '\u4f24', '\u5f30', '\u5962', '\u7533', '\u8398', '\u6552', '\u5347', '\u5c38', '\u53ce', '\u4e66', '\u5237', '\u8870', '\u95e9', '\u53cc', '\u8c01', '\u542e', '\u8bf4', '\u53b6', '\u5fea', '\u635c', '\u82cf', '\u72fb', '\u590a', '\u5b59', '\u5506', '\u4ed6', '\u56fc', '\u574d', '\u6c64', '\u5932', '\u5fd1', '\u71a5', '\u5254', '\u5929', '\u65eb', '\u5e16', '\u5385', '\u56f2', '\u5077', '\u51f8', '\u6e4d', '\u63a8', '\u541e', '\u4e47', '\u7a75', '\u6b6a', '\u5f2f', '\u5c23', '\u5371', '\u6637', '\u7fc1', '\u631d', '\u4e4c', '\u5915', '\u8672', '\u4eda', '\u4e61', '\u7071', '\u4e9b', '\u5fc3', '\u661f', '\u51f6', '\u4f11', '\u5401', '\u5405', '\u524a', '\u5743', '\u4e2b', '\u6079', '\u592e', '\u5e7a', '\u503b', '\u4e00', '\u56d9', '\u5e94', '\u54df', '\u4f63', '\u4f18', '\u625c', '\u56e6', '\u66f0', '\u6655', '\u7b60', '\u7b7c', '\u5e00', '\u707d', '\u5142', '\u5328', '\u50ae', '\u5219', '\u8d3c', '\u600e', '\u5897', '\u624e', '\u635a', '\u6cbe', '\u5f20', '\u957f', '\u9577', '\u4f4b', '\u8707', '\u8d1e', '\u4e89', '\u4e4b', '\u5cd9', '\u5ea2', '\u4e2d', '\u5dde', '\u6731', '\u6293', '\u62fd', '\u4e13', '\u5986', '\u96b9', '\u5b92', '\u5353', '\u4e72', '\u5b97', '\u90b9', '\u79df', '\u94bb', '\u539c', '\u5c0a', '\u6628', '\u5159', '\u9fc3', '\u9fc4',}; /** * Pinyin array. *

* Each pinyin is corresponding to unihans of same * offset in the unihans array. */ public static final byte[][] PINYINS = { {65, 0, 0, 0, 0, 0}, {65, 73, 0, 0, 0, 0}, {65, 78, 0, 0, 0, 0}, {65, 78, 71, 0, 0, 0}, {65, 79, 0, 0, 0, 0}, {66, 65, 0, 0, 0, 0}, {66, 65, 73, 0, 0, 0}, {66, 65, 78, 0, 0, 0}, {66, 65, 78, 71, 0, 0}, {66, 65, 79, 0, 0, 0}, {66, 69, 73, 0, 0, 0}, {66, 69, 78, 0, 0, 0}, {66, 69, 78, 71, 0, 0}, {66, 73, 0, 0, 0, 0}, {66, 73, 65, 78, 0, 0}, {66, 73, 65, 79, 0, 0}, {66, 73, 69, 0, 0, 0}, {66, 73, 78, 0, 0, 0}, {66, 73, 78, 71, 0, 0}, {66, 79, 0, 0, 0, 0}, {66, 85, 0, 0, 0, 0}, {67, 65, 0, 0, 0, 0}, {67, 65, 73, 0, 0, 0}, {67, 65, 78, 0, 0, 0}, {67, 65, 78, 71, 0, 0}, {67, 65, 79, 0, 0, 0}, {67, 69, 0, 0, 0, 0}, {67, 69, 78, 0, 0, 0}, {67, 69, 78, 71, 0, 0}, {90, 69, 78, 71, 0, 0}, {67, 69, 78, 71, 0, 0}, {67, 72, 65, 0, 0, 0}, {67, 72, 65, 73, 0, 0}, {67, 72, 65, 78, 0, 0}, {67, 72, 65, 78, 71, 0}, {67, 72, 65, 79, 0, 0}, {67, 72, 69, 0, 0, 0}, {67, 72, 69, 78, 0, 0}, {83, 72, 69, 78, 0, 0}, {67, 72, 69, 78, 0, 0}, {67, 72, 69, 78, 71, 0}, {67, 72, 73, 0, 0, 0}, {67, 72, 79, 78, 71, 0}, {67, 72, 79, 85, 0, 0}, {67, 72, 85, 0, 0, 0}, {67, 72, 85, 65, 0, 0}, {67, 72, 85, 65, 73, 0}, {67, 72, 85, 65, 78, 0}, {67, 72, 85, 65, 78, 71}, {67, 72, 85, 73, 0, 0}, {67, 72, 85, 78, 0, 0}, {67, 72, 85, 79, 0, 0}, {67, 73, 0, 0, 0, 0}, {67, 79, 78, 71, 0, 0}, {67, 79, 85, 0, 0, 0}, {67, 85, 0, 0, 0, 0}, {67, 85, 65, 78, 0, 0}, {67, 85, 73, 0, 0, 0}, {67, 85, 78, 0, 0, 0}, {67, 85, 79, 0, 0, 0}, {68, 65, 0, 0, 0, 0}, {68, 65, 73, 0, 0, 0}, {68, 65, 78, 0, 0, 0}, {68, 65, 78, 71, 0, 0}, {68, 65, 79, 0, 0, 0}, {68, 69, 0, 0, 0, 0}, {68, 69, 78, 0, 0, 0}, {68, 69, 78, 71, 0, 0}, {68, 73, 0, 0, 0, 0}, {68, 73, 65, 0, 0, 0}, {68, 73, 65, 78, 0, 0}, {68, 73, 65, 79, 0, 0}, {68, 73, 69, 0, 0, 0}, {68, 73, 78, 71, 0, 0}, {68, 73, 85, 0, 0, 0}, {68, 79, 78, 71, 0, 0}, {68, 79, 85, 0, 0, 0}, {68, 85, 0, 0, 0, 0}, {68, 85, 65, 78, 0, 0}, {68, 85, 73, 0, 0, 0}, {68, 85, 78, 0, 0, 0}, {68, 85, 79, 0, 0, 0}, {69, 0, 0, 0, 0, 0}, {69, 73, 0, 0, 0, 0}, {69, 78, 0, 0, 0, 0}, {69, 78, 71, 0, 0, 0}, {69, 82, 0, 0, 0, 0}, {70, 65, 0, 0, 0, 0}, {70, 65, 78, 0, 0, 0}, {70, 65, 78, 71, 0, 0}, {70, 69, 73, 0, 0, 0}, {70, 69, 78, 0, 0, 0}, {70, 69, 78, 71, 0, 0}, {70, 73, 65, 79, 0, 0}, {70, 79, 0, 0, 0, 0}, {70, 79, 85, 0, 0, 0}, {70, 85, 0, 0, 0, 0}, {71, 65, 0, 0, 0, 0}, {71, 65, 73, 0, 0, 0}, {71, 65, 78, 0, 0, 0}, {71, 65, 78, 71, 0, 0}, {71, 65, 79, 0, 0, 0}, {71, 69, 0, 0, 0, 0}, {71, 69, 73, 0, 0, 0}, {71, 69, 78, 0, 0, 0}, {71, 69, 78, 71, 0, 0}, {71, 79, 78, 71, 0, 0}, {71, 79, 85, 0, 0, 0}, {71, 85, 0, 0, 0, 0}, {71, 85, 65, 0, 0, 0}, {71, 85, 65, 73, 0, 0}, {71, 85, 65, 78, 0, 0}, {71, 85, 65, 78, 71, 0}, {71, 85, 73, 0, 0, 0}, {71, 85, 78, 0, 0, 0}, {71, 85, 79, 0, 0, 0}, {72, 65, 0, 0, 0, 0}, {72, 65, 73, 0, 0, 0}, {72, 65, 78, 0, 0, 0}, {72, 65, 78, 71, 0, 0}, {72, 65, 79, 0, 0, 0}, {72, 69, 0, 0, 0, 0}, {72, 69, 73, 0, 0, 0}, {72, 69, 78, 0, 0, 0}, {72, 69, 78, 71, 0, 0}, {72, 77, 0, 0, 0, 0}, {72, 79, 78, 71, 0, 0}, {72, 79, 85, 0, 0, 0}, {72, 85, 0, 0, 0, 0}, {72, 85, 65, 0, 0, 0}, {72, 85, 65, 73, 0, 0}, {72, 85, 65, 78, 0, 0}, {72, 85, 65, 78, 71, 0}, {72, 85, 73, 0, 0, 0}, {72, 85, 78, 0, 0, 0}, {72, 85, 79, 0, 0, 0}, {74, 73, 0, 0, 0, 0}, {74, 73, 65, 0, 0, 0}, {74, 73, 65, 78, 0, 0}, {74, 73, 65, 78, 71, 0}, {74, 73, 65, 79, 0, 0}, {74, 73, 69, 0, 0, 0}, {74, 73, 78, 0, 0, 0}, {74, 73, 78, 71, 0, 0}, {74, 73, 79, 78, 71, 0}, {74, 73, 85, 0, 0, 0}, {74, 85, 0, 0, 0, 0}, {74, 85, 65, 78, 0, 0}, {74, 85, 69, 0, 0, 0}, {74, 85, 78, 0, 0, 0}, {75, 65, 0, 0, 0, 0}, {75, 65, 73, 0, 0, 0}, {75, 65, 78, 0, 0, 0}, {75, 65, 78, 71, 0, 0}, {75, 65, 79, 0, 0, 0}, {75, 69, 0, 0, 0, 0}, {75, 69, 78, 0, 0, 0}, {75, 69, 78, 71, 0, 0}, {75, 79, 78, 71, 0, 0}, {75, 79, 85, 0, 0, 0}, {75, 85, 0, 0, 0, 0}, {75, 85, 65, 0, 0, 0}, {75, 85, 65, 73, 0, 0}, {75, 85, 65, 78, 0, 0}, {75, 85, 65, 78, 71, 0}, {75, 85, 73, 0, 0, 0}, {75, 85, 78, 0, 0, 0}, {75, 85, 79, 0, 0, 0}, {76, 65, 0, 0, 0, 0}, {76, 65, 73, 0, 0, 0}, {76, 65, 78, 0, 0, 0}, {76, 65, 78, 71, 0, 0}, {76, 65, 79, 0, 0, 0}, {76, 69, 0, 0, 0, 0}, {76, 69, 73, 0, 0, 0}, {76, 69, 78, 71, 0, 0}, {76, 73, 0, 0, 0, 0}, {76, 73, 65, 0, 0, 0}, {76, 73, 65, 78, 0, 0}, {76, 73, 65, 78, 71, 0}, {76, 73, 65, 79, 0, 0}, {76, 73, 69, 0, 0, 0}, {76, 73, 78, 0, 0, 0}, {76, 73, 78, 71, 0, 0}, {76, 73, 85, 0, 0, 0}, {76, 79, 0, 0, 0, 0}, {76, 79, 78, 71, 0, 0}, {76, 79, 85, 0, 0, 0}, {76, 85, 0, 0, 0, 0}, {76, 85, 65, 78, 0, 0}, {76, 85, 69, 0, 0, 0}, {76, 85, 78, 0, 0, 0}, {76, 85, 79, 0, 0, 0}, {77, 0, 0, 0, 0, 0}, {77, 65, 0, 0, 0, 0}, {77, 65, 73, 0, 0, 0}, {77, 65, 78, 0, 0, 0}, {77, 65, 78, 71, 0, 0}, {77, 65, 79, 0, 0, 0}, {77, 69, 0, 0, 0, 0}, {77, 69, 73, 0, 0, 0}, {77, 69, 78, 0, 0, 0}, {77, 69, 78, 71, 0, 0}, {77, 73, 0, 0, 0, 0}, {77, 73, 65, 78, 0, 0}, {77, 73, 65, 79, 0, 0}, {77, 73, 69, 0, 0, 0}, {77, 73, 78, 0, 0, 0}, {77, 73, 78, 71, 0, 0}, {77, 73, 85, 0, 0, 0}, {77, 79, 0, 0, 0, 0}, {77, 79, 85, 0, 0, 0}, {77, 85, 0, 0, 0, 0}, {78, 0, 0, 0, 0, 0}, {78, 65, 0, 0, 0, 0}, {78, 65, 73, 0, 0, 0}, {78, 65, 78, 0, 0, 0}, {78, 65, 78, 71, 0, 0}, {78, 65, 79, 0, 0, 0}, {78, 69, 0, 0, 0, 0}, {78, 69, 73, 0, 0, 0}, {78, 69, 78, 0, 0, 0}, {78, 69, 78, 71, 0, 0}, {78, 73, 0, 0, 0, 0}, {78, 73, 65, 78, 0, 0}, {78, 73, 65, 78, 71, 0}, {78, 73, 65, 79, 0, 0}, {78, 73, 69, 0, 0, 0}, {78, 73, 78, 0, 0, 0}, {78, 73, 78, 71, 0, 0}, {78, 73, 85, 0, 0, 0}, {78, 79, 78, 71, 0, 0}, {78, 79, 85, 0, 0, 0}, {78, 85, 0, 0, 0, 0}, {78, 85, 65, 78, 0, 0}, {78, 85, 69, 0, 0, 0}, {78, 85, 78, 0, 0, 0}, {78, 85, 79, 0, 0, 0}, {79, 0, 0, 0, 0, 0}, {79, 85, 0, 0, 0, 0}, {80, 65, 0, 0, 0, 0}, {80, 65, 73, 0, 0, 0}, {80, 65, 78, 0, 0, 0}, {80, 65, 78, 71, 0, 0}, {80, 65, 79, 0, 0, 0}, {80, 69, 73, 0, 0, 0}, {80, 69, 78, 0, 0, 0}, {80, 69, 78, 71, 0, 0}, {80, 73, 0, 0, 0, 0}, {80, 73, 65, 78, 0, 0}, {80, 73, 65, 79, 0, 0}, {80, 73, 69, 0, 0, 0}, {80, 73, 78, 0, 0, 0}, {80, 73, 78, 71, 0, 0}, {80, 79, 0, 0, 0, 0}, {80, 79, 85, 0, 0, 0}, {80, 85, 0, 0, 0, 0}, {81, 73, 0, 0, 0, 0}, {81, 73, 65, 0, 0, 0}, {81, 73, 65, 78, 0, 0}, {81, 73, 65, 78, 71, 0}, {81, 73, 65, 79, 0, 0}, {81, 73, 69, 0, 0, 0}, {81, 73, 78, 0, 0, 0}, {81, 73, 78, 71, 0, 0}, {81, 73, 79, 78, 71, 0}, {81, 73, 85, 0, 0, 0}, {81, 85, 0, 0, 0, 0}, {81, 85, 65, 78, 0, 0}, {81, 85, 69, 0, 0, 0}, {81, 85, 78, 0, 0, 0}, {82, 65, 78, 0, 0, 0}, {82, 65, 78, 71, 0, 0}, {82, 65, 79, 0, 0, 0}, {82, 69, 0, 0, 0, 0}, {82, 69, 78, 0, 0, 0}, {82, 69, 78, 71, 0, 0}, {82, 73, 0, 0, 0, 0}, {82, 79, 78, 71, 0, 0}, {82, 79, 85, 0, 0, 0}, {82, 85, 0, 0, 0, 0}, {82, 85, 65, 0, 0, 0}, {82, 85, 65, 78, 0, 0}, {82, 85, 73, 0, 0, 0}, {82, 85, 78, 0, 0, 0}, {82, 85, 79, 0, 0, 0}, {83, 65, 0, 0, 0, 0}, {83, 65, 73, 0, 0, 0}, {83, 65, 78, 0, 0, 0}, {83, 65, 78, 71, 0, 0}, {83, 65, 79, 0, 0, 0}, {83, 69, 0, 0, 0, 0}, {83, 69, 78, 0, 0, 0}, {83, 69, 78, 71, 0, 0}, {83, 72, 65, 0, 0, 0}, {83, 72, 65, 73, 0, 0}, {83, 72, 65, 78, 0, 0}, {83, 72, 65, 78, 71, 0}, {83, 72, 65, 79, 0, 0}, {83, 72, 69, 0, 0, 0}, {83, 72, 69, 78, 0, 0}, {88, 73, 78, 0, 0, 0}, {83, 72, 69, 78, 0, 0}, {83, 72, 69, 78, 71, 0}, {83, 72, 73, 0, 0, 0}, {83, 72, 79, 85, 0, 0}, {83, 72, 85, 0, 0, 0}, {83, 72, 85, 65, 0, 0}, {83, 72, 85, 65, 73, 0}, {83, 72, 85, 65, 78, 0}, {83, 72, 85, 65, 78, 71}, {83, 72, 85, 73, 0, 0}, {83, 72, 85, 78, 0, 0}, {83, 72, 85, 79, 0, 0}, {83, 73, 0, 0, 0, 0}, {83, 79, 78, 71, 0, 0}, {83, 79, 85, 0, 0, 0}, {83, 85, 0, 0, 0, 0}, {83, 85, 65, 78, 0, 0}, {83, 85, 73, 0, 0, 0}, {83, 85, 78, 0, 0, 0}, {83, 85, 79, 0, 0, 0}, {84, 65, 0, 0, 0, 0}, {84, 65, 73, 0, 0, 0}, {84, 65, 78, 0, 0, 0}, {84, 65, 78, 71, 0, 0}, {84, 65, 79, 0, 0, 0}, {84, 69, 0, 0, 0, 0}, {84, 69, 78, 71, 0, 0}, {84, 73, 0, 0, 0, 0}, {84, 73, 65, 78, 0, 0}, {84, 73, 65, 79, 0, 0}, {84, 73, 69, 0, 0, 0}, {84, 73, 78, 71, 0, 0}, {84, 79, 78, 71, 0, 0}, {84, 79, 85, 0, 0, 0}, {84, 85, 0, 0, 0, 0}, {84, 85, 65, 78, 0, 0}, {84, 85, 73, 0, 0, 0}, {84, 85, 78, 0, 0, 0}, {84, 85, 79, 0, 0, 0}, {87, 65, 0, 0, 0, 0}, {87, 65, 73, 0, 0, 0}, {87, 65, 78, 0, 0, 0}, {87, 65, 78, 71, 0, 0}, {87, 69, 73, 0, 0, 0}, {87, 69, 78, 0, 0, 0}, {87, 69, 78, 71, 0, 0}, {87, 79, 0, 0, 0, 0}, {87, 85, 0, 0, 0, 0}, {88, 73, 0, 0, 0, 0}, {88, 73, 65, 0, 0, 0}, {88, 73, 65, 78, 0, 0}, {88, 73, 65, 78, 71, 0}, {88, 73, 65, 79, 0, 0}, {88, 73, 69, 0, 0, 0}, {88, 73, 78, 0, 0, 0}, {88, 73, 78, 71, 0, 0}, {88, 73, 79, 78, 71, 0}, {88, 73, 85, 0, 0, 0}, {88, 85, 0, 0, 0, 0}, {88, 85, 65, 78, 0, 0}, {88, 85, 69, 0, 0, 0}, {88, 85, 78, 0, 0, 0}, {89, 65, 0, 0, 0, 0}, {89, 65, 78, 0, 0, 0}, {89, 65, 78, 71, 0, 0}, {89, 65, 79, 0, 0, 0}, {89, 69, 0, 0, 0, 0}, {89, 73, 0, 0, 0, 0}, {89, 73, 78, 0, 0, 0}, {89, 73, 78, 71, 0, 0}, {89, 79, 0, 0, 0, 0}, {89, 79, 78, 71, 0, 0}, {89, 79, 85, 0, 0, 0}, {89, 85, 0, 0, 0, 0}, {89, 85, 65, 78, 0, 0}, {89, 85, 69, 0, 0, 0}, {89, 85, 78, 0, 0, 0}, {74, 85, 78, 0, 0, 0}, {89, 85, 78, 0, 0, 0}, {90, 65, 0, 0, 0, 0}, {90, 65, 73, 0, 0, 0}, {90, 65, 78, 0, 0, 0}, {90, 65, 78, 71, 0, 0}, {90, 65, 79, 0, 0, 0}, {90, 69, 0, 0, 0, 0}, {90, 69, 73, 0, 0, 0}, {90, 69, 78, 0, 0, 0}, {90, 69, 78, 71, 0, 0}, {90, 72, 65, 0, 0, 0}, {90, 72, 65, 73, 0, 0}, {90, 72, 65, 78, 0, 0}, {90, 72, 65, 78, 71, 0}, {67, 72, 65, 78, 71, 0}, {90, 72, 65, 78, 71, 0}, {90, 72, 65, 79, 0, 0}, {90, 72, 69, 0, 0, 0}, {90, 72, 69, 78, 0, 0}, {90, 72, 69, 78, 71, 0}, {90, 72, 73, 0, 0, 0}, {83, 72, 73, 0, 0, 0}, {90, 72, 73, 0, 0, 0}, {90, 72, 79, 78, 71, 0}, {90, 72, 79, 85, 0, 0}, {90, 72, 85, 0, 0, 0}, {90, 72, 85, 65, 0, 0}, {90, 72, 85, 65, 73, 0}, {90, 72, 85, 65, 78, 0}, {90, 72, 85, 65, 78, 71}, {90, 72, 85, 73, 0, 0}, {90, 72, 85, 78, 0, 0}, {90, 72, 85, 79, 0, 0}, {90, 73, 0, 0, 0, 0}, {90, 79, 78, 71, 0, 0}, {90, 79, 85, 0, 0, 0}, {90, 85, 0, 0, 0, 0}, {90, 85, 65, 78, 0, 0}, {90, 85, 73, 0, 0, 0}, {90, 85, 78, 0, 0, 0}, {90, 85, 79, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {83, 72, 65, 78, 0, 0}, {0, 0, 0, 0, 0, 0},}; /** * First and last Chinese character with known Pinyin according to zh collation */ private static final String FIRST_PINYIN_UNIHAN = "\u963F"; private static final String LAST_PINYIN_UNIHAN = "\u9FFF"; private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA); private static HanziToPinyin sInstance; private final boolean mHasChinaCollator; public static class Token { /** * Separator between target string for each source char */ public static final String SEPARATOR = " "; public static final int LATIN = 1; public static final int PINYIN = 2; public static final int UNKNOWN = 3; public Token() { } public Token(int type, String source, String target) { this.type = type; this.source = source; this.target = target; } /** * Type of this token, ASCII, PINYIN or UNKNOWN. */ public int type; /** * Original string before translation. */ public String source; /** * Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is * original string in source. */ public String target; } protected HanziToPinyin(boolean hasChinaCollator) { mHasChinaCollator = hasChinaCollator; } public static HanziToPinyin getInstance() { synchronized (HanziToPinyin.class) { if (sInstance != null) { return sInstance; } // Check if zh_CN collation data is available final Locale locale[] = Collator.getAvailableLocales(); for (int i = 0; i < locale.length; i++) { if (locale[i].equals(Locale.CHINA) || locale[i].getLanguage().contains("zh")) { // Do self validation just once. if (DEBUG) { Log.d(TAG, "Self validation. Result: " + doSelfValidation()); } sInstance = new HanziToPinyin(true); return sInstance; } } if (sInstance == null){//这个判断是用于处理国产ROM的兼容性问题 if (Locale.CHINA.equals(Locale.getDefault())){ sInstance = new HanziToPinyin(true); return sInstance; } } Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled"); sInstance = new HanziToPinyin(false); return sInstance; } } /** * Validate if our internal table has some wrong value. * * @return true when the table looks correct. */ private static boolean doSelfValidation() { char lastChar = UNIHANS[0]; String lastString = Character.toString(lastChar); for (char c : UNIHANS) { if (lastChar == c) { continue; } final String curString = Character.toString(c); int cmp = COLLATOR.compare(lastString, curString); if (cmp >= 0) { Log.e(TAG, "Internal error in Unihan table. " + "The last string \"" + lastString + "\" is greater than current string \"" + curString + "\"."); return false; } lastString = curString; } return true; } private Token getToken(char character) { Token token = new Token(); final String letter = Character.toString(character); token.source = letter; int offset = -1; int cmp; if (character < 256) { token.type = Token.LATIN; token.target = letter; return token; } else { cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN); if (cmp < 0) { token.type = Token.UNKNOWN; token.target = letter; return token; } else if (cmp == 0) { token.type = Token.PINYIN; offset = 0; } else { cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN); if (cmp > 0) { token.type = Token.UNKNOWN; token.target = letter; return token; } else if (cmp == 0) { token.type = Token.PINYIN; offset = UNIHANS.length - 1; } } } token.type = Token.PINYIN; if (offset < 0) { int begin = 0; int end = UNIHANS.length - 1; while (begin <= end) { offset = (begin + end) / 2; final String unihan = Character.toString(UNIHANS[offset]); cmp = COLLATOR.compare(letter, unihan); if (cmp == 0) { break; } else if (cmp > 0) { begin = offset + 1; } else { end = offset - 1; } } } if (cmp < 0) { offset--; } StringBuilder pinyin = new StringBuilder(); for (int j = 0; j < PINYINS[offset].length && PINYINS[offset][j] != 0; j++) { pinyin.append((char) PINYINS[offset][j]); } token.target = pinyin.toString(); if (TextUtils.isEmpty(token.target)) { token.type = Token.UNKNOWN; token.target = token.source; } return token; } /** * Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without * space will be put into a Token, One Hanzi character which has pinyin will be treated as a * Token. If these is no China collator, the empty token array is returned. */ public ArrayList get(final String input) { ArrayList tokens = new ArrayList(); if (!mHasChinaCollator || TextUtils.isEmpty(input)) { // return empty tokens. return tokens; } final int inputLength = input.length(); final StringBuilder sb = new StringBuilder(); int tokenType = Token.LATIN; // Go through the input, create a new token when // a. Token type changed // b. Get the Pinyin of current charater. // c. current character is space. for (int i = 0; i < inputLength; i++) { final char character = input.charAt(i); if (character == ' ') { if (sb.length() > 0) { addToken(sb, tokens, tokenType); } } else if (character < 256) { if (tokenType != Token.LATIN && sb.length() > 0) { addToken(sb, tokens, tokenType); } tokenType = Token.LATIN; sb.append(character); } else { Token t = getToken(character); if (t.type == Token.PINYIN) { if (sb.length() > 0) { addToken(sb, tokens, tokenType); } tokens.add(t); tokenType = Token.PINYIN; } else { if (tokenType != t.type && sb.length() > 0) { addToken(sb, tokens, tokenType); } tokenType = t.type; sb.append(character); } } } if (sb.length() > 0) { addToken(sb, tokens, tokenType); } return tokens; } private void addToken( final StringBuilder sb, final ArrayList tokens, final int tokenType) { String str = sb.toString(); tokens.add(new Token(tokenType, str, str)); sb.setLength(0); } public String toPinyinString(String string) { if (string == null) { return null; } StringBuilder sb = new StringBuilder(); ArrayList tokens = get(string); for (Token token : tokens) { sb.append(token.target); } return sb.toString().toLowerCase(); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/utils/Misc.java ================================================ package io.virtualapp.utils; import android.app.Activity; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.support.v7.app.AlertDialog; import android.widget.Toast; import io.virtualapp.R; import moe.feng.alipay.zerosdk.AlipayZeroSdk; /** * @author weishu * @date 2018/10/29. */ public class Misc { public static void showDonate(Activity context) { final String alipay = context.getResources().getString(R.string.donate_alipay); final String[] items = {alipay, "PayPal", "Bitcoin"}; AlertDialog chooseDialog = new AlertDialog.Builder(context, R.style.Theme_AppCompat_DayNight_Dialog_Alert) .setTitle(R.string.donate_choose_title) .setItems(items, (dialog1, which1) -> { dialog1.dismiss(); if (which1 == 0) { if (!AlipayZeroSdk.hasInstalledAlipayClient(context)) { Toast.makeText(context, R.string.prompt_alipay_not_found, Toast.LENGTH_SHORT).show(); return; } AlipayZeroSdk.startAlipayClient(context, "FKX016770URBZGZSR37U37"); } else if (which1 == 1) { try { Intent t = new Intent(Intent.ACTION_VIEW); t.setData(Uri.parse("https://paypal.me/virtualxposed")); context.startActivity(t); } catch (Throwable ignored) { ignored.printStackTrace(); } } else if (which1 == 2) { final String address = "39Wst8oL74pRP2vKPkPihH6RFQF4hWoBqU"; try { ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); if (clipboardManager != null) { clipboardManager.setPrimaryClip(ClipData.newPlainText(null, address)); } Toast.makeText(context, context.getResources().getString(R.string.donate_bitconins_tips), Toast.LENGTH_SHORT).show(); } catch (Throwable ignored) { ignored.printStackTrace(); } } }) .create(); chooseDialog.show(); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/BaseView.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import android.view.animation.LinearInterpolator; public abstract class BaseView extends View { public ValueAnimator valueAnimator; public BaseView(Context context) { this(context, null); } public BaseView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BaseView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); InitPaint(); } public void startAnim() { stopAnim(); startViewAnim(0f, 1f, 1250); } public void startAnim(int time) { stopAnim(); startViewAnim(0f, 1f, time); } public void stopAnim() { if (valueAnimator != null) { clearAnimation(); valueAnimator.setRepeatCount(0); valueAnimator.cancel(); valueAnimator.end(); if (OnStopAnim() == 0) { valueAnimator.setRepeatCount(0); valueAnimator.cancel(); valueAnimator.end(); } } } private ValueAnimator startViewAnim(float startF, final float endF, long time) { valueAnimator = ValueAnimator.ofFloat(startF, endF); valueAnimator.setDuration(time); valueAnimator.setInterpolator(new LinearInterpolator()); valueAnimator.setRepeatCount(SetAnimRepeatCount()); if (ValueAnimator.RESTART == SetAnimRepeatMode()) { valueAnimator.setRepeatMode(ValueAnimator.RESTART); } else if (ValueAnimator.REVERSE == SetAnimRepeatMode()) { valueAnimator.setRepeatMode(ValueAnimator.REVERSE); } valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { OnAnimationUpdate(valueAnimator); } }); valueAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); } @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); } @Override public void onAnimationRepeat(Animator animation) { super.onAnimationRepeat(animation); OnAnimationRepeat(animation); } }); if (!valueAnimator.isRunning()) { AnimIsRunning(); valueAnimator.start(); } return valueAnimator; } protected abstract void InitPaint(); protected abstract void OnAnimationUpdate(ValueAnimator valueAnimator); protected abstract void OnAnimationRepeat(Animator animation); protected abstract int OnStopAnim(); protected abstract int SetAnimRepeatMode(); protected abstract int SetAnimRepeatCount(); protected abstract void AnimIsRunning(); public float getFontlength(Paint paint, String str) { Rect rect = new Rect(); paint.getTextBounds(str, 0, str.length(), rect); return rect.width(); } public float getFontHeight(Paint paint, String str) { Rect rect = new Rect(); paint.getTextBounds(str, 0, str.length(), rect); return rect.height(); } public float getFontHeight(Paint paint) { Paint.FontMetrics fm = paint.getFontMetrics(); return fm.descent - fm.ascent; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/CardStackAdapter.java ================================================ package io.virtualapp.widgets; import java.util.ArrayList; import java.util.List; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Resources; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import io.virtualapp.R; /** * This class acts as an adapter for the {@link CardStackLayout} view. This * adapter is intentionally made an abstract class with following abstract * methods - *

*

* {@link #getCount()} - Decides the number of views present in the view *

* {@link #createView(int, ViewGroup)} - Creates the view for all positions in * range [0, {@link #getCount()}) *

* Contains the logic for touch events in {@link #onTouch(View, MotionEvent)} */ public abstract class CardStackAdapter implements View.OnTouchListener, View.OnClickListener { public static final int ANIM_DURATION = 600; public static final int DECELERATION_FACTOR = 2; public static final int INVALID_CARD_POSITION = -1; private final int mScreenHeight; private final int dp30; // Settings for the adapter from layout private float mCardGapBottom; private float mCardGap; private int mParallaxScale; private boolean mParallaxEnabled; private boolean mShowInitAnimation; private int fullCardHeight; private View[] mCardViews; private float dp8; private CardStackLayout mParent; private boolean mScreenTouchable = false; private float mTouchFirstY = -1; private float mTouchPrevY = -1; private float mTouchDistance = 0; private int mSelectedCardPosition = INVALID_CARD_POSITION; private float scaleFactorForElasticEffect; private int mParentPaddingTop = 0; private int mCardPaddingInternal = 0; public CardStackAdapter(Context context) { Resources resources = context.getResources(); DisplayMetrics dm = Resources.getSystem().getDisplayMetrics(); mScreenHeight = dm.heightPixels; dp30 = (int) resources.getDimension(R.dimen.dp30); scaleFactorForElasticEffect = (int) resources.getDimension(R.dimen.dp8); dp8 = (int) resources.getDimension(R.dimen.dp8); } protected float getCardGapBottom() { return mCardGapBottom; } /** * Defines and initializes the view to be shown in the * {@link CardStackLayout} Provides two parameters to the sub-class namely - * * @param position * @param container * @return View corresponding to the position and parent container */ public abstract View createView(int position, ViewGroup container); /** * Defines the number of cards that are present in the * {@link CardStackLayout} * * @return cardCount - Number of views in the related * {@link CardStackLayout} */ public abstract int getCount(); /** * Returns true if no animation is in progress currently. Can be used to * disable any events if they are not allowed during an animation. Returns * false if an animation is in progress. * * @return - true if animation in progress, false otherwise */ public boolean isScreenTouchable() { return mScreenTouchable; } private void setScreenTouchable(boolean screenTouchable) { this.mScreenTouchable = screenTouchable; } void addView(final int position) { View root = createView(position, mParent); root.setOnTouchListener(this); root.setTag(R.id.cardstack_internal_position_tag, position); root.setLayerType(View.LAYER_TYPE_HARDWARE, null); mCardPaddingInternal = root.getPaddingTop(); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, fullCardHeight); root.setLayoutParams(lp); if (mShowInitAnimation) { root.setY(getCardFinalY(position)); setScreenTouchable(false); } else { root.setY(getCardOriginalY(position) - mParentPaddingTop); setScreenTouchable(true); } mCardViews[position] = root; mParent.addView(root); } protected float getCardFinalY(int position) { return mScreenHeight - dp30 - ((getCount() - position) * mCardGapBottom) - mCardPaddingInternal; } protected float getCardOriginalY(int position) { return mParentPaddingTop + mCardGap * position; } /** * Resets all cards in {@link CardStackLayout} to their initial positions * * @param r * Execute r.run() once the reset animation is done */ public void resetCards(Runnable r) { List animations = new ArrayList<>(getCount()); for (int i = 0; i < getCount(); i++) { final View child = mCardViews[i]; animations.add(ObjectAnimator.ofFloat(child, View.Y, (int) child.getY(), getCardOriginalY(i))); } startAnimations(animations, r, true); } /** * Plays together all animations passed in as parameter. Once animation is * completed, r.run() is executed. If parameter isReset is set to true, * {@link #mSelectedCardPosition} is set to {@link #INVALID_CARD_POSITION} * * @param animations * @param r * @param isReset */ private void startAnimations(List animations, final Runnable r, final boolean isReset) { AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether(animations); animatorSet.setDuration(ANIM_DURATION); animatorSet.setInterpolator(new DecelerateInterpolator(DECELERATION_FACTOR)); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (r != null) r.run(); setScreenTouchable(true); if (isReset) mSelectedCardPosition = INVALID_CARD_POSITION; } }); animatorSet.start(); } @Override public boolean onTouch(View v, MotionEvent event) { if (!isScreenTouchable()) { return false; } float y = event.getRawY(); int positionOfCardToMove = (int) v.getTag(R.id.cardstack_internal_position_tag); switch (event.getAction()) { case MotionEvent.ACTION_DOWN : if (mTouchFirstY != -1) { return false; } mTouchPrevY = mTouchFirstY = y; mTouchDistance = 0; break; case MotionEvent.ACTION_MOVE : if (mSelectedCardPosition == INVALID_CARD_POSITION) moveCards(positionOfCardToMove, y - mTouchFirstY); mTouchDistance += Math.abs(y - mTouchPrevY); break; case MotionEvent.ACTION_CANCEL : case MotionEvent.ACTION_UP : if (mTouchDistance < dp8 && Math.abs(y - mTouchFirstY) < dp8 && mSelectedCardPosition == INVALID_CARD_POSITION) { onClick(v); } else { resetCards(); } mTouchPrevY = mTouchFirstY = -1; mTouchDistance = 0; return false; } return true; } @Override public void onClick(final View v) { if (!isScreenTouchable()) { return; } setScreenTouchable(false); if (mSelectedCardPosition == INVALID_CARD_POSITION) { mSelectedCardPosition = (int) v.getTag(R.id.cardstack_internal_position_tag); List animations = new ArrayList<>(getCount()); for (int i = 0; i < getCount(); i++) { View child = mCardViews[i]; animations.add(getAnimatorForView(child, i, mSelectedCardPosition)); } startAnimations(animations, () -> { setScreenTouchable(true); if (mParent.getOnCardSelectedListener() != null) { mParent.getOnCardSelectedListener().onCardSelected(v, mSelectedCardPosition); } }, false); } } /** * This method can be overridden to have different animations for each card * when a click event happens on any card view. This method will be called * for every * * @param view * The view for which this method needs to return an animator * @param selectedCardPosition * Position of the card that was clicked * @param currentCardPosition * Position of the current card * @return animator which has to be applied on the current card */ protected Animator getAnimatorForView(View view, int currentCardPosition, int selectedCardPosition) { if (currentCardPosition != selectedCardPosition) { return ObjectAnimator.ofFloat(view, View.Y, (int) view.getY(), getCardFinalY(currentCardPosition)); } else { return ObjectAnimator.ofFloat(view, View.Y, (int) view.getY(), getCardOriginalY(0) + (currentCardPosition * mCardGapBottom)); } } private void moveCards(int positionOfCardToMove, float diff) { if (diff < 0 || positionOfCardToMove < 0 || positionOfCardToMove >= getCount()) return; for (int i = positionOfCardToMove; i < getCount(); i++) { final View child = mCardViews[i]; float diffCard = diff / scaleFactorForElasticEffect; if (mParallaxEnabled) { if (mParallaxScale > 0) { diffCard = diffCard * (mParallaxScale / 3) * (getCount() + 1 - i); } else { int scale = mParallaxScale * -1; diffCard = diffCard * (i * (scale / 3) + 1); } } else diffCard = diffCard * (getCount() * 2 + 1); child.setY(getCardOriginalY(i) + diffCard); } } /** * Provides an API to {@link CardStackLayout} to set the parameters provided * to it in its XML * * @param cardStackLayout * Parent of all cards */ void setAdapterParams(CardStackLayout cardStackLayout) { mParent = cardStackLayout; mCardViews = new View[getCount()]; mCardGapBottom = cardStackLayout.getCardGapBottom(); mCardGap = cardStackLayout.getCardGap(); mParallaxScale = cardStackLayout.getParallaxScale(); mParallaxEnabled = cardStackLayout.isParallaxEnabled(); if (mParallaxEnabled && mParallaxScale == 0) mParallaxEnabled = false; mShowInitAnimation = cardStackLayout.isShowInitAnimation(); mParentPaddingTop = cardStackLayout.getPaddingTop(); fullCardHeight = (int) (mScreenHeight - dp30 - dp8 - getCount() * mCardGapBottom); } /** * Resets all cards in {@link CardStackLayout} to their initial positions */ public void resetCards() { resetCards(null); } /** * Returns false if all the cards are in their initial position i.e. no card * is selected *

* Returns true if the {@link CardStackLayout} has a card selected and all * other cards are at the bottom of the screen. * * @return true if any card is selected, false otherwise */ public boolean isCardSelected() { return mSelectedCardPosition != INVALID_CARD_POSITION; } /** * Returns the position of selected card. If no card is selected, returns * {@link #INVALID_CARD_POSITION} */ public int getSelectedCardPosition() { return mSelectedCardPosition; } /** * Since there is no view recycling in {@link CardStackLayout}, we maintain * an instance of every view that is set for every position. This method * returns a view at the requested position. * * @param position * Position of card in {@link CardStackLayout} * @return View at requested position */ public View getCardView(int position) { if (mCardViews == null) return null; return mCardViews[position]; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/CardStackLayout.java ================================================ package io.virtualapp.widgets; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; import io.virtualapp.R; /** * Displays a list of cards as a stack on the screen. *

* XML attributes *

* See {@link R.styleable#CardStackLayout CardStackLayout Attributes} *

* {@link R.styleable#CardStackLayout_showInitAnimation} * {@link R.styleable#CardStackLayout_card_gap} * {@link R.styleable#CardStackLayout_card_gap_bottom} * {@link R.styleable#CardStackLayout_parallax_enabled} * {@link R.styleable#CardStackLayout_parallax_scale} */ public class CardStackLayout extends FrameLayout { public static final boolean PARALLAX_ENABLED_DEFAULT = false; public static final boolean SHOW_INIT_ANIMATION_DEFAULT = true; private float mCardGapBottom; private float mCardGap; private boolean mShowInitAnimation; private boolean mParallaxEnabled; private int mParallaxScale; private OnCardSelected mOnCardSelectedListener = null; private CardStackAdapter mAdapter = null; public CardStackLayout(Context context) { super(context); resetDefaults(); } public CardStackLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CardStackLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); handleArgs(context, attrs, defStyleAttr, 0); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public CardStackLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); handleArgs(context, attrs, defStyleAttr, defStyleRes); } /** * package restricted */ OnCardSelected getOnCardSelectedListener() { return mOnCardSelectedListener; } /** * Listen on card selection events for {@link CardStackLayout}. Sends * clicked view and it's corresponding position in the callback. * * @param onCardSelectedListener * listener */ public void setOnCardSelectedListener(OnCardSelected onCardSelectedListener) { this.mOnCardSelectedListener = onCardSelectedListener; } private void resetDefaults() { mOnCardSelectedListener = null; } private void handleArgs(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { resetDefaults(); final TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CardStackLayout, defStyleAttr, defStyleRes); mParallaxEnabled = a.getBoolean(R.styleable.CardStackLayout_parallax_enabled, PARALLAX_ENABLED_DEFAULT); mShowInitAnimation = a.getBoolean(R.styleable.CardStackLayout_showInitAnimation, SHOW_INIT_ANIMATION_DEFAULT); mParallaxScale = a.getInteger(R.styleable.CardStackLayout_parallax_scale, getResources().getInteger(R.integer.parallax_scale_default)); mCardGap = a.getDimension(R.styleable.CardStackLayout_card_gap, getResources().getDimension(R.dimen.card_gap)); mCardGapBottom = a.getDimension(R.styleable.CardStackLayout_card_gap_bottom, getResources().getDimension(R.dimen.card_gap_bottom)); a.recycle(); } /** * @return adapter of type {@link CardStackAdapter} that is set for this * view. */ public CardStackAdapter getAdapter() { return mAdapter; } /** * Set the adapter for this {@link CardStackLayout} * * @param adapter * Should extend {@link CardStackAdapter} */ public void setAdapter(CardStackAdapter adapter) { this.mAdapter = adapter; mAdapter.setAdapterParams(this); for (int i = 0; i < mAdapter.getCount(); i++) { mAdapter.addView(i); } if (mShowInitAnimation) { postDelayed(this::restoreCards, 500); } } /** * @return currently set parallax scale value. */ public int getParallaxScale() { return mParallaxScale; } /** * Sets the value of parallax scale. Parallax scale is the factor which * decides how much distance a card will scroll when the user drags it down. */ public void setParallaxScale(int mParallaxScale) { this.mParallaxScale = mParallaxScale; } public boolean isParallaxEnabled() { return mParallaxEnabled; } public void setParallaxEnabled(boolean mParallaxEnabled) { this.mParallaxEnabled = mParallaxEnabled; } public boolean isShowInitAnimation() { return mShowInitAnimation; } public void setShowInitAnimation(boolean mShowInitAnimation) { this.mShowInitAnimation = mShowInitAnimation; } /** * @return the gap (in pixels) between two consecutive cards */ public float getCardGap() { return mCardGap; } /** * Set the gap (in pixels) between two consecutive cards */ public void setCardGap(float mCardGap) { this.mCardGap = mCardGap; } /** * @return gap between the two consecutive cards when collapsed to the * bottom of the screen */ public float getCardGapBottom() { return mCardGapBottom; } public void setCardGapBottom(float mCardGapBottom) { this.mCardGapBottom = mCardGapBottom; } /** * @return true if a card is selected, false otherwise */ public boolean isCardSelected() { return mAdapter.isCardSelected(); } /** * Removes the adapter that was previously set using * {@link #setAdapter(CardStackAdapter)} */ public void removeAdapter() { if (getChildCount() > 0) removeAllViews(); mAdapter = null; mOnCardSelectedListener = null; } /** * Animates the cards to their initial position in the layout. */ public void restoreCards() { mAdapter.resetCards(); } /** * Intimates the implementing class about the selection of a card */ public interface OnCardSelected { void onCardSelected(View v, int position); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/DragSelectRecyclerView.java ================================================ package io.virtualapp.widgets; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.os.Handler; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import io.virtualapp.R; /** * @author Aidan Follestad (afollestad) */ public class DragSelectRecyclerView extends RecyclerView { private static final boolean LOGGING = false; private static final int AUTO_SCROLL_DELAY = 25; private int mLastDraggedIndex = -1; private DragSelectRecyclerViewAdapter mAdapter; private int mInitialSelection; private boolean mDragSelectActive; private int mMinReached; private int mMaxReached; private int mHotspotHeight; private int mHotspotOffsetTop; private int mHotspotOffsetBottom; private int mHotspotTopBoundStart; private int mHotspotTopBoundEnd; private int mHotspotBottomBoundStart; private int mHotspotBottomBoundEnd; private int mAutoScrollVelocity; private FingerListener mFingerListener; private boolean mInTopHotspot; private boolean mInBottomHotspot; private Handler mAutoScrollHandler; private Runnable mAutoScrollRunnable = new Runnable() { @Override public void run() { if (mAutoScrollHandler == null) return; if (mInTopHotspot) { scrollBy(0, -mAutoScrollVelocity); mAutoScrollHandler.postDelayed(this, AUTO_SCROLL_DELAY); } else if (mInBottomHotspot) { scrollBy(0, mAutoScrollVelocity); mAutoScrollHandler.postDelayed(this, AUTO_SCROLL_DELAY); } } }; private RectF mTopBoundRect; private RectF mBottomBoundRect; private Paint mDebugPaint; private boolean mDebugEnabled = false; public DragSelectRecyclerView(Context context) { super(context); init(context, null); } public DragSelectRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public DragSelectRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } private static void LOG(String message, Object... args) { //noinspection PointlessBooleanExpression if (!LOGGING) return; if (args != null) { Log.d("DragSelectRecyclerView", String.format(message, args)); } else { Log.d("DragSelectRecyclerView", message); } } private void init(Context context, AttributeSet attrs) { mAutoScrollHandler = new Handler(); final int defaultHotspotHeight = context.getResources().getDimensionPixelSize(R.dimen.dsrv_defaultHotspotHeight); if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DragSelectRecyclerView, 0, 0); try { boolean autoScrollEnabled = a.getBoolean(R.styleable.DragSelectRecyclerView_dsrv_autoScrollEnabled, true); if (!autoScrollEnabled) { mHotspotHeight = -1; mHotspotOffsetTop = -1; mHotspotOffsetBottom = -1; LOG("Auto-scroll disabled"); } else { mHotspotHeight = a.getDimensionPixelSize( R.styleable.DragSelectRecyclerView_dsrv_autoScrollHotspotHeight, defaultHotspotHeight); mHotspotOffsetTop = a.getDimensionPixelSize( R.styleable.DragSelectRecyclerView_dsrv_autoScrollHotspot_offsetTop, 0); mHotspotOffsetBottom = a.getDimensionPixelSize( R.styleable.DragSelectRecyclerView_dsrv_autoScrollHotspot_offsetBottom, 0); LOG("Hotspot height = %d", mHotspotHeight); } } finally { a.recycle(); } } else { mHotspotHeight = defaultHotspotHeight; LOG("Hotspot height = %d", mHotspotHeight); } } public void setFingerListener(@Nullable FingerListener listener) { this.mFingerListener = listener; } @Override protected void onMeasure(int widthSpec, int heightSpec) { super.onMeasure(widthSpec, heightSpec); if (mHotspotHeight > -1) { mHotspotTopBoundStart = mHotspotOffsetTop; mHotspotTopBoundEnd = mHotspotOffsetTop + mHotspotHeight; mHotspotBottomBoundStart = (getMeasuredHeight() - mHotspotHeight) - mHotspotOffsetBottom; mHotspotBottomBoundEnd = getMeasuredHeight() - mHotspotOffsetBottom; LOG("RecyclerView height = %d", getMeasuredHeight()); LOG("Hotspot top bound = %d to %d", mHotspotTopBoundStart, mHotspotTopBoundStart); LOG("Hotspot bottom bound = %d to %d", mHotspotBottomBoundStart, mHotspotBottomBoundEnd); } } public boolean setDragSelectActive(boolean active, int initialSelection) { if (active && mDragSelectActive) { LOG("Drag selection is already active."); return false; } mLastDraggedIndex = -1; mMinReached = -1; mMaxReached = -1; if (!mAdapter.isIndexSelectable(initialSelection)) { mDragSelectActive = false; mInitialSelection = -1; mLastDraggedIndex = -1; LOG("Index %d is not selectable.", initialSelection); return false; } mAdapter.setSelected(initialSelection, true); mDragSelectActive = active; mInitialSelection = initialSelection; mLastDraggedIndex = initialSelection; if (mFingerListener != null) mFingerListener.onDragSelectFingerAction(true); LOG("Drag selection initialized, starting at index %d.", initialSelection); return true; } /** * Use {@link #setAdapter(DragSelectRecyclerViewAdapter)} instead. */ @Override @Deprecated public void setAdapter(Adapter adapter) { if (!(adapter instanceof DragSelectRecyclerViewAdapter)) throw new IllegalArgumentException("Adapter must be a DragSelectRecyclerViewAdapter."); setAdapter((DragSelectRecyclerViewAdapter) adapter); } public void setAdapter(DragSelectRecyclerViewAdapter adapter) { super.setAdapter(adapter); mAdapter = adapter; } private int getItemPosition(MotionEvent e) { final View v = findChildViewUnder(e.getX(), e.getY()); if (v == null) return NO_POSITION; if (v.getTag() == null || !(v.getTag() instanceof ViewHolder)) throw new IllegalStateException("Make sure your adapter makes a call to super.onBindViewHolder(), and doesn't override itemView tags."); final ViewHolder holder = (ViewHolder) v.getTag(); return holder.getAdapterPosition(); } public final void enableDebug() { mDebugEnabled = true; invalidate(); } @Override public void onDraw(Canvas c) { super.onDraw(c); if (mDebugEnabled) { if (mDebugPaint == null) { mDebugPaint = new Paint(); mDebugPaint.setColor(Color.BLACK); mDebugPaint.setAntiAlias(true); mDebugPaint.setStyle(Paint.Style.FILL); mTopBoundRect = new RectF(0, mHotspotTopBoundStart, getMeasuredWidth(), mHotspotTopBoundEnd); mBottomBoundRect = new RectF(0, mHotspotBottomBoundStart, getMeasuredWidth(), mHotspotBottomBoundEnd); } c.drawRect(mTopBoundRect, mDebugPaint); c.drawRect(mBottomBoundRect, mDebugPaint); } } @Override public boolean dispatchTouchEvent(MotionEvent e) { if (mAdapter.getItemCount() == 0) return super.dispatchTouchEvent(e); if (mDragSelectActive) { if (e.getAction() == MotionEvent.ACTION_UP) { mDragSelectActive = false; mInTopHotspot = false; mInBottomHotspot = false; mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable); if (mFingerListener != null) mFingerListener.onDragSelectFingerAction(false); return true; } else if (e.getAction() == MotionEvent.ACTION_MOVE) { // Check for auto-scroll hotspot if (mHotspotHeight > -1) { if (e.getY() >= mHotspotTopBoundStart && e.getY() <= mHotspotTopBoundEnd) { mInBottomHotspot = false; if (!mInTopHotspot) { mInTopHotspot = true; LOG("Now in TOP hotspot"); mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable); mAutoScrollHandler.postDelayed(mAutoScrollRunnable, AUTO_SCROLL_DELAY); } final float simulatedFactor = mHotspotTopBoundEnd - mHotspotTopBoundStart; final float simulatedY = e.getY() - mHotspotTopBoundStart; mAutoScrollVelocity = (int) (simulatedFactor - simulatedY) / 2; LOG("Auto scroll velocity = %d", mAutoScrollVelocity); } else if (e.getY() >= mHotspotBottomBoundStart && e.getY() <= mHotspotBottomBoundEnd) { mInTopHotspot = false; if (!mInBottomHotspot) { mInBottomHotspot = true; LOG("Now in BOTTOM hotspot"); mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable); mAutoScrollHandler.postDelayed(mAutoScrollRunnable, AUTO_SCROLL_DELAY); } final float simulatedY = e.getY() + mHotspotBottomBoundEnd; final float simulatedFactor = mHotspotBottomBoundStart + mHotspotBottomBoundEnd; mAutoScrollVelocity = (int) (simulatedY - simulatedFactor) / 2; LOG("Auto scroll velocity = %d", mAutoScrollVelocity); } else if (mInTopHotspot || mInBottomHotspot) { LOG("Left the hotspot"); mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable); mInTopHotspot = false; mInBottomHotspot = false; } } // Drag selection logic // NOTE: DISABLE IT // if (itemPosition != NO_POSITION && mLastDraggedIndex != itemPosition) { // mLastDraggedIndex = itemPosition; // if (mMinReached == -1) mMinReached = mLastDraggedIndex; // if (mMaxReached == -1) mMaxReached = mLastDraggedIndex; // if (mLastDraggedIndex > mMaxReached) // mMaxReached = mLastDraggedIndex; // if (mLastDraggedIndex < mMinReached) // mMinReached = mLastDraggedIndex; // if (mAdapter != null) // mAdapter.selectRange(mInitialSelection, mLastDraggedIndex, mMinReached, mMaxReached); // if (mInitialSelection == mLastDraggedIndex) { // mMinReached = mLastDraggedIndex; // mMaxReached = mLastDraggedIndex; // } // } return true; } } return super.dispatchTouchEvent(e); } public interface FingerListener { void onDragSelectFingerAction(boolean fingerDown); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/DragSelectRecyclerViewAdapter.java ================================================ package io.virtualapp.widgets; import android.os.Bundle; import android.support.annotation.CallSuper; import android.support.v7.widget.RecyclerView; import java.util.ArrayList; /** * @author Aidan Follestad (afollestad) */ public abstract class DragSelectRecyclerViewAdapter extends RecyclerView.Adapter { private ArrayList mSelectedIndices; private SelectionListener mSelectionListener; private int mLastCount = -1; private int mMaxSelectionCount = -1; protected DragSelectRecyclerViewAdapter() { mSelectedIndices = new ArrayList<>(); } private void fireSelectionListener() { if (mLastCount == mSelectedIndices.size()) return; mLastCount = mSelectedIndices.size(); if (mSelectionListener != null) mSelectionListener.onDragSelectionChanged(mLastCount); } public void setMaxSelectionCount(int maxSelectionCount) { this.mMaxSelectionCount = maxSelectionCount; } public void setSelectionListener(SelectionListener selectionListener) { this.mSelectionListener = selectionListener; } public void saveInstanceState(Bundle out) { saveInstanceState("selected_indices", out); } public void saveInstanceState(String key, Bundle out) { out.putSerializable(key, mSelectedIndices); } public void restoreInstanceState(Bundle in) { restoreInstanceState("selected_indices", in); } public void restoreInstanceState(String key, Bundle in) { if (in != null && in.containsKey(key)) { //noinspection unchecked mSelectedIndices = (ArrayList) in.getSerializable(key); if (mSelectedIndices == null) mSelectedIndices = new ArrayList<>(); else fireSelectionListener(); } } public final void setSelected(int index, boolean selected) { if (!isIndexSelectable(index)) selected = false; if (selected) { if (!mSelectedIndices.contains(index) && (mMaxSelectionCount == -1 || mSelectedIndices.size() < mMaxSelectionCount)) { mSelectedIndices.add(index); notifyItemChanged(index); } } else if (mSelectedIndices.contains(index)) { mSelectedIndices.remove((Integer) index); notifyItemChanged(index); } fireSelectionListener(); } public final boolean toggleSelected(int index) { boolean selectedNow = false; if (isIndexSelectable(index)) { if (mSelectedIndices.contains(index)) { mSelectedIndices.remove((Integer) index); } else if (mMaxSelectionCount == -1 || mSelectedIndices.size() < mMaxSelectionCount) { mSelectedIndices.add(index); selectedNow = true; } notifyItemChanged(index); } fireSelectionListener(); return selectedNow; } protected boolean isIndexSelectable(int index) { return true; } @CallSuper @Override public void onBindViewHolder(VH holder, int position) { holder.itemView.setTag(holder); } public final void selectRange(int from, int to, int min, int max) { if (from == to) { // Finger is back on the initial item, unselect everything else for (int i = min; i <= max; i++) { if (i == from) continue; setSelected(i, false); } fireSelectionListener(); return; } if (to < from) { // When selecting from one to previous items for (int i = to; i <= from; i++) setSelected(i, true); if (min > -1 && min < to) { // Unselect items that were selected during this drag but no longer are for (int i = min; i < to; i++) { if (i == from) continue; setSelected(i, false); } } if (max > -1) { for (int i = from + 1; i <= max; i++) setSelected(i, false); } } else { // When selecting from one to next items for (int i = from; i <= to; i++) setSelected(i, true); if (max > -1 && max > to) { // Unselect items that were selected during this drag but no longer are for (int i = to + 1; i <= max; i++) { if (i == from) continue; setSelected(i, false); } } if (min > -1) { for (int i = min; i < from; i++) setSelected(i, false); } } fireSelectionListener(); } public final void selectAll() { int max = getItemCount(); mSelectedIndices.clear(); for (int i = 0; i < max; i++) { if (isIndexSelectable(i)) { mSelectedIndices.add(i); } } notifyDataSetChanged(); fireSelectionListener(); } public final void clearSelected() { mSelectedIndices.clear(); notifyDataSetChanged(); fireSelectionListener(); } public final int getSelectedCount() { return mSelectedIndices.size(); } public final Integer[] getSelectedIndices() { return mSelectedIndices.toArray(new Integer[mSelectedIndices.size()]); } public final boolean isIndexSelected(int index) { return mSelectedIndices.contains(index); } public interface SelectionListener { void onDragSelectionChanged(int count); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/EatBeansView.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; public class EatBeansView extends BaseView { int eatSpeed = 8; private Paint mPaint, mPaintEye, mPaintBeans; private float mWidth = 0f; private float mHigh = 0f; private float mPadding = 5f; private float eatErWidth = 50f; private float eatErPositionX = 0f; private float beansWidth = 10f; private float mAngle = 34; private float eatErStartAngle = mAngle; private float eatErEndAngle = 360 - 2 * eatErStartAngle; private RectF mRect = new RectF(); public EatBeansView(Context context) { super(context); } public EatBeansView(Context context, AttributeSet attrs) { super(context, attrs); } public EatBeansView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mWidth = getMeasuredWidth(); mHigh = getMeasuredHeight(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); float eatRightX = mPadding + eatErWidth + eatErPositionX; mRect.set(mPadding + eatErPositionX, mHigh / 2 - eatErWidth / 2, eatRightX, mHigh / 2 + eatErWidth / 2); canvas.drawArc(mRect, eatErStartAngle, eatErEndAngle , true, mPaint); canvas.drawCircle(mPadding + eatErPositionX + eatErWidth / 2, mHigh / 2 - eatErWidth / 4, beansWidth / 2, mPaintEye); int beansCount = (int) ((mWidth - mPadding * 2 - eatErWidth) / beansWidth / 2); for (int i = 0; i < beansCount; i++) { float x = beansCount * i + beansWidth / 2 + mPadding + eatErWidth; if (x > eatRightX) { canvas.drawCircle(x, mHigh / 2, beansWidth / 2, mPaintBeans); } } } private void initPaint() { mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(0xDDDDDDDD); mPaintBeans = new Paint(); mPaintBeans.setAntiAlias(true); mPaintBeans.setStyle(Paint.Style.FILL); mPaintBeans.setColor(0xFFBBBBBB); mPaintEye = new Paint(); mPaintEye.setAntiAlias(true); mPaintEye.setStyle(Paint.Style.FILL); mPaintEye.setColor(0xFF888888); } public void setViewColor(int color) { mPaint.setColor(color); postInvalidate(); } public void setEyeColor(int color) { mPaintEye.setColor(color); postInvalidate(); } @Override protected void InitPaint() { initPaint(); } @Override protected void OnAnimationUpdate(ValueAnimator valueAnimator) { float mAnimatedValue = (float) valueAnimator.getAnimatedValue(); eatErPositionX = (mWidth - 2 * mPadding - eatErWidth) * mAnimatedValue; eatErStartAngle = mAngle * (1 - (mAnimatedValue * eatSpeed - (int) (mAnimatedValue * eatSpeed))); eatErEndAngle = 360 - eatErStartAngle * 2; invalidate(); } @Override protected void OnAnimationRepeat(Animator animation) { } @Override protected int OnStopAnim() { eatErPositionX = 0; postInvalidate(); return 1; } @Override protected int SetAnimRepeatMode() { return ValueAnimator.RESTART; } @Override protected void AnimIsRunning() { } @Override protected int SetAnimRepeatCount() { return ValueAnimator.INFINITE; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/Indicator.java ================================================ package io.virtualapp.widgets; import android.animation.ValueAnimator; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import java.util.ArrayList; import java.util.HashMap; public abstract class Indicator extends Drawable implements Animatable { private static final Rect ZERO_BOUNDS_RECT = new Rect(); protected Rect drawBounds = ZERO_BOUNDS_RECT; private HashMap mUpdateListeners = new HashMap<>(); private ArrayList mAnimators; private int alpha = 255; private boolean mHasAnimators; private Paint mPaint = new Paint(); public Indicator() { mPaint.setColor(Color.WHITE); mPaint.setStyle(Paint.Style.FILL); mPaint.setAntiAlias(true); } public int getColor() { return mPaint.getColor(); } public void setColor(int color) { mPaint.setColor(color); } @Override public int getAlpha() { return alpha; } @Override public void setAlpha(int alpha) { this.alpha = alpha; } @Override public int getOpacity() { return PixelFormat.OPAQUE; } @Override public void setColorFilter(ColorFilter colorFilter) { } @Override public void draw(Canvas canvas) { draw(canvas, mPaint); } public abstract void draw(Canvas canvas, Paint paint); public abstract ArrayList onCreateAnimators(); @Override public void start() { ensureAnimators(); if (mAnimators == null) { return; } // If the animators has not ended, do nothing. if (isStarted()) { return; } startAnimators(); invalidateSelf(); } private void startAnimators() { for (int i = 0; i < mAnimators.size(); i++) { ValueAnimator animator = mAnimators.get(i); //when the animator restart , add the updateListener again because they // was removed by animator stop . ValueAnimator.AnimatorUpdateListener updateListener = mUpdateListeners.get(animator); if (updateListener != null) { animator.addUpdateListener(updateListener); } animator.start(); } } private void stopAnimators() { if (mAnimators != null) { for (ValueAnimator animator : mAnimators) { if (animator != null && animator.isStarted()) { animator.removeAllUpdateListeners(); animator.end(); } } } } private void ensureAnimators() { if (!mHasAnimators) { mAnimators = onCreateAnimators(); mHasAnimators = true; } } @Override public void stop() { stopAnimators(); } private boolean isStarted() { for (ValueAnimator animator : mAnimators) { return animator.isStarted(); } return false; } @Override public boolean isRunning() { for (ValueAnimator animator : mAnimators) { return animator.isRunning(); } return false; } /** * Your should use this to add AnimatorUpdateListener when * create animator , otherwise , animator doesn't work when * the animation restart . * * @param updateListener */ public void addUpdateListener(ValueAnimator animator, ValueAnimator.AnimatorUpdateListener updateListener) { mUpdateListeners.put(animator, updateListener); } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); setDrawBounds(bounds); } public void setDrawBounds(int left, int top, int right, int bottom) { this.drawBounds = new Rect(left, top, right, bottom); } public void postInvalidate() { invalidateSelf(); } public Rect getDrawBounds() { return drawBounds; } public void setDrawBounds(Rect drawBounds) { setDrawBounds(drawBounds.left, drawBounds.top, drawBounds.right, drawBounds.bottom); } public int getWidth() { return drawBounds.width(); } public int getHeight() { return drawBounds.height(); } public int centerX() { return drawBounds.centerX(); } public int centerY() { return drawBounds.centerY(); } public float exactCenterX() { return drawBounds.exactCenterX(); } public float exactCenterY() { return drawBounds.exactCenterY(); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/LabelView.java ================================================ package io.virtualapp.widgets; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import io.virtualapp.R; public class LabelView extends View { private static final int DEFAULT_DEGREES = 45; private String mTextContent; private int mTextColor; private float mTextSize; private boolean mTextBold; private boolean mFillTriangle; private boolean mTextAllCaps; private int mBackgroundColor; private float mMinSize; private float mPadding; private int mGravity; private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private Path mPath = new Path(); public LabelView(Context context) { this(context, null); } public LabelView(Context context, AttributeSet attrs) { super(context, attrs); obtainAttributes(context, attrs); mTextPaint.setTextAlign(Paint.Align.CENTER); } private void obtainAttributes(Context context, AttributeSet attrs) { TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LabelView); mTextContent = ta.getString(R.styleable.LabelView_lv_text); mTextColor = ta.getColor(R.styleable.LabelView_lv_text_color, Color.parseColor("#ffffff")); mTextSize = ta.getDimension(R.styleable.LabelView_lv_text_size, sp2px(11)); mTextBold = ta.getBoolean(R.styleable.LabelView_lv_text_bold, true); mTextAllCaps = ta.getBoolean(R.styleable.LabelView_lv_text_all_caps, true); mFillTriangle = ta.getBoolean(R.styleable.LabelView_lv_fill_triangle, false); mBackgroundColor = ta.getColor(R.styleable.LabelView_lv_background_color, Color.parseColor("#FF4081")); mMinSize = ta.getDimension(R.styleable.LabelView_lv_min_size, mFillTriangle ? dp2px(35) : dp2px(50)); mPadding = ta.getDimension(R.styleable.LabelView_lv_padding, dp2px(3.5f)); mGravity = ta.getInt(R.styleable.LabelView_lv_gravity, Gravity.TOP | Gravity.LEFT); ta.recycle(); } public String getText() { return mTextContent; } public void setText(String text) { mTextContent = text; invalidate(); } public int getTextColor() { return mTextColor; } public void setTextColor(int textColor) { mTextColor = textColor; invalidate(); } public float getTextSize() { return mTextSize; } public void setTextSize(float textSize) { mTextSize = sp2px(textSize); invalidate(); } public boolean isTextBold() { return mTextBold; } public void setTextBold(boolean textBold) { mTextBold = textBold; invalidate(); } public boolean isFillTriangle() { return mFillTriangle; } public void setFillTriangle(boolean fillTriangle) { mFillTriangle = fillTriangle; invalidate(); } public boolean isTextAllCaps() { return mTextAllCaps; } public void setTextAllCaps(boolean textAllCaps) { mTextAllCaps = textAllCaps; invalidate(); } public int getBgColor() { return mBackgroundColor; } public void setBgColor(int backgroundColor) { mBackgroundColor = backgroundColor; invalidate(); } public float getMinSize() { return mMinSize; } public void setMinSize(float minSize) { mMinSize = dp2px(minSize); invalidate(); } public float getPadding() { return mPadding; } public void setPadding(float padding) { mPadding = dp2px(padding); invalidate(); } public int getGravity() { return mGravity; } /** * Gravity.TOP | Gravity.LEFT * Gravity.TOP | Gravity.RIGHT * Gravity.BOTTOM | Gravity.LEFT * Gravity.BOTTOM | Gravity.RIGHT */ public void setGravity(int gravity) { mGravity = gravity; } @Override protected void onDraw(Canvas canvas) { int size = getHeight(); mTextPaint.setColor(mTextColor); mTextPaint.setTextSize(mTextSize); mTextPaint.setFakeBoldText(mTextBold); mBackgroundPaint.setColor(mBackgroundColor); float textHeight = mTextPaint.descent() - mTextPaint.ascent(); if (mFillTriangle) { if (mGravity == (Gravity.TOP | Gravity.LEFT)) { mPath.reset(); mPath.moveTo(0, 0); mPath.lineTo(0, size); mPath.lineTo(size, 0); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawTextWhenFill(size, -DEFAULT_DEGREES, canvas, true); } else if (mGravity == (Gravity.TOP | Gravity.RIGHT)) { mPath.reset(); mPath.moveTo(size, 0); mPath.lineTo(0, 0); mPath.lineTo(size, size); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawTextWhenFill(size, DEFAULT_DEGREES, canvas, true); } else if (mGravity == (Gravity.BOTTOM | Gravity.LEFT)) { mPath.reset(); mPath.moveTo(0, size); mPath.lineTo(0, 0); mPath.lineTo(size, size); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawTextWhenFill(size, DEFAULT_DEGREES, canvas, false); } else if (mGravity == (Gravity.BOTTOM | Gravity.RIGHT)) { mPath.reset(); mPath.moveTo(size, size); mPath.lineTo(0, size); mPath.lineTo(size, 0); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawTextWhenFill(size, -DEFAULT_DEGREES, canvas, false); } } else { double delta = (textHeight + mPadding * 2) * Math.sqrt(2); if (mGravity == (Gravity.TOP | Gravity.LEFT)) { mPath.reset(); mPath.moveTo(0, (float) (size - delta)); mPath.lineTo(0, size); mPath.lineTo(size, 0); mPath.lineTo((float) (size - delta), 0); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawText(size, -DEFAULT_DEGREES, canvas, textHeight, true); } else if (mGravity == (Gravity.TOP | Gravity.RIGHT)) { mPath.reset(); mPath.moveTo(0, 0); mPath.lineTo((float) delta, 0); mPath.lineTo(size, (float) (size - delta)); mPath.lineTo(size, size); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawText(size, DEFAULT_DEGREES, canvas, textHeight, true); } else if (mGravity == (Gravity.BOTTOM | Gravity.LEFT)) { mPath.reset(); mPath.moveTo(0, 0); mPath.lineTo(0, (float) delta); mPath.lineTo((float) (size - delta), size); mPath.lineTo(size, size); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawText(size, DEFAULT_DEGREES, canvas, textHeight, false); } else if (mGravity == (Gravity.BOTTOM | Gravity.RIGHT)) { mPath.reset(); mPath.moveTo(0, size); mPath.lineTo((float) delta, size); mPath.lineTo(size, (float) delta); mPath.lineTo(size, 0); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawText(size, -DEFAULT_DEGREES, canvas, textHeight, false); } } } private void drawText(int size, float degrees, Canvas canvas, float textHeight, boolean isTop) { canvas.save(); canvas.rotate(degrees, size / 2f, size / 2f); float delta = isTop ? -(textHeight + mPadding * 2) / 2 : (textHeight + mPadding * 2) / 2; float textBaseY = size / 2 - (mTextPaint.descent() + mTextPaint.ascent()) / 2 + delta; canvas.drawText(mTextAllCaps ? mTextContent.toUpperCase() : mTextContent, getPaddingLeft() + (size - getPaddingLeft() - getPaddingRight()) / 2, textBaseY, mTextPaint); canvas.restore(); } private void drawTextWhenFill(int size, float degrees, Canvas canvas, boolean isTop) { canvas.save(); canvas.rotate(degrees, size / 2f, size / 2f); float delta = isTop ? -size / 4 : size / 4; float textBaseY = size / 2 - (mTextPaint.descent() + mTextPaint.ascent()) / 2 + delta; canvas.drawText(mTextAllCaps ? mTextContent.toUpperCase() : mTextContent, getPaddingLeft() + (size - getPaddingLeft() - getPaddingRight()) / 2, textBaseY, mTextPaint); canvas.restore(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int measuredWidth = measureWidth(widthMeasureSpec); setMeasuredDimension(measuredWidth, measuredWidth); } /** * 确定View宽度大小 */ private int measureWidth(int widthMeasureSpec) { int result; int specMode = MeasureSpec.getMode(widthMeasureSpec); int specSize = MeasureSpec.getSize(widthMeasureSpec); if (specMode == MeasureSpec.EXACTLY) {//大小确定直接使用 result = specSize; } else { int padding = getPaddingLeft() + getPaddingRight(); mTextPaint.setColor(mTextColor); mTextPaint.setTextSize(mTextSize); float textWidth = mTextPaint.measureText(mTextContent + ""); result = (int) ((padding + (int) textWidth) * Math.sqrt(2)); //如果父视图的测量要求为AT_MOST,即限定了一个最大值,则再从系统建议值和自己计算值中去一个较小值 if (specMode == MeasureSpec.AT_MOST) { result = Math.min(result, specSize); } result = Math.max((int) mMinSize, result); } return result; } protected int dp2px(float dp) { final float scale = getResources().getDisplayMetrics().density; return (int) (dp * scale + 0.5f); } protected int sp2px(float sp) { final float scale = getResources().getDisplayMetrics().scaledDensity; return (int) (sp * scale + 0.5f); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/MarqueeTextView.java ================================================ package io.virtualapp.widgets; import android.content.Context; import android.graphics.Canvas; import android.support.v7.widget.AppCompatTextView; import android.util.AttributeSet; public class MarqueeTextView extends AppCompatTextView { private boolean isStop = false; public MarqueeTextView(Context context) { super(context); } public MarqueeTextView(Context context, AttributeSet attrs) { super(context, attrs); } public MarqueeTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public boolean isFocused() { if (this.isStop) { return super.isFocused(); } return true; } public void stopScroll() { this.isStop = true; } public void start() { this.isStop = false; } protected void onDetachedFromWindow() { stopScroll(); super.onDetachedFromWindow(); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/fittext/BaseTextView.java ================================================ package io.virtualapp.widgets.fittext; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.os.Build; import android.text.Layout; import android.text.TextPaint; import android.text.TextUtils; import android.util.AttributeSet; import android.widget.TextView; class BaseTextView extends TextView { protected boolean mSingleLine = false; protected boolean mIncludeFontPadding = true; protected float mLineSpacingMult = 1; protected float mLineSpacingAdd = 0; protected int mMaxLines = Integer.MAX_VALUE; protected boolean mLineEndNoSpace = true; protected boolean mJustify = false; /*** * 不拆分单词 */ protected boolean mKeepWord = true; @SuppressWarnings("deprecation") private static final int[] ANDROID_ATTRS = new int[]{ android.R.attr.includeFontPadding, android.R.attr.lineSpacingMultiplier, android.R.attr.lineSpacingExtra, android.R.attr.maxLines, android.R.attr.singleLine, }; public BaseTextView(Context context) { this(context, null); } public BaseTextView(Context context, AttributeSet attrs) { super(context, attrs); if (attrs != null) { TypedArray a = context.obtainStyledAttributes(attrs, ANDROID_ATTRS); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { mIncludeFontPadding = a.getBoolean(a.getIndex(0), mIncludeFontPadding); mLineSpacingMult = a.getFloat(a.getIndex(1), mLineSpacingMult); mLineSpacingAdd = a.getDimensionPixelSize(a.getIndex(2), (int) mLineSpacingAdd); mMaxLines = a.getInteger(a.getIndex(3), mMaxLines); } mSingleLine = a.getBoolean(android.R.attr.singleLine, mSingleLine); a.recycle(); } } public BaseTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs); } public boolean isKeepWord() { return mKeepWord; } public void setKeepWord(boolean keepWord) { mKeepWord = keepWord; } public boolean isJustify() { return mJustify; } public void setJustify(boolean justify) { mJustify = justify; } public boolean isLineEndNoSpace() { return mLineEndNoSpace; } public void setLineEndNoSpace(boolean lineEndNoSpace) { mLineEndNoSpace = lineEndNoSpace; } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public boolean getIncludeFontPaddingCompat() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { return getIncludeFontPadding(); } else { return mIncludeFontPadding; } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public float getLineSpacingMultiplierCompat() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { return getLineSpacingMultiplier(); } else { return mLineSpacingMult; } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public float getLineSpacingExtraCompat() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { return getLineSpacingExtra(); } else { return mLineSpacingAdd; } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public int getMaxLinesCompat() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { return getMaxLines(); } else { return mMaxLines; } } @Override public void setLineSpacing(float add, float mult) { super.setLineSpacing(add, mult); mLineSpacingAdd = add; mLineSpacingMult = mult; } @Override public void setIncludeFontPadding(boolean includepad) { super.setIncludeFontPadding(includepad); mIncludeFontPadding = includepad; } @Override public void setMaxLines(int maxlines) { super.setMaxLines(maxlines); mMaxLines = maxlines; } @Override public void setSingleLine(boolean singleLine) { super.setSingleLine(singleLine); mSingleLine = singleLine; } public int getTextWidth() { return FitTextHelper.getTextWidth(this); } public int getTextHeight() { return getMeasuredHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom(); } /** * 设置粗体 * * @param bold 粗体 */ public void setBoldText(boolean bold) { getPaint().setFakeBoldText(bold); } /** * 设置斜体 * * @param italic 斜体 */ public void setItalicText(boolean italic) { getPaint().setTextSkewX(italic ? -0.25f : 0f); } public boolean isItalicText() { return getPaint().getTextSkewX() != 0f; } public boolean isSingleLine() { return mSingleLine; } public float getTextLineHeight() { return getLineHeight(); } public TextView getTextView() { return this; } protected void onDraw(Canvas canvas) { if (!mJustify || mSingleLine) { super.onDraw(canvas); return; } TextPaint paint = getPaint(); // paint.drawableState = getDrawableState(); float mViewWidth = getTextWidth(); if (isItalicText()) { float letterW = getPaint().measureText("a"); mViewWidth -= letterW; } CharSequence text = getText(); Layout layout = getLayout(); if (layout == null) { layout = FitTextHelper.getStaticLayout(this, getText(), getPaint()); } int count = layout.getLineCount(); for (int i = 0; i < count; i++) { int lineStart = layout.getLineStart(i); int lineEnd = layout.getLineEnd(i); // int top = layout.getLineTop(i); float x = layout.getLineLeft(i); int mLineY = layout.getTopPadding() + (i + 1) * getLineHeight(); CharSequence line = text.subSequence(lineStart, lineEnd); if (line.length() == 0) { continue; } if (mLineEndNoSpace) { if (TextUtils.equals(line.subSequence(line.length() - 1, line.length()), " ")) { line = line.subSequence(0, line.length() - 1); } if (TextUtils.equals(line.subSequence(0, 1), " ")) { line = line.subSequence(1, line.length() - 1); } } float lineWidth = getPaint().measureText(text, lineStart, lineEnd); boolean needScale = i < (count - 1) && (needScale(text.subSequence(lineEnd - 1, lineEnd))); // if (i < (count - 1) && needScale(line)) { //float width = StaticLayout.getDesiredWidth(text, lineStart, lineEnd, getPaint()); // drawScaledText(canvas, mViewWidth, mLineY, lineStart, line, width - getCompoundPaddingLeft() - getCompoundPaddingRight()); // } else { // canvas.drawText(line, 0, line.length(), 0, mLineY, paint); // } // float x = getCompoundPaddingLeft(); if (needScale && mViewWidth > lineWidth) { // float sc = mViewWidth / lineWidth; //标点数 int clen = countEmpty(line); float d = (mViewWidth - lineWidth) / clen; for (int j = 0; j < line.length(); j++) { float cw = getPaint().measureText(line, j, j + 1); canvas.drawText(line, j, j + 1, x, mLineY, getPaint()); x += cw; // 后面是标点 if (isEmpty(line, j + 1, j + 2)) { x += d / 2; } //当前是标点 if (isEmpty(line, j, j + 1)) { x += d / 2; } } } else { canvas.drawText(line, 0, line.length(), x, mLineY, paint); } } } /** * 共有多少个标点/空白字符 * * @param text 内容 * @return 数量 */ protected int countEmpty(CharSequence text) { int len = text.length(); int count = 0; for (int i = 0; i < len; i++) { if (isEmpty(text, i, i + 1)) { count++; } } return count; } /** * 是否是标点/空白字符 * * @param c 内容 * @param start 开始 * @param end 结束 */ protected boolean isEmpty(CharSequence c, int start, int end) { if (end >= c.length()) { return false; } CharSequence ch = c.subSequence(start, end); return TextUtils.equals(ch, "\t") || TextUtils.equals(ch, " ") || FitTextHelper.sSpcaeList.contains(ch); } // private void drawScaledText(Canvas canvas, int mViewWidth, int mLineY, int lineStart, CharSequence line, float lineWidth) { // float x = 0; // if (isFirstLineOfParagraph(lineStart, line)) { // String blanks = " "; // canvas.drawText(blanks, x, mLineY, getPaint()); // float bw = StaticLayout.getDesiredWidth(blanks, getPaint()); // x += bw; // // line = line.subSequence(3, line.length() - 3); // } // // float d = (mViewWidth - lineWidth) / line.length() - 1; // for (int i = 0; i < line.length(); i++) { // String c = String.valueOf(line.charAt(i)); // float cw = StaticLayout.getDesiredWidth(c, getPaint()); // canvas.drawText(c, x, mLineY, getPaint()); // x += cw + d; // } // } // // private boolean isFirstLineOfParagraph(int lineStart, CharSequence line) { // return line.length() > 3 && line.charAt(0) == ' ' && line.charAt(1) == ' '; // } /** * 是否需要两端对齐 * * @param end 结束字符 */ protected boolean needScale(CharSequence end) { return TextUtils.equals(end, " ");// || !TextUtils.equals(end, "\n"); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/fittext/FitTextHelper.java ================================================ package io.virtualapp.widgets.fittext; import android.annotation.TargetApi; import android.os.Build; import android.text.Layout; import android.text.SpannableStringBuilder; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.view.Gravity; import android.view.inputmethod.EditorInfo; import android.widget.TextView; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; /*** * 两端对齐 * 标点句尾 */ class FitTextHelper { protected static final float LIMIT = 0.001f;// 误差 private static final boolean LastNoSpace = false; protected BaseTextView textView; //region space list public final static List sSpcaeList = new ArrayList<>(); static { sSpcaeList.add(","); sSpcaeList.add("."); sSpcaeList.add(";"); sSpcaeList.add("'"); sSpcaeList.add("\""); sSpcaeList.add(":"); sSpcaeList.add("?"); sSpcaeList.add("~"); sSpcaeList.add("!"); sSpcaeList.add("‘"); sSpcaeList.add("’"); sSpcaeList.add("”"); sSpcaeList.add("“"); sSpcaeList.add(";"); sSpcaeList.add(":"); sSpcaeList.add(","); sSpcaeList.add("。"); sSpcaeList.add("?"); sSpcaeList.add("!"); sSpcaeList.add("("); sSpcaeList.add(")"); sSpcaeList.add("["); sSpcaeList.add("]"); sSpcaeList.add("@"); sSpcaeList.add("/"); sSpcaeList.add("#"); sSpcaeList.add("$"); sSpcaeList.add("%"); sSpcaeList.add("^"); sSpcaeList.add("&"); sSpcaeList.add("*"); // sSpcaeList.add("{"); // sSpcaeList.add("}"); sSpcaeList.add("<"); sSpcaeList.add(">"); // sSpcaeList.add("/"); // sSpcaeList.add("\\"); sSpcaeList.add("+"); sSpcaeList.add("-"); sSpcaeList.add("·"); // sSpcaeList.add("●"); // sSpcaeList.add("【"); // sSpcaeList.add("】"); // sSpcaeList.add("《"); // sSpcaeList.add("》"); // sSpcaeList.add("『"); // sSpcaeList.add("』"); // sSpcaeList.add("/"); } //endregion protected volatile boolean mFittingText = false; public FitTextHelper(BaseTextView textView) { this.textView = textView; } /*** * @param textView textview * @return 是否是单行 */ public static boolean isSingleLine(TextView textView) { if (textView == null) return false; if (textView instanceof BaseTextView) { return ((BaseTextView) textView).isSingleLine(); } if (textView == null) { return false; } int type = textView.getInputType(); return (type & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; } // public float getLineHieght() { // Paint.FontMetrics fm = textView.getPaint().getFontMetrics(); // float baseline = fm.descent - fm.ascent; // float multi = textView.getLineSpacingMultiplierCompat(); // float space = textView.getLineSpacingExtraCompat(); // //字距 // return (baseline + fm.leading) // * multi + space; // } /** * @return 文本框的当前最大行数 */ protected int getMaxLineCount() { float vspace = textView.getTextLineHeight(); float height = textView.getTextHeight(); return (int) (height / vspace); } // // protected boolean isSingle(TextView textView) { // int inputType = textView.getInputType(); // return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; // } /** * 文本框的宽度 * * @param textView 文本框 * @return 宽度 */ public static int getTextWidth(TextView textView) { return textView.getMeasuredWidth() - textView.getCompoundPaddingLeft() - textView.getCompoundPaddingRight(); } /*** * @param text 文本 * @param paint 画笔 * @return 文本布局 */ public StaticLayout getStaticLayout(CharSequence text, TextPaint paint) { return getStaticLayout(textView.getTextView(), text, paint); } /** * @param textView 文本框 * @param text 文本 * @param paint 画笔 * @return 文本布局 */ public static StaticLayout getStaticLayout(TextView textView, CharSequence text, TextPaint paint) { StaticLayout layout; if (textView instanceof FitTextView) { FitTextView fitTextView = (FitTextView) textView; layout = new StaticLayout(text, paint, getTextWidth(textView), getLayoutAlignment(fitTextView), fitTextView.getLineSpacingMultiplierCompat(), fitTextView.getLineSpacingExtraCompat(), fitTextView.getIncludeFontPaddingCompat()); } else { if (Build.VERSION.SDK_INT <= 16) { layout = new StaticLayout(text, paint, getTextWidth(textView), getLayoutAlignment(textView), 0, 0, false); } else { layout = new StaticLayout(text, paint, getTextWidth(textView), getLayoutAlignment(textView), textView.getLineSpacingMultiplier(), textView.getLineSpacingExtra(), textView.getIncludeFontPadding()); } } if(isSingleLine(textView)) { try { Field field = StaticLayout.class.getDeclaredField("mMaximumVisibleLineCount"); if (field != null) { field.setAccessible(true); field.set(layout, 1); } } catch (Exception e) { e.printStackTrace(); } } return layout; } /** * 判断内容是否在框内 * * @param text 文本 * @param paint 画笔 * @return 没有超过框 */ protected boolean isFit(CharSequence text, TextPaint paint) { // 自动换行 boolean mSingleLine = textView.isSingleLine(); int maxLines = textView.getMaxLinesCompat(); float multi = textView.getLineSpacingMultiplierCompat(); float space = textView.getLineSpacingExtraCompat(); space = space * multi; int height = textView.getTextHeight(); if (!mSingleLine) { if (!LastNoSpace) { height += Math.round(space); } } int lines = mSingleLine ? 1 : Math.max(1, maxLines); StaticLayout layout = getStaticLayout(text, paint); return layout.getLineCount() <= lines && layout.getHeight() <= height; } /** * 调整字体大小 * * @param oldPaint 旧画笔 * @param text 内容 * @param max 最大字体 * @param min 最小字体 * @return 适合字体大小 */ public float fitTextSize(TextPaint oldPaint, CharSequence text, float max, float min) { if (TextUtils.isEmpty(text)) { if (oldPaint != null) { return oldPaint.getTextSize(); } if (textView != null) { return textView.getTextSize(); } } float low = min; float high = max; TextPaint paint = new TextPaint(oldPaint); while (Math.abs(high - low) > LIMIT) { paint.setTextSize((low + high) / 2.0f); if (isFit(getLineBreaks(text, paint), paint)) { low = paint.getTextSize(); } else { high = paint.getTextSize(); } } return low; } /** * 拆入换行符,解决中英文的换行问题 * * @param text 内容 * @param paint 画笔 * @return 调整后的内容 */ public CharSequence getLineBreaks( CharSequence text, TextPaint paint) { int width = textView.getTextWidth(); boolean keepWord = textView.isKeepWord(); if (width <= 0 || keepWord) return text; int length = text.length(); int start = 0, end = 1; SpannableStringBuilder ssb = new SpannableStringBuilder(); while (end <= length) { CharSequence c = text.subSequence(end - 1, end); // char c = text.charAt(end - 1);// cs最后一个字符 // boolean needCheck = false; if (TextUtils.equals(c, "\n")) {// 已经换行 ssb.append(text, start, end); start = end; // needCheck = true; } else { float lw = paint.measureText(text, start, end); if (lw > width) {// 超出宽度,退回一个位置 ssb.append(text, start, end - 1); start = end - 1; if (end < length) { CharSequence c2 = text.subSequence(end - 1, end); if (!TextUtils.equals(c2, "\n")) ssb.append('\n'); } // needCheck = true; } else if (lw == width) { ssb.append(text, start, end); start = end; if (end < length) { CharSequence c2 = text.subSequence(end, end + 1); if (!TextUtils.equals(c2, "\n")) ssb.append('\n'); } // needCheck = true; } else if (end == length) { // 已经是最后一个字符 ssb.append(text, start, end); start = end; } } end++; } return ssb; } /*** * 获取文本框的布局 * * @param textView * @return */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static Layout.Alignment getLayoutAlignment(TextView textView) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { return Layout.Alignment.ALIGN_NORMAL; } Layout.Alignment alignment; switch (textView.getTextAlignment()) { case TextView.TEXT_ALIGNMENT_GRAVITY: switch (textView.getGravity() & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) { case Gravity.START: alignment = Layout.Alignment.ALIGN_NORMAL; break; case Gravity.END: alignment = Layout.Alignment.ALIGN_OPPOSITE; break; case Gravity.LEFT: alignment = (textView.getLayoutDirection() == TextView.LAYOUT_DIRECTION_RTL) ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_NORMAL; break; case Gravity.RIGHT: alignment = (textView.getLayoutDirection() == TextView.LAYOUT_DIRECTION_RTL) ? Layout.Alignment.ALIGN_NORMAL : Layout.Alignment.ALIGN_OPPOSITE; break; case Gravity.CENTER_HORIZONTAL: alignment = Layout.Alignment.ALIGN_CENTER; break; default: alignment = Layout.Alignment.ALIGN_NORMAL; break; } break; case TextView.TEXT_ALIGNMENT_TEXT_START: alignment = Layout.Alignment.ALIGN_NORMAL; break; case TextView.TEXT_ALIGNMENT_TEXT_END: alignment = Layout.Alignment.ALIGN_OPPOSITE; break; case TextView.TEXT_ALIGNMENT_CENTER: alignment = Layout.Alignment.ALIGN_CENTER; break; case TextView.TEXT_ALIGNMENT_VIEW_START: alignment = Layout.Alignment.ALIGN_NORMAL; break; case TextView.TEXT_ALIGNMENT_VIEW_END: alignment = Layout.Alignment.ALIGN_OPPOSITE; break; case TextView.TEXT_ALIGNMENT_INHERIT: // default: alignment = Layout.Alignment.ALIGN_NORMAL; break; } return alignment; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/fittext/FitTextView.java ================================================ package io.virtualapp.widgets.fittext; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.text.TextPaint; import android.text.TextUtils; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.widget.TextView; import io.virtualapp.R; public class FitTextView extends BaseTextView { private boolean mMeasured = false; /** * 不需要调整大小 */ private boolean mNeedFit = true; protected float mOriginalTextSize = 0; private float mMinTextSize, mMaxTextSize; protected CharSequence mOriginalText; /** * 正在调整字体大小 */ protected volatile boolean mFittingText = false; protected FitTextHelper mFitTextHelper; public FitTextView(Context context) { this(context, null); } public FitTextView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FitTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mOriginalTextSize = getTextSize(); if (attrs != null) { TypedArray a = context.obtainStyledAttributes(attrs, new int[]{ R.attr.ftMaxTextSize, R.attr.ftMinTextSize, }); mMaxTextSize = a.getDimension(0, mOriginalTextSize * 2.0f); mMinTextSize = a.getDimension(1, mOriginalTextSize / 2.0f); a.recycle(); } else { mMinTextSize = mOriginalTextSize; mMaxTextSize = mOriginalTextSize; } } protected FitTextHelper getFitTextHelper() { if (mFitTextHelper == null) { mFitTextHelper = new FitTextHelper(this); } return mFitTextHelper; } /** * @return 最小字体大小 */ public float getMinTextSize() { return mMinTextSize; } /** * @param minTextSize 最小字体大小 */ public void setMinTextSize(float minTextSize) { mMinTextSize = minTextSize; } /** * @return 最大字体大小 */ public float getMaxTextSize() { return mMaxTextSize; } /** * @param maxTextSize 最大字体大小 */ public void setMaxTextSize(float maxTextSize) { mMaxTextSize = maxTextSize; } /** * 是否需要调整字体 * * @return */ public boolean isNeedFit() { return mNeedFit; } /** * @param needFit 是否需要调整字体大小 */ public void setNeedFit(boolean needFit) { mNeedFit = needFit; } @Override public void setTextSize(int unit, float size) { super.setTextSize(unit, size); mOriginalTextSize = getTextSize(); } public float getOriginalTextSize() { return mOriginalTextSize; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); int heightMode = View.MeasureSpec.getMode(heightMeasureSpec); if (widthMode == View.MeasureSpec.UNSPECIFIED && heightMode == View.MeasureSpec.UNSPECIFIED) { super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mOriginalTextSize); mMeasured = false; } else { mMeasured = true; fitText(getOriginalText()); } } @Override public void setText(CharSequence text, TextView.BufferType type) { mOriginalText = text; super.setText(text, type); fitText(text); } public CharSequence getOriginalText() { return mOriginalText; } /** * 调整字体大小 * * @param text 内容 */ protected void fitText(CharSequence text) { if (!mNeedFit) { return; } if (!mMeasured || mFittingText || mSingleLine || TextUtils.isEmpty(text)) return; mFittingText = true; TextPaint oldPaint = getPaint(); float size = getFitTextHelper().fitTextSize(oldPaint, text, mMaxTextSize, mMinTextSize); super.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); super.setText(getFitTextHelper().getLineBreaks(text, getPaint())); mFittingText = false; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } } ================================================ FILE: VirtualApp/app/src/main/res/drawable/blue_circle.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/drawable/fab_bg.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/drawable/home_bg.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/drawable/icon_bg.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/drawable/sel_clone_app_btn.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/drawable/sel_guide_btn.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/drawable/shape_clone_app_btn.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/drawable/shape_clone_app_btn_pressed.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/drawable-nodpi/about_icon_copy_right.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/drawable-nodpi/ic_more.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/layout/activity_clone_app.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/layout/activity_install.xml ================================================