Repository: asLody/VirtualApp Branch: master Commit: dd01dce84601 Files: 900 Total size: 2.9 MB Directory structure: gitextract_bfy_ta4s/ ├── .gitignore ├── README.md ├── README_eng.md ├── VirtualApp/ │ ├── .gitignore │ ├── app/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── libs/ │ │ │ ├── TencentLocationSDK_v6.1.2_r1df4baaa_170627_1056.jar │ │ │ ├── TencentMapSDK_Raster_v_1.2.8_e45bcd0.jar │ │ │ └── TencentMapSearch_v1.1.7.3207904.jar │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── assets/ │ │ │ └── OaceOaT8w5Xda6wa │ │ ├── java/ │ │ │ └── io/ │ │ │ └── virtualapp/ │ │ │ ├── VApp.java │ │ │ ├── VCommends.java │ │ │ ├── abs/ │ │ │ │ ├── BasePresenter.java │ │ │ │ ├── BaseView.java │ │ │ │ ├── Callback.java │ │ │ │ ├── Value.java │ │ │ │ ├── nestedadapter/ │ │ │ │ │ ├── RecyclerViewAdapterWrapper.java │ │ │ │ │ └── SmartRecyclerAdapter.java │ │ │ │ ├── percent/ │ │ │ │ │ └── PercentLinearLayout.java │ │ │ │ ├── reflect/ │ │ │ │ │ └── ReflectException.java │ │ │ │ └── ui/ │ │ │ │ ├── BaseAdapterPlus.java │ │ │ │ ├── VActivity.java │ │ │ │ ├── VFragment.java │ │ │ │ └── VUiKit.java │ │ │ ├── delegate/ │ │ │ │ ├── MyAppRequestListener.java │ │ │ │ ├── MyComponentDelegate.java │ │ │ │ ├── MyPhoneInfoDelegate.java │ │ │ │ └── MyTaskDescriptionDelegate.java │ │ │ ├── effects/ │ │ │ │ ├── ExplosionAnimator.java │ │ │ │ └── ExplosionField.java │ │ │ ├── home/ │ │ │ │ ├── FlurryROMCollector.java │ │ │ │ ├── HomeActivity.java │ │ │ │ ├── HomeContract.java │ │ │ │ ├── HomePresenterImpl.java │ │ │ │ ├── ListAppActivity.java │ │ │ │ ├── ListAppContract.java │ │ │ │ ├── ListAppFragment.java │ │ │ │ ├── ListAppPresenterImpl.java │ │ │ │ ├── LoadingActivity.java │ │ │ │ ├── adapters/ │ │ │ │ │ ├── AppLocationAdapter.java │ │ │ │ │ ├── AppPagerAdapter.java │ │ │ │ │ ├── CloneAppListAdapter.java │ │ │ │ │ ├── LaunchpadAdapter.java │ │ │ │ │ └── decorations/ │ │ │ │ │ └── ItemOffsetDecoration.java │ │ │ │ ├── location/ │ │ │ │ │ ├── MarkerActivity.java │ │ │ │ │ └── VirtualLocationSettings.java │ │ │ │ ├── models/ │ │ │ │ │ ├── AddAppButton.java │ │ │ │ │ ├── AppData.java │ │ │ │ │ ├── AppInfo.java │ │ │ │ │ ├── AppInfoLite.java │ │ │ │ │ ├── EmptyAppData.java │ │ │ │ │ ├── LocationData.java │ │ │ │ │ ├── MultiplePackageAppData.java │ │ │ │ │ └── PackageAppData.java │ │ │ │ ├── platform/ │ │ │ │ │ ├── PlatformInfo.java │ │ │ │ │ └── WechatPlatformInfo.java │ │ │ │ └── repo/ │ │ │ │ ├── AppDataSource.java │ │ │ │ ├── AppRepository.java │ │ │ │ └── PackageAppDataStorage.java │ │ │ ├── splash/ │ │ │ │ └── SplashActivity.java │ │ │ ├── vs/ │ │ │ │ └── VSManagerActivity.java │ │ │ └── widgets/ │ │ │ ├── BallGridBeatIndicator.java │ │ │ ├── BallPulseIndicator.java │ │ │ ├── BaseView.java │ │ │ ├── CardStackAdapter.java │ │ │ ├── CardStackLayout.java │ │ │ ├── CircularAnim.java │ │ │ ├── DragSelectRecyclerView.java │ │ │ ├── DragSelectRecyclerViewAdapter.java │ │ │ ├── EatBeansView.java │ │ │ ├── Indicator.java │ │ │ ├── LabelView.java │ │ │ ├── LauncherIconView.java │ │ │ ├── LoadingIndicatorView.java │ │ │ ├── MarqueeTextView.java │ │ │ ├── MaterialRippleLayout.java │ │ │ ├── RippleButton.java │ │ │ ├── ShadowProperty.java │ │ │ ├── ShadowViewDrawable.java │ │ │ ├── Shimmer.java │ │ │ ├── ShimmerViewBase.java │ │ │ ├── ShimmerViewHelper.java │ │ │ ├── TwoGearsView.java │ │ │ ├── ViewHelper.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 │ │ ├── layout/ │ │ │ ├── activity_clone_app.xml │ │ │ ├── activity_home.xml │ │ │ ├── activity_install.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_clone_app.xml │ │ │ ├── item_launcher_app.xml │ │ │ ├── item_location_app.xml │ │ │ └── item_user.xml │ │ ├── menu/ │ │ │ ├── marktet_map.xml │ │ │ └── user_menu.xml │ │ ├── values/ │ │ │ ├── attrs.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── fitTextView.xml │ │ │ ├── ids.xml │ │ │ ├── integers.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-zh-rCN/ │ │ │ └── strings.xml │ │ └── values-zh-rTW/ │ │ └── strings.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/ │ │ │ ├── IBinderDelegateService.aidl │ │ │ ├── IPackageInstaller.aidl │ │ │ ├── IPackageInstallerSession.aidl │ │ │ ├── interfaces/ │ │ │ │ ├── IAppRequestListener.aidl │ │ │ │ ├── IIntentFilterObserver.aidl │ │ │ │ ├── IPackageObserver.aidl │ │ │ │ ├── IProcessObserver.aidl │ │ │ │ ├── IServiceFetcher.aidl │ │ │ │ └── IUiCallback.aidl │ │ │ └── pm/ │ │ │ └── installer/ │ │ │ ├── SessionInfo.aidl │ │ │ └── SessionParams.aidl │ │ ├── java/ │ │ │ ├── android/ │ │ │ │ ├── content/ │ │ │ │ │ ├── SyncStatusInfo.java │ │ │ │ │ └── pm/ │ │ │ │ │ └── PackageParser.java │ │ │ │ └── location/ │ │ │ │ └── LocationRequest.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 │ │ │ │ │ │ │ ├── ProviderHook.java │ │ │ │ │ │ │ └── SettingsProviderHook.java │ │ │ │ │ │ ├── proxies/ │ │ │ │ │ │ │ ├── account/ │ │ │ │ │ │ │ │ └── AccountManagerStub.java │ │ │ │ │ │ │ ├── alarm/ │ │ │ │ │ │ │ │ └── AlarmManagerStub.java │ │ │ │ │ │ │ ├── am/ │ │ │ │ │ │ │ │ ├── ActivityManagerStub.java │ │ │ │ │ │ │ │ ├── HCallbackStub.java │ │ │ │ │ │ │ │ └── MethodProxies.java │ │ │ │ │ │ │ ├── appops/ │ │ │ │ │ │ │ │ └── AppOpsManagerStub.java │ │ │ │ │ │ │ ├── appwidget/ │ │ │ │ │ │ │ │ └── AppWidgetManagerStub.java │ │ │ │ │ │ │ ├── audio/ │ │ │ │ │ │ │ │ └── AudioManagerStub.java │ │ │ │ │ │ │ ├── backup/ │ │ │ │ │ │ │ │ └── BackupManagerStub.java │ │ │ │ │ │ │ ├── bluetooth/ │ │ │ │ │ │ │ │ └── BluetoothStub.java │ │ │ │ │ │ │ ├── clipboard/ │ │ │ │ │ │ │ │ └── ClipBoardStub.java │ │ │ │ │ │ │ ├── connectivity/ │ │ │ │ │ │ │ │ └── ConnectivityStub.java │ │ │ │ │ │ │ ├── content/ │ │ │ │ │ │ │ │ └── ContentServiceStub.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 │ │ │ │ │ │ │ ├── persistent_data_block/ │ │ │ │ │ │ │ │ └── PersistentDataBlockServiceStub.java │ │ │ │ │ │ │ ├── phonesubinfo/ │ │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ │ └── PhoneSubInfoStub.java │ │ │ │ │ │ │ ├── pm/ │ │ │ │ │ │ │ │ ├── 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 │ │ │ │ │ ├── DaemonService.java │ │ │ │ │ ├── ResolverActivity.java │ │ │ │ │ ├── ShortcutHandleActivity.java │ │ │ │ │ ├── StubActivity.java │ │ │ │ │ ├── StubContentProvider.java │ │ │ │ │ ├── StubDialog.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 │ │ │ │ │ ├── ipcbus/ │ │ │ │ │ │ ├── IPCBus.java │ │ │ │ │ │ ├── IPCInvocationBridge.java │ │ │ │ │ │ ├── IPCMethod.java │ │ │ │ │ │ ├── IPCSingleton.java │ │ │ │ │ │ ├── IServerCache.java │ │ │ │ │ │ ├── ServerInterface.java │ │ │ │ │ │ └── TransformBinder.java │ │ │ │ │ └── utils/ │ │ │ │ │ ├── ArrayUtils.java │ │ │ │ │ ├── AtomicFile.java │ │ │ │ │ ├── BitmapUtils.java │ │ │ │ │ ├── ClassUtils.java │ │ │ │ │ ├── ComponentUtils.java │ │ │ │ │ ├── DrawableUtils.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 │ │ │ │ ├── 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 │ │ │ │ ├── interfaces/ │ │ │ │ │ ├── IAccountManager.java │ │ │ │ │ ├── IActivityManager.java │ │ │ │ │ ├── IAppManager.java │ │ │ │ │ ├── IDeviceInfoManager.java │ │ │ │ │ ├── IJobService.java │ │ │ │ │ ├── INotificationManager.java │ │ │ │ │ ├── IPackageManager.java │ │ │ │ │ ├── IUserManager.java │ │ │ │ │ ├── IVirtualLocationManager.java │ │ │ │ │ └── IVirtualStorageService.java │ │ │ │ ├── job/ │ │ │ │ │ └── VJobSchedulerService.java │ │ │ │ ├── location/ │ │ │ │ │ └── VirtualLocationService.java │ │ │ │ ├── memory/ │ │ │ │ │ ├── MappedMemoryRegion.java │ │ │ │ │ ├── MemoryRegionParser.java │ │ │ │ │ ├── MemoryScanEngine.java │ │ │ │ │ ├── MemoryValue.java │ │ │ │ │ └── ProcessMemory.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 │ │ │ │ │ ├── IAlarmManager.java │ │ │ │ │ ├── IApplicationThread.java │ │ │ │ │ ├── IApplicationThreadICSMR1.java │ │ │ │ │ ├── IApplicationThreadJBMR1.java │ │ │ │ │ ├── IApplicationThreadKitkat.java │ │ │ │ │ ├── IApplicationThreadOreo.java │ │ │ │ │ ├── ISearchManager.java │ │ │ │ │ ├── IServiceConnectionO.java │ │ │ │ │ ├── IUsageStatsManager.java │ │ │ │ │ ├── LoadedApk.java │ │ │ │ │ ├── LoadedApkHuaWei.java │ │ │ │ │ ├── LoadedApkICS.java │ │ │ │ │ ├── LoadedApkKitkat.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 │ │ │ │ │ └── IBluetoothManager.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 │ │ │ │ │ │ ├── PackageUserState.java │ │ │ │ │ │ ├── ParceledListSlice.java │ │ │ │ │ │ ├── ParceledListSliceJBMR2.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 │ │ │ │ │ ├── INetworkManagementService.java │ │ │ │ │ ├── IPowerManager.java │ │ │ │ │ ├── IUserManager.java │ │ │ │ │ ├── Message.java │ │ │ │ │ ├── Process.java │ │ │ │ │ ├── ServiceManager.java │ │ │ │ │ ├── StrictMode.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/ │ │ │ │ │ ├── CompatibilityInfoHolder.java │ │ │ │ │ ├── Display.java │ │ │ │ │ ├── DisplayAdjustments.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 │ │ │ │ ├── appwidget/ │ │ │ │ │ └── IAppWidgetService.java │ │ │ │ ├── content/ │ │ │ │ │ ├── NativeLibraryHelper.java │ │ │ │ │ └── ReferrerIntent.java │ │ │ │ ├── os/ │ │ │ │ │ ├── IDropBoxManagerService.java │ │ │ │ │ ├── IVibratorService.java │ │ │ │ │ └── UserManager.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 │ │ ├── jni/ │ │ │ ├── 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 │ │ │ ├── HookZz/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .gitmodules │ │ │ │ ├── Android.mk │ │ │ │ ├── LICENSE │ │ │ │ ├── Makefile │ │ │ │ ├── README.md │ │ │ │ ├── docs/ │ │ │ │ │ ├── HookFrameworkDesign.md │ │ │ │ │ ├── hookzz-docs.md │ │ │ │ │ ├── hookzz-example.md │ │ │ │ │ └── hookzz-getting-started.md │ │ │ │ ├── include/ │ │ │ │ │ └── hookzz.h │ │ │ │ ├── src/ │ │ │ │ │ ├── allocator.c │ │ │ │ │ ├── allocator.h │ │ │ │ │ ├── interceptor.c │ │ │ │ │ ├── interceptor.h │ │ │ │ │ ├── memory.c │ │ │ │ │ ├── memory.h │ │ │ │ │ ├── platforms/ │ │ │ │ │ │ ├── arch-arm/ │ │ │ │ │ │ │ ├── instructions.c │ │ │ │ │ │ │ ├── instructions.h │ │ │ │ │ │ │ ├── reader-arm.c │ │ │ │ │ │ │ ├── reader-arm.h │ │ │ │ │ │ │ ├── reader-thumb.c │ │ │ │ │ │ │ ├── reader-thumb.h │ │ │ │ │ │ │ ├── regs-arm.c │ │ │ │ │ │ │ ├── regs-arm.h │ │ │ │ │ │ │ ├── relocator-arm.c │ │ │ │ │ │ │ ├── relocator-arm.h │ │ │ │ │ │ │ ├── relocator-thumb.c │ │ │ │ │ │ │ ├── relocator-thumb.h │ │ │ │ │ │ │ ├── writer-arm.c │ │ │ │ │ │ │ ├── writer-arm.h │ │ │ │ │ │ │ ├── writer-thumb.c │ │ │ │ │ │ │ └── writer-thumb.h │ │ │ │ │ │ ├── arch-arm64/ │ │ │ │ │ │ │ ├── instructions.c │ │ │ │ │ │ │ ├── instructions.h │ │ │ │ │ │ │ ├── reader-arm64.c │ │ │ │ │ │ │ ├── reader-arm64.h │ │ │ │ │ │ │ ├── regs-arm64.c │ │ │ │ │ │ │ ├── regs-arm64.h │ │ │ │ │ │ │ ├── relocator-arm64.c │ │ │ │ │ │ │ ├── relocator-arm64.h │ │ │ │ │ │ │ ├── writer-arm64.c │ │ │ │ │ │ │ └── writer-arm64.h │ │ │ │ │ │ ├── arch-x86/ │ │ │ │ │ │ │ ├── instructions.c │ │ │ │ │ │ │ ├── instructions.h │ │ │ │ │ │ │ ├── reader-x86.c │ │ │ │ │ │ │ ├── reader-x86.h │ │ │ │ │ │ │ ├── regs-x86.c │ │ │ │ │ │ │ ├── regs-x86.h │ │ │ │ │ │ │ ├── relocator-x86.c │ │ │ │ │ │ │ ├── relocator-x86.h │ │ │ │ │ │ │ ├── writer-x86.c │ │ │ │ │ │ │ └── writer-x86.h │ │ │ │ │ │ ├── backend-arm/ │ │ │ │ │ │ │ ├── interceptor-arm.c │ │ │ │ │ │ │ ├── interceptor-arm.h │ │ │ │ │ │ │ ├── interceptor-template-arm.s │ │ │ │ │ │ │ ├── thunker-arm.c │ │ │ │ │ │ │ └── thunker-arm.h │ │ │ │ │ │ ├── backend-arm64/ │ │ │ │ │ │ │ ├── interceptor-arm64.c │ │ │ │ │ │ │ ├── interceptor-arm64.h │ │ │ │ │ │ │ ├── interceptor-template-arm64.s │ │ │ │ │ │ │ ├── thunker-arm64.c │ │ │ │ │ │ │ └── thunker-arm64.h │ │ │ │ │ │ ├── backend-darwin/ │ │ │ │ │ │ │ ├── memory-darwin.c │ │ │ │ │ │ │ └── memory-darwin.h │ │ │ │ │ │ ├── backend-linux/ │ │ │ │ │ │ │ ├── memory-linux.c │ │ │ │ │ │ │ └── memory-linux.h │ │ │ │ │ │ ├── backend-posix/ │ │ │ │ │ │ │ ├── thread-posix.c │ │ │ │ │ │ │ └── thread-posix.h │ │ │ │ │ │ ├── backend-x86/ │ │ │ │ │ │ │ ├── interceptor-template-x86.s │ │ │ │ │ │ │ ├── interceptor-x86.c │ │ │ │ │ │ │ ├── interceptor-x86.h │ │ │ │ │ │ │ ├── thunker-x86.c │ │ │ │ │ │ │ └── thunker-x86.h │ │ │ │ │ │ └── x86/ │ │ │ │ │ │ ├── instructions.h │ │ │ │ │ │ ├── reader.c │ │ │ │ │ │ ├── reader.h │ │ │ │ │ │ ├── writer.c │ │ │ │ │ │ └── writer.h │ │ │ │ │ ├── relocator.h │ │ │ │ │ ├── stack.c │ │ │ │ │ ├── stack.h │ │ │ │ │ ├── thread.h │ │ │ │ │ ├── thunker.h │ │ │ │ │ ├── trampoline.c │ │ │ │ │ ├── trampoline.h │ │ │ │ │ ├── writer.h │ │ │ │ │ ├── zzdefs.h │ │ │ │ │ ├── zzdeps/ │ │ │ │ │ │ ├── .gitignore │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ ├── common/ │ │ │ │ │ │ │ ├── LEB128.h │ │ │ │ │ │ │ ├── debugbreak.h │ │ │ │ │ │ │ ├── memory-utils-common.c │ │ │ │ │ │ │ └── memory-utils-common.h │ │ │ │ │ │ ├── darwin/ │ │ │ │ │ │ │ ├── mach_vm.h │ │ │ │ │ │ │ ├── macho-utils-darwin.c │ │ │ │ │ │ │ ├── macho-utils-darwin.h │ │ │ │ │ │ │ ├── memory-utils-darwin.c │ │ │ │ │ │ │ └── memory-utils-darwin.h │ │ │ │ │ │ ├── linux/ │ │ │ │ │ │ │ ├── memory-utils-linux.c │ │ │ │ │ │ │ └── memory-utils-linux.h │ │ │ │ │ │ ├── memory-utils.h │ │ │ │ │ │ ├── posix/ │ │ │ │ │ │ │ ├── memory-utils-posix.c │ │ │ │ │ │ │ ├── memory-utils-posix.h │ │ │ │ │ │ │ ├── thread-utils-posix.c │ │ │ │ │ │ │ └── thread-utils-posix.h │ │ │ │ │ │ └── zz.h │ │ │ │ │ ├── zzinfo.c │ │ │ │ │ └── zzinfo.h │ │ │ │ ├── tests/ │ │ │ │ │ ├── arm-android/ │ │ │ │ │ │ ├── makefile │ │ │ │ │ │ ├── test_hook_address_thumb.c │ │ │ │ │ │ ├── test_hook_open_arm.c │ │ │ │ │ │ └── test_hook_printf.c │ │ │ │ │ ├── arm-insn-fix/ │ │ │ │ │ │ ├── makefile │ │ │ │ │ │ └── test_insn_fix.c │ │ │ │ │ ├── arm-ios/ │ │ │ │ │ │ ├── makefile │ │ │ │ │ │ ├── test_hook_address_thumb.c │ │ │ │ │ │ ├── test_hook_freeaddr.c │ │ │ │ │ │ ├── test_hook_oc_thumb.m │ │ │ │ │ │ ├── test_hook_open_arm.c │ │ │ │ │ │ └── test_hook_printf.c │ │ │ │ │ ├── arm64-insn-fix/ │ │ │ │ │ │ ├── makefile │ │ │ │ │ │ └── test_insn_fix.c │ │ │ │ │ └── arm64-ios/ │ │ │ │ │ ├── makefile │ │ │ │ │ ├── test_hook_address.c │ │ │ │ │ ├── test_hook_oc.m │ │ │ │ │ └── test_hook_printf.c │ │ │ │ └── tools/ │ │ │ │ └── ZzSolidifyHook/ │ │ │ │ ├── solidifyhook │ │ │ │ ├── solidifyhook.cpp │ │ │ │ └── solidifytrampoline.c │ │ │ ├── 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 │ └── settings.gradle └── doc/ ├── VADev.md └── VADev_eng.md ================================================ FILE CONTENTS ================================================ ================================================ 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: README.md ================================================ [English Doc](README_eng.md "English")

VA产品说明&开发指导

## VA是什么? ## VirtualApp(简称:VA)是一款运行于Android系统的沙盒产品,可以理解为轻量级的“Android虚拟机”。其产品形态为高可扩展,可定制的集成SDK,您可以基于VA或者使用VA定制开发各种看似不可能完成的项目。VA目前被广泛应用于APP多开、小游戏合集、手游加速器、手游租号、手游手柄免激活、VR程序移植、区块链、移动办公安全、军队政府数据隔离、手机模拟信息、脚本自动化、插件化开发、无感知热更新、云控等技术领域。
**Github上代码已在2017年12月份停止更新,商业版代码在持续更新中,如需授权获得最新代码,请联系微信:10890** ## VA中的术语 ## 术语 | 解释 ---- | --- 宿主 | 集成VirtualApp类库(lib)的App叫做宿主 宿主插件 | 用于在同一个手机,运行另一种ABI的宿主包,又称做插件包,扩展包,宿主插件包,宿主扩展包 虚拟App/VApp | VA的虚拟环境多开的app 外部App | 手机真实环境安装的app
## VA技术架构 ## ![](https://cdn.jsdelivr.net/gh/xxxyanchenxxx/temp@1.0/doc/va_architecture.jpg) VA技术一共涉及到了Android的APP层,Framework层以及Native层。 一个APP想要在Android系统上运行,必须要安装后系统才会接纳。安装到VA内部的APP实际上并没有安装到系统中,所以正常情况下是无法运行的。那如何才能让它运行呢? 答:那就只有“欺骗”系统,让系统认为已经安装。而这个“欺骗”过程就是VA Framework的核心工作内容,也是整个VA的核心技术原理。 **下面介绍下在这3个层次分别做了什么事情:** 层次 | 主要工作 ---- | --- VA Space | 由VA提供了一个内部的空间,用于安装要在其内部运行的APP,这个空间是系统隔离的。 VA Framework | 这一层主要给Android Framework和VAPP做代理,这也是VA的核心。VA提供了一套自己的VA Framework,处于Android Framework与VA APP之间。
1. 对于VAPP,其访问的所有系统Service均已被 `VA Framework` 代理,它会修改VAPP的请求参数,将其中与VAPP安装信息相关的全部参数修改为宿主的参数之后发送给Android Framework(有部分请求会发送给自己的VA Server直接处理而不再发送给Android系统)。这样Android Framework收到VAPP请求后检查参数就会认为没有问题。
2. 待Android系统对该请求处理完成返回结果时,VA Framework同样也会拦截住该返回结果,此时再将原来修改过的参数全部还原为VAPP请求时发送的。
这样VAPP与Android系统的交互也就能跑通了。 VA Native | 在这一层主要为了完成2个工作,IO重定向和VA APP与Android系统交互的请求修改。
1. IO重定向是因为可能有部分APP会通过写死的绝对路径访问,但是如果APP没有安装到系统,这个路径是不存在的,通过IO重定向,则将其转向VA内部安装的路径。
2. 另外有部分jni函数在VA Framework中无法hook的,所以需要在native层来做hook。
总结: 通过上面技术架构可以看到,VA内部的APP实际是跑在VA自己的VA Framework之上。 VA已将其内部APP的全部系统请求进行拦截,通过这项技术也能对APP进行全面控制,而不仅仅只是多开。并且为了方便开发者,VA还提供了SDK以及Hook SDK。 ## VA进程架构 # ![](https://cdn.jsdelivr.net/gh/xxxyanchenxxx/temp@1.0/doc/va_process.jpg) VA运行时有5类进程:CHILD进程,VA Host Main进程,VA Host Plugin进程,VAPP Client进程,VAServer进程。 VA为了同时支持32位APP与64位APP,需要安装2个包:一个主包,一个插件包(在本文档中主包是32位,插件包是64位)。 2个包也是必须的,因为一个包只能运行在一种模式下,要么32位,要么64位。所以对于32位的APP,VA使用32位的主包去运行,对于64位的APP,VA则使用64位的插件包去运行。 在主包中含了VA的所有代码,插件包中只有一段加载主包代码执行的代码,无其他代码。所以插件包几乎不用更新,只需要更新主包即可。 另外主包是选择用32位还是64位,可以在配置文件中修改(比如对于要上GooglePlay的用户,会修改为主包64位,插件包32位)。 **各类进程的作用与解释如下:**
进程类型 | 作用 ---- | --- CHILD | 由VA Host集成的其他进程,比如:保活进程,推送进程等。 VA Host Main | VA主包的UI主界面所在的进程。默认主包是32位,插件包是64位,可在配置文件中修改切换。 VA Host Plugin | 支持64位APP的插件包所在进程。默认主包是32位,插件包是64位,可在配置文件中修改切换。 VAPP Client | 安装到VA中的APP启动后产生的进程,在运行时会将io.busniess.va:pxxx进程名修改VAPP的真实进程名。 VAServer | VA Server的所在的进程,用于处理VA中不交予系统处理的请求。比如APP的安装处理。
## VA几乎能满足您的一切需求 ## 通过上面的技术架构,我们可以了解到VA可以对APP进行全面的控制,并且提供了Hook SDK,几乎能满足您在各个领域的一切需求: 1. 可以满足您的**双开/多开**需求 VA可以让您在同一部手机上安装多个微信/QQ/WhatsApp/Facebook等APP,实现一部手机,多个账号同时登录。 2. 可以满足您的**移动安全**需求 VA提供了一整套内部与外部的隔离机制,包括但不限于(文件隔离/组件隔离/进程通讯隔离),简单的说VA内部就是一个“完全独立的空间”。 通过VA可将工作事务与个人事务安全的隔离,互不干扰。稍作定制即可实现应用行为审计、数据加密、数据采集、数据防泄漏、防攻击泄密等移动安全相关的需求。 **2.1 应用行为审计** 通过VA提供的HOOK能力可以实现实时监测用户使用行为,将违规信息上传到服务器;并能轻易实现诸如时间围栏(在某个时间段内能否使用应用的某个功能)、地理围栏(在某个区域内能否使用应用的某个功能)、敏感关键字过滤拦截等功能需求。 **2.2 数据加密** 通过VA提供的HOOK能力可以实现对应用的全部数据/文件加密,保证数据/文件落地安全。 **2.3 数据采集** 通过VA提供的HOOK能力可以实现应用数据的实时无感上传需求,如聊天记录、转账记录等,防止事后删除无法追溯。 **2.4 数据防泄漏** 通过VA提供的HOOK能力可以实现应用防复制/粘贴、防截屏/录屏、防分享/转发、水印溯源等需求。 **2.5 防攻击泄密** 通过VA提供的应用管控能力可以将APP获取短信/通讯录/通话记录/后台录音/后台拍照/浏览历史/位置信息等隐私相关的行为完全控制在沙盒中,防止木马/恶意APP获取到用户真实的隐私数据,造成泄密等严重后果。 3. 可以满足您的**免ROOT HOOK**需求 VA提供了Java与Native的Hook能力,通过VA,您可以轻易实现诸如虚拟定位、改机、APP监控管理、移动安全等各种场景需要的功能。 4. 可以满足您的**APP静默安装**需求 VA提供了APP静默安装,静默升级,静默卸载的能力。如应用商店或游戏中心在集成VA后可以避免需要用户手动点击确认安装的操作,做到下载后立即安装到VA内,给用户带来“小程序”搬的体验,彻底避免了应用不易被用户安装上的问题。 5. 可以满足您的**APP管控**需求 您可以通过VA清楚的掌握APP访问了哪些系统API,哪些敏感数据,哪些设备信息等。比如APP是否访问了联系人,相册,通话记录,是否访问了用户的地理位置等信息。 当然,您还可以通过VA控制或者构造自定义的信息给这些APP。不仅于此,您还可以获取到APP的私有数据,比如聊天数据库等。总之通过VA提供的应用管控能力,您可以轻易控制APP的一切行为,甚至修改APP与服务器交互内容等。
6. 可以满足您的**海外市场**需求 VA实现了对Google服务的支持,以支持海外的App运行,比如Twitter、Messenger、WhatsApp、Instagram、FaceBook、Youtube等。 7. 可以满足您的**VR程序移植**需求 可以通过VA的Hook能力拦截VR设备的API,让您无需改动代码即可将VR程序移植到新的设备。 8. 可以满足您**几乎一切能想到**的需求 VA对于内部的App具有完全的监管和控制能力,几乎能满足您的一切需求! 9. 同时VA也是该技术领域**唯一一款**对外商业授权的产品 截止目前已有**上百家**授权客户在付费使用VirtualApp商业版代码,集成VirtualApp代码的APP**日启动**次数**超过2亿次**,众多安卓工程师向我们提供不同场景下的用户反馈,通过我们技术团队不断优化迭代,不断提升产品性能与兼容性! VA的特有能力 --- - 克隆能力
可以克隆外部系统中已经安装的App,并在内部运行,互不干扰。典型应用场景为App双开。 - 免安装能力
除了克隆已安装之外,VA可以直接在内部安装(外部无感知)apk,并在内部直接运行。典型应用场景为插件化,独立应用市场等。 - 多开能力
VA不仅可以“双开”,独特的多用户模式支持用户在内部无限多开同一个App。 - 内外隔离能力
VA是一个标准的沙盒,或者说“虚拟机”,提供了一整套内部与外部的隔离机制,包括但不限于(文件隔离/组件隔离/进程通讯隔离),简单的说VA内部就是一个“完全独立的空间”。在此基础之上,稍作定制即可实现一部手机上的“虚拟手机”。当然您也可以发挥想象,定制成应用于数据加密,数据隔离,隐私保护,企业管理的应用系统。 - 对于内部App的完全控制能力
VA对于内部的App具有完全的监控和控制能力,这点在未Root的外部环境中是绝对无法实现的。
详细(下拉打开) 1. 服务请求控制,首先VA直接提供了一些服务请求的拦截,您可以在集成VA的时候方便的定制这些服务请求,包括但远不限于(App请求安装apk/App请求打开某些文件/App请求定位数据/App请求手机信息等等)

2. 系统API控制,VA虚拟并实现了整个安卓系统框架,这也是VA可以免安装在内部运行apk的原理,您可以对虚拟框架的实现进行修改以动态监测分析App的行为等;除此之外,您还可模拟一些系统行为以实现一些在外部难以实现的需求(例如游戏手柄)。

3. 内存读写,VA可以无需Root读写内部App进程的内存。

4. 免Root调试,VA可以免Root调试(ptrace)内部的App进程,基于此您还可以实现免Root的进程注入。

5. 加载任意“插件”和“行为”,VA内部的App进程由VA的框架Client端代码派生而来,所以您可以在进程的入口代码插入任何“加载”以及“控制”逻辑。这些实现都非常简单。

6. 方法Hook,VA内置了一套运行于Android各个版本(直到AndroidQ)的Xposed框架以及native hook框架,基于此您可以方便的Hook任意内部App的任意Java/Native方法。可以轻松实现一个免Root的Xposed环境(有实例代码)。

7. 文件控制,VA内置了完整的文件重定向,可以方便的控制内部App的文件的读写,基于此可以实现对文件的保护加密等功能。

8. 注:以上控制能力均有实现代码或者实例以作参考。
VA的其他特性 --- - 高性能
进程级“虚拟机”,VA独特的实现方式让其性能几乎于原生App一致,更不需要普通虚拟机漫长的启动。 - 全版本支持
支持5.0-17.0,支持32位/64位app,支持ARM以及X86处理器。并且支持未来将更新的Android版本。 - 易扩展与集成
VA 的集成方式与普通Android库类似,即使您的App已经完成上线,您也方便的可以集成VA,享受VA带来的能力。 - 支持Google服务
提供Google服务的支持,以支持海外的App ## VA与其他技术方案对比 ## 在做企业级移动安全时,往往需要对APP进行管控,以下是列出的可能技术方案对比: 技术方案 | 原理简介 | 点评 | 运行性能 | 兼容稳定性 | 项目维护成本 ---- | --- | --- | --- | --- | --- 二次打包 | 通过反编译目标APP,加入自己的控制代码,重新打包 | 1.现在的APP几乎都有加固或防篡改保护,重打包已是一件非常困难的事
2.手机系统也会检测APP是否被重打包,如果重打包,会直接提示用户存在安全风险,甚至不让安装
3.针对每一个APP,甚至每一个版本都要深入去逆向分析,耗时耗力,难于维护 | 优秀 | 差 | 高 定制ROM | 通过定制系统源码,编译刷到指定手机 | 只能针对指定的内部手机,局限性太大,无法扩展 | 优秀 | 优秀 | 高 ROOT手机 | 通过ROOT手机,刷入xposed等类似框架 | 1.ROOT手机现在本身已是一件不太可能的事
2.现实中也很难让用户能去ROOT自己的手机 | 优秀 | 差 | 高 VA | 轻量级虚拟机,速度快,对设备要求低 | 无上述风险点 | 优秀 | 优秀,有上百家企业在同时测试反馈 | 低,VA提供了API并有专业的技术团队保障项目稳定运行
通过以上对比可以看出,VA是一款优秀的产品,并且能降低您的开发维护成本! ## 集成VA步骤 ## 第1步:在您的Application中调用VA接口```VirtualCore.get().startup()```来启动VA引擎 第2步:调用VA接口```VirtualCore.get().installPackageAsUser(userId, packageName)```将目标APP安装到VA中 第3步:调用VA接口```VActivityManager.get().launchApp(userId, packageName)```启动APP **仅通过以上3个API就完成了基础使用,VA已屏蔽了复杂的技术细节,并提供了接口API,让您的开发变得很简单!** ## VA的兼容稳定性 ## VA已被**上百家**企业进行了广泛测试,包含**数十家上市公司高标准**的测试及反馈,几乎涵盖了海内外的各种机型设备和场景! 为您的稳定运行提供了充分的保障! 截止目前,支持的系统版本: 系统版本 | 是否支持 ---- | --- 5.0 | 支持 5.1 | 支持 6.0 | 支持 7.0 | 支持 8.0 | 支持 9.1 | 支持 10.0 | 支持 11.0 | 支持 12.0 | 支持 13.0 | 支持 14.0 | 支持 15.0 | 支持 16.0 | 支持 17.0 | 支持
支持的APP类型: APP类型 | 是否支持 ---- | --- 32位APP | 支持 64位APP | 支持
支持的HOOK类型: Hook类型 | 是否支持 ---- | --- Java Hook | 支持 Native Hook | 支持 支持的CPU类型: Hook类型 | 是否支持 ---- | --- ARM 32 | 支持 ARM 64 | 支持
## 集成VA遇到问题如何反馈? ## 购买授权后我们会建立微信群,有任何问题可以随时反馈给我们,并根据优先级在第一时间处理! ## VA开发文档 ## VA开发文档请参考:[开发文档](doc/VADev.md) 授权说明 ------ VirtualApp虚拟机技术归属于:山东盒一网络科技有限公司(原:济宁市罗盒网络科技有限公司),于2015年至2026年申请多项VirtualApp知识产权,`受中华人民共和国知识产权法保护`。当您需要使用Github上的代码时,**请购买商业授权**,获取商业授权后将可以收到最新VirtualApp商业版全部源代码。上百家授权客户在付费使用VirtualApp商业版代码,集成VirtualApp代码的APP日启动次数超过2亿次,众多安卓工程师向我们提供不同场景下的用户反馈,通过我们技术团队不断优化迭代,VirtualApp商业版代码性能更好、兼容性更高。`当您的公司获取授权后,将成为其中一员,享受这些不断迭代完善后的技术成果。并可以和我们的授权客户进行运营、技术及商业上的互动合作。`
负责人:张总
微信:10890

严重声明 ------ 您如果未经授权将VirtualApp用于**内部使用、商业牟利或上传应用市场**,我们将取证后报警(侵犯著作权罪)或起诉,这将对您所属公司造成刑事责任及法律诉讼,影响到您公司的商誉和投资。`购买商业授权为您节省大量开发、测试和完善兼容性的时间,让您更多时间用于创新及盈利。`罗盒科技已在2020年报警和起诉了一些个人及公司。
**为响应国家对于知识产权的保护号召!凡举报自己所在公司或其他公司未经授权,违法使用VirtualApp代码开发产品的,一经核实给予现金奖励。我们会对举报人身份保密!举报联系微信:10890**
商业版主要更新 ------ 1. 兼容最新Android 17.0 2. 支持Binder拦截,不再使用动态代理 3. 支持Seccomp-Bpf拦截 4. 不易被杀毒软件误报 5. 框架优化,性能大幅提升 6. 手机系统及APP兼容性大幅提升 7. 完美运行Google服务 8. 支持运行纯64位App 9. 内置`XPosed Hook`框架 10. 增加定位模拟代码 11. 增加改机代码 12. 其他600+项问题的修复和改进,详情请见下表
2017年-2026年商业版代码更新详细 ------ **2026年3月5号 至 2026年 3月16号 商业版代码更新内容** 663、17.0 beta2上的报错修复
662、 Seccomp-bpf only相关的调整
661、反射代码调整
**2026年2月12号 至 2026年 3月4号 商业版代码更新内容** 660、增加对部分加固的支持
659、 fp指针调整避免某些App无法打开
2017年 12月 至 2026年 2 月 11 日 商业版代码更新内容(下拉打开)
**2026年2月1号 至 2026年 2月11号 商业版代码更新内容** 658、 修复NativeEngine.onGetCallingUid在VisitRoots的时候crash
657、 Secomp-Bpf开启后,执行execve时支持fd
656、fix getFromLocation crash on 15.0+
**2025年12月31号 至 2026年 1月30号 商业版代码更新内容** 655、增强环境检测方面的处理
654、.0上AdvancedProtectionService以及ISupervisionManager适配
653、fix AttributionSource子类验证报错:AttributionSource should be unparceled during a binder
652、fix ParceledListSlice->getList返回null
651、对于一些没有权限的函数调用进行处理
650、maps处理方式更新
649、对fp指令范围进行判断,避免crash
**2025年12月19号 至 2025年 12月30号 商业版代码更新内容** 648、从处理openat2与faccessat2,seccomp等调用
647、适配APP中对seccomp-bpf的调用
646、处理某些应用因为检测打不开
**2025年12月3号 至 2025年 12月18号 商业版代码更新内容** 645、某些svc处理
644、系统路径调整
643、maps调整
**2025年11月12号 至 2025年 12月2号 商业版代码更新内容** 642、新功能:Native层增加机型模拟支持
641、支持对应用强制开启机型模拟/强制使用真机信息
640、seccomp-bpf对ptrace的一些处理
639、修复执行Java命令时进程可能崩溃的问题
638、修复其他几处崩溃的问题
**2025年10月29号 至 2025年 11月11号 商业版代码更新内容** 637、新功能:Seccomp-Bpf下增加了对execve的调用支持
636、处理了与反射相关的几个地方
635、对几个内存文件的chmod处理
**2025年10月11号 至 2025年 10月28号 商业版代码更新内容** 634、修复IBatteryStats相关的空指针
**2025年9月18号 至 2025年 10月10号 商业版代码更新内容** 633、处理onNewIntent()中的activity Referrer
632、适配NotificationProviderPublic
631、修复15.0+上isDirectlyHandlingTransaction()的多线程处理
630、隐藏Libcore.os的反射获取
629、binderproxy模式支持IBatteryStats
628、适配某些加固APP
**2025年9月2号 至 2025年 9月17号 商业版代码更新内容** 627、Android 16kb page size 适配
626、fix蓝牙几个代理类的代码错误
625、处理某些APP在Application->attach中获取到宿主Application的场景
624、处理Activity referrer相关的
623、处理processOutsideIntent时intent包含vapp class的情况
622、针对某些oppo 13.0机型适配
621、处理调用ArrayUtils.indexOf时,有的地方按0开始起步有的地方按1开始起步导致异常,都统一为1
620、适配LockSettings/WifiScanner/NetworkScoreManager/WifiManager/SensitiveContentProtectionManager等几个manager
619、新功能模式下BinderProxyInjectManager.addInjector增加判断,避免有些因为class不存在导致crash
618、新功能模式下支持Instrumentation注入
617、增加对native层获取宿主信息的处理
**2025年8月9号 至 2025年 9月1号 商业版代码更新内容** 616、增加对setxattr/lsetxattr/bind/connect/syscall等几个libc api处理
615、增加对execve启动的进程实现inline hook
614、增加可对application中的全部路径模拟为系统安装路径
613、增加对logcat等命令的过滤处理
612、修复app 崩溃/anr等系列问题
611、修复ParceledListSlice.getList返回Null的问题
**2025年7月22号 至 2025年 8月8号 商业版代码更新内容** 610、增加对INetworkScoreService的处理
609、修复新功能在release下的一些错误
608、修复某些手机上新功能无法打开APP
607、修复Tethering相关的crash **2025年7月3号 至 2025年 7月21 商业版代码更新内容** 606、与新功能相关的路径处理
605、一些bug处理
**2025年5月1号 至 2025年 7月2号 商业版代码更新内容** 604、新功能开发:基于拦截Binder的方式来实现对系统AIDL调用的拦截。通过这种方式可不再使用动态代理,对于稳定性将会有比较大提升
603、16.0最新版本继续适配
**2025年4月16号 至 2025年 4月30号 商业版代码更新内容** 602、对Android 16.0 beta 4 适配
601、完善seccomp-bpf重定向相关的功能
600、增加配置可以让APP只使用seccomp-bpf,不使用inline hook
599、将target sdk升级到34
598、修复va core进程由于client为空导致的crash
597、启动进程的时候增加重试,避免在某些机型上由于进程死亡太频繁导致无法启动进程
596、对于某类型加固后是否需要安装provider的部分改为动态判断
595、修复demo在某些设备上由于title为null导致的crash
**2025年3月27号 至 2025年 4月15号 商业版代码更新内容** 594、修复GMS无法调起登录的问题
593、增加对IInputMethodManagerGlobalInvoker的hook
592、修复工作空间中无法打开VAPP的问题
591、适配微信8.0.57
**2025年3月20号 至 2025年 3月26号 商业版代码更新内容** 590、增加对AppSearchManager的适配
589、增加对DomainVerificationManager的适配
588、增加对SystemUpdateManager的适配
587、修复多个进程同时启动同一个进程时的crash问题
**2025年2月28号 至 2025年 3月19号 商业版代码更新内容** 586、seccomp 相关的调整
585、修复微信在14.0+上开启seccomp-bpf无法打开的问题
584、新增对StorageStatsManager的适配
583、UsageStatsManager相关API适配
**2025年2月11号 至 2025年 2月27号 商业版代码更新内容** 582、适配最新版微信
**2025年1月24号 至 2025年 2月10号 商业版代码更新内容** 581、对IO进行inline hook时暂停所有Java线程,避免冷启动时因多线程导致的低概率crash
**2025年1月8号 至 2025年 1月23号 商业版代码更新内容** 580、installer静默安装部分适配
579、修复静态广播收不到消息
578、修复pending intent数据丢失问题
577、input manager 14.0+上的适配
576、蓝牙适配
575、queryStatsForPackage适配
574、修复有些手机上显示不出应用列表
573、其他一些小问题适配
**2024年12月21号 至 2025年 1月7号 商业版代码更新内容** 572、Seccomp-bpf支持32位
571、修复某些情况下路径多次重定向的问题
570、修复抖音人脸识别时可能白屏的问题
569、去掉几年前为抖音打不开做的一些修改
**2024年12月7号 至 2024年 12月20号 商业版代码更新内容** 568、修复startIntentSenderFoeResult无法工作的问题
567、修复GMS由于StatsManager无法hook导致crash的问题
**2024年11月27号 至 2024年 12月6号 商业版代码更新内容** 566、修复release打包后IJobService中的onNetworkChanged等几个函数被混淆导致找不到crash的问题
565、修复由于BluetoothAdapter.sService为null导致IBluetooth hook失败的问题
564、修复packagesettings被覆盖的问题
563、删除getCallingUid()中的缓存代码
562、Seccomp条件判断时处理Application.name为Null的情况
561、AGP升级到8.2.0
**2024年11月12号 至 2024年 11月26号 商业版代码更新内容** 560、重定向路径调整,区分/data/data/com.xxx以及/data/user/0/com.xxx,使其更符合APP使用实际使用的路径
559、适配fixupAppDir
558、reverseRedirectedPath增加检查,防止路径多次重复转换
557、修复splitNames信息缺失导致部分应用无法正常使用
556、补充一批normal权限,解决部分APP因为权限丢失无法正常使用的问题
**2024年10月31号 至 2024年 11月11号 商业版代码更新内容** 555、新功能:增加Seccomp-Bpf支持,实现更底层的拦截
554、增加对部分加固APP的支持
553、系统OTA升级时对Split Apks重新安装
**2024年10月14号 至 2024年 10月29号 商业版代码更新内容** 552、所有手机package.ini版本升级到7,避免某些情况下出现应用丢失
551、修改Demo包名
**2024年9月15号 至 2024年 10月13号 商业版代码更新内容** 550、移除对Sandhook的依赖
549、移除几处hook,避免部分机型crash
548、移除未使用到的goAsync,避免ANR
547、移除部分对1.X的升级处理废弃代码
546、修复手机系统升级到13.0+上后,应用列表消失的问题
**2024年8月23号 至 2024年 9月14号 商业版代码更新内容** 545、修复IO重定向中一系列函数未对dfd正确处理
**2024年8月9号 至 2024年 8月22号 商业版代码更新内容** 544、修复Native的一个空指针问题
543、修复Native的某个函数由于栈上随机数导致判断出错
542、修复unity中的检测问题
541、修复publishService crash问题
540、修复getPid死循环崩溃问题
**2024年8月3号 至 2024年 8月8号 商业版代码更新内容** 539、修复微信等APP因为webview导致的crash问题
**2024年7月19号 至 2024年 8月2号 商业版代码更新内容** 538、修复sandhook崩溃问题
537、修复sandhook hook不起效问题
536、替换sandhook中inline hook部分
535、修复android.permission.DETECT_SCREEN_CAPTURE权限导致的crash问题
534、修复静态广播导致的crash问题
533、修复百度搜索crash的问题
532、修复ResolveActivity跳转到外部应用时没有过滤export为false的场景导致crash
531、修复在某些华为设备上微信白屏的问题
530、修复微信流量异常的问题
529、class_linker适配android 15
528、修复 readlinkat参数问题导致的crash
527、解决某些unity路径检测的问题
**2024年7月3号 至 2024年 7月18号 商业版代码更新内容** 526、适配了几十个API,很大程度提升了稳定性
525、调整stopService不再走initProcess流程,解决了某些情况下的死锁问题
524、修复 startprocess启动App后再次进入VActivityManagerService导致死锁的问题
523、修复锁屏/亮屏广播引起的crash问题
**2024年6月17号 至 2024年 7月2号 商业版代码更新内容** 522、AttributionSoure中的uid调整
521、修复微信注册,找回账号等几个界面白屏的问题
**2024年6月4号 至 2024年 6月16号 商业版代码更新内容** 520、将内置的Java hook框架SandHook调整为可选配置
519、修复VA_ENABLE_EXT_FULL_SOURCE_CODE功能选项开启时,加载so错误的问题
**2024年5月8号 至 2024年 6月3号 商业版代码更新内容** 518、修复微信在鸿蒙4.0+上无法使用的问题
517、调整VA demo package name
**2024年4月20号 至 2024年 5月7号 商业版代码更新内容** 516、适配华为账户登录和授权登录等功能
515、适配荣耀账户登录和授权登录等功能
514、修复Service中getApplicationContext返回Null的问题
**2024年4月4号 至 2024年 4月19号 商业版代码更新内容** 513、修复link&unlink参数没有处理重定向的问题
512、修复AutoFillManagerStub未生效问题
511、适配高版本ShadowJobService
**2024年3月7号 至 2024年 4月2号 商业版代码更新内容** 510、修复数款游因为戏二次注册provider导致无法打开
**2024年2月19号 至 2024年 3月6号 商业版代码更新内容** 509、修通知跳转Crash
508、AMS API适配
507、DevicePolicyManager API适配
506、BlueTooth API适配
505、修复抖音crash问题
**2024年1月25号 至 2024年 2月18号 商业版代码更新内容** 504、修复抖音在部分手机无法打开的问题
503、修复抖音在部分手机运行一小段时间后崩溃的问题
502、修复抖音在部分手机crash后一直打不开的问题
501、修复抖音极速版在部分手机无法打开的问题
500、修复抖音极速版在部分手机运行一小段时间后崩溃的问题
499、修复抖音极速版在部分手机crash后一直打不开的问题
498、UserManager相关API适配
497、PackageManager相关API适配
496、Notification相关API适配
495、FingerprintManager相关API适配
**2024年1月5号 至 2024年 1月24号 商业版代码更新内容** 494、Activity Token获取适配
493、适配最新版微信
**2023年12月21号 至 2024年 1月4号 商业版代码更新内容** 492、适配libc可能没有R权限的情况
**2023年12月5号 至 2023年 12月20号 商业版代码更新内容** 491、修复储存空间异常的问题
**2023年10月24号 至 2023年 12月4号 商业版代码更新内容** 490、取消对Xposed依赖
489、适配最新版微信
488、适配setCompatibilityVersion
487、取消hookGetCallingUid对xposed的依赖
486、蓝牙适配
485、AddToDisplayAsUser处理
478、PendingIntent适配
484、MediaRecorder适配
483、处理dispatchVolumeKeyEvent API
482、修复AttributionSource cast crash
481、增加配置:是否优先使用外部app
480、修复启动前台service crash
479、修复14.0上renameat太短导致hook后覆盖其他函数的问题
**2023年10月8号 至 2023年 10月23号 商业版代码更新内容** 478、修复Annotation依赖包为空的问题
477、修复抖音等APP由于动态框架导致无法打开Activity的问题
476、修复纯Java APP在64位下以32位模式安装的问题
475、修复了13.0+上的class linker偏移检测问题
474、调整默认使用isUseRealDataDir模拟真实路径
473、JobServiceStub适配
472、IO重定向增加对renameat2的hook
471、修复APK安装模式下某些APP拍照黑屏
470、修复APK安装模式下微信小程序无法使用的问题
**2023年9月16号 至 2023年 10月7号 商业版代码更新内容** 469、移除已经废弃的虚拟定位代码
468、修复WhatsApp 来电通知bug
467、修复GMS相关问题
466、修复WhatsApp无法跳过短息验证界面问题
465、修复WhatsApp等部分APP启动后界面白屏问题
464、适配Alarms 相关API **2023年9月6号 至 2023年 9月15号 商业版代码更新内容** 463、14.0上JobScheduler API适配
462、修复从sdcard上安装时signature可能获取的可能不是最旧的问题
461、LocaleManager适配
**2023年8月16号 至 2023年 9月5号 商业版代码更新内容** 460、14.0上JobScheduler适配
459、修复API broadcastIntentWithFeature
458、修复WhatsApp验证跳转的问题
457、内部Provider访问适配
**2023年8月2号 至 2023年 8月15号 商业版代码更新内容** 456、修复Twitter白屏的问题
455、修复ContentProvider在12.0+上的适配问题
454、修复微信在nova9z上崩溃的问题
453、修复微信等APP发送定位时黑屏的问题
452、编译SDK版本升级到14.0
**2023年7月13号 至 2023年 8月1号 商业版代码更新内容** 451、适配12.0+上蓝牙相关的10来个API
450、适配UserManager相关的10来个API
**2023年6月30号 至 2023年 7月12号 商业版代码更新内容** 449、修复JobService unbind崩溃问题
448、修复JobService persisted崩溃问题
**2023年5月26号 至 2023年 6月29号 商业版代码更新内容** 447、修复部分APP无法录音的问题
446、修复从Sdcard安装APK失败的问题
445、更改VA Demo包名
**2023年4月28号 至 2023年 5月25号 商业版代码更新内容** 444、适配Android 14.0
**2023年3月18号 至 2023年 4月27号 商业版代码更新内容** 443、修复GMS支持,修复各种crash,权限等问题
442、修复GooglePlay无法打开的问题
441、修复GooglePlay无法登录Google账号的问题
440、修复Youtube,WhatsApp等APP无法登录Google账号的问题
439、修复Facebook无法打开的问题
**2023年2月17号 至 2023年 3月17号 商业版代码更新内容** 438、修setPictureInPictureParams crash
**2023年1月27号 至 2023年 2月16号 商业版代码更新内容** 437、修复mOpPackageName空指针
436、修复13.0上PackageManager几个flags参数导致的crash
435、修复VAPP返回主页的Intent crash
434、TelecomManagerStub API适配
**2022年12月9号 至 2023年 1月26号 商业版代码更新内容** 433、修复PendingIntent flag处理问题
**2022年11月9号 至 2022年 12月8号 商业版代码更新内容** 432、修复Facebook在某些情况下无法启动的问题
431、启动外部App时,排除对VA自身的判断
430、修复queryIntentServices过滤规则
**2022年10月9号 至 2022年 11月8号 商业版代码更新内容** 429、修复当VA_AUTHORITY_PREFIX不等于包名时找不到Provider的问题:"Failed to find provider info ..."
428、getPermissionActivityIntent处理
427、修复特殊情况下,检查权限无限弹窗
426、强调Intent使用外部通讯录(如果被第三方接管,建议使用外部可见)
425、新增几个Java API适配
424、修复修复部分后台Activity跳转问题
423、修复在10.0+上后台Activity无法启动的问题
**2022年 8月20号 至 2022年 10月8号 商业版代码更新内容** 422、新功能:增加启动插件Activity代理,绕过后台5s限制
421、修复Provider在10.0+上crash的问题
420、适配最新版微信
419、适配克隆时不显示sdcard上的APK
418、适配12.0+上PendingIntent Flags必须为FLAG_UPDATE_CURRENT/FLAG_IMMUTABLE
417、修复MediaProvider因为ACCESS_MEDIA_LOCATION 权限检查导致的crash
416、修复12.0+上debug模式下hook失败的问题
415、适配在Multi User账户下crash的问题
414、适配由于后台限制导致VA Core启动插件中Activity失败的问题
**2022年 7月27号 至 2022年 8月19号 商业版代码更新内容** 413、Android 13.0继续适配
412、主版调整为64bit
411、修复某些华为手机上App无法打开的问题
410、修复OPPO 13.0上无法打开应用的问题
409、修复百度语音TTS的调用问题
408、修复数据隔离后仍可以访问sdcard根目录的问题
407、修复鸿蒙手机上的崩溃问题
406、修复Debug模式下Hook失效问题
405、添加对BinderInvocationStub的混淆处理,避免混淆后崩溃问题
404、修复Native层调用free函数可能导致崩溃的问题
403、修复微信由于虚拟文件系统导致的崩溃问题
**2022年 7月9号 至 2022年 7月26号 商业版代码更新内容** 402、Android 13.0适配
401、修复开启虚拟储存后文件路径的处理问题
400、修复12.0上Notification没有提示的问题
**2022年 4月28号 至 2022年 5月31号 商业版代码更新内容** 399、修复onGetCallingUid hook引起的崩溃问题
398、修复微信8.0.22启动崩溃的问题
**2022年 4月5号 至 2022年 4月27号 商业版代码更新内容** 397、去掉sandhook中一些多余的hook代码,避免某些APP无法启动
**2022年 3月13号 至 2022年 4月5号 商业版代码更新内容** 396、新增功能:在VA中实现内置media provider,以支持媒体库隔离等需求
395、修复微信/QQ使用语音时崩溃
394、蓝牙崩溃问题适配
393、增加部分Log
392、删除一些无用代码
**2022年 1月22号 至 2022年 3月12号 商业版代码更新内容** 391、修复华为手机上StorageManager没有被hook的问题
390、修复最新版微信无法从SD卡启动的问题
389、PackageInfo中增加对requestedPermissionsFlags字段的处理
388、新增VSettingsProvider,避免内置应用没有权限操作settings provider导致异常
387、修复微信等APP启动黑屏,ANR等问题
386、新增对MediaProvider的hook
385、新增对插件shareUserId的处理,从而可以配置将插件的数据放到主包中
384、新增可以配置是否将Tinker禁用
383、修复Android 12权限处理适配
**2021年 12月30号 至 2022年 1月21号 商业版代码更新内容** 382、Sandhook适配12.0
381、修复Sandhook在部分11.0上不生效的问题
380、增加编译选项VA_FULL_EXT控制是否将VA源码编译到插件,满足加固场景。
**2021年 11月24号 至 2021年 12月29号 商业版代码更新内容** 379、Android 12.0第一轮适配已完成
378、Demo App在11.0上增加文件权限检测
377、修复静态广播接收者在独立进程无法接收广播的问题
376、修复微信第一次登录可能crash问题
375、修复部分APP无法显示头像问题
374、修复在部分OPPO手机上打不开问题
**2021年 9月21号 至 2021年 11月23号 商业版代码更新内容** 373、修复WhatsApp在360手机上黑屏问题
372、增加VA内外广播通信测试demo
371、修复抖音极速版兼容性问题
370、修复readlinkat返回值精度
369、修复从外部安装app,没有引用org.apache.http.legacy的问题
368、修复华为Nova 5i, 64位主包兼容性
367、修复11.0上外部存储重定向问题
366、修复11.0上GMS登录问题
365、修复11.0 部分APP读写sdcard报错的问题
364、修复va core进程死亡后,APP可能打不开的问题
363、增加未安装插件时无法启动的错误日志
**2021年 8月22号 至 2021年 9月20号 商业版代码更新内容** 362、横屏重新适配
361、修复部分APP通过file协议安装后无法打开的问题
360、修复传递给JobIntentService中Intent数据丢失问题
359、修复JobIntentService第二次调用无法工作的问题
358、修复华为手机上某些APP奔溃的问题
357、修复小米手机上游戏登录问题
356、修复某些应用加固后无法打开的问题
355、增加对关联启动权限检测
354、targetSdk 30适配
353、修复targetSdk为30时,某些应用无法上网的问题
352、修复targetSdk为30时,sdcard无法访问的问题
351、编译脚本中使用cmake替换gradle task
350、移除过时文档
**2021年 8月7号 至 2021年 8月21号 商业版代码更新内容** 349、调整优化gradle脚本
348、hidedenApiBypass支持Android R+
347、targetSdk 30 支持
346、修复VIVO系统服务bug
345、修复VIVO手机无法使用摄像头的bug
344、修复dex加载异常状态的获取
343、修复Android R上libart.so路径问题
342、修复Andoid Q+ 删除通知的bug
341、修复APN uri的权限检查
340、修复Android R暂停恢复线程状态
339、修复debug模式下部分hook失效情况
338、修复hook在R之后的一些bug
**2021年 4月25号 至 2021年 8月6号 商业版代码更新内容** 337、修复探探部分手机不能上传头像问题
336、修复Android 10 华为设备IO重定向问题
335、调整横竖屏逻辑,减少异常情况发生
334、添加Activity生命周期的回调接口
333、修复Android 12的广播问题
332、修复微信部分界面状态异常的BUG
331、修复Outlook、One drive、Teams、Zoom等海外app的支持
330、修复Android 11 一个权限请求BUG
329、修复部分cocos2d引擎只显示半屏的问题
328、修复微信在多用户下不能发送文件的问题
327、split apk 支持
326、Android S 支持
**2021年 2月24号 至 2021年 4月24号 商业版代码更新内容** 325、适配多用户环境
324、修复新版微信的兼容问题
323、兼容更多企业级加固
322、支持VAPP设置电源优化
321、修复缺失权限声明
320、修复Android 11上android.test.base库的引用
319、优化ext插件判断
318、优化安装时ABI的选择
317、修复Google文档在Android 11上崩溃的问题
**2020年 10月15号 至 2021年 2月23号 商业版代码更新内容** 316、解决新版爱加密、邦邦等加固的兼容性
315、修复WhatsApp不显示冷启动Splash的问题
314、优化对系统app的识别
313、完善多用户环境下的支持
312、解决ext插件部分情况下卡死的问题
311、支持Google Play在容器中下载APP
310、修复Android 11 QQ无法显示图片的问题
309、兼容Android 11运行Google Service
308、解决Android 11无法运行chromium
307、支持Hook @CriticalNative Method
306、修复JDK 13无法编译运行的问题
305、修复Service部分情况可能crash的问题
304、修复Android 11无法加载外部存储私有数据的问题
303、修复低版本app无法使用org.apache.http.legacy的问题
302、修复某些情况系统任务栈只显示最后一个的问题
301、完善不同平台的构建脚本
300、修复Android 11无法读取obb的问题
299、解决软件无法向后兼容的问题
298、重构VApp安装框架
297、重构virtual文件系统
296、修复某些情况下WebView无法启动的问题
295、修复VApp卸载重装的BUG
294、修复LOL手游的登录异常问题
293、支持安装Splits APK
292、支持动态配置主包环境
291、修复32位QQ调用64位微信卡顿的问题
290、修复Messenger调用Facebook崩溃的问题
289、优化对Google服务框架的支持
288、实现新的扩展包同步机制
287、修复Android 11正式版的异常问题
286、添加系统Package缓存,优化性能
285、修复disabled组件还能被PMS查询的BUG
284、修复微信部分界面Launch行为异常的问题
283、修复ContentProvider.getCallingPackage返回Host包名的BUG
282、修复uid虚拟化的BUG,解决部分app权限检查失败的问题
281、重写PendingIntent, IntentSender的实现
280、优化进程管理,修复长期存在的概率性进程死锁问题
279、重写Service实现,Service生命周期更准确,不容易被杀死
**2020年 9月13号 至 2020年 10月15号 商业版代码更新内容** 278、修复 64 位 App 无法调用 32 位 App 的问题
277、修复 Android R 加载 HttpClient 的问题
276、修复 Android R debug 模式下的崩溃问题
**2020年 8月23号 至 2020年 9月12号 商业版代码更新内容** 275、添加缺失的 service hook
274、修复百度翻译无法启动的问题
273、修复 GP 下载的 split app 无法启动的问题
**2020年 7月10号 至 2020年 8月22号 商业版代码更新内容** 272、修复 Service 创建
271、添加 NotificationService 缺失的 Hook
270、修复 Yotube 崩溃
**2020年 5月19号 至 2020年 7月9号 商业版代码更新内容** 269、初步适配 Android 11 beta1
268、修复小红书多开闪退的问题
267、修复某些 App 多开报“应用签名被篡改”的问题
**2020年 4月24号 至 2020年 5月18号 商业版代码更新内容** 266、修复 sh 调用错误
265、修复 9.0 以上最新版 Facebook 无法登陆的问题
264、帮助企业微信修复启动虚拟存储的情况下无法拍照的问题
263、修复某些情况下 64位 app 打不开 Activity 的问题
**2020年 3月24号 至 2020年 4月23号 商业版代码更新内容** 262、修复 Vivo 设备提示安装游戏 SDK 的问题
261、修复 Android Q 无法加载部分系统 so 的问题
260、修复华为设备微博未响应
259、忽略不必要的权限检查造成的崩溃
258、修复 WPS 分享文件崩溃的问题
257、部分 10.0 设备的闪退问题
**2020年 3月7号 至 2020年 3月23号 商业版代码更新内容** 256、修复微信同时打开两个页面问题
255、修复微信登陆成功但是返回登陆页面的问题
254、修复最新版 QQ 无法下载附件的问题
253、更新 SandHook 版本
252、修复 9.0 以上安装未签名Apk问题
251、修复 10.0 的定位问题
**2020年 1月16号 至 2020年 3月6号 商业版代码更新内容** 250、调整 lib 重定向逻辑
249、修复三星 10.0 系统上的崩溃问题
248、修复 release build 的 hook 异常
247、增加 SandHook 的 proguard 规则
246、修复对部分 App 中 VirtualApk 的兼容问题
245、修复 VA 内部请求安装 apk 失败的问题
**2019年 12月26号 至 2020年 1月15号 商业版代码更新内容** 244、修复 Android Q 遗漏的 hook
243、禁用 Emui10 的 AutoFill
242、增加新 api 结束所有 activity
**2019年 12月15号 至 2019年 12月25号 商业版代码更新内容** 241、修复 Emui10 上企业微信等 App 无法启动的问题
240、修复在 4.x 可能导致的崩溃
239、升级 SandHook 修复对 Thread 类的 Hook
238、修复 Android Q 某些接口导致的权限问题
**2019年 11月20号 至 2019年 12月14号 商业版代码更新内容** 237、修复 Notification 缓存导致的崩溃
236、修复高版本 Notification 的 classloader 问题
**2019年 11月9号 至 2019年 11月19号 商业版代码更新内容** 235、修复 Android 5.x 的 ART Hook
234、修复 ART Hook 可能导致的死锁问题
**2019年 11月2号 至 2019年 11月8号 商业版代码更新内容** 233、修复 WPS, 网易邮箱等在 Q 设备上崩溃的问题
232、修复汤姆猫跑酷在部分 Q 设备上崩溃的问题
231、修复 QQ 在部分 Q 设备上崩溃的问题
**2019年 10月25号 至 2019年 11月1号 商业版代码更新内容** 230、修复克隆 Google Play 下载的 64位 App
229、修复企业微信
228、修复 Telegram
**2019年 10月8号 至 2019年 10月24号 商业版代码更新内容** 227、修复 Android P 下 AppOspManager 的异常
226、添加 Android P 下 ActivityTaskManager 丢失的 Hook
225、修复 Android P 下 Activity Top Resume 异常
224、支持在系统多用户模式下运行!
**2019年 10月8号 商业版代码更新内容** 223、修复Android P 以上内部 app 返回桌面异常的问题
222、64位分支支持 Android Q
**2019年 9月20号 至 2019年 10月7号 商业版代码更新内容** 221、修复安装在扩展插件中的 apk 无法正确显示图标和名称的问题
220、修复 twitter 无法打开的问题
219、正式兼容 Android Q 正式版!
218、修复 Android Q 某些 Activity 无法再次打开的问题
217、初步适配 Android Q 正式版
216、修复数个64位分支的 Bug
215、新增加支持32位插件的64位分支,该分支支持32位旧设备并且64位设备在32位插件的情况下可以支持32位旧应用
**2017年 12月 至 2019年 7月 30 日 商业版代码更新内容** 214、改进 App 层提示信息
213、改进部分编码
212、修复从宿主向插件发送广播的方法
211、兼容最新版 gradle 插件
210、增加广播命名空间以避免多个使用 VA 技术的 App 互相干扰
209、修复 IMO 打不开的问题
208、修复部分 ContentProvider 找不到的问题
207、支持纯32位模式,以兼容老设备
206、初步支持纯64位模式,以应对8月份的谷歌市场的策略变化
205、适配到 Android Q beta4
204、修复了货拉拉无法安装的问题
203、优化了64位apk的判定逻辑
202、修复配置网络证书的 App 的联网
201、重构组件状态管理
200、优化 MIUI/EMUI ContentProvider 兼容性
199、修复 StorageStats Hook
198、修复快手无法登陆
197、修复 YY 无法启动,更好的兼容插件化框架
196、修复 Facebook 登陆
195、修复 Google Play 下载的 App 无法找到 so 的问题(皇室战争)
194、修复 split apk 支持
193、修复 Youtube 无法启动
192、修复优酷无法启动的问题
191、修复多开时app间可能存在广播namespace冲突的BUG
190、采用新的策略绕过Android P以后的Hidden Policy API
189、适配Android Q(beta1)
188、修复华为设备部分app无法识别存储的问题
187、修复启动进程可能失败导致app无法运行的问题
186、修复4.4设备部分native符号无法找到的问题
185、修复部分设备WebView包名获取失败的问题
184、修复Service细节处理的问题
183、优化启动速度
182、修复WebView在少数机型加载失败的情况
181、修复Lib决策的问题
180、修复部分华为机型无法读取内存卡的问题
179、修复Service可能存在的问题
178、允许根据intent判断Activity是否在外部启动
177、修复部分机型上Gms和Google Play启动到了不正确的环境
176、修复新实现的StaticBroadcast导致的兼容性问题
175、修复Android P上无法使用apache.http.legacy的问题
174、实现Native trace
173、优化IO Redirect性能
172、修复wechat部分时候出现网络无法连接的问题
171、修复小概率process attach不正确的BUG
170、开始下一阶段的ROADMAP
169、解决Android P无法注册超过1000个广播导致的问题
168、修复可能导致ANR的DeadLock
167、修复部分app动态加载so失败的问题
166、修复免安装运行环境下部分机型第一次打开出现黑屏的问题
165、兼容适配多款主流的Android模拟器
164、优化启动性能
163、解决多个内存泄露问题
162、修复IO Redirect优先级的问题
161、修复8.0以下设备Messenger无网络连接的问题
160、修复双开时外部app卸载时内部app仍然保留的BUG
159、修复部分腾讯加固无法运行的问题
158、修复Instagram无法登录Facebook的BUG
157、修复进程小概率可能重复启动的BUG
156、修复GET_PERMISSIONS没有获取权限的BUG
155、修复startActivityIntentSender的BUG
154、修复vivo设备部分Activity无法启动的问题
153、修复app无法调用外部app选择文件的问题
152、完善Android P的兼容
151、兼容Android P的Google服务
150、解决Messenger部分功能异常
149、完善IO Redirect
148、大量适配Gms, 修复Gms运行过程中进程无限重启的问题
147、重新实现Service的运行机制
146、完善64bit,提供了部分ROM配置64bit Engine权限的API
145、修复了4.4设备上的Activity启动问题
144、支持excludeFromRecent属性
143、修复Instagram无法Facebook登录的问题
142、修复Facebook第一次登录闪退的问题
141、支持以64位模式运行Gms、Google play、Play game
140、支持在双开/免安装运行的Google play中下载和安装app
139、修复DownloadManager的BUG
138、修复Google play返回上层时重启界面的BUG
137、修复免安装模式下so决策问题
136、优化构建脚本,便于引入项目
135、修复移动MM SDK无法启动的问题
134、修复微信摇一摇的BUG
133、修复中兴设备不稳定的BUG
132、支持ARM64下的IO Redirect
131、修复USE_OUTSIDE模式下外部app更新时,内部app没有更新的BUG
130、兼容最新Android 9.0(代号: pie) 及正式版之前发布的四个Preview版本
129、兼容内置houdini的x86设备
128、WindowPreview技术,使app启动与真实app达到一样的速度
127、新的ActivityStack以提高app运行质量
126、解决接入Atlas Framework的app运行异常的问题
125、现在可以定义虚拟app返回桌面的具体行为
124、现在双开模式下app随系统动态更新,不需要手动检查
123、支持targetSdkVersion >= 26时仍可正常运行低版本的app
122、兼容腾讯游戏管家的QDroid虚拟引擎 (beta)
121、大量重构底层代码,大幅提升运行速度
120、修复网易新闻分享到微博后无法取消的问题
119、修复App自定义权限无法识别的问题
118、修复墨迹天气app无法启动的问题
117、修复部分政府app无法启动的问题
116、API的变动详见代码
115、修复三星系列应用的相互调用问题
114、修复小米应用在非小米系统的账号问题
113、修复分享/发送等第三方调用,返回页面不正常
112、修复应用宝提示不能安装
111、调用第三方app,对uri进行加密
110、适配前刘海
109、适配小米rom的hook
108、适配努比亚录音问题
107、内部悬浮窗权限控制
106、优化自定义通知栏的处理
105、修复Context的INCLUDE_CODE权限问题
104、适配华为,oppo的角标
103、修复百度视频的进程重启问题
102、修复某些snapchat的无法启动问题
101、适配autofill服务,例如piexl系列
100、完善64位的io hook
99、优化hook库的兼容性,加回dlopen
98、64位扩展包的so移到32位主包。(jni代码改动后,在Run之前,请先build一次)
97、通知栏改动:适配8.1的通知渠道;移除应用时,移除应用的全部通知
96、兼容部分app,需要设置android:largeHeap=true
95、修复ffmpeg库的视频无法播放问题
94、优化横竖屏切换
93、降低通过Intent.ACTION_VIEW调用外部Activity限制。
92、兼容MG SDK
91、64位支持还在开发阶段
90、更新混淆配置app/proguard-rules.pro,必须加规则-dontshrink
89、优化模拟机型,例如:模拟后,某些app不出现设备验证
88、提高dex2oat兼容性
87、优化模拟定位
86、移除dlopen
85、targetVersion可以改为26:支持targetVersion<23的app动态权限申请,支持targetVersion<24的文件Uri
84、installPackage改为默认异步形式
83、为了支持64位模式,换回aidl
82、去掉SettingHandler现在可以动态设置特殊规则,规则会存储,不需要重复设置
81、增加2个native_setup
80、提高jobService兼容性
79、ShortcutService相关:关联VASettings.ENABLE_INNER_SHORTCUT
78、为了稳定性和运行效率,去掉上个版本的蓝牙,wifi,不声明权限的适配。
77、增加app启动异常的广播Constants.ACTION_PROCESS_ERROR
76、修复少数游戏横屏判断问题
75、demo增加机型模拟
74、适配vivo一个自定义权限(后台弹窗)VA是把一个历史acitivty返回前台,vivo需要这个权限。
73、如果没有蓝牙权限,返回默认值(海外用)
72、修复uid权限检查问题
71、安全性更新,内部应用的文件权限控制
70、提高内部app调用的兼容性,第三方登录,分享
69、自动过滤没权限的外部ContentProvider
68、增加功能:内部app的权限检查(默认关闭)
67、机型模拟:Build类和build.prop
66、提高对乐固加固的app兼容性
65、适配三星wifimanager
64、修复ipc框架一个参数传递问题(IPCMethod这个类必须更新)
63、补全7.0通知栏的hook
62、修正8.0动态快捷菜单的hook
61、SettingHandler新增一个适配接口,主要适配各种游戏
60、功能改动:google自动安装改为手动安装,避免第一次启动时间过久
59、可以禁止访问外部某个ContentProvider
58、适配华为桌面图标数量
57、权限分类注释,标注可删除权限。
56、增加双开模式的app跟随外部升级的开关。
55、提高app的jni兼容性。
54、提高对app集成其他插件框架的兼容性。
53、增加设置接口,根据包名进行设置。
52、增加Uri的适配范围,支持通过Uri分享和查看文件。
51、修复一个在三星8.0的问题。
50、提高对系统自带的app组件兼容性,更好兼容chrome webview,google service。
49、提高ART稳定性
48、增加相机适配范围
47、支持内部App在8.0下的快捷方式管理
46、修复exec异常
45、提高稳定性(修复微信登录闪退)
44、解决微信数据库崩溃问题
43、修复部分4.4设备崩溃问题
42、修复后台应用易被杀死,土豆视频黑屏,新浪微博无法打开,优酷两次返回无法退出。
41、增加应用的保活机制,双开APP更不易被杀死。
40、优化虚拟引擎启动性能。
39、兼容了大部分的加固,第三方APP兼容性对比上一版提升40%+。
38、修复某些rom下,快捷方式图标不正确
37、兼容以前组件StubFileProvider
36、适配部分新ROM的虚拟IMEI
35、改善进程初始化代码,增加稳定性
34、添加内部发送Intent.ACTION_BOOT_COMPLETED的广播,可以设置开关
33、适配关联google play游戏,支持游戏使用google登录
32、适配android O的google service框架
31、适配android O 快捷方式
30、适配耳机模式
29、某些rom对intent的大小限制,demo添加缩放快捷方式图标代码
28、修复多开情况下一个bug
27、修复某些情况下MediaController的bug
26、修复4.1.2的StubFileProvider报错
25、分享的uri处理
24、修复跨app调用Activity的回调
23、前台服务的通知栏拦截开关
22、附带doc
21、完善VA内部的intent的CHOOSE回调
20、Android O的通知栏适配2
19、ipc框架优化, 提高判断binder的存活准确性
18、jni的log开关 Android.mk:LOCAL_CFLAGS += -DLOG_ENABLE
17、混淆配置
16、Android O的通知栏适配
15、修复部分app网络卡的问题
14、适配 android 8.0的dl_open(jni加载)
13、修复华为emui8.0的一个bug
12、完善定位
11、设置手机信息,imei伪装算法
10、适配8.0某个功能(主要app:whatsapp)
9、修复内部微信等应用,无法更新图片,视频
8、demo增加安装监听,自动升级克隆模式的应用
7、7.0的file provider适配
6、增加了定位代码
5、代码进行了架构优化
4、与开源版不同的特征
3、解决了微信被封的一些问题
2、修复了部分机型兼容性
1、修复了12个小BUG
================================================ FILE: README_eng.md ================================================ [中文文档](README.md "中文")

VA Product description & Development guidance

## What is VA? ## VirtualAPP (abbreviation: VA) is a sandbox product running on Android system, which can be understood as a lightweight "Android virtual machine". Its product form is a highly extensible, customizable, integrated SDK that allows you to develop a variety of seemingly impossible projects based on or using VA. Now, VA is widely used in many technology fields as following: mini game collection, blockchain, cloud control, silent hot fix and so on. On the one hand, you can realize cloud control mobile office security and achieve military and government data isolation with VA. On the other hand, you can implement script automation, device-info-mock, and plug-in development. Meanwhile, you can realize multi space and games booster. You can also rent the mobile game account and use the mobile controller without activation by VA.
**The code on Github has stopped updating in December 2017. The code of business version is continuously being updated. If you need license to obtain the latest code, please contact WeChat: 10890.** ## Terminology in VA ## Terminology | Explanation ---- | --- Host | The APP that integrates the VirtualAPP SDK is called host. Host Plug-in | A host package is used to run another ABI on the same device. It also called plug-in package,extension package, host plug-in package, host extension package. Virtual APP / VAPP | App installed in the VA space External APP | App installed in the device
## VA Technical architecture ## ![](https://cdn.jsdelivr.net/gh/xxxyanchenxxx/temp@1.0/doc/va_architecture.jpg) VA technology involves the APP layer, Framework layer and Native layer of Android in total. App must be installed on the system before it can run. The APP installed inside the VA space is not actually installed into the system, so it cannot run. Then how to get it to run? Answer: The only way to do this is to "cheat" the system into thinking it has been installed. This "cheat" process is the core work of the VA Framework, and is also the core technical principle of the VA. **Here is the description of what did each layer do:** Layer | Main work ---- | --- VA Space | An internal space is provided by the VA for the installation of the APP to be run inside it, and this space is system isolated. VA Framework | This layer is mainly a proxy for Android Framework and VAPP, which is the core of VA. And VA provides a set of VA Framework of its own, which is between Android Framework and VA APP.
1. For VAPP, all the system services it accesses have been proxied by VA Framework, which will modify the request parameters of VAPP and send all the parameters related to VAPP installation information to Android Framework after changing them to the parameters of the host (Some of the requests will be sent to their own VA Server to be processed directly, and no longer send to the Android system). This way Android Framework receives the VAPP request and checks the parameters, and it will think there is no problem.
2. When the Android system finishes processing the request and returns the result, the VA Framework will also intercept the return result and restore all the parameters that have been original modified to those that were sent during the VAPP request. This way the interaction between VAPP and Android system can work. VA Native | The main purpose of this layer is to accomplish 2 tasks: IO redirection and the request modification for VA APP to interact with Android system.
1. IO redirection is some APPs may be accessed through the hard code absolute path. But if the APP is not installed to the system, this path does not exist. Through IO redirection, it will be redirected to the path to install inside VA.
2. In addition, there are some jni functions that cannot be hooked in VA Framework, so they need to be hooked in the native layer.
In summary: As you can see from the above technical architecture, the internal VA APP actually runs on top of VA's own VA Framework. VA has intercepted all system requests from its internal APP, and through this technology it can also have full control over the APP, not just the multi space. And for the convenience of developers, VA also provides SDK and Hook SDK. ## VA Process architecture# ![](https://cdn.jsdelivr.net/gh/xxxyanchenxxx/temp@1.0/doc/va_process.jpg) There are five types of processes in the VA’s runtime: CHILD process, VA Host Main process, VA Host Plugin process, VAPP Client process, and VAServer process. To support both 32-bit and 64-bit APPs, VA needs to install two packages: a master package and a plug-in package ( In this document, the main package is 32 bits and the plug-in package is 64 bit ). Two packages are also necessary because a package can only run in one mode, either 32-bit or 64-bit. So for 32-bit APPs, VA uses the 32-bit main package to run, and for 64-bit APPs, VA uses the 64-bit plug-in package to run. The main package contains all the code of VA, and the plug-in package contains only one piece of code that loads the main package code for execution, no other code. So plug-in package rarely needs to be updated, just the main package. In addition, whether the main package is chosen to use 32-bit or 64-bit can be modified in the configuration file ( For example, for users who want to access GooglePlay, it will be modified to 64-bit for the main package and 32-bit for the plug-in package ). **The functions and explanations of the each type of process are as follows:**
Process Type | Function ---- | --- CHILD | Other processes integrated by VA Host, such as: keepalive process, push process, etc. VA Host Main | The process where the UI main interface of the VA main package is located. The default main package is 32-bit and the plug-in package is 64-bit, which can be modified and switched in the configuration file VA Host Plugin | The process that supports the plug-in package of 64-bit APP. The default main package is 32-bit and the plug-in package is 64-bit, which can be modified and switched in the configuration file. VAPP Client | The process generated by the APP installed into VA after it starts, it will modify io.busniess.va:pxxx process name to the real process name of VAPP when it runs. VAServer | The process where the VA Server is located, it is used to handle requests in VA that are not assigned to the system for processing, such as APP installation processing.
## VA can satisfy almost all your needs ## Through the above technical architecture, we can know that VA can fully control APP and provide Hook SDK, which can satisfy almost all your needs in various fields: 1. Satisfy the need of **dual/multi space** VA allows you to install multiple WeChat/QQ/WhatsAPP/Facebook and other APPs on the same mobile phone, so you can have one phone with multiple accounts logged in at the same time. 2. Satisfy the need of **mobile security** VA provides a set of internal and external isolation mechanisms, including but not limited to (file isolation / component isolation / process communication isolation). Simply speaking, VA internal is a "completely independent space". Through VA, work affairs and personal affairs can be safely separated without mutual interference. With a little customization, you can achieve mobile security-related needs such as application behavior audit, data encryption, data acquisition, data leakage prevention, anti-attack leaks and so on. **2.1 Application behavior audit** The HOOK capability provided by VA can realize real-time monitoring of user usage behavior and upload violation information to the server. And it's easy to implement things like Time Fence ( whether a feature of the APP can be used in a certain time ), Geo Fence ( whether a feature of the APP can be used in a certain area ), sensitive keyword filtering interception and other functional requirements. **2.2 Data encryption** The HOOK capability provided by VA can realize all data/file encryption of the application, ensuring data/file landing security. **2.3 Data acquisition** The HOOK capability provided by VA can realize the demand for real-time silent upload of application data, such as chat records and transfer records, preventing them from being deleted afterwards without traceability. **2.4 Data leakage prevention** The HOOK capability provided by VA can realize application anti-copy/paste, anti-screenshot/recording, anti-sharing/forwarding, watermark traceability and other requirements. **2.5 Anti-attack leaks** With the application control capability provided by VA, privacy-related behaviors such as SMS/ address book/call log/ background recording/background photo/ browsing history and location information can be completely controlled in sandbox, prevent Trojan horses/malicious APPs from acquiring users' real private data, causing serious consequences such as leakage of secrets. 3. Satisfy the need of **ROOT without HOOK** VA provides Hook capability of Java and Native. With VA, you can easily achieve functions required by various scenarios, such as virtual positioning, changing device, APP monitoring and management, mobile security and so on. 4. Satisfy the need of **silent installation** VA provides the ability to silently install, silently upgrade and silently uninstall APPs. For example, the application store or game center can be integrated with VA to avoid the need for users to manually click to confirm the installation operation, so that it can be installed into VA immediately after downloading, bringing users an experience like "small program" , completely avoiding the problem of applications not easily installed by users. 5. Satisfy the need of **APP controlled** You can clearly grasp the system API, sensitive data, device information, etc. accessed by the APP through VA. For example, whether the APP accesses the contacts, photo albums, call log, whether it accesses the user's geographic location and other information. Of course, you can also control or construct custom messages to these APPs via VA, and not only that, you can also get access to the APP's private data, such as chat database and so on. In a word, through the application control capability provided by VA, you can easily control all the behaviors of the APP, even modify the content of the APP and server interaction and so on .
6. Satisfy the need of **overseas markets** VA implements support for Google services to support overseas APPs running, such as Twitter, Messenger, WhatsAPP, Instagram, FaceBook, Youtube and so on. 7. Satisfy the need of **almost everything you can think of** VA has complete oversight and control over the internal APP, and can meet almost any of your needs! 8. VA is also the only commercially licensed product in this technology area **Hundreds of** licensed customers are currently paying to use the business version of VirtualAPP code, and the APP integrated with VirtualAPP code is launched more than 200 million times per day. Many Android engineers provide us with user feedback in different scenarios, and through our technical team's continuous optimization and iteration, we continue to improve product performance and compatibility. VA Specialized capabilities --- - Cloning ability
You can clone the APP already installed in the external system and run it internally without mutual interference. Typical application scenario is double space. - Without installation ability
In addition to cloning already installed, VA can install (externally silent ) apk's directly internally and run them directly internally. Typical application scenarios are plug-in, standalone APP marketplace and so on. - Double space ability
VA is not only "double space", but also has a unique multi-user mode that allows users to open the same APP internally for an unlimited number of times. - Internal and external isolation ability
VA is a standard sandbox, or "virtual machine", that provides a set of internal and external isolation mechanisms, including but not limited to (file isolation/component isolation/process communication isolation). Simply put, the inside of a VA is a "completely separate space". Simply put, the inside of a VA is a "completely separate space". Based on it, you can realize a "virtual phone" on your cell phone with a little customization. Of course, you can also use your imagination to customize it for data encryption, data isolation, privacy protection, and enterprise management applications. - Full control over internal APPs ability
VA has complete monitoring and control over the internal APP, which is absolutely impossible to achieve in an external environment without Root.
Details(Drop down to open) 1. Service request control. First, VA directly provides some service request interception, you can easily customize these service requests when integrating VA, including but far from limited to (APP request to install apk / APP request to open certain files / APP request for location data / APP request for phone information, etc.)

2. System API control. VA virtualizes and implements the entire Android system framework, which is the principle that VA can run apk internally without installation. And you can through modify the virtual framework's implementation to dynamically monitor and analyze the behavior of the app, etc. In addition, you can also mock some system behavior to achieve some needs that are difficult to achieve externally (e.g. game controller).

3. Memory read and write. VA can read and write the memory of internal APP processes without Root.

4. Root without debugging. VA can debug (ptrace) internal APP processes without Root, based on which you can also achieve Root-free process injection.

5. Load arbitrary "plug-in" and "behaviors". The APP process inside VA is derived from the Client side code of the VA framework, so you can insert any "load" and "control" logic into the entry code of the process. These are very simple to implement.

6. Hook. VA has a set of built-in Xposed framework and native hook framework running on all versions of Android (until AndroidQ), based on it, you can easily Hook any Java/Native of any internal APP.

7. File control. VA built in a complete file redirection, which allows easy control of reading and writing of files from internal apps. Based on it, you can realize many functions such as protection and encryption of files can be achieved.

8. Note: The above control capabilities are implemented with code or examples for reference.
VA Other features --- - High performance
Process-level "virtual machine", VA's unique implementation model makes its performance almost the same as that of the native APP, and does not need a long startup of ordinary virtual machines. - Full version support
Support 5.0-17.0, 32-bit/64-bit APP, ARM and X86 processor. And support Android version in the future which will be updated. - Easy Expansion and Integration
The integration of VA is similar to the normal Android library, even if your APP has been online, you can conveniently integrate VA and enjoy the capability brought by VA. - Support Google services
Provide support for Google services in order to support overseas APPs. ## Comparison between VA and other technical solutions ## When doing enterprise-level mobile security, it is often necessary to control the APP, and the following is a comparison of possible technical solutions listed: Technical solution | Principle introduction | Comment | Running performance | Compatibility stability | Project maintenance cost ---- | --- | --- | --- | --- | --- Repackage | Repackage the target APP by decompiling it and adding your own control code | 1. Nowadays, almost all APPs have hardened or tamper-proof protection, and repackaging is already a very difficult task
2.The mobile phone system will also detect whether the APP is repackaged, if it is repackaged, it will directly prompt the user that there is a security risk, and even not allow the installation
3.For each APP, even each version to go deep to reverse analysis, time-consuming and difficult to maintain | Excellent | Poor | High Custom ROM | By customizing the system source code and compiling it to flash to the designated mobile phone | Only for specified internal mobile phones, too limited to be extended | Excellent | Excellent | High ROOT the mobile phone | By rooting the mobile phone,flashing a framework which is similar to Xposed | 1.Now, root the mobile phone is an unlikely thing
2.In reality, it is difficult for users to root their own mobile phones | Excellent | Poor | High VA | Lightweight virtual machine with high speed and low device requirements | No risk point mentioned above | Excellent | Excellent. Hundreds of companies testing feedback at the same time | Low. VA provides API and a professional technical team to ensure the stable operation of the project
As you can see from the above comparison, VA is an excellent product and can reduce your development and maintenance costs. ## Integrating VA Steps ## Step 1: Call the VA interface```VirtualCore.get().startup()```in your application to start the VA engine. Step 2: Call VA interface```VirtualCore.get().installPackageAsUser(userId, packageName)```to install the target APP into VA. Step 3: Call VA interface```VActivityManager.get().launchApp(userId, packageName)```to start the APP. **With only the above 3 APIs to complete the basic use, VA has shielded the complex technical details and provided the interface API to make your development easy.** ## VA compatible stability ## VA has been extensively tested by ** hundreds of **companies, including **high standards of testing and feedback of dozens of listed companies**, covering almost all types of equipment and scenarios at home and abroad, providing full protection for your stable operation! Up to now, the supported system versions: System version | Whether to support ---- | --- 5.0 | support 5.1 | support 6.0 | support 7.0 | support 8.0 | support 9.1 | support 10.0 | support 11.0 | support 12.0 | support 13.0 | support 14.0 | support 15.0 | support 16.0 | support 17.0 | support
Supported App Types: App Type | Whether to support ---- | --- 32-bit APP | support 64-bit APP | support
Supported HOOK Types: Hook Type | Whether to support ---- | --- Java Hook | support Native Hook | support Supported CPU Types: Hook Type | Whether to support ---- | --- ARM 32 | support ARM 64 | support
## How to give feedback on problems encountered with integrated VA ? ## After the purchase of the license we will establish a WeChat group, any problems can always feedback to us, and according to the priority in the first time to deal with. ## VA Development document ## Please refer to the VA development documentation:[Development document](doc/VADev_eng.md) License Instructions ------ VirtualApp virtual machine technology belongs to: Jining Luohe Network Technology Co., LTD. It applied for several VirtualApp intellectual property rights from 2015 to 2026 and` is protected by the Intellectual property Law of the People's Republic of China`.When you need to use the code on Github, **please purchase a business license**,and receive the full source code of the latest VirtualApp business version.Hundreds of licensed customers are paying to use the business version of VirtualApp code, and the app integrated with VirtualApp code is launched more than 200 million times a day. Many Android engineers provided us with user feedback in different scenarios, and through our technical team's continuous optimization and iteration, VirtualApp Business Edition code has better performance and higher compatibility. `The company of that year will become one of them after obtaining the license, and enjoy the technological achievements after the continuous iteration. And we can interact and collaborate with our licensed customers operationally, technically and commercially.`
Person in charge: Mr. Zhang
WeChat:10890

Serious statement ------ If you use VirtualApp for **internal use, business profit or upload it to the application market**without licensing. We will take evidence and then report you to the police (for copyright infringement) or prosecute you. It will cause your company to undertake criminal liability and legal action, and affect your company's goodwill and investment.`Purchasing a business license can save you a lot of time developing, testing and refining compatibility, leaving you more time for innovation and profitability.`Luo He Technology has called to the police and sued a number of individuals and companies in 2020.
**In response to the national call for the protection of intellectual property rights! Anyone who reports that his or her company or other companies are using VirtualApp code to develop products without licensing will be given a cash reward upon verification. We will keep the identity of the whistleblower confidential! Reporting WeChat: 10890.**
Major updates of the business version ------ 1. Support Android 17.0 2. Support Seccomp-Bpf. 3. Not easily misreported by anti-virus software 4. Framework optimization, performance greatly improved 5. Mobile system and APP compatibility greatly improved 6. Run Google services perfectly 7. Supports running pure 64-bit Apps 8. Built-in `XPosed Hook` framework 9. Add positioning mock code 10. Add code to change device 11. Nearly 700 other fixes and improvements
================================================ 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' android { compileSdkVersion 26 buildToolsVersion '26.0.2' defaultConfig { applicationId "io.virtualapp" minSdkVersion 15 targetSdkVersion 22 versionCode 24 versionName "1.2.5" multiDexEnabled true android { defaultConfig { ndk { abiFilters "armeabi", "armeabi-v7a", "x86" } } } } sourceSets { main{ jniLibs.srcDirs = ['libs'] } } buildTypes { release { minifyEnabled false } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } android { 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 { compile fileTree(include: ['*.jar'], dir: 'libs') compile project(':lib') //Android Lib compile 'com.android.support:multidex:1.0.2' compile 'com.android.support:appcompat-v7:25.4.0' compile 'com.melnykov:floatingactionbutton:1.3.0' compile 'com.android.support:recyclerview-v7:25.4.0' compile 'com.android.support:percent:25.4.0' compile 'com.android.support:design:25.4.0' compile 'com.android.support:cardview-v7:25.4.0' //Promise Support compile 'org.jdeferred:jdeferred-android-aar:1.2.4' // ThirdParty compile 'com.jonathanfinerty.once:once:1.0.3' compile 'com.flurry.android:analytics:6.9.2' compile 'com.kyleduo.switchbutton:library:1.4.6' } ================================================ 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.**{*;} ================================================ FILE: VirtualApp/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/VApp.java ================================================ package io.virtualapp; import android.content.Context; import android.content.SharedPreferences; import android.support.multidex.MultiDexApplication; import com.flurry.android.FlurryAgent; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.client.stub.VASettings; import io.virtualapp.delegate.MyAppRequestListener; import io.virtualapp.delegate.MyComponentDelegate; import io.virtualapp.delegate.MyPhoneInfoDelegate; import io.virtualapp.delegate.MyTaskDescriptionDelegate; import jonathanfinerty.once.Once; /** * @author Lody */ public class VApp extends MultiDexApplication { private static VApp gApp; private SharedPreferences mPreferences; public static VApp getApp() { return gApp; } @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); mPreferences = base.getSharedPreferences("va", Context.MODE_MULTI_PROCESS); VASettings.ENABLE_IO_REDIRECT = true; VASettings.ENABLE_INNER_SHORTCUT = false; try { VirtualCore.get().startup(base); } catch (Throwable e) { e.printStackTrace(); } } @Override public void onCreate() { gApp = this; super.onCreate(); VirtualCore virtualCore = VirtualCore.get(); virtualCore.initialize(new VirtualCore.VirtualInitializer() { @Override public void onMainProcess() { Once.initialise(VApp.this); new FlurryAgent.Builder() .withLogEnabled(true) .withListener(() -> { // nothing }) .build(VApp.this, "48RJJP7ZCZZBB6KMMWW5"); } @Override public void onVirtualProcess() { //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 MyTaskDescriptionDelegate()); } @Override public void onServerProcess() { virtualCore.setAppRequestListener(new MyAppRequestListener(VApp.this)); 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"); } }); } public static SharedPreferences getPreferences() { return getApp().mPreferences; } } ================================================ 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/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/Value.java ================================================ package io.virtualapp.abs; /** * @author Lody */ public class Value { public T val; } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/abs/nestedadapter/RecyclerViewAdapterWrapper.java ================================================ package io.virtualapp.abs.nestedadapter; import android.support.v7.widget.RecyclerView; import android.view.ViewGroup; public class RecyclerViewAdapterWrapper extends RecyclerView.Adapter { protected final RecyclerView.Adapter wrapped; public RecyclerViewAdapterWrapper(RecyclerView.Adapter wrapped) { super(); this.wrapped = wrapped; this.wrapped.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { public void onChanged() { notifyDataSetChanged(); } public void onItemRangeChanged(int positionStart, int itemCount) { notifyItemRangeChanged(positionStart, itemCount); } public void onItemRangeInserted(int positionStart, int itemCount) { notifyItemRangeInserted(positionStart, itemCount); } public void onItemRangeRemoved(int positionStart, int itemCount) { notifyItemRangeRemoved(positionStart, itemCount); } public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { notifyItemMoved(fromPosition, toPosition); } }); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return wrapped.onCreateViewHolder(parent, viewType); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { wrapped.onBindViewHolder(holder, position); } @Override public int getItemCount() { return wrapped.getItemCount(); } @Override public int getItemViewType(int position) { return wrapped.getItemViewType(position); } @Override public void setHasStableIds(boolean hasStableIds) { wrapped.setHasStableIds(hasStableIds); } @Override public long getItemId(int position) { return wrapped.getItemId(position); } @Override public void onViewRecycled(RecyclerView.ViewHolder holder) { wrapped.onViewRecycled(holder); } @Override public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) { return wrapped.onFailedToRecycleView(holder); } @Override public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { wrapped.onViewAttachedToWindow(holder); } @Override public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) { wrapped.onViewDetachedFromWindow(holder); } @Override public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { wrapped.registerAdapterDataObserver(observer); } @Override public void unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { wrapped.unregisterAdapterDataObserver(observer); } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { wrapped.onAttachedToRecyclerView(recyclerView); } @Override public void onDetachedFromRecyclerView(RecyclerView recyclerView) { wrapped.onDetachedFromRecyclerView(recyclerView); } public RecyclerView.Adapter getWrappedAdapter() { return wrapped; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/abs/nestedadapter/SmartRecyclerAdapter.java ================================================ package io.virtualapp.abs.nestedadapter; import android.support.annotation.NonNull; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import android.view.View; import android.view.ViewGroup; public class SmartRecyclerAdapter extends RecyclerViewAdapterWrapper { public static final int TYPE_HEADER = -1; public static final int TYPE_FOOTER = -2; private RecyclerView.LayoutManager layoutManager; private View headerView, footerView; public SmartRecyclerAdapter(@NonNull RecyclerView.Adapter targetAdapter) { super(targetAdapter); } public void setHeaderView(View view) { headerView = view; getWrappedAdapter().notifyDataSetChanged(); } public void removeHeaderView() { headerView = null; getWrappedAdapter().notifyDataSetChanged(); } public void setFooterView(View view) { footerView = view; getWrappedAdapter().notifyDataSetChanged(); } public void removeFooterView() { footerView = null; getWrappedAdapter().notifyDataSetChanged(); } private void setGridHeaderFooter(RecyclerView.LayoutManager layoutManager) { if (layoutManager instanceof GridLayoutManager) { final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { boolean isShowHeader = (position == 0 && hasHeader()); boolean isShowFooter = (position == getItemCount() - 1 && hasFooter()); if (isShowFooter || isShowHeader) { return gridLayoutManager.getSpanCount(); } return 1; } }); } } private boolean hasHeader() { return headerView != null; } private boolean hasFooter() { return footerView != null; } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); layoutManager = recyclerView.getLayoutManager(); setGridHeaderFooter(layoutManager); } @Override public int getItemCount() { return super.getItemCount() + (hasHeader() ? 1 : 0) + (hasFooter() ? 1 : 0); } @Override public int getItemViewType(int position) { if (hasHeader() && position == 0) { return TYPE_HEADER; } if (hasFooter() && position == getItemCount() - 1) { return TYPE_FOOTER; } return super.getItemViewType(hasHeader() ? position - 1 : position); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = null; if (viewType == TYPE_HEADER) { itemView = headerView; } else if (viewType == TYPE_FOOTER) { itemView = footerView; } if (itemView != null) { //set StaggeredGridLayoutManager header & footer view if (layoutManager instanceof StaggeredGridLayoutManager) { ViewGroup.LayoutParams targetParams = itemView.getLayoutParams(); StaggeredGridLayoutManager.LayoutParams StaggerLayoutParams; if (targetParams != null) { StaggerLayoutParams = new StaggeredGridLayoutManager.LayoutParams(targetParams.width, targetParams.height); } else { StaggerLayoutParams = new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } StaggerLayoutParams.setFullSpan(true); itemView.setLayoutParams(StaggerLayoutParams); } return new RecyclerView.ViewHolder(itemView) { }; } return super.onCreateViewHolder(parent, viewType); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (getItemViewType(position) == TYPE_HEADER || getItemViewType(position) == TYPE_FOOTER) { //if you need get header & footer state , do here return; } super.onBindViewHolder(holder, hasHeader() ? position - 1 : position); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/abs/percent/PercentLinearLayout.java ================================================ package io.virtualapp.abs.percent; import android.content.Context; import android.content.res.TypedArray; import android.support.percent.PercentLayoutHelper; import android.util.AttributeSet; import android.view.ViewGroup; import android.widget.LinearLayout; /** * @author Lody */ public class PercentLinearLayout extends LinearLayout { private PercentLayoutHelper mPercentLayoutHelper; public PercentLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); mPercentLayoutHelper = new PercentLayoutHelper(this); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mPercentLayoutHelper.adjustChildren(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mPercentLayoutHelper.handleMeasuredStateTooSmall()) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mPercentLayoutHelper.restoreOriginalParams(); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } public static class LayoutParams extends LinearLayout.LayoutParams implements PercentLayoutHelper.PercentLayoutParams { private PercentLayoutHelper.PercentLayoutInfo mPercentLayoutInfo; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); mPercentLayoutInfo = PercentLayoutHelper.getPercentLayoutInfo(c, attrs); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } public LayoutParams(MarginLayoutParams source) { super(source); } @Override public PercentLayoutHelper.PercentLayoutInfo getPercentLayoutInfo() { return mPercentLayoutInfo; } @Override protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) { PercentLayoutHelper.fetchWidthAndHeight(this, a, widthAttr, heightAttr); } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/abs/reflect/ReflectException.java ================================================ package io.virtualapp.abs.reflect; /** * @author Lody */ public class ReflectException extends RuntimeException { private static final long serialVersionUID = 663038727503637969L; public ReflectException(Throwable cause) { super(cause); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/abs/ui/BaseAdapterPlus.java ================================================ package io.virtualapp.abs.ui; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.SpinnerAdapter; import java.util.ArrayList; import java.util.Collection; import java.util.List; public abstract class BaseAdapterPlus extends BaseAdapter implements SpinnerAdapter { protected Context context; private LayoutInflater mLayoutInflater; protected final List mItems = new ArrayList(); public BaseAdapterPlus(Context context) { this.context = context; mLayoutInflater = LayoutInflater.from(context); } public Context getContext() { return context; } public boolean add(T item) { return add(-1, item, false); } public boolean add(int pos, T item, boolean onlyone) { if (item != null) { if (onlyone) { if (exist(item)) { return false; } } if (pos >= 0) { mItems.add(pos, item); } else { mItems.add(item); } return true; } return true; } public T remove(int pos) { return mItems.remove(pos); } public List getItems() { return mItems; } protected VW inflate(int resource, ViewGroup root) { return (VW) mLayoutInflater.inflate(resource, root); } protected VW inflate(int resource, ViewGroup root, boolean attachToRoot) { return (VW) mLayoutInflater.inflate(resource, root, attachToRoot); } public void clear() { mItems.clear(); } public void set(Collection items) { clear(); addAll(items); } public void addAll(Collection items) { if (items != null) { mItems.addAll(items); } } public int findItem(T item) { return mItems.indexOf(item); } public boolean exist(T item) { if (item == null) return false; return mItems.contains(item); } @Override public final int getCount() { return mItems.size(); } public final T getDataItem(int position) { return mItems.get(position); } @Override public final T getItem(int position) { if (position >= 0 && position < getCount()) { return mItems.get(position); } return null; } public final T getItemById(long id) { return getItem((int) id); } @Override public long getItemId(int position) { return position; } @Override public final View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = createView(position, parent); } T t = getItem(position); attach(convertView, t, position); return convertView; } @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = createView(position, parent); } T t = getItem(position); attach(convertView, t, position); return convertView; } protected abstract View createView(int position, ViewGroup parent); protected abstract void attach(View view, T item, int position); public static class BaseViewHolder { protected View view; protected Context context; public BaseViewHolder(View view) { this.view = view; this.context = view.getContext(); } protected T $(int id) { return (T) view.findViewById(id); } } } ================================================ 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 com.flurry.android.FlurryAgent; 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(); FlurryAgent.onStartSession(this); } @Override protected void onStop() { super.onStop(); FlurryAgent.onEndSession(this); } } ================================================ 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/MyAppRequestListener.java ================================================ package io.virtualapp.delegate; import android.content.Context; 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 java.io.IOException; /** * @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) { Toast.makeText(context, "Installing: " + path, Toast.LENGTH_SHORT).show(); InstallResult res = VirtualCore.get().installPackage(path, InstallStrategy.UPDATE_IF_EXIST); if (res.isSuccess) { try { VirtualCore.get().preOpt(res.packageName); } catch (IOException e) { e.printStackTrace(); } if (res.isUpdate) { Toast.makeText(context, "Update: " + res.packageName + " success!", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(context, "Install: " + res.packageName + " success!", Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(context, "Install failed: " + res.error, Toast.LENGTH_SHORT).show(); } } @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/MyTaskDescriptionDelegate.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 MyTaskDescriptionDelegate 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/effects/ExplosionAnimator.java ================================================ package io.virtualapp.effects; import java.util.Random; import android.animation.ValueAnimator; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.view.View; import android.view.animation.AccelerateInterpolator; import android.view.animation.Interpolator; import io.virtualapp.VApp; import io.virtualapp.abs.ui.VUiKit; public class ExplosionAnimator extends ValueAnimator { private static final Interpolator DEFAULT_INTERPOLATOR = new AccelerateInterpolator(0.6f); private static final float END_VALUE = 1.4f; private static final float X = VUiKit.dpToPx(VApp.getApp(), 5); private static final float Y = VUiKit.dpToPx(VApp.getApp(), 20); private static final float V = VUiKit.dpToPx(VApp.getApp(), 2); private static final float W = VUiKit.dpToPx(VApp.getApp(), 1); static long DEFAULT_DURATION = 0x450; private Paint mPaint; private Particle[] mParticles; private Rect mBound; private View mContainer; public ExplosionAnimator(View container, Bitmap bitmap, Rect bound) { mPaint = new Paint(); mBound = new Rect(bound); int partLen = 15; mParticles = new Particle[partLen * partLen]; Random random = new Random(System.currentTimeMillis()); int w = bitmap.getWidth() / (partLen + 2); int h = bitmap.getHeight() / (partLen + 2); for (int i = 0; i < partLen; i++) { for (int j = 0; j < partLen; j++) { mParticles[(i * partLen) + j] = generateParticle(bitmap.getPixel((j + 1) * w, (i + 1) * h), random); } } mContainer = container; setFloatValues(0f, END_VALUE); setInterpolator(DEFAULT_INTERPOLATOR); setDuration(DEFAULT_DURATION); } private Particle generateParticle(int color, Random random) { Particle particle = new Particle(); particle.color = color; particle.radius = V; if (random.nextFloat() < 0.2f) { particle.baseRadius = V + ((X - V) * random.nextFloat()); } else { particle.baseRadius = W + ((V - W) * random.nextFloat()); } float nextFloat = random.nextFloat(); particle.top = mBound.height() * ((0.18f * random.nextFloat()) + 0.2f); particle.top = nextFloat < 0.2f ? particle.top : particle.top + ((particle.top * 0.2f) * random.nextFloat()); particle.bottom = (mBound.height() * (random.nextFloat() - 0.5f)) * 1.8f; float f = nextFloat < 0.2f ? particle.bottom : nextFloat < 0.8f ? particle.bottom * 0.6f : particle.bottom * 0.3f; particle.bottom = f; particle.mag = 4.0f * particle.top / particle.bottom; particle.neg = (-particle.mag) / particle.bottom; f = mBound.centerX() + (Y * (random.nextFloat() - 0.5f)); particle.baseCx = f; particle.cx = f; f = mBound.centerY() + (Y * (random.nextFloat() - 0.5f)); particle.baseCy = f; particle.cy = f; particle.life = END_VALUE / 10 * random.nextFloat(); particle.overflow = 0.4f * random.nextFloat(); particle.alpha = 1f; return particle; } public boolean draw(Canvas canvas) { if (!isStarted()) { return false; } for (Particle particle : mParticles) { particle.advance((float) getAnimatedValue()); if (particle.alpha > 0f) { mPaint.setColor(particle.color); mPaint.setAlpha((int) (Color.alpha(particle.color) * particle.alpha)); canvas.drawCircle(particle.cx, particle.cy, particle.radius, mPaint); } } mContainer.invalidate(); return true; } @Override public void start() { super.start(); mContainer.invalidate(mBound); } private class Particle { float alpha; int color; float cx; float cy; float radius; float baseCx; float baseCy; float baseRadius; float top; float bottom; float mag; float neg; float life; float overflow; public void advance(float factor) { float f = 0f; float normalization = factor / END_VALUE; if (normalization < life || normalization > 1f - overflow) { alpha = 0f; return; } normalization = (normalization - life) / (1f - life - overflow); float f2 = normalization * END_VALUE; if (normalization >= 0.7f) { f = (normalization - 0.7f) / 0.3f; } alpha = 1f - f; f = bottom * f2; cx = baseCx + f; cy = (float) (baseCy - this.neg * Math.pow(f, 2.0)) - f * mag; radius = V + (baseRadius - V) * f2; } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/effects/ExplosionField.java ================================================ package io.virtualapp.effects; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.ImageView; import io.virtualapp.VApp; import io.virtualapp.abs.ui.VUiKit; public class ExplosionField extends View { private static final Canvas sCanvas = new Canvas(); private List mExplosions = new ArrayList<>(); private int[] mExpandInset = new int[2]; public ExplosionField(Context context) { super(context); init(); } public ExplosionField(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ExplosionField(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } public static Bitmap createBitmapFromView(View view) { if (view instanceof ImageView) { Drawable drawable = ((ImageView) view).getDrawable(); if (drawable != null && drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } } view.clearFocus(); Bitmap bitmap = createBitmapSafely(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888, 1); if (bitmap != null) { synchronized (sCanvas) { Canvas canvas = sCanvas; canvas.setBitmap(bitmap); view.draw(canvas); canvas.setBitmap(null); } } return bitmap; } public static Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int retryCount) { try { return Bitmap.createBitmap(width, height, config); } catch (OutOfMemoryError e) { e.printStackTrace(); if (retryCount > 0) { System.gc(); return createBitmapSafely(width, height, config, retryCount - 1); } return null; } } public static ExplosionField attachToWindow(Activity activity) { ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT); ExplosionField explosionField = new ExplosionField(activity); rootView.addView(explosionField, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return explosionField; } public static ExplosionField attachToWindow(ViewGroup rootView, Activity activity) { ExplosionField explosionField = new ExplosionField(activity); rootView.addView(explosionField, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return explosionField; } private void init() { Arrays.fill(mExpandInset, VUiKit.dpToPx(VApp.getApp(), 32)); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (ExplosionAnimator explosion : mExplosions) { explosion.draw(canvas); } } public void expandExplosionBound(int dx, int dy) { mExpandInset[0] = dx; mExpandInset[1] = dy; } public void explode(Bitmap bitmap, Rect bound, long startDelay, long duration) { final ExplosionAnimator explosion = new ExplosionAnimator(this, bitmap, bound); explosion.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mExplosions.remove(animation); } }); explosion.setStartDelay(startDelay); explosion.setDuration(duration); mExplosions.add(explosion); explosion.start(); } public void explode(final View view) { explode(view, null); } public void explode(final View view, OnExplodeFinishListener listener) { Rect r = new Rect(); view.getGlobalVisibleRect(r); int[] location = new int[2]; getLocationOnScreen(location); r.offset(-location[0], -location[1]); r.inset(-mExpandInset[0], -mExpandInset[1]); int startDelay = 100; ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(150); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { Random random = new Random(); @Override public void onAnimationUpdate(ValueAnimator animation) { view.setTranslationX((random.nextFloat() - 0.5f) * view.getWidth() * 0.05f); view.setTranslationY((random.nextFloat() - 0.5f) * view.getHeight() * 0.05f); } }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (listener != null) { listener.onExplodeFinish(view); } } }); animator.start(); view.animate().setDuration(150).setStartDelay(startDelay).scaleX(0f).scaleY(0f).alpha(0f).start(); explode(createBitmapFromView(view), r, startDelay, ExplosionAnimator.DEFAULT_DURATION); } public void clear() { mExplosions.clear(); invalidate(); } public interface OnExplodeFinishListener { void onExplodeFinish(View v); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/FlurryROMCollector.java ================================================ package io.virtualapp.home; import android.hardware.Camera; import android.os.Build; import android.util.Log; import com.flurry.android.FlurryAgent; import com.flurry.android.FlurryEventRecordStatus; import com.lody.virtual.client.natives.NativeMethods; import com.lody.virtual.helper.utils.Reflect; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; /** * @author Lody */ public class FlurryROMCollector { private static final String TAG = FlurryROMCollector.class.getSimpleName(); public static void startCollect() { Log.d(TAG, "start collect..."); NativeMethods.init(); if (NativeMethods.gCameraNativeSetup == null) { reportCameraNativeSetup(); } Log.d(TAG, "end collect..."); } private static void reportCameraNativeSetup() { for (Method method : Camera.class.getDeclaredMethods()) { if ("native_setup".equals(method.getName())) { FlurryEventRecordStatus status = FlurryAgent.logEvent("camera::native_setup", createLogContent("method_details", Reflect.getMethodDetails(method))); Log.d(TAG, "report CNS: " + status); break; } } } private static Map createLogContent(String tag, String value) { Map content = new HashMap<>(3); addRomInfo(content); content.put(tag, value); return content; } private static void addRomInfo(Map content) { content.put("device", Build.DEVICE); content.put("brand", Build.BRAND); content.put("manufacturer", Build.MANUFACTURER); content.put("display", Build.DISPLAY); content.put("model", Build.MODEL); content.put("protect", Build.PRODUCT); content.put("sdk_version", "API-" + Build.VERSION.SDK_INT); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/HomeActivity.java ================================================ package io.virtualapp.home; import android.animation.Animator; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.graphics.Canvas; import android.graphics.Color; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; import android.support.v7.widget.OrientationHelper; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import android.support.v7.widget.helper.ItemTouchHelper; import android.view.ContextThemeWrapper; import android.view.Menu; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import com.lody.virtual.GmsSupport; import com.lody.virtual.client.stub.ChooseTypeAndAccountActivity; import com.lody.virtual.os.VUserInfo; import com.lody.virtual.os.VUserManager; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import io.virtualapp.R; import io.virtualapp.VCommends; import io.virtualapp.abs.nestedadapter.SmartRecyclerAdapter; import io.virtualapp.abs.ui.VActivity; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.adapters.LaunchpadAdapter; import io.virtualapp.home.adapters.decorations.ItemOffsetDecoration; import io.virtualapp.home.location.VirtualLocationSettings; import io.virtualapp.home.models.AddAppButton; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.AppInfoLite; import io.virtualapp.home.models.EmptyAppData; import io.virtualapp.home.models.MultiplePackageAppData; import io.virtualapp.home.models.PackageAppData; import io.virtualapp.widgets.TwoGearsView; import static android.support.v7.widget.helper.ItemTouchHelper.ACTION_STATE_DRAG; import static android.support.v7.widget.helper.ItemTouchHelper.DOWN; import static android.support.v7.widget.helper.ItemTouchHelper.END; import static android.support.v7.widget.helper.ItemTouchHelper.LEFT; import static android.support.v7.widget.helper.ItemTouchHelper.RIGHT; import static android.support.v7.widget.helper.ItemTouchHelper.START; import static android.support.v7.widget.helper.ItemTouchHelper.UP; /** * @author Lody */ public class HomeActivity extends VActivity implements HomeContract.HomeView { private static final String TAG = HomeActivity.class.getSimpleName(); private HomeContract.HomePresenter mPresenter; private TwoGearsView mLoadingView; private RecyclerView mLauncherView; private View mMenuView; private PopupMenu mPopupMenu; private View mBottomArea; private View mCreateShortcutBox; private TextView mCreateShortcutTextView; private View mDeleteAppBox; private TextView mDeleteAppTextView; private LaunchpadAdapter mLaunchpadAdapter; private Handler mUiHandler; public static void goHome(Context context) { Intent intent = new Intent(context, HomeActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { overridePendingTransition(0, 0); super.onCreate(savedInstanceState); setContentView(R.layout.activity_home); mUiHandler = new Handler(Looper.getMainLooper()); bindViews(); initLaunchpad(); initMenu(); new HomePresenterImpl(this).start(); } private void initMenu() { mPopupMenu = new PopupMenu(new ContextThemeWrapper(this, R.style.Theme_AppCompat_Light), mMenuView); Menu menu = mPopupMenu.getMenu(); setIconEnable(menu, true); menu.add("Accounts").setIcon(R.drawable.ic_account).setOnMenuItemClickListener(item -> { List users = VUserManager.get().getUsers(); List names = new ArrayList<>(users.size()); for (VUserInfo info : users) { names.add(info.name); } CharSequence[] items = new CharSequence[names.size()]; for (int i = 0; i < names.size(); i++) { items[i] = names.get(i); } new AlertDialog.Builder(this) .setTitle("Please select an user") .setItems(items, (dialog, which) -> { VUserInfo info = users.get(which); Intent intent = new Intent(this, ChooseTypeAndAccountActivity.class); intent.putExtra(ChooseTypeAndAccountActivity.KEY_USER_ID, info.id); startActivity(intent); }).show(); return false; }); menu.add("Virtual Storage").setIcon(R.drawable.ic_vs).setOnMenuItemClickListener(item -> { Toast.makeText(this, "The coming", Toast.LENGTH_SHORT).show(); return false; }); menu.add("Notification").setIcon(R.drawable.ic_notification).setOnMenuItemClickListener(item -> { Toast.makeText(this, "The coming", Toast.LENGTH_SHORT).show(); return false; }); menu.add("Virtual Location").setIcon(R.drawable.ic_notification).setOnMenuItemClickListener(item -> { startActivity(new Intent(this, VirtualLocationSettings.class)); return true; }); menu.add("Settings").setIcon(R.drawable.ic_settings).setOnMenuItemClickListener(item -> { Toast.makeText(this, "The coming", Toast.LENGTH_SHORT).show(); return false; }); mMenuView.setOnClickListener(v -> mPopupMenu.show()); } private static void setIconEnable(Menu menu, boolean enable) { try { @SuppressLint("PrivateApi") Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class); m.setAccessible(true); m.invoke(menu, enable); } catch (Exception e) { e.printStackTrace(); } } private void bindViews() { mLoadingView = (TwoGearsView) findViewById(R.id.pb_loading_app); mLauncherView = (RecyclerView) findViewById(R.id.home_launcher); mMenuView = findViewById(R.id.home_menu); mBottomArea = findViewById(R.id.bottom_area); mCreateShortcutBox = findViewById(R.id.create_shortcut_area); mCreateShortcutTextView = (TextView) findViewById(R.id.create_shortcut_text); mDeleteAppBox = findViewById(R.id.delete_app_area); mDeleteAppTextView = (TextView) findViewById(R.id.delete_app_text); } private void initLaunchpad() { mLauncherView.setHasFixedSize(true); StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, OrientationHelper.VERTICAL); mLauncherView.setLayoutManager(layoutManager); mLaunchpadAdapter = new LaunchpadAdapter(this); SmartRecyclerAdapter wrap = new SmartRecyclerAdapter(mLaunchpadAdapter); View footer = new View(this); footer.setLayoutParams(new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, VUiKit.dpToPx(this, 60))); wrap.setFooterView(footer); mLauncherView.setAdapter(wrap); mLauncherView.addItemDecoration(new ItemOffsetDecoration(this, R.dimen.desktop_divider)); ItemTouchHelper touchHelper = new ItemTouchHelper(new LauncherTouchCallback()); touchHelper.attachToRecyclerView(mLauncherView); mLaunchpadAdapter.setAppClickListener((pos, data) -> { if (!data.isLoading()) { if (data instanceof AddAppButton) { onAddAppButtonClick(); } mLaunchpadAdapter.notifyItemChanged(pos); mPresenter.launchApp(data); } }); } private void onAddAppButtonClick() { ListAppActivity.gotoListApp(this); } private void deleteApp(int position) { AppData data = mLaunchpadAdapter.getList().get(position); new AlertDialog.Builder(this) .setTitle("Delete app") .setMessage("Do you want to delete " + data.getName() + "?") .setPositiveButton(android.R.string.yes, (dialog, which) -> { mPresenter.deleteApp(data); }) .setNegativeButton(android.R.string.no, null) .show(); } private void createShortcut(int position) { AppData model = mLaunchpadAdapter.getList().get(position); if (model instanceof PackageAppData || model instanceof MultiplePackageAppData) { mPresenter.createShortcut(model); } } @Override public void setPresenter(HomeContract.HomePresenter presenter) { mPresenter = presenter; } @Override public void showBottomAction() { mBottomArea.setTranslationY(mBottomArea.getHeight()); mBottomArea.setVisibility(View.VISIBLE); mBottomArea.animate().translationY(0).setDuration(500L).start(); } @Override public void hideBottomAction() { mBottomArea.setTranslationY(0); ObjectAnimator transAnim = ObjectAnimator.ofFloat(mBottomArea, "translationY", 0, mBottomArea.getHeight()); transAnim.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { mBottomArea.setVisibility(View.GONE); } @Override public void onAnimationCancel(Animator animator) { mBottomArea.setVisibility(View.GONE); } @Override public void onAnimationRepeat(Animator animator) { } }); transAnim.setDuration(500L); transAnim.start(); } @Override public void showLoading() { mLoadingView.setVisibility(View.VISIBLE); mLoadingView.startAnim(); } @Override public void hideLoading() { mLoadingView.setVisibility(View.GONE); mLoadingView.stopAnim(); } @Override public void loadFinish(List list) { list.add(new AddAppButton(this)); mLaunchpadAdapter.setList(list); hideLoading(); } @Override public void loadError(Throwable err) { err.printStackTrace(); hideLoading(); } @Override public void showGuide() { } @Override public void addAppToLauncher(AppData model) { List dataList = mLaunchpadAdapter.getList(); boolean replaced = false; for (int i = 0; i < dataList.size(); i++) { AppData data = dataList.get(i); if (data instanceof EmptyAppData) { mLaunchpadAdapter.replace(i, model); replaced = true; break; } } if (!replaced) { mLaunchpadAdapter.add(model); mLauncherView.smoothScrollToPosition(mLaunchpadAdapter.getItemCount() - 1); } } @Override public void removeAppToLauncher(AppData model) { mLaunchpadAdapter.remove(model); } @Override public void refreshLauncherItem(AppData model) { mLaunchpadAdapter.refresh(model); } @Override public void askInstallGms() { new AlertDialog.Builder(this) .setTitle("Hi") .setMessage("We found that your device has been installed the Google service, whether you need to install them?") .setPositiveButton(android.R.string.ok, (dialog, which) -> { defer().when(() -> { GmsSupport.installGApps(0); }).done((res) -> { mPresenter.dataChanged(); }); }) .setNegativeButton(android.R.string.cancel, (dialog, which) -> Toast.makeText(HomeActivity.this, "You can also find it in the Settings~", Toast.LENGTH_LONG).show()) .setCancelable(false) .show(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK && data != null) { List appList = data.getParcelableArrayListExtra(VCommends.EXTRA_APP_INFO_LIST); if (appList != null) { for (AppInfoLite info : appList) { mPresenter.addApp(info); } } } } private class LauncherTouchCallback extends ItemTouchHelper.SimpleCallback { int[] location = new int[2]; boolean upAtDeleteAppArea; boolean upAtCreateShortcutArea; RecyclerView.ViewHolder dragHolder; LauncherTouchCallback() { super(UP | DOWN | LEFT | RIGHT | START | END, 0); } @Override public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, int viewSizeOutOfBounds, int totalSize, long msSinceStartScroll) { return 0; } @Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { try { AppData data = mLaunchpadAdapter.getList().get(viewHolder.getAdapterPosition()); if (!data.canReorder()) { return makeMovementFlags(0, 0); } } catch (IndexOutOfBoundsException e) { e.printStackTrace(); } return super.getMovementFlags(recyclerView, viewHolder); } @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { int pos = viewHolder.getAdapterPosition(); int targetPos = target.getAdapterPosition(); mLaunchpadAdapter.moveItem(pos, targetPos); return true; } @Override public boolean isLongPressDragEnabled() { return true; } @Override public boolean isItemViewSwipeEnabled() { return false; } @Override public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { if (viewHolder instanceof LaunchpadAdapter.ViewHolder) { if (actionState == ACTION_STATE_DRAG) { if (dragHolder != viewHolder) { dragHolder = viewHolder; viewHolder.itemView.setScaleX(1.2f); viewHolder.itemView.setScaleY(1.2f); if (mBottomArea.getVisibility() == View.GONE) { showBottomAction(); } } } } super.onSelectedChanged(viewHolder, actionState); } @Override public boolean canDropOver(RecyclerView recyclerView, RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) { if (upAtCreateShortcutArea || upAtDeleteAppArea) { return false; } try { AppData data = mLaunchpadAdapter.getList().get(target.getAdapterPosition()); return data.canReorder(); } catch (IndexOutOfBoundsException e) { e.printStackTrace(); } return false; } @Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { if (viewHolder instanceof LaunchpadAdapter.ViewHolder) { LaunchpadAdapter.ViewHolder holder = (LaunchpadAdapter.ViewHolder) viewHolder; viewHolder.itemView.setScaleX(1f); viewHolder.itemView.setScaleY(1f); viewHolder.itemView.setBackgroundColor(holder.color); } super.clearView(recyclerView, viewHolder); if (dragHolder == viewHolder) { if (mBottomArea.getVisibility() == View.VISIBLE) { mUiHandler.postDelayed(HomeActivity.this::hideBottomAction, 200L); if (upAtCreateShortcutArea) { createShortcut(viewHolder.getAdapterPosition()); } else if (upAtDeleteAppArea) { deleteApp(viewHolder.getAdapterPosition()); } } dragHolder = null; } } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { } @Override public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); if (actionState != ACTION_STATE_DRAG || !isCurrentlyActive) { return; } View itemView = viewHolder.itemView; itemView.getLocationInWindow(location); int x = (int) (location[0] + dX); int y = (int) (location[1] + dY); mBottomArea.getLocationInWindow(location); int baseLine = location[1] - mBottomArea.getHeight(); if (y >= baseLine) { mDeleteAppBox.getLocationInWindow(location); int deleteAppAreaStartX = location[0]; if (x < deleteAppAreaStartX) { upAtCreateShortcutArea = true; upAtDeleteAppArea = false; mCreateShortcutTextView.setTextColor(Color.parseColor("#0099cc")); mDeleteAppTextView.setTextColor(Color.WHITE); } else { upAtDeleteAppArea = true; upAtCreateShortcutArea = false; mDeleteAppTextView.setTextColor(Color.parseColor("#0099cc")); mCreateShortcutTextView.setTextColor(Color.WHITE); } } else { upAtCreateShortcutArea = false; upAtDeleteAppArea = false; mDeleteAppTextView.setTextColor(Color.WHITE); mCreateShortcutTextView.setTextColor(Color.WHITE); } } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/HomeContract.java ================================================ package io.virtualapp.home; import java.util.List; import io.virtualapp.abs.BasePresenter; import io.virtualapp.abs.BaseView; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.AppInfoLite; /** * @author Lody */ /* package */ class HomeContract { /* package */ interface HomeView extends BaseView { void showBottomAction(); void hideBottomAction(); void showLoading(); void hideLoading(); void loadFinish(List appModels); void loadError(Throwable err); void showGuide(); void addAppToLauncher(AppData model); void removeAppToLauncher(AppData model); void refreshLauncherItem(AppData model); void askInstallGms(); } /* package */ interface HomePresenter extends BasePresenter { void launchApp(AppData data); void dataChanged(); void addApp(AppInfoLite info); void deleteApp(AppData data); void createShortcut(AppData data); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/HomePresenterImpl.java ================================================ package io.virtualapp.home; import android.app.Activity; import android.graphics.Bitmap; import com.lody.virtual.GmsSupport; import com.lody.virtual.client.core.VirtualCore; 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 io.virtualapp.VCommends; 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.AppRepository; import io.virtualapp.home.repo.PackageAppDataStorage; import jonathanfinerty.once.Once; /** * @author Lody */ class HomePresenterImpl implements HomeContract.HomePresenter { private HomeContract.HomeView mView; private Activity mActivity; private AppRepository mRepo; private AppData mTempAppData; HomePresenterImpl(HomeContract.HomeView view) { mView = view; mActivity = view.getActivity(); mRepo = new AppRepository(mActivity); mView.setPresenter(this); } @Override public void start() { dataChanged(); if (!Once.beenDone(VCommends.TAG_SHOW_ADD_APP_GUIDE)) { mView.showGuide(); Once.markDone(VCommends.TAG_SHOW_ADD_APP_GUIDE); } if (!Once.beenDone(VCommends.TAG_ASK_INSTALL_GMS) && GmsSupport.isOutsideGoogleFrameworkExist()) { mView.askInstallGms(); Once.markDone(VCommends.TAG_ASK_INSTALL_GMS); } } @Override public void launchApp(AppData data) { try { if (data instanceof PackageAppData) { PackageAppData appData = (PackageAppData) data; appData.isFirstOpen = false; LoadingActivity.launch(mActivity, appData.packageName, 0); } else if (data instanceof MultiplePackageAppData) { MultiplePackageAppData multipleData = (MultiplePackageAppData) data; multipleData.isFirstOpen = false; LoadingActivity.launch(mActivity, multipleData.appInfo.packageName, ((MultiplePackageAppData) data).userId); } } catch (Throwable e) { e.printStackTrace(); } } @Override public void dataChanged() { mView.showLoading(); mRepo.getVirtualApps().done(mView::loadFinish).fail(mView::loadError); } @Override public void addApp(AppInfoLite info) { 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 (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 { InstallResult res = mRepo.addVirtualApp(info); if (!res.isSuccess) { throw new IllegalStateException(); } } }).then((res) -> { addResult.appData = PackageAppDataStorage.get().acquire(info.packageName); }).done(res -> { boolean multipleVersion = addResult.justEnableHidden && addResult.userId != 0; if (!multipleVersion) { PackageAppData data = addResult.appData; data.isLoading = true; mView.addAppToLauncher(data); handleOptApp(data, info.packageName, true); } else { MultiplePackageAppData data = new MultiplePackageAppData(addResult.appData, addResult.userId); data.isLoading = true; mView.addAppToLauncher(data); handleOptApp(data, info.packageName, false); } }); } private void handleOptApp(AppData data, String packageName, boolean needOpt) { 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; } mView.refreshLauncherItem(data); }); } @Override public void deleteApp(AppData data) { try { mView.removeAppToLauncher(data); if (data instanceof PackageAppData) { mRepo.removeVirtualApp(((PackageAppData) data).packageName, 0); } else { MultiplePackageAppData appData = (MultiplePackageAppData) data; mRepo.removeVirtualApp(appData.appInfo.packageName, appData.userId); } } catch (Throwable e) { e.printStackTrace(); } } @Override public void createShortcut(AppData data) { VirtualCore.OnEmitShortcutListener listener = new VirtualCore.OnEmitShortcutListener() { @Override public Bitmap getIcon(Bitmap originIcon) { return originIcon; } @Override public String getName(String originName) { return originName + "(VA)"; } }; if (data instanceof PackageAppData) { VirtualCore.get().createShortcut(0, ((PackageAppData) data).packageName, listener); } else if (data instanceof MultiplePackageAppData) { MultiplePackageAppData appData = (MultiplePackageAppData) data; VirtualCore.get().createShortcut(appData.userId, appData.appInfo.packageName, listener); } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/ListAppActivity.java ================================================ package io.virtualapp.home; import android.Manifest; import android.app.Activity; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.TabLayout; import android.support.v4.app.ActivityCompat; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; 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); getWindow().setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.colorPrimaryDark))); setContentView(R.layout.activity_clone_app); mToolBar = (Toolbar) findViewById(R.id.clone_app_tool_bar); mTabLayout = (TabLayout) mToolBar.findViewById(R.id.clone_app_tab_layout); mViewPager = (ViewPager) findViewById(R.id.clone_app_view_pager); setupToolBar(); mViewPager.setAdapter(new AppPagerAdapter(getSupportFragmentManager())); mTabLayout.setupWithViewPager(mViewPager); // Request permission to access external storage if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0); } } } private void setupToolBar() { setSupportActionBar(mToolBar); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return super.onOptionsItemSelected(item); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { for (int result : grantResults) { if (result == PackageManager.PERMISSION_GRANTED) { mViewPager.setAdapter(new AppPagerAdapter(getSupportFragmentManager())); break; } } } } ================================================ 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.content.Intent; import android.os.Bundle; import android.support.annotation.Nullable; 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 java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Locale; import io.virtualapp.R; import io.virtualapp.VCommends; import io.virtualapp.abs.ui.VFragment; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.adapters.CloneAppListAdapter; import io.virtualapp.home.adapters.decorations.ItemOffsetDecoration; import io.virtualapp.home.models.AppInfo; import io.virtualapp.home.models.AppInfoLite; 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 DragSelectRecyclerView mRecyclerView; private ProgressBar mProgressBar; private Button mInstallButton; private CloneAppListAdapter mAdapter; 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); } @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); mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, OrientationHelper.VERTICAL)); mRecyclerView.addItemDecoration(new ItemOffsetDecoration(VUiKit.dpToPx(getContext(), 2))); mAdapter = new CloneAppListAdapter(getActivity()); mRecyclerView.setAdapter(mAdapter); mAdapter.setOnItemClickListener(new CloneAppListAdapter.ItemEventListener() { @Override public void onItemClick(AppInfo info, int position) { 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, 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)); } Intent data = new Intent(); data.putParcelableArrayListExtra(VCommends.EXTRA_APP_INFO_LIST, dataList); getActivity().setResult(Activity.RESULT_OK, data); getActivity().finish(); }); 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; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/ListAppPresenterImpl.java ================================================ package io.virtualapp.home; import android.app.Activity; import android.content.Intent; import java.io.File; import io.virtualapp.VCommends; import io.virtualapp.home.repo.AppDataSource; import io.virtualapp.home.models.PackageAppData; 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); else mRepository.getStorageApps(mActivity, from).done(mView::loadFinish); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/LoadingActivity.java ================================================ package io.virtualapp.home; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.RemoteException; import android.widget.ImageView; import android.widget.TextView; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.client.ipc.VActivityManager; import java.util.Locale; 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; /** * @author Lody */ public class LoadingActivity extends VActivity { private static final String PKG_NAME_ARGUMENT = "MODEL_ARGUMENT"; private static final String KEY_INTENT = "KEY_INTENT"; private static final String KEY_USER = "KEY_USER"; private PackageAppData appModel; private EatBeansView loadingView; public static void 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(PKG_NAME_ARGUMENT, packageName); loadingPageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); loadingPageIntent.putExtra(KEY_INTENT, intent); loadingPageIntent.putExtra(KEY_USER, userId); context.startActivity(loadingPageIntent); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_loading); loadingView = (EatBeansView) findViewById(R.id.loading_anim); int userId = getIntent().getIntExtra(KEY_USER, -1); String pkg = getIntent().getStringExtra(PKG_NAME_ARGUMENT); appModel = PackageAppDataStorage.get().acquire(pkg); 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(KEY_INTENT); if (intent == null) { return; } VirtualCore.get().setUiCallback(intent, mUiCallback); VUiKit.defer().when(() -> { if (!appModel.fastOpen) { try { VirtualCore.get().preOpt(appModel.packageName); } catch (Exception e) { e.printStackTrace(); } } VActivityManager.get().startActivity(intent, userId); }); } private final VirtualCore.UiCallback mUiCallback = new VirtualCore.UiCallback() { @Override public void onAppOpened(String packageName, int userId) throws RemoteException { finish(); } }; @Override protected void onResume() { super.onResume(); loadingView.startAnim(); } @Override protected void onPause() { super.onPause(); loadingView.stopAnim(); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/adapters/AppLocationAdapter.java ================================================ package io.virtualapp.home.adapters; import android.content.Context; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import io.virtualapp.R; import io.virtualapp.abs.ui.BaseAdapterPlus; import io.virtualapp.home.models.LocationData; public class AppLocationAdapter extends BaseAdapterPlus { public AppLocationAdapter(Context context) { super(context); } @Override protected View createView(int position, ViewGroup parent) { View view = inflate(R.layout.item_location_app, parent, false); ViewHolder viewHolder = new ViewHolder(view); view.setTag(viewHolder); return view; } @Override protected void attach(View view, LocationData item, int position) { ViewHolder viewHolder = (ViewHolder) view.getTag(); viewHolder.icon.setImageDrawable(item.icon); viewHolder.label.setText(item.name); if (item.location != null && item.mode != 0) { viewHolder.location.setText(item.location.latitude + "," + item.location.longitude); } else { viewHolder.location.setText("real location"); } } static class ViewHolder extends BaseAdapterPlus.BaseViewHolder { public ViewHolder(View view) { super(view); icon = $(R.id.item_app_icon); label = $(R.id.item_app_name); location = $(R.id.item_location); } final ImageView icon; final TextView label, location; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/adapters/AppPagerAdapter.java ================================================ package io.virtualapp.home.adapters; import android.content.Context; import android.os.Build; import android.os.Environment; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import com.lody.virtual.helper.utils.Reflect; import java.io.File; import java.util.ArrayList; import java.util.List; import io.virtualapp.R; import io.virtualapp.VApp; 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(VApp.getApp().getResources().getString(R.string.clone_apps)); dirs.add(null); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Context ctx = VApp.getApp(); StorageManager storage = (StorageManager) ctx.getSystemService(Context.STORAGE_SERVICE); for (StorageVolume volume : storage.getStorageVolumes()) { //Why the fuck are getPathFile and getUserLabel hidden?! //StorageVolume is kinda useless without those... File dir = Reflect.on(volume).call("getPathFile").get(); String label = Reflect.on(volume).call("getUserLabel").get(); if (dir.listFiles() != null) { titles.add(label); dirs.add(dir); } } } else { // Fallback: only support the default storage sources File storageFir = Environment.getExternalStorageDirectory(); if (storageFir != null && storageFir.isDirectory()) { titles.add(VApp.getApp().getResources().getString(R.string.external_storage)); dirs.add(storageFir); } } } @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.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.util.List; import io.virtualapp.R; import io.virtualapp.abs.ui.VUiKit; 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; public CloneAppListAdapter(Context context) { 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); holder.iconView.setImageDrawable(info.icon); holder.nameView.setText(info.name); 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/adapters/LaunchpadAdapter.java ================================================ package io.virtualapp.home.adapters; import android.content.Context; import android.support.v7.widget.RecyclerView; import android.util.SparseIntArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.List; import io.virtualapp.R; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.MultiplePackageAppData; import io.virtualapp.widgets.LabelView; import io.virtualapp.widgets.LauncherIconView; /** * @author Lody */ public class LaunchpadAdapter extends RecyclerView.Adapter { private LayoutInflater mInflater; private List mList; private SparseIntArray mColorArray = new SparseIntArray(); private OnAppClickListener mAppClickListener; public LaunchpadAdapter(Context context) { mInflater = LayoutInflater.from(context); } public void add(AppData model) { int insertPos = mList.size() - 1; mList.add(insertPos, model); notifyItemInserted(insertPos); } public void replace(int index, AppData data) { mList.set(index, data); notifyItemChanged(index); } public void remove(AppData data) { if (mList.remove(data)) { notifyDataSetChanged(); } } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ViewHolder(mInflater.inflate(R.layout.item_launcher_app, null)); } @Override public void onBindViewHolder(ViewHolder holder, int position) { AppData data = mList.get(position); holder.color = getColor(position); holder.iconView.setImageDrawable(data.getIcon()); holder.nameView.setText(data.getName()); if (data.isFirstOpen() && !data.isLoading()) { holder.firstOpenDot.setVisibility(View.VISIBLE); } else { holder.firstOpenDot.setVisibility(View.INVISIBLE); } holder.itemView.setBackgroundColor(holder.color); holder.itemView.setOnClickListener(v -> { if (mAppClickListener != null) { mAppClickListener.onAppClick(position, data); } }); if (data instanceof MultiplePackageAppData) { MultiplePackageAppData multipleData = (MultiplePackageAppData) data; holder.spaceLabelView.setVisibility(View.VISIBLE); holder.spaceLabelView.setText(multipleData.userId + 1 + ""); } else { holder.spaceLabelView.setVisibility(View.INVISIBLE); } if (data.isLoading()) { startLoadingAnimation(holder.iconView); } else { holder.iconView.setProgress(100, false); } } private void startLoadingAnimation(LauncherIconView iconView) { iconView.setProgress(40, true); VUiKit.defer().when(() -> { try { Thread.sleep(900L); } catch (InterruptedException e) { e.printStackTrace(); } }).done((res) -> iconView.setProgress(80, true)); } private int getColor(int position) { int color = mColorArray.get(position); if (color == 0) { int type = position % 3; int row = position / 3; int rowType = row % 3; if (rowType == 0) { if (type == 0) { color = mInflater.getContext().getResources().getColor(R.color.desktopColorA); } else if (type == 1) { color = mInflater.getContext().getResources().getColor(R.color.desktopColorB); } else { color = mInflater.getContext().getResources().getColor(R.color.desktopColorC); } } else if (rowType == 1) { if (type == 0) { color = mInflater.getContext().getResources().getColor(R.color.desktopColorB); } else if (type == 1) { color = mInflater.getContext().getResources().getColor(R.color.desktopColorC); } else { color = mInflater.getContext().getResources().getColor(R.color.desktopColorA); } } else { if (type == 0) { color = mInflater.getContext().getResources().getColor(R.color.desktopColorC); } else if (type == 1) { color = mInflater.getContext().getResources().getColor(R.color.desktopColorA); } else { color = mInflater.getContext().getResources().getColor(R.color.desktopColorB); } } mColorArray.put(position, color); } return color; } @Override public int getItemCount() { return mList == null ? 0 : mList.size(); } public List getList() { return mList; } public void setList(List list) { this.mList = list; notifyDataSetChanged(); } public void setAppClickListener(OnAppClickListener mAppClickListener) { this.mAppClickListener = mAppClickListener; } public void moveItem(int pos, int targetPos) { AppData model = mList.remove(pos); mList.add(targetPos, model); notifyItemMoved(pos, targetPos); } public void refresh(AppData model) { int index = mList.indexOf(model); if (index >= 0) { notifyItemChanged(index); } } public interface OnAppClickListener { void onAppClick(int position, AppData model); } public class ViewHolder extends RecyclerView.ViewHolder { public int color; LauncherIconView iconView; TextView nameView; LabelView spaceLabelView; View firstOpenDot; ViewHolder(View itemView) { super(itemView); iconView = (LauncherIconView) itemView.findViewById(R.id.item_app_icon); nameView = (TextView) itemView.findViewById(R.id.item_app_name); spaceLabelView = (LabelView) itemView.findViewById(R.id.item_app_space_idx); firstOpenDot = itemView.findViewById(R.id.item_first_open_dot); } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/adapters/decorations/ItemOffsetDecoration.java ================================================ package io.virtualapp.home.adapters.decorations; import android.content.Context; import android.graphics.Rect; import android.support.annotation.DimenRes; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.View; public class ItemOffsetDecoration extends RecyclerView.ItemDecoration { private int mItemOffset; public ItemOffsetDecoration(int itemOffset) { mItemOffset = itemOffset; } public ItemOffsetDecoration(@NonNull Context context, @DimenRes int itemOffsetId) { this(context.getResources().getDimensionPixelSize(itemOffsetId)); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { outRect.set(mItemOffset, mItemOffset, mItemOffset, mItemOffset); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/location/MarkerActivity.java ================================================ package io.virtualapp.home.location; import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Intent; import android.os.Bundle; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.TextView; import android.widget.Toast; import com.lody.virtual.helper.utils.VLog; import com.lody.virtual.remote.vloc.VLocation; import com.tencent.lbssearch.TencentSearch; import com.tencent.lbssearch.httpresponse.BaseObject; import com.tencent.lbssearch.httpresponse.HttpResponseListener; import com.tencent.lbssearch.object.Location; import com.tencent.lbssearch.object.param.Geo2AddressParam; import com.tencent.lbssearch.object.result.Geo2AddressResultObject; import com.tencent.map.geolocation.TencentLocation; import com.tencent.map.geolocation.TencentLocationListener; import com.tencent.map.geolocation.TencentLocationManager; import com.tencent.map.geolocation.TencentLocationRequest; import com.tencent.mapsdk.raster.model.BitmapDescriptorFactory; import com.tencent.mapsdk.raster.model.CameraPosition; import com.tencent.mapsdk.raster.model.LatLng; import com.tencent.mapsdk.raster.model.MarkerOptions; import com.tencent.tencentmap.mapsdk.map.CameraUpdateFactory; import com.tencent.tencentmap.mapsdk.map.MapView; import com.tencent.tencentmap.mapsdk.map.TencentMap; import io.virtualapp.abs.ui.VActivity; import io.virtualapp.R; public class MarkerActivity extends VActivity implements TencentMap.OnMapClickListener, TencentLocationListener { private TencentMap mMap; private MapView mapView; private LatLng mLatLng = new LatLng(39.9182645956, 116.3970032689); private TextView pathText; private TencentSearch geocoderSearch; private String mAddress; private boolean isNoPoint = true; private VLocation mVLocation; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_marker); setResult(Activity.RESULT_CANCELED); Toolbar toolbar = bind(R.id.task_top_toolbar); setSupportActionBar(toolbar); //地址显示,暂时不用 pathText = bind(R.id.address); pathText.setVisibility(View.VISIBLE); enableBackHome(); mapView = (MapView) findViewById(R.id.map); mapView.onCreate(savedInstanceState); // 此方法必须重写 mMap = mapView.getMap(); mMap.setOnMapClickListener(this); geocoderSearch = new TencentSearch(this); // Intent data = getIntent(); if (data != null) { mVLocation = data.getParcelableExtra(EXTRA_LOCATION); if (mVLocation != null && mVLocation.latitude != 0 && mVLocation.longitude != 0) { mLatLng = new LatLng(mVLocation.latitude, mVLocation.longitude); isNoPoint = false; } } if (isNoPoint) { startLocation(); } else { onMapClick(mLatLng); } } @SuppressWarnings("unchecked") protected T bind(int id) { return (T) findViewById(id); } public void enableBackHome() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } private void startLocation() { Toast.makeText(this, "start location", Toast.LENGTH_SHORT).show(); TencentLocationRequest request = TencentLocationRequest.create() .setRequestLevel(TencentLocationRequest.REQUEST_LEVEL_GEO) .setAllowGPS(true); int error = TencentLocationManager.getInstance(this) .requestLocationUpdates(request, this); if (error != 0) { VLog.w("TMap", "startLocation:error=" + error); } } @Override public void onLocationChanged(TencentLocation location, int error, String msg) { if (location != null) { TencentLocationManager.getInstance(this).removeUpdates(this); onMapClick(new LatLng(location.getLatitude(), location.getLongitude())); } else { String errText = "定位失败," + error + ": " + msg; VLog.e("TMap", errText); } } @Override public void onStatusUpdate(String s, int i, String s1) { } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.marktet_map, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: finish(); break; case R.id.action_clear: AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("Question"); builder.setMessage("Clear virtual location"); builder.setNegativeButton(android.R.string.ok, (d, s) -> { if (mMap != null) { mMap.clearAllOverlays(); } setResultOk(null); finish(); d.dismiss(); }); builder.setNeutralButton(android.R.string.cancel, (d, s) -> { d.dismiss(); }); builder.show(); break; case R.id.action_ok: if (mLatLng != null) { /** * TODO edit info * @see com.lody.virtual.remote.vloc.VLocation#altitude * @see com.lody.virtual.remote.vloc.VLocation#accuracy * @see com.lody.virtual.remote.vloc.VLocation#speed * @see com.lody.virtual.remote.vloc.VLocation#bearing */ if (mVLocation == null) { mVLocation = new VLocation(); mVLocation.accuracy = 50; } mVLocation.latitude = mLatLng.getLatitude(); mVLocation.longitude = mLatLng.getLongitude(); setResultOk(mVLocation); finish(); } break; } return super.onOptionsItemSelected(item); } @Override public void onMapClick(LatLng latLng) { mLatLng = latLng; MarkerOptions markerOption = new MarkerOptions() .icon(BitmapDescriptorFactory.defaultMarker()) .position(mLatLng) .draggable(true); mMap.clearAllOverlays(); mMap.addMarker(markerOption); int level = Math.min(mMap.getZoomLevel(), mMap.getMaxZoomLevel() / 3 * 2); mMap.moveCamera(CameraUpdateFactory.newCameraPosition(new CameraPosition(latLng, level))); //查询地理位置 ProgressDialog dialog = ProgressDialog.show(this, null, "get address of location"); Geo2AddressParam param = new Geo2AddressParam() .location(new Location() .lat((float) latLng.getLatitude()) .lng((float) latLng.getLongitude())); geocoderSearch.geo2address(param, new HttpResponseListener() { @Override public void onSuccess(int i, BaseObject object) { Geo2AddressResultObject oj = (Geo2AddressResultObject) object; if (oj.result != null) { pathText.setText(oj.result.address); mAddress = oj.result.address; } dialog.dismiss(); } @Override public void onFailure(int i, String s, Throwable throwable) { dialog.dismiss(); pathText.setText("error:" + s); } }); } /** * 方法必须重写 */ @Override protected void onResume() { super.onResume(); mapView.onResume(); } /** * 方法必须重写 */ @Override protected void onPause() { super.onPause(); mapView.onPause(); } /** * 方法必须重写 */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mapView.onSaveInstanceState(outState); } /** * 方法必须重写 */ @Override protected void onDestroy() { super.onDestroy(); mapView.onDestroy(); } private void setResultOk(VLocation location) { Intent intent = new Intent(); intent.putExtra(EXTRA_LOCATION, location); setResult(Activity.RESULT_OK, intent); } public static final String EXTRA_LOCATION = "virtual_location"; } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/location/VirtualLocationSettings.java ================================================ package io.virtualapp.home.location; import android.app.ProgressDialog; import android.content.Intent; import android.os.Bundle; import android.support.annotation.Nullable; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.ListView; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.client.ipc.VirtualLocationManager; import com.lody.virtual.helper.utils.VLog; import com.lody.virtual.remote.InstalledAppInfo; import com.lody.virtual.remote.vloc.VLocation; 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.home.adapters.AppLocationAdapter; import io.virtualapp.home.models.LocationData; import io.virtualapp.home.repo.AppRepository; import static io.virtualapp.home.location.MarkerActivity.EXTRA_LOCATION; public class VirtualLocationSettings extends VActivity implements AdapterView.OnItemClickListener { private static final int REQUSET_CODE = 1001; private AppRepository mRepo; private ListView mListView; private AppLocationAdapter mAppLocationAdapter; private LocationData mSelectData; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_location_settings); mListView = (ListView) findViewById(R.id.appdata_list); mRepo = new AppRepository(this); mAppLocationAdapter = new AppLocationAdapter(this); mListView.setAdapter(mAppLocationAdapter); mListView.setOnItemClickListener(this); loadData(); } private void readLocation(LocationData locationData) { locationData.mode = VirtualLocationManager.get().getMode(locationData.userId, locationData.packageName); locationData.location = VirtualLocationManager.get().getLocation(locationData.userId, locationData.packageName); } private void saveLocation(LocationData locationData) { if(locationData.location == null||locationData.location.isEmpty()){ VirtualLocationManager.get().setMode(locationData.userId, locationData.packageName, 0); }else if(locationData.mode != 2){ VirtualLocationManager.get().setMode(locationData.userId, locationData.packageName, 2); } VirtualLocationManager.get().setLocation(locationData.userId, locationData.packageName, locationData.location); } private void loadData() { ProgressDialog dialog = ProgressDialog.show(this, null, "loading"); VUiKit.defer().when(() -> { List infos = VirtualCore.get().getInstalledApps(0); List models = new ArrayList<>(); for (InstalledAppInfo info : infos) { if (!VirtualCore.get().isPackageLaunchable(info.packageName)) { continue; } int[] userIds = info.getInstalledUsers(); for (int userId : userIds) { LocationData data = new LocationData(this, info, userId); readLocation(data); models.add(data); } } return models; }).done((list) -> { dialog.dismiss(); mAppLocationAdapter.set(list); mAppLocationAdapter.notifyDataSetChanged(); }).fail((e) -> { dialog.dismiss(); }); } @Override public void onItemClick(AdapterView parent, View view, int position, long id) { mSelectData = mAppLocationAdapter.getItem(position); Intent intent = new Intent(this, MarkerActivity.class); if (mSelectData.location != null) { intent.putExtra(EXTRA_LOCATION, mSelectData.location); } startActivityForResult(intent, REQUSET_CODE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUSET_CODE) { if (resultCode == RESULT_OK) { VLocation location = data.getParcelableExtra(EXTRA_LOCATION); if (mSelectData != null) { mSelectData.location = location; VLog.i("kk", "set" + mSelectData); saveLocation(mSelectData); mSelectData = null; loadData(); } } } super.onActivityResult(requestCode, resultCode, data); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/models/AddAppButton.java ================================================ package io.virtualapp.home.models; import android.content.Context; import android.graphics.drawable.Drawable; import io.virtualapp.R; /** * @author Lody */ public class AddAppButton implements AppData { private String name; private Drawable icon; public AddAppButton(Context context) { name = context.getResources().getString(R.string.add_app); icon = context.getResources().getDrawable(R.drawable.ic_add_circle); } @Override public boolean isLoading() { return false; } @Override public boolean isFirstOpen() { return false; } @Override public Drawable getIcon() { return icon; } @Override public String getName() { return name; } @Override public boolean canReorder() { return false; } @Override public boolean canLaunch() { return false; } @Override public boolean canDelete() { return false; } @Override public boolean canCreateShortcut() { return false; } } ================================================ 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 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 int cloneCount; } ================================================ 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 AppInfoLite(String packageName, String path, boolean fastOpen) { this.packageName = packageName; this.path = path; this.fastOpen = fastOpen; } protected AppInfoLite(Parcel in) { this.packageName = in.readString(); this.path = in.readString(); this.fastOpen = 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); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/models/EmptyAppData.java ================================================ package io.virtualapp.home.models; import android.graphics.drawable.Drawable; /** * @author Lody */ public class EmptyAppData implements AppData { @Override public boolean isLoading() { return false; } @Override public boolean isFirstOpen() { return false; } @Override public Drawable getIcon() { return null; } @Override public String getName() { return null; } @Override public boolean canReorder() { return false; } @Override public boolean canLaunch() { return false; } @Override public boolean canDelete() { return false; } @Override public boolean canCreateShortcut() { return false; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/models/LocationData.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; import com.lody.virtual.remote.vloc.VLocation; /** * @see android.location.Location */ public class LocationData { public String packageName; public int userId; public String name; public Drawable icon; public int mode; public VLocation location; public LocationData() { } public LocationData(Context context, InstalledAppInfo installedAppInfo, int userId) { this.packageName = installedAppInfo.packageName; this.userId = userId; loadData(context, installedAppInfo.getApplicationInfo(installedAppInfo.getInstalledUsers()[0])); } 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 String toString() { return "LocationData{" + "packageName='" + packageName + '\'' + ", userId=" + userId + ", location=" + location + '}'; } } ================================================ 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 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 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 PackageAppData(Context context, InstalledAppInfo installedAppInfo) { this.packageName = installedAppInfo.packageName; this.isFirstOpen = !installedAppInfo.isLaunched(0); loadData(context, installedAppInfo.getApplicationInfo(installedAppInfo.getInstalledUsers()[0])); } 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 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/platform/PlatformInfo.java ================================================ package io.virtualapp.home.platform; import android.content.pm.PackageInfo; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** * @author Lody */ public abstract class PlatformInfo { private final Set platformPkgs = new HashSet<>(); public PlatformInfo(String... pkgs) { Collections.addAll(platformPkgs, pkgs); } public abstract boolean relyOnPlatform(PackageInfo info); } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/home/platform/WechatPlatformInfo.java ================================================ package io.virtualapp.home.platform; import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; /** * @author Lody */ public class WechatPlatformInfo extends PlatformInfo { public WechatPlatformInfo() { super("com.tencent.mm"); } @Override public boolean relyOnPlatform(PackageInfo info) { if (info.activities == null) { return false; } for (ActivityInfo activityInfo : info.activities) { if (activityInfo.name.endsWith("WXEntryActivity")) { return true; } } return false; } } ================================================ 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.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.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; /** * @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 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(0), true)); } @Override public Promise, Throwable, Void> getStorageApps(Context context, File rootDir) { return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, findAndParseAPKs(context, rootDir, SCAN_PATH_LIST), false)); } 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 the System package if (isSystemApplication(pkg)) { continue; } ApplicationInfo ai = pkg.applicationInfo; String path = ai.publicSourceDir != null ? ai.publicSourceDir : ai.sourceDir; if (path == null) { continue; } AppInfo info = new AppInfo(); info.packageName = pkg.packageName; info.fastOpen = fastOpen; info.path = path; info.icon = ai.loadIcon(pm); info.name = ai.loadLabel(pm); InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(pkg.packageName, 0); if (installedAppInfo != null) { info.cloneCount = installedAppInfo.getInstalledUsers().length; } list.add(info); } return list; } @Override public InstallResult addVirtualApp(AppInfoLite info) { int flags = InstallStrategy.COMPARE_VERSION | InstallStrategy.SKIP_DEX_OPT; if (info.fastOpen) { flags |= InstallStrategy.DEPEND_SYSTEM_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 com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.remote.InstalledAppInfo; import java.util.HashMap; import java.util.Map; import io.virtualapp.VApp; 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(VApp.getApp(), setting); synchronized (packageDataMap) { packageDataMap.put(packageName, data); } return data; } return null; } } ================================================ 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.FlurryROMCollector; import io.virtualapp.home.HomeActivity; 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(() -> { if (!Once.beenDone("collect_flurry")) { FlurryROMCollector.startCollect(); Once.markDone("collect_flurry"); } long time = System.currentTimeMillis(); doActionInThread(); time = System.currentTimeMillis() - time; long delta = 3000L - time; if (delta > 0) { VUiKit.sleep(delta); } }).done((res) -> { HomeActivity.goHome(this); finish(); }); } private void doActionInThread() { if (!VirtualCore.get().isEngineLaunched()) { VirtualCore.get().waitForEngine(); } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/vs/VSManagerActivity.java ================================================ package io.virtualapp.vs; import io.virtualapp.abs.ui.VActivity; /** * @author Lody * * * */ public class VSManagerActivity extends VActivity { } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/BallGridBeatIndicator.java ================================================ package io.virtualapp.widgets; import android.animation.ValueAnimator; import android.graphics.Canvas; import android.graphics.Paint; import java.util.ArrayList; public class BallGridBeatIndicator extends Indicator { private static final int ALPHA = 255; private static final int[] ALPHAS = new int[]{ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA}; @Override public void draw(Canvas canvas, Paint paint) { float circleSpacing = 4; float radius = (getWidth() - circleSpacing * 4) / 6; float x = getWidth() / 2 - (radius * 2 + circleSpacing); float y = getWidth() / 2 - (radius * 2 + circleSpacing); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { canvas.save(); float translateX = x + (radius * 2) * j + circleSpacing * j; float translateY = y + (radius * 2) * i + circleSpacing * i; canvas.translate(translateX, translateY); paint.setAlpha(ALPHAS[3 * i + j]); canvas.drawCircle(0, 0, radius, paint); canvas.restore(); } } } @Override public ArrayList onCreateAnimators() { ArrayList animators = new ArrayList<>(); int[] durations = {960, 930, 1190, 1130, 1340, 940, 1200, 820, 1190}; int[] delays = {360, 400, 680, 410, 710, -150, -120, 10, 320}; for (int i = 0; i < 9; i++) { final int index = i; ValueAnimator alphaAnim = ValueAnimator.ofInt(255, 168, 255); alphaAnim.setDuration(durations[i]); alphaAnim.setRepeatCount(-1); alphaAnim.setStartDelay(delays[i]); addUpdateListener(alphaAnim, animation -> { ALPHAS[index] = (int) animation.getAnimatedValue(); postInvalidate(); }); animators.add(alphaAnim); } return animators; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/BallPulseIndicator.java ================================================ package io.virtualapp.widgets; import android.animation.ValueAnimator; import android.graphics.Canvas; import android.graphics.Paint; import java.util.ArrayList; public class BallPulseIndicator extends Indicator { public static final float SCALE = 1.0f; //scale x ,y private float[] scaleFloats = new float[]{SCALE, SCALE, SCALE}; @Override public void draw(Canvas canvas, Paint paint) { float circleSpacing = 4; float radius = (Math.min(getWidth(), getHeight()) - circleSpacing * 2) / 6; float x = getWidth() / 2 - (radius * 2 + circleSpacing); float y = getHeight() / 2; for (int i = 0; i < 3; i++) { canvas.save(); float translateX = x + (radius * 2) * i + circleSpacing * i; canvas.translate(translateX, y); canvas.scale(scaleFloats[i], scaleFloats[i]); canvas.drawCircle(0, 0, radius, paint); canvas.restore(); } } @Override public ArrayList onCreateAnimators() { ArrayList animators = new ArrayList<>(); int[] delays = new int[]{120, 240, 360}; for (int i = 0; i < 3; i++) { final int index = i; ValueAnimator scaleAnim = ValueAnimator.ofFloat(1, 0.3f, 1); scaleAnim.setDuration(750); scaleAnim.setRepeatCount(-1); scaleAnim.setStartDelay(delays[i]); addUpdateListener(scaleAnim, animation -> { scaleFloats[index] = (float) animation.getAnimatedValue(); postInvalidate(); }); animators.add(scaleAnim); } return animators; } } ================================================ 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, 500); } 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/CircularAnim.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.SuppressLint; import android.app.Activity; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.widget.ImageView; public class CircularAnim { public static final long PERFECT_MILLS = 618; public static final int MINI_RADIUS = 0; private static Long sPerfectMills; private static Long sFullActivityPerfectMills; private static Integer sColorOrImageRes; private static long getPerfectMills() { if (sPerfectMills != null) return sPerfectMills; else return PERFECT_MILLS; } private static long getFullActivityMills() { if (sFullActivityPerfectMills != null) return sFullActivityPerfectMills; else return PERFECT_MILLS; } private static int getColorOrImageRes() { if (sColorOrImageRes != null) return sColorOrImageRes; else return android.R.color.white; } public static VisibleBuilder show(View animView) { return new VisibleBuilder(animView, true); } public static VisibleBuilder hide(View animView) { return new VisibleBuilder(animView, false); } public static FullActivityBuilder fullActivity(Activity activity, View triggerView) { return new FullActivityBuilder(activity, triggerView); } public static void init(long perfectMills, long fullActivityPerfectMills, int colorOrImageRes) { sPerfectMills = perfectMills; sFullActivityPerfectMills = fullActivityPerfectMills; sColorOrImageRes = colorOrImageRes; } public interface OnAnimationEndListener { void onAnimationEnd(); } @SuppressLint("NewApi") public static class VisibleBuilder { private View mAnimView, mTriggerView; private Float mStartRadius, mEndRadius; private long mDurationMills = getPerfectMills(); private boolean isShow; private OnAnimationEndListener mOnAnimationEndListener; public VisibleBuilder(View animView, boolean isShow) { mAnimView = animView; this.isShow = isShow; if (isShow) { mStartRadius = MINI_RADIUS + 0F; } else { mEndRadius = MINI_RADIUS + 0F; } } public VisibleBuilder triggerView(View triggerView) { mTriggerView = triggerView; return this; } public VisibleBuilder startRadius(float startRadius) { mStartRadius = startRadius; return this; } public VisibleBuilder endRadius(float endRadius) { mEndRadius = endRadius; return this; } public VisibleBuilder duration(long durationMills) { mDurationMills = durationMills; return this; } @Deprecated //You can use method - go(OnAnimationEndListener onAnimationEndListener). public VisibleBuilder onAnimationEndListener(OnAnimationEndListener onAnimationEndListener) { mOnAnimationEndListener = onAnimationEndListener; return this; } public void go() { go(null); } public void go(OnAnimationEndListener onAnimationEndListener) { mOnAnimationEndListener = onAnimationEndListener; // 版本判断 if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { doOnEnd(); return; } int rippleCX, rippleCY, maxRadius; if (mTriggerView != null) { int[] tvLocation = new int[2]; mTriggerView.getLocationInWindow(tvLocation); final int tvCX = tvLocation[0] + mTriggerView.getWidth() / 2; final int tvCY = tvLocation[1] + mTriggerView.getHeight() / 2; int[] avLocation = new int[2]; mAnimView.getLocationInWindow(avLocation); final int avLX = avLocation[0]; final int avTY = avLocation[1]; int triggerX = Math.max(avLX, tvCX); triggerX = Math.min(triggerX, avLX + mAnimView.getWidth()); int triggerY = Math.max(avTY, tvCY); triggerY = Math.min(triggerY, avTY + mAnimView.getHeight()); // 以上全为绝对坐标 int avW = mAnimView.getWidth(); int avH = mAnimView.getHeight(); rippleCX = triggerX - avLX; rippleCY = triggerY - avTY; // 计算水波中心点至 @mAnimView 边界的最大距离 int maxW = Math.max(rippleCX, avW - rippleCX); int maxH = Math.max(rippleCY, avH - rippleCY); maxRadius = (int) Math.sqrt(maxW * maxW + maxH * maxH) + 1; } else { rippleCX = (mAnimView.getLeft() + mAnimView.getRight()) / 2; rippleCY = (mAnimView.getTop() + mAnimView.getBottom()) / 2; int w = mAnimView.getWidth(); int h = mAnimView.getHeight(); // 勾股定理 & 进一法 maxRadius = (int) Math.sqrt(w * w + h * h) + 1; } if (isShow && mEndRadius == null) mEndRadius = maxRadius + 0F; else if (!isShow && mStartRadius == null) mStartRadius = maxRadius + 0F; try { Animator anim = ViewAnimationUtils.createCircularReveal( mAnimView, rippleCX, rippleCY, mStartRadius, mEndRadius); mAnimView.setVisibility(View.VISIBLE); anim.setDuration(mDurationMills); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); doOnEnd(); } }); anim.start(); } catch (Exception e) { e.printStackTrace(); doOnEnd(); } } private void doOnEnd() { if (isShow) mAnimView.setVisibility(View.VISIBLE); else mAnimView.setVisibility(View.INVISIBLE); if (mOnAnimationEndListener != null) mOnAnimationEndListener.onAnimationEnd(); } } @SuppressLint("NewApi") public static class FullActivityBuilder { private Activity mActivity; private View mTriggerView; private float mStartRadius = MINI_RADIUS; private int mColorOrImageRes = getColorOrImageRes(); private Long mDurationMills; private OnAnimationEndListener mOnAnimationEndListener; private int mEnterAnim = android.R.anim.fade_in, mExitAnim = android.R.anim.fade_out; public FullActivityBuilder(Activity activity, View triggerView) { mActivity = activity; mTriggerView = triggerView; } public FullActivityBuilder startRadius(float startRadius) { mStartRadius = startRadius; return this; } public FullActivityBuilder colorOrImageRes(int colorOrImageRes) { mColorOrImageRes = colorOrImageRes; return this; } public FullActivityBuilder duration(long durationMills) { mDurationMills = durationMills; return this; } public FullActivityBuilder overridePendingTransition(int enterAnim, int exitAnim) { mEnterAnim = enterAnim; mExitAnim = exitAnim; return this; } public void go(OnAnimationEndListener onAnimationEndListener) { mOnAnimationEndListener = onAnimationEndListener; // 版本判断,小于5.0则无动画. if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { doOnEnd(); return; } int[] location = new int[2]; mTriggerView.getLocationInWindow(location); final int cx = location[0] + mTriggerView.getWidth() / 2; final int cy = location[1] + mTriggerView.getHeight() / 2; final ImageView view = new ImageView(mActivity); view.setScaleType(ImageView.ScaleType.CENTER_CROP); view.setImageResource(mColorOrImageRes); final ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView(); int w = decorView.getWidth(); int h = decorView.getHeight(); decorView.addView(view, w, h); int maxW = Math.max(cx, w - cx); int maxH = Math.max(cy, h - cy); final int finalRadius = (int) Math.sqrt(maxW * maxW + maxH * maxH) + 1; try { Animator anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, mStartRadius, finalRadius); int maxRadius = (int) Math.sqrt(w * w + h * h) + 1; if (mDurationMills == null) { double rate = 1d * finalRadius / maxRadius; mDurationMills = (long) (getFullActivityMills() * Math.sqrt(rate)); } final long finalDuration = mDurationMills; anim.setDuration((long) (finalDuration * 0.9)); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); doOnEnd(); mActivity.overridePendingTransition(mEnterAnim, mExitAnim); mTriggerView.postDelayed(new Runnable() { @Override public void run() { if (mActivity.isFinishing()) return; try { Animator anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, finalRadius, mStartRadius); anim.setDuration(finalDuration); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); try { decorView.removeView(view); } catch (Exception e) { e.printStackTrace(); } } }); anim.start(); } catch (Exception e) { e.printStackTrace(); try { decorView.removeView(view); } catch (Exception e1) { e1.printStackTrace(); } } } }, 1000); } }); anim.start(); } catch (Exception e) { e.printStackTrace(); doOnEnd(); } } private void doOnEnd() { mOnAnimationEndListener.onAnimationEnd(); } } } ================================================ 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.Color; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; public class EatBeansView extends BaseView { int eatSpeed = 5; private Paint mPaint, mPaintEye; private float mWidth = 0f; private float mHigh = 0f; private float mPadding = 5f; private float eatErWidth = 60f; private float eatErPositionX = 0f; private float beansWidth = 10f; private float mAngle = 34; private float eatErStartAngle = mAngle; private float eatErEndAngle = 360 - 2 * eatErStartAngle; 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; RectF rectF = new RectF(mPadding + eatErPositionX, mHigh / 2 - eatErWidth / 2, eatRightX, mHigh / 2 + eatErWidth / 2); canvas.drawArc(rectF, 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, mPaint); } } } private void initPaint() { mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(Color.WHITE); mPaintEye = new Paint(); mPaintEye.setAntiAlias(true); mPaintEye.setStyle(Paint.Style.FILL); mPaintEye.setColor(Color.BLACK); } 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/LauncherIconView.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.support.v7.widget.AppCompatImageView; import android.util.AttributeSet; import android.util.Log; import android.view.animation.DecelerateInterpolator; import io.virtualapp.R; import static android.graphics.Canvas.ALL_SAVE_FLAG; public class LauncherIconView extends AppCompatImageView implements ShimmerViewBase { private static final int SMOOTH_ANIM_THRESHOLD = 5; private static final String TAG = "LauncherIconView"; private ShimmerViewHelper mShimmerViewHelper; private Shimmer mShimmer; private float mProgress; private int mHeight; private int mWidth; private int mStrokeWidth; private float mRadius; private float mInterDelta; private int mMaskColor; private float mMaxMaskRadius; private float mMaskAnimDelta; private boolean mIsSquare; private boolean mMaskAnimRunning; private long mMediumAnimTime; private Paint mShimmerPaint; private Paint mPaint; private RectF mProgressOval; private ValueAnimator mInterAnim; private ValueAnimator mProgressAnimator; public LauncherIconView(Context context) { super(context); init(context, null); } public LauncherIconView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public LauncherIconView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { mMediumAnimTime = getContext().getResources().getInteger(android.R.integer.config_mediumAnimTime); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressImageView); try { this.mProgress = a.getInteger(R.styleable.ProgressImageView_pi_progress, 0); this.mStrokeWidth = a.getDimensionPixelOffset(R.styleable.ProgressImageView_pi_stroke, 8); this.mRadius = a.getDimensionPixelOffset(R.styleable.ProgressImageView_pi_radius, 0); this.mIsSquare = a.getBoolean(R.styleable.ProgressImageView_pi_force_square, false); this.mMaskColor = a.getColor(R.styleable.ProgressImageView_pi_mask_color, Color.argb(180, 0, 0, 0)); this.mPaint = new Paint(); mPaint.setColor(mMaskColor); mPaint.setAntiAlias(true); this.mShimmerPaint = new Paint(); mShimmerPaint.setColor(Color.WHITE); } finally { a.recycle(); } mShimmerViewHelper = new ShimmerViewHelper(this, mShimmerPaint, attrs); } private void initParams() { if (mWidth == 0) mWidth = getWidth(); if (mHeight == 0) mHeight = getHeight(); if (mWidth != 0 && mHeight != 0) { if (mRadius == 0) mRadius = Math.min(mWidth, mHeight) / 4f; if (mMaxMaskRadius == 0) mMaxMaskRadius = (float) (0.5f * Math.sqrt(mWidth * mWidth + mHeight * mHeight)); if (mProgressOval == null) mProgressOval = new RectF( mWidth / 2f - mRadius + mStrokeWidth, mHeight / 2f - mRadius + mStrokeWidth, mWidth / 2f + mRadius - mStrokeWidth, mHeight / 2f + mRadius - mStrokeWidth); } } @Override protected void onDraw(Canvas canvas) { if (mShimmerViewHelper != null) { mShimmerViewHelper.onDraw(); } super.onDraw(canvas); int sc = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, ALL_SAVE_FLAG); initParams(); if (mProgress < 100) { drawMask(canvas); if (mProgress == 0) updateInterAnim(canvas); else drawProgress(canvas); } if (mMaskAnimRunning) updateMaskAnim(canvas); canvas.restoreToCount(sc); } private void drawMask(Canvas canvas) { canvas.drawRect(0, 0, mWidth, mHeight, mPaint); } private void drawProgress(Canvas canvas) { mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); canvas.drawCircle(mWidth / 2f, mHeight / 2f, mRadius, mPaint); mPaint.setXfermode(null); //start angle : -90 ~ 270;sweep Angle : 360 ~ 0; canvas.drawArc(mProgressOval, -90 + mProgress * 3.6f, 360 - mProgress * 3.6f, true, mPaint); } private void updateInterAnim(Canvas canvas) { // if (!mInterAnimRunning) mInterDelta = 0.f; //outer circle mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); canvas.drawCircle(mWidth / 2.f, mHeight / 2.f, mRadius, mPaint); mPaint.setXfermode(null); //inner circle canvas.drawCircle(mWidth / 2.f, mHeight / 2.f, mRadius - mInterDelta, mPaint); } private void updateMaskAnim(Canvas canvas) { canvas.drawRect(0, 0, mWidth, mHeight, mPaint); mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); canvas.drawCircle(mWidth / 2f, mHeight / 2f, mRadius + mMaskAnimDelta, mPaint);//mRatio : 0 ~ mRatio * 1.5 mPaint.setXfermode(null); } private void startInterAnim(final int progress) { if (mInterAnim != null) mInterAnim.cancel(); mInterAnim = ValueAnimator.ofFloat(0.f, mStrokeWidth); mInterAnim.setInterpolator(new DecelerateInterpolator()); mInterAnim.setDuration(getContext().getResources().getInteger(android.R.integer.config_shortAnimTime)); mInterAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mInterDelta = (float) animation.getAnimatedValue(); invalidate(); } }); mInterAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); // mInterAnimRunning = true; } @Override public void onAnimationCancel(Animator animation) { super.onAnimationCancel(animation); // mInterAnimRunning = false; } @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); // mInterAnimRunning = false; if (progress > 0) startProgressAnim(0, progress); } }); mInterAnim.start(); } private void startProgressAnim(float from, float to) { if (mProgressAnimator != null) mProgressAnimator.cancel(); final boolean isReverse = from > to; mProgressAnimator = ValueAnimator.ofFloat(from, to); mProgressAnimator.setInterpolator(new DecelerateInterpolator()); mProgressAnimator.setDuration(mMediumAnimTime); mProgressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mProgress = (float) animation.getAnimatedValue(); if (0 < mProgress && mProgress < 100) invalidate(); else if (mProgress == 100 && !isReverse) startMaskAnim(); } }); mProgressAnimator.start(); } private void startMaskAnim() { if (mProgressAnimator != null) mProgressAnimator.cancel(); ValueAnimator animator = ValueAnimator.ofFloat(0.f, mMaxMaskRadius); animator.setInterpolator(new DecelerateInterpolator()); animator.setDuration(mMediumAnimTime); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mMaskAnimRunning = true; mMaskAnimDelta = (float) animation.getAnimatedValue(); invalidate(); } }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); mMaskAnimRunning = true; } @Override public void onAnimationCancel(Animator animation) { super.onAnimationCancel(animation); mMaskAnimRunning = false; } @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mMaskAnimRunning = false; } }); animator.start(); } /** * get the stroke width. * * @return the stroke width in pixel. */ public int getStrokeWidth() { return mStrokeWidth; } /** * set the stroke width.default is 8dp. * * @param strokeWidth stroke width in pixel */ public void setStrokeWidth(int strokeWidth) { this.mStrokeWidth = strokeWidth; this.mProgressOval = null; invalidate(); } /** * get the radius of inner progress circle. * * @return the inner circle radius in pixel. */ public float getRadius() { return mRadius; } /** * set the radius of the inner progress circle. * * @param radius radius in pixel */ public void setRadius(float radius) { this.mRadius = radius; this.mProgressOval = null; invalidate(); } /** * get the color for mask . * * @return the mask color */ public int getMaskColor() { return mMaskColor; } /** * set the color for mask. Argb will looks better. Default is Color.argb(180,0,0,0) * * @param maskColor the color value. */ public void setMaskColor(int maskColor) { mMaskColor = maskColor; mPaint.setColor(mMaskColor); invalidate(); } /** * get current progress. * * @return current progress value. */ public int getProgress() { return (int) mProgress; } /** * @param progress the progress ,range [0,100] */ public void setProgress(int progress) { setProgress(progress, true); } /** * @param progress the progress in [0,100] * @param animate true to enable smooth animation when progress changed more than 5. */ public void setProgress(int progress, boolean animate) { progress = Math.min(Math.max(progress, 0), 100); Log.d(TAG, "setProgress: p:" + progress + ",mp:" + mProgress); if (Math.abs(progress - mProgress) > SMOOTH_ANIM_THRESHOLD && animate) { if (mProgress == 0) { startInterAnim(progress); } else { startProgressAnim(mProgress, progress); } } else if (progress == 100 && animate) { mProgress = 100; startMaskAnim(); } else { mProgress = progress; if (mProgress == 0.f) mInterDelta = 0.f; invalidate(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mIsSquare) { int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); int size = measuredWidth == 0 ? MeasureSpec.getSize(heightMeasureSpec) : measuredWidth; setMeasuredDimension(size, size); } } @Override public float getGradientX() { return mShimmerViewHelper.getGradientX(); } @Override public void setGradientX(float gradientX) { mShimmerViewHelper.setGradientX(gradientX); } @Override public boolean isShimmering() { return mShimmerViewHelper.isShimmering(); } @Override public void setShimmering(boolean isShimmering) { mShimmerViewHelper.setShimmering(isShimmering); } @Override public boolean isSetUp() { return mShimmerViewHelper.isSetUp(); } @Override public void setAnimationSetupCallback(ShimmerViewHelper.AnimationSetupCallback callback) { mShimmerViewHelper.setAnimationSetupCallback(callback); } @Override public int getPrimaryColor() { return mShimmerViewHelper.getPrimaryColor(); } @Override public void setPrimaryColor(int primaryColor) { mShimmerViewHelper.setPrimaryColor(primaryColor); } @Override public int getReflectionColor() { return mShimmerViewHelper.getReflectionColor(); } @Override public void setReflectionColor(int reflectionColor) { mShimmerViewHelper.setReflectionColor(reflectionColor); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (mShimmerViewHelper != null) { mShimmerViewHelper.onSizeChanged(); } } public void stopShimmer() { if (mShimmer != null && mShimmer.isAnimating()) { mShimmer.cancel(); mShimmer = null; } } public void startShimmer() { stopShimmer(); mShimmer = new Shimmer(); mShimmer.setRepeatCount(1) .setStartDelay(800L) .setDirection(Shimmer.ANIMATION_DIRECTION_LTR) .start(this); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/LoadingIndicatorView.java ================================================ package io.virtualapp.widgets; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.AnimationUtils; import io.virtualapp.R; public class LoadingIndicatorView extends View { private static final String TAG = "LoadingIndicatorView"; private static final Indicator DEFAULT_INDICATOR = new BallGridBeatIndicator(); private static final int MIN_SHOW_TIME = 500; // ms private static final int MIN_DELAY = 500; // ms int mMinWidth; int mMaxWidth; int mMinHeight; int mMaxHeight; private long mStartTime = -1; private boolean mPostedHide = false; private boolean mPostedShow = false; private boolean mDismissed = false; private Indicator mIndicator; private int mIndicatorColor; private boolean mShouldStartAnimationDrawable; private final Runnable mDelayedHide = new Runnable() { @Override public void run() { mPostedHide = false; mStartTime = -1; setVisibility(View.GONE); } }; private final Runnable mDelayedShow = new Runnable() { @Override public void run() { mPostedShow = false; if (!mDismissed) { mStartTime = System.currentTimeMillis(); setVisibility(View.VISIBLE); } } }; public LoadingIndicatorView(Context context) { super(context); init(context, null, 0, 0); } public LoadingIndicatorView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0, R.style.AVLoadingIndicatorView); } public LoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr, R.style.AVLoadingIndicatorView); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public LoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs, defStyleAttr, R.style.AVLoadingIndicatorView); } private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { mMinWidth = 24; mMaxWidth = 48; mMinHeight = 24; mMaxHeight = 48; final TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.LoadingIndicatorView, defStyleAttr, defStyleRes); mMinWidth = a.getDimensionPixelSize(R.styleable.LoadingIndicatorView_minWidth, mMinWidth); mMaxWidth = a.getDimensionPixelSize(R.styleable.LoadingIndicatorView_maxWidth, mMaxWidth); mMinHeight = a.getDimensionPixelSize(R.styleable.LoadingIndicatorView_minHeight, mMinHeight); mMaxHeight = a.getDimensionPixelSize(R.styleable.LoadingIndicatorView_maxHeight, mMaxHeight); String indicatorName = a.getString(R.styleable.LoadingIndicatorView_indicatorName); mIndicatorColor = a.getColor(R.styleable.LoadingIndicatorView_indicatorColor, Color.WHITE); setIndicator(indicatorName); if (mIndicator == null) { setIndicator(DEFAULT_INDICATOR); } a.recycle(); } public Indicator getIndicator() { return mIndicator; } /** * You should pay attention to pass this parameter with two way: * for example: * 1. Only class Name,like "SimpleIndicator".(This way would use default package name with * "com.wang.avi.indicators") * 2. Class name with full package,like "com.my.android.indicators.SimpleIndicator". * * @param indicatorName the class must be extend Indicator . */ public void setIndicator(String indicatorName) { if (TextUtils.isEmpty(indicatorName)) { return; } StringBuilder drawableClassName = new StringBuilder(); if (!indicatorName.contains(".")) { String defaultPackageName = getClass().getPackage().getName(); drawableClassName.append(defaultPackageName) .append("."); } drawableClassName.append(indicatorName); try { Class drawableClass = Class.forName(drawableClassName.toString()); Indicator indicator = (Indicator) drawableClass.newInstance(); setIndicator(indicator); } catch (ClassNotFoundException e) { Log.e(TAG, "Didn't find your class , check the name again !"); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } public void setIndicator(Indicator d) { if (mIndicator != d) { if (mIndicator != null) { mIndicator.setCallback(null); unscheduleDrawable(mIndicator); } mIndicator = d; //need to set indicator color again if you didn't specified when you update the indicator . setIndicatorColor(mIndicatorColor); if (d != null) { d.setCallback(this); } postInvalidate(); } } /** * setIndicatorColor(0xFF00FF00) * or * setIndicatorColor(Color.BLUE) * or * setIndicatorColor(Color.parseColor("#FF4081")) * or * setIndicatorColor(0xFF00FF00) * or * setIndicatorColor(getResources().getColor(android.R.color.black)) * * @param color */ public void setIndicatorColor(int color) { this.mIndicatorColor = color; mIndicator.setColor(color); } public void smoothToShow() { startAnimation(AnimationUtils.loadAnimation(getContext(), android.R.anim.fade_in)); setVisibility(VISIBLE); } public void smoothToHide() { startAnimation(AnimationUtils.loadAnimation(getContext(), android.R.anim.fade_out)); setVisibility(GONE); } public void hide() { mDismissed = true; removeCallbacks(mDelayedShow); long diff = System.currentTimeMillis() - mStartTime; if (diff >= MIN_SHOW_TIME || mStartTime == -1) { // The progress spinner has been shown long enough // OR was not shown yet. If it wasn't shown yet, // it will just never be shown. setVisibility(View.GONE); } else { // The progress spinner is shown, but not long enough, // so put a delayed message in to hide it when its been // shown long enough. if (!mPostedHide) { postDelayed(mDelayedHide, MIN_SHOW_TIME - diff); mPostedHide = true; } } } public void show() { // Reset the start time. mStartTime = -1; mDismissed = false; removeCallbacks(mDelayedHide); if (!mPostedShow) { postDelayed(mDelayedShow, MIN_DELAY); mPostedShow = true; } } @Override protected boolean verifyDrawable(Drawable who) { return who == mIndicator || super.verifyDrawable(who); } void startAnimation() { if (getVisibility() != VISIBLE) { return; } if (mIndicator instanceof Animatable) { mShouldStartAnimationDrawable = true; } postInvalidate(); } void stopAnimation() { if (mIndicator instanceof Animatable) { mIndicator.stop(); mShouldStartAnimationDrawable = false; } postInvalidate(); } @Override public void setVisibility(int v) { if (getVisibility() != v) { super.setVisibility(v); if (v == GONE || v == INVISIBLE) { stopAnimation(); } else { startAnimation(); } } } @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (visibility == GONE || visibility == INVISIBLE) { stopAnimation(); } else { startAnimation(); } } @Override public void invalidateDrawable(Drawable dr) { if (verifyDrawable(dr)) { final Rect dirty = dr.getBounds(); final int scrollX = getScrollX() + getPaddingLeft(); final int scrollY = getScrollY() + getPaddingTop(); invalidate(dirty.left + scrollX, dirty.top + scrollY, dirty.right + scrollX, dirty.bottom + scrollY); } else { super.invalidateDrawable(dr); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { updateDrawableBounds(w, h); } private void updateDrawableBounds(int w, int h) { // onDraw will translate the canvas so we draw starting at 0,0. // Subtract out padding for the purposes of the calculations below. w -= getPaddingRight() + getPaddingLeft(); h -= getPaddingTop() + getPaddingBottom(); int right = w; int bottom = h; int top = 0; int left = 0; if (mIndicator != null) { // Maintain aspect ratio. Certain kinds of animated drawables // get very confused otherwise. final int intrinsicWidth = mIndicator.getIntrinsicWidth(); final int intrinsicHeight = mIndicator.getIntrinsicHeight(); final float intrinsicAspect = (float) intrinsicWidth / intrinsicHeight; final float boundAspect = (float) w / h; if (intrinsicAspect != boundAspect) { if (boundAspect > intrinsicAspect) { // New width is larger. Make it smaller to match height. final int width = (int) (h * intrinsicAspect); left = (w - width) / 2; right = left + width; } else { // New height is larger. Make it smaller to match width. final int height = (int) (w * (1 / intrinsicAspect)); top = (h - height) / 2; bottom = top + height; } } mIndicator.setBounds(left, top, right, bottom); } } @Override protected synchronized void onDraw(Canvas canvas) { super.onDraw(canvas); drawTrack(canvas); } void drawTrack(Canvas canvas) { final Drawable d = mIndicator; if (d != null) { // Translate canvas so a indeterminate circular progress bar with padding // rotates properly in its animation final int saveCount = canvas.save(); canvas.translate(getPaddingLeft(), getPaddingTop()); d.draw(canvas); canvas.restoreToCount(saveCount); if (mShouldStartAnimationDrawable && d instanceof Animatable) { ((Animatable) d).start(); mShouldStartAnimationDrawable = false; } } } @Override protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int dw = 0; int dh = 0; final Drawable d = mIndicator; if (d != null) { dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth())); dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight())); } updateDrawableState(); dw += getPaddingLeft() + getPaddingRight(); dh += getPaddingTop() + getPaddingBottom(); final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0); final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0); setMeasuredDimension(measuredWidth, measuredHeight); } @Override protected void drawableStateChanged() { super.drawableStateChanged(); updateDrawableState(); } private void updateDrawableState() { final int[] state = getDrawableState(); if (mIndicator != null && mIndicator.isStateful()) { mIndicator.setState(state); } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void drawableHotspotChanged(float x, float y) { super.drawableHotspotChanged(x, y); if (mIndicator != null) { mIndicator.setHotspot(x, y); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); startAnimation(); removeCallbacks(); } @Override protected void onDetachedFromWindow() { stopAnimation(); // This should come after stopAnimation(), otherwise an invalidate message remains in the // queue, which can prevent the entire view hierarchy from being GC'ed during a rotation super.onDetachedFromWindow(); removeCallbacks(); } private void removeCallbacks() { removeCallbacks(mDelayedHide); removeCallbacks(mDelayedShow); } } ================================================ 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/MaterialRippleLayout.java ================================================ package io.virtualapp.widgets; 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.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; import android.util.Property; import android.util.TypedValue; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.LinearInterpolator; import android.widget.AdapterView; import android.widget.FrameLayout; import io.virtualapp.R; import static android.view.GestureDetector.SimpleOnGestureListener; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; public class MaterialRippleLayout extends FrameLayout { private static final int DEFAULT_DURATION = 350; private static final int DEFAULT_FADE_DURATION = 75; private static final float DEFAULT_DIAMETER_DP = 35; private static final float DEFAULT_ALPHA = 0.2f; private static final int DEFAULT_COLOR = Color.BLACK; private static final int DEFAULT_BACKGROUND = Color.TRANSPARENT; private static final boolean DEFAULT_HOVER = true; private static final boolean DEFAULT_DELAY_CLICK = true; private static final boolean DEFAULT_PERSISTENT = false; private static final boolean DEFAULT_SEARCH_ADAPTER = false; private static final boolean DEFAULT_RIPPLE_OVERLAY = false; private static final int DEFAULT_ROUNDED_CORNERS = 0; private static final int FADE_EXTRA_DELAY = 50; private static final long HOVER_DURATION = 2500; private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Rect bounds = new Rect(); private int rippleColor; private boolean rippleOverlay; private boolean rippleHover; private int rippleDiameter; private int rippleDuration; private int rippleAlpha; private boolean rippleDelayClick; private int rippleFadeDuration; private boolean ripplePersistent; private Drawable rippleBackground; private boolean rippleInAdapter; private float rippleRoundedCorners; private float radius; private AdapterView parentAdapter; private View childView; private AnimatorSet rippleAnimator; private ObjectAnimator hoverAnimator; private Point currentCoords = new Point(); private Point previousCoords = new Point(); private int layerType; private boolean eventCancelled; private boolean prepressed; private int positionInAdapter; private GestureDetector gestureDetector; private PerformClickEvent pendingClickEvent; private PressedEvent pendingPressEvent; private boolean hasPerformedLongPress; /* * Animations */ private Property radiusProperty = new Property(Float.class, "radius") { @Override public Float get(MaterialRippleLayout object) { return object.getRadius(); } @Override public void set(MaterialRippleLayout object, Float value) { object.setRadius(value); } }; private Property circleAlphaProperty = new Property(Integer.class, "rippleAlpha") { @Override public Integer get(MaterialRippleLayout object) { return object.getRippleAlpha(); } @Override public void set(MaterialRippleLayout object, Integer value) { object.setRippleAlpha(value); } }; private SimpleOnGestureListener longClickListener = new GestureDetector.SimpleOnGestureListener() { public void onLongPress(MotionEvent e) { hasPerformedLongPress = childView.performLongClick(); if (hasPerformedLongPress) { if (rippleHover) { startRipple(null); } cancelPressedEvent(); } } @Override public boolean onDown(MotionEvent e) { hasPerformedLongPress = false; return super.onDown(e); } }; public MaterialRippleLayout(Context context) { this(context, null, 0); } public MaterialRippleLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MaterialRippleLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setWillNotDraw(false); gestureDetector = new GestureDetector(context, longClickListener); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MaterialRippleLayout); rippleColor = a.getColor(R.styleable.MaterialRippleLayout_mrl_rippleColor, DEFAULT_COLOR); rippleDiameter = a.getDimensionPixelSize( R.styleable.MaterialRippleLayout_mrl_rippleDimension, (int) dpToPx(getResources(), DEFAULT_DIAMETER_DP) ); rippleOverlay = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleOverlay, DEFAULT_RIPPLE_OVERLAY); rippleHover = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleHover, DEFAULT_HOVER); rippleDuration = a.getInt(R.styleable.MaterialRippleLayout_mrl_rippleDuration, DEFAULT_DURATION); rippleAlpha = (int) (255 * a.getFloat(R.styleable.MaterialRippleLayout_mrl_rippleAlpha, DEFAULT_ALPHA)); rippleDelayClick = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleDelayClick, DEFAULT_DELAY_CLICK); rippleFadeDuration = a.getInteger(R.styleable.MaterialRippleLayout_mrl_rippleFadeDuration, DEFAULT_FADE_DURATION); rippleBackground = new ColorDrawable(a.getColor(R.styleable.MaterialRippleLayout_mrl_rippleBackground, DEFAULT_BACKGROUND)); ripplePersistent = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_ripplePersistent, DEFAULT_PERSISTENT); rippleInAdapter = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleInAdapter, DEFAULT_SEARCH_ADAPTER); rippleRoundedCorners = a.getDimensionPixelSize(R.styleable.MaterialRippleLayout_mrl_rippleRoundedCorners, DEFAULT_ROUNDED_CORNERS); a.recycle(); paint.setColor(rippleColor); paint.setAlpha(rippleAlpha); enableClipPathSupportIfNecessary(); } public static RippleBuilder on(View view) { return new RippleBuilder(view); } static float dpToPx(Resources resources, float dp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.getDisplayMetrics()); } @SuppressWarnings("unchecked") public T getChildView() { return (T) childView; } @Override public final void addView(View child, int index, ViewGroup.LayoutParams params) { if (getChildCount() > 0) { throw new IllegalStateException("MaterialRippleLayout can host only one child"); } //noinspection unchecked childView = child; super.addView(child, index, params); } @Override public void setOnClickListener(OnClickListener onClickListener) { if (childView == null) { throw new IllegalStateException("MaterialRippleLayout must have a child view to handle clicks"); } childView.setOnClickListener(onClickListener); } @Override public void setOnLongClickListener(OnLongClickListener onClickListener) { if (childView == null) { throw new IllegalStateException("MaterialRippleLayout must have a child view to handle clicks"); } childView.setOnLongClickListener(onClickListener); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { return !findClickableViewInChild(childView, (int) event.getX(), (int) event.getY()); } @Override public boolean onTouchEvent(MotionEvent event) { boolean superOnTouchEvent = super.onTouchEvent(event); if (!isEnabled() || !childView.isEnabled()) return superOnTouchEvent; boolean isEventInBounds = bounds.contains((int) event.getX(), (int) event.getY()); if (isEventInBounds) { previousCoords.set(currentCoords.x, currentCoords.y); currentCoords.set((int) event.getX(), (int) event.getY()); } boolean gestureResult = gestureDetector.onTouchEvent(event); if (gestureResult || hasPerformedLongPress) { return true; } else { int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_UP: pendingClickEvent = new PerformClickEvent(); if (prepressed) { childView.setPressed(true); postDelayed( new Runnable() { @Override public void run() { childView.setPressed(false); } }, ViewConfiguration.getPressedStateDuration()); } if (isEventInBounds) { startRipple(pendingClickEvent); } else if (!rippleHover) { setRadius(0); } if (!rippleDelayClick && isEventInBounds) { pendingClickEvent.run(); } cancelPressedEvent(); break; case MotionEvent.ACTION_DOWN: setPositionInAdapter(); eventCancelled = false; pendingPressEvent = new PressedEvent(event); if (isInScrollingContainer()) { cancelPressedEvent(); prepressed = true; postDelayed(pendingPressEvent, ViewConfiguration.getTapTimeout()); } else { pendingPressEvent.run(); } break; case MotionEvent.ACTION_CANCEL: if (rippleInAdapter) { // dont use current coords in adapter since they tend to jump drastically on scroll currentCoords.set(previousCoords.x, previousCoords.y); previousCoords = new Point(); } childView.onTouchEvent(event); if (rippleHover) { if (!prepressed) { startRipple(null); } } else { childView.setPressed(false); } cancelPressedEvent(); break; case MotionEvent.ACTION_MOVE: if (rippleHover) { if (isEventInBounds && !eventCancelled) { invalidate(); } else if (!isEventInBounds) { startRipple(null); } } if (!isEventInBounds) { cancelPressedEvent(); if (hoverAnimator != null) { hoverAnimator.cancel(); } childView.onTouchEvent(event); eventCancelled = true; } break; } return true; } } private void cancelPressedEvent() { if (pendingPressEvent != null) { removeCallbacks(pendingPressEvent); prepressed = false; } } private void startHover() { if (eventCancelled) return; if (hoverAnimator != null) { hoverAnimator.cancel(); } final float radius = (float) (Math.sqrt(Math.pow(getWidth(), 2) + Math.pow(getHeight(), 2)) * 1.2f); hoverAnimator = ObjectAnimator.ofFloat(this, radiusProperty, rippleDiameter, radius) .setDuration(HOVER_DURATION); hoverAnimator.setInterpolator(new LinearInterpolator()); hoverAnimator.start(); } private void startRipple(final Runnable animationEndRunnable) { if (eventCancelled) return; float endRadius = getEndRadius(); cancelAnimations(); rippleAnimator = new AnimatorSet(); rippleAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (!ripplePersistent) { setRadius(0); setRippleAlpha(rippleAlpha); } if (animationEndRunnable != null && rippleDelayClick) { animationEndRunnable.run(); } childView.setPressed(false); } }); ObjectAnimator ripple = ObjectAnimator.ofFloat(this, radiusProperty, radius, endRadius); ripple.setDuration(rippleDuration); ripple.setInterpolator(new DecelerateInterpolator()); ObjectAnimator fade = ObjectAnimator.ofInt(this, circleAlphaProperty, rippleAlpha, 0); fade.setDuration(rippleFadeDuration); fade.setInterpolator(new AccelerateInterpolator()); fade.setStartDelay(rippleDuration - rippleFadeDuration - FADE_EXTRA_DELAY); if (ripplePersistent) { rippleAnimator.play(ripple); } else if (getRadius() > endRadius) { fade.setStartDelay(0); rippleAnimator.play(fade); } else { rippleAnimator.playTogether(ripple, fade); } rippleAnimator.start(); } private void cancelAnimations() { if (rippleAnimator != null) { rippleAnimator.cancel(); rippleAnimator.removeAllListeners(); } if (hoverAnimator != null) { hoverAnimator.cancel(); } } private float getEndRadius() { final int width = getWidth(); final int height = getHeight(); final int halfWidth = width / 2; final int halfHeight = height / 2; final float radiusX = halfWidth > currentCoords.x ? width - currentCoords.x : currentCoords.x; final float radiusY = halfHeight > currentCoords.y ? height - currentCoords.y : currentCoords.y; return (float) Math.sqrt(Math.pow(radiusX, 2) + Math.pow(radiusY, 2)) * 1.2f; } private boolean isInScrollingContainer() { ViewParent p = getParent(); while (p != null && p instanceof ViewGroup) { if (((ViewGroup) p).shouldDelayChildPressedState()) { return true; } p = p.getParent(); } return false; } private AdapterView findParentAdapterView() { if (parentAdapter != null) { return parentAdapter; } ViewParent current = getParent(); while (true) { if (current instanceof AdapterView) { parentAdapter = (AdapterView) current; return parentAdapter; } else { try { current = current.getParent(); } catch (NullPointerException npe) { throw new RuntimeException("Could not find a parent AdapterView"); } } } } private void setPositionInAdapter() { if (rippleInAdapter) { positionInAdapter = findParentAdapterView().getPositionForView(MaterialRippleLayout.this); } } private boolean adapterPositionChanged() { if (rippleInAdapter) { int newPosition = findParentAdapterView().getPositionForView(MaterialRippleLayout.this); final boolean changed = newPosition != positionInAdapter; positionInAdapter = newPosition; if (changed) { cancelPressedEvent(); cancelAnimations(); childView.setPressed(false); setRadius(0); } return changed; } return false; } private boolean findClickableViewInChild(View view, int x, int y) { if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) { View child = viewGroup.getChildAt(i); final Rect rect = new Rect(); child.getHitRect(rect); final boolean contains = rect.contains(x, y); if (contains) { return findClickableViewInChild(child, x - rect.left, y - rect.top); } } } else if (view != childView) { return (view.isEnabled() && (view.isClickable() || view.isLongClickable() || view.isFocusableInTouchMode())); } return view.isFocusableInTouchMode(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); bounds.set(0, 0, w, h); rippleBackground.setBounds(bounds); } @Override public boolean isInEditMode() { return true; } /* * Drawing */ @Override public void draw(Canvas canvas) { final boolean positionChanged = adapterPositionChanged(); if (rippleOverlay) { if (!positionChanged) { rippleBackground.draw(canvas); } super.draw(canvas); if (!positionChanged) { if (rippleRoundedCorners != 0) { Path clipPath = new Path(); RectF rect = new RectF(0, 0, canvas.getWidth(), canvas.getHeight()); clipPath.addRoundRect(rect, rippleRoundedCorners, rippleRoundedCorners, Path.Direction.CW); canvas.clipPath(clipPath); } canvas.drawCircle(currentCoords.x, currentCoords.y, radius, paint); } } else { if (!positionChanged) { rippleBackground.draw(canvas); canvas.drawCircle(currentCoords.x, currentCoords.y, radius, paint); } super.draw(canvas); } } private float getRadius() { return radius; } public void setRadius(float radius) { this.radius = radius; invalidate(); } public int getRippleAlpha() { return paint.getAlpha(); } public void setRippleAlpha(Integer rippleAlpha) { paint.setAlpha(rippleAlpha); invalidate(); } /* * Accessor */ public void setRippleColor(int rippleColor) { this.rippleColor = rippleColor; paint.setColor(rippleColor); paint.setAlpha(rippleAlpha); invalidate(); } public void setRippleOverlay(boolean rippleOverlay) { this.rippleOverlay = rippleOverlay; } public void setRippleDiameter(int rippleDiameter) { this.rippleDiameter = rippleDiameter; } public void setRippleDuration(int rippleDuration) { this.rippleDuration = rippleDuration; } public void setRippleBackground(int color) { rippleBackground = new ColorDrawable(color); rippleBackground.setBounds(bounds); invalidate(); } public void setRippleHover(boolean rippleHover) { this.rippleHover = rippleHover; } public void setRippleDelayClick(boolean rippleDelayClick) { this.rippleDelayClick = rippleDelayClick; } public void setRippleFadeDuration(int rippleFadeDuration) { this.rippleFadeDuration = rippleFadeDuration; } public void setRipplePersistent(boolean ripplePersistent) { this.ripplePersistent = ripplePersistent; } public void setRippleInAdapter(boolean rippleInAdapter) { this.rippleInAdapter = rippleInAdapter; } public void setRippleRoundedCorners(int rippleRoundedCorner) { this.rippleRoundedCorners = rippleRoundedCorner; enableClipPathSupportIfNecessary(); } public void setDefaultRippleAlpha(float alpha) { this.rippleAlpha = (int) (255 * alpha); paint.setAlpha(rippleAlpha); invalidate(); } public void performRipple() { currentCoords = new Point(getWidth() / 2, getHeight() / 2); startRipple(null); } public void performRipple(Point anchor) { currentCoords = new Point(anchor.x, anchor.y); startRipple(null); } /** * {@link Canvas#clipPath(Path)} is not supported in hardware accelerated layers * before API 18. Use software layer instead *

* https://developer.android.com/guide/topics/graphics/hardware-accel.html#unsupported */ private void enableClipPathSupportIfNecessary() { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (rippleRoundedCorners != 0) { layerType = getLayerType(); setLayerType(LAYER_TYPE_SOFTWARE, null); } else { setLayerType(layerType, null); } } } public static class RippleBuilder { private final Context context; private final View child; private int rippleColor = DEFAULT_COLOR; private boolean rippleOverlay = DEFAULT_RIPPLE_OVERLAY; private boolean rippleHover = DEFAULT_HOVER; private float rippleDiameter = DEFAULT_DIAMETER_DP; private int rippleDuration = DEFAULT_DURATION; private float rippleAlpha = DEFAULT_ALPHA; private boolean rippleDelayClick = DEFAULT_DELAY_CLICK; private int rippleFadeDuration = DEFAULT_FADE_DURATION; private boolean ripplePersistent = DEFAULT_PERSISTENT; private int rippleBackground = DEFAULT_BACKGROUND; private boolean rippleSearchAdapter = DEFAULT_SEARCH_ADAPTER; private float rippleRoundedCorner = DEFAULT_ROUNDED_CORNERS; public RippleBuilder(View child) { this.child = child; this.context = child.getContext(); } public RippleBuilder rippleColor(int color) { this.rippleColor = color; return this; } public RippleBuilder rippleOverlay(boolean overlay) { this.rippleOverlay = overlay; return this; } public RippleBuilder rippleHover(boolean hover) { this.rippleHover = hover; return this; } public RippleBuilder rippleDiameterDp(int diameterDp) { this.rippleDiameter = diameterDp; return this; } public RippleBuilder rippleDuration(int duration) { this.rippleDuration = duration; return this; } public RippleBuilder rippleAlpha(float alpha) { this.rippleAlpha = alpha; return this; } public RippleBuilder rippleDelayClick(boolean delayClick) { this.rippleDelayClick = delayClick; return this; } public RippleBuilder rippleFadeDuration(int fadeDuration) { this.rippleFadeDuration = fadeDuration; return this; } public RippleBuilder ripplePersistent(boolean persistent) { this.ripplePersistent = persistent; return this; } public RippleBuilder rippleBackground(int color) { this.rippleBackground = color; return this; } public RippleBuilder rippleInAdapter(boolean inAdapter) { this.rippleSearchAdapter = inAdapter; return this; } public RippleBuilder rippleRoundedCorners(int radiusDp) { this.rippleRoundedCorner = radiusDp; return this; } public MaterialRippleLayout create() { MaterialRippleLayout layout = new MaterialRippleLayout(context); layout.setRippleColor(rippleColor); layout.setDefaultRippleAlpha(rippleAlpha); layout.setRippleDelayClick(rippleDelayClick); layout.setRippleDiameter((int) dpToPx(context.getResources(), rippleDiameter)); layout.setRippleDuration(rippleDuration); layout.setRippleFadeDuration(rippleFadeDuration); layout.setRippleHover(rippleHover); layout.setRipplePersistent(ripplePersistent); layout.setRippleOverlay(rippleOverlay); layout.setRippleBackground(rippleBackground); layout.setRippleInAdapter(rippleSearchAdapter); layout.setRippleRoundedCorners((int) dpToPx(context.getResources(), rippleRoundedCorner)); ViewGroup.LayoutParams params = child.getLayoutParams(); ViewGroup parent = (ViewGroup) child.getParent(); int index = 0; if (parent != null && parent instanceof MaterialRippleLayout) { throw new IllegalStateException("MaterialRippleLayout could not be created: parent of the view already is a MaterialRippleLayout"); } if (parent != null) { index = parent.indexOfChild(child); parent.removeView(child); } layout.addView(child, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); if (parent != null) { parent.addView(layout, index, params); } return layout; } } /* * Helper */ private class PerformClickEvent implements Runnable { @Override public void run() { if (hasPerformedLongPress) return; // if parent is an AdapterView, try to call its ItemClickListener if (getParent() instanceof AdapterView) { // try clicking direct child first if (!childView.performClick()) // if it did not handle it dispatch to adapterView clickAdapterView((AdapterView) getParent()); } else if (rippleInAdapter) { // find adapter view clickAdapterView(findParentAdapterView()); } else { // otherwise, just perform click on child childView.performClick(); } } private void clickAdapterView(AdapterView parent) { final int position = parent.getPositionForView(MaterialRippleLayout.this); final long itemId = parent.getAdapter() != null ? parent.getAdapter().getItemId(position) : 0; if (position != AdapterView.INVALID_POSITION) { parent.performItemClick(MaterialRippleLayout.this, position, itemId); } } } /* * Builder */ private final class PressedEvent implements Runnable { private final MotionEvent event; public PressedEvent(MotionEvent event) { this.event = event; } @Override public void run() { prepressed = false; childView.setLongClickable(false);//prevent the child's long click,let's the ripple layout call it's performLongClick childView.onTouchEvent(event); childView.setPressed(true); if (rippleHover) { startHover(); } } } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/RippleButton.java ================================================ package io.virtualapp.widgets; import android.annotation.SuppressLint; 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.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.Shader; import android.os.Build; import android.support.v7.widget.AppCompatButton; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.animation.AccelerateDecelerateInterpolator; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.ObjectAnimator; import com.nineoldandroids.view.ViewHelper; import io.virtualapp.R; @SuppressLint("ClickableViewAccessibility") public class RippleButton extends AppCompatButton { private float mDownX; private float mDownY; private float mAlphaFactor; private float mDensity; private float mRadius; private float mMaxRadius; private int mRippleColor; private boolean mIsAnimating = false; private boolean mHover = true; private RadialGradient mRadialGradient; private Paint mPaint; private ObjectAnimator mRadiusAnimator; private boolean mAnimationIsCancel; private Rect mRect; private Path mPath = new Path(); public RippleButton(Context context) { this(context, null); } public RippleButton(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RippleButton(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RippleButton); mRippleColor = a.getColor(R.styleable.RippleButton_rippleColor, mRippleColor); mAlphaFactor = a.getFloat(R.styleable.RippleButton_alphaFactor, mAlphaFactor); mHover = a.getBoolean(R.styleable.RippleButton_hover, mHover); a.recycle(); } private int dp(int dp) { return (int) (dp * mDensity + 0.5f); } public void init() { mDensity = getContext().getResources().getDisplayMetrics().density; mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setAlpha(100); setRippleColor(Color.BLACK, 0.2f); } public void setRippleColor(int rippleColor, float alphaFactor) { mRippleColor = rippleColor; mAlphaFactor = alphaFactor; } public void setHover(boolean enabled) { mHover = enabled; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mMaxRadius = (float) Math.sqrt(w * w + h * h); } @Override public boolean onTouchEvent(final MotionEvent event) { Log.d("TouchEvent", String.valueOf(event.getActionMasked())); Log.d("mIsAnimating", String.valueOf(mIsAnimating)); Log.d("mAnimationIsCancel", String.valueOf(mAnimationIsCancel)); boolean superResult = super.onTouchEvent(event); if (event.getActionMasked() == MotionEvent.ACTION_DOWN && this.isEnabled() && mHover) { mRect = new Rect(getLeft(), getTop(), getRight(), getBottom()); mAnimationIsCancel = false; mDownX = event.getX(); mDownY = event.getY(); mRadiusAnimator = ObjectAnimator.ofFloat(this, "radius", 0, dp(50)) .setDuration(400); mRadiusAnimator .setInterpolator(new AccelerateDecelerateInterpolator()); mRadiusAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { mIsAnimating = true; } @Override public void onAnimationEnd(Animator animator) { setRadius(0); ViewHelper.setAlpha(RippleButton.this, 1); mIsAnimating = false; } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); mRadiusAnimator.start(); if (!superResult) { return true; } } else if (event.getActionMasked() == MotionEvent.ACTION_MOVE && this.isEnabled() && mHover) { mDownX = event.getX(); mDownY = event.getY(); // Cancel the ripple animation when moved outside if (mAnimationIsCancel = !mRect.contains( getLeft() + (int) event.getX(), getTop() + (int) event.getY())) { setRadius(0); } else { setRadius(dp(50)); } if (!superResult) { return true; } } else if (event.getActionMasked() == MotionEvent.ACTION_UP && !mAnimationIsCancel && this.isEnabled()) { mDownX = event.getX(); mDownY = event.getY(); final float tempRadius = (float) Math.sqrt(mDownX * mDownX + mDownY * mDownY); float targetRadius = Math.max(tempRadius, mMaxRadius); if (mIsAnimating) { mRadiusAnimator.cancel(); } mRadiusAnimator = ObjectAnimator.ofFloat(this, "radius", dp(50), targetRadius); mRadiusAnimator.setDuration(500); mRadiusAnimator .setInterpolator(new AccelerateDecelerateInterpolator()); mRadiusAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { mIsAnimating = true; } @Override public void onAnimationEnd(Animator animator) { setRadius(0); ViewHelper.setAlpha(RippleButton.this, 1); mIsAnimating = false; } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); mRadiusAnimator.start(); if (!superResult) { return true; } } return superResult; } public int adjustAlpha(int color, float factor) { int alpha = Math.round(Color.alpha(color) * factor); int red = Color.red(color); int green = Color.green(color); int blue = Color.blue(color); return Color.argb(alpha, red, green, blue); } public void setRadius(final float radius) { mRadius = radius; if (mRadius > 0) { mRadialGradient = new RadialGradient(mDownX, mDownY, mRadius, adjustAlpha(mRippleColor, mAlphaFactor), mRippleColor, Shader.TileMode.MIRROR); mPaint.setShader(mRadialGradient); } invalidate(); } @Override protected void onDraw(final Canvas canvas) { super.onDraw(canvas); if (isInEditMode()) { return; } canvas.save(Canvas.CLIP_SAVE_FLAG); mPath.reset(); mPath.addCircle(mDownX, mDownY, mRadius, Path.Direction.CW); canvas.clipPath(mPath); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) canvas.restore(); canvas.drawCircle(mDownX, mDownY, mRadius, mPaint); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/ShadowProperty.java ================================================ package io.virtualapp.widgets; public class ShadowProperty { public static final int ALL = 0x1111; public static final int LEFT = 0x0001; public static final int TOP = 0x0010; public static final int RIGHT = 0x0100; public static final int BOTTOM = 0x1000; /** * 阴影颜色 */ private int shadowColor; /** * 阴影半径 */ private int shadowRadius; /** * 阴影x偏移 */ private int shadowDx; /** * 阴影y偏移 */ private int shadowDy; /** * 阴影边 */ private int shadowSide = ALL; public int getShadowSide() { return shadowSide; } public ShadowProperty setShadowSide(int shadowSide) { this.shadowSide = shadowSide; return this; } public int getShadowOffset() { return getShadowOffsetHalf() * 2; } public int getShadowOffsetHalf() { return 0 >= shadowRadius ? 0 : Math.max(shadowDx, shadowDy) + shadowRadius; } public int getShadowColor() { return shadowColor; } public ShadowProperty setShadowColor(int shadowColor) { this.shadowColor = shadowColor; return this; } public int getShadowRadius() { return shadowRadius; } public ShadowProperty setShadowRadius(int shadowRadius) { this.shadowRadius = shadowRadius; return this; } public int getShadowDx() { return shadowDx; } public ShadowProperty setShadowDx(int shadowDx) { this.shadowDx = shadowDx; return this; } public int getShadowDy() { return shadowDy; } public ShadowProperty setShadowDy(int shadowDy) { this.shadowDy = shadowDy; return this; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/ShadowViewDrawable.java ================================================ package io.virtualapp.widgets; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; public class ShadowViewDrawable extends Drawable { private Paint paint; private RectF bounds = new RectF(); private int width; private int height; private ShadowProperty shadowProperty; private int shadowOffset; private RectF drawRect; private float rx; private float ry; private PorterDuffXfermode srcOut = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT); public ShadowViewDrawable(ShadowProperty shadowProperty, int color, float rx, float ry) { this.shadowProperty = shadowProperty; shadowOffset = this.shadowProperty.getShadowOffset(); this.rx = rx; this.ry = ry; paint = new Paint(); paint.setAntiAlias(true); /** * 解决旋转时的锯齿问题 */ paint.setFilterBitmap(true); paint.setDither(true); paint.setStyle(Paint.Style.FILL); paint.setColor(color); /** * 设置阴影 */ paint.setShadowLayer(shadowProperty.getShadowRadius(), shadowProperty.getShadowDx(), shadowProperty.getShadowDy(), shadowProperty.getShadowColor()); drawRect = new RectF(); } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); if (bounds.right - bounds.left > 0 && bounds.bottom - bounds.top > 0) { this.bounds.left = bounds.left; this.bounds.right = bounds.right; this.bounds.top = bounds.top; this.bounds.bottom = bounds.bottom; width = (int) (this.bounds.right - this.bounds.left); height = (int) (this.bounds.bottom - this.bounds.top); int shadowSide = shadowProperty.getShadowSide(); int left = (shadowSide & ShadowProperty.LEFT) == ShadowProperty.LEFT ? shadowOffset : 0; int top = (shadowSide & ShadowProperty.TOP) == ShadowProperty.TOP ? shadowOffset : 0; int right = width - ((shadowSide & ShadowProperty.RIGHT) == ShadowProperty.RIGHT ? shadowOffset : 0); int bottom = height - ((shadowSide & ShadowProperty.BOTTOM) == ShadowProperty.BOTTOM ? shadowOffset : 0); drawRect = new RectF(left, top, right, bottom); invalidateSelf(); } } @Override public void draw(@NonNull Canvas canvas) { paint.setXfermode(null); canvas.drawRoundRect( drawRect, rx, ry, paint ); paint.setXfermode(srcOut); canvas.drawRoundRect(drawRect, rx, ry, paint); } public ShadowViewDrawable setColor(int color) { paint.setColor(color); return this; } @Override public void setAlpha(int alpha) { } @Override public void setColorFilter(ColorFilter cf) { } @Override public int getOpacity() { return PixelFormat.UNKNOWN; } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/Shimmer.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.os.Build; import android.view.View; public class Shimmer { public static final int ANIMATION_DIRECTION_LTR = 0; public static final int ANIMATION_DIRECTION_RTL = 1; private static final int DEFAULT_REPEAT_COUNT = ValueAnimator.INFINITE; private static final long DEFAULT_DURATION = 1000; private static final long DEFAULT_START_DELAY = 0; private static final int DEFAULT_DIRECTION = ANIMATION_DIRECTION_LTR; private int repeatCount; private long duration; private long startDelay; private int direction; private Animator.AnimatorListener animatorListener; private ObjectAnimator animator; public Shimmer() { repeatCount = DEFAULT_REPEAT_COUNT; duration = DEFAULT_DURATION; startDelay = DEFAULT_START_DELAY; direction = DEFAULT_DIRECTION; } public int getRepeatCount() { return repeatCount; } public Shimmer setRepeatCount(int repeatCount) { this.repeatCount = repeatCount; return this; } public long getDuration() { return duration; } public Shimmer setDuration(long duration) { this.duration = duration; return this; } public long getStartDelay() { return startDelay; } public Shimmer setStartDelay(long startDelay) { this.startDelay = startDelay; return this; } public int getDirection() { return direction; } public Shimmer setDirection(int direction) { if (direction != ANIMATION_DIRECTION_LTR && direction != ANIMATION_DIRECTION_RTL) { throw new IllegalArgumentException("The animation direction must be either ANIMATION_DIRECTION_LTR or ANIMATION_DIRECTION_RTL"); } this.direction = direction; return this; } public Animator.AnimatorListener getAnimatorListener() { return animatorListener; } public Shimmer setAnimatorListener(Animator.AnimatorListener animatorListener) { this.animatorListener = animatorListener; return this; } public void start(final V shimmerView) { if (isAnimating()) { return; } final Runnable animate = new Runnable() { @Override public void run() { shimmerView.setShimmering(true); float fromX = 0; float toX = shimmerView.getWidth(); if (direction == ANIMATION_DIRECTION_RTL) { fromX = shimmerView.getWidth(); toX = 0; } animator = ObjectAnimator.ofFloat(shimmerView, "gradientX", fromX, toX); animator.setRepeatCount(repeatCount); animator.setDuration(duration); animator.setStartDelay(startDelay); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { shimmerView.setShimmering(false); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { shimmerView.postInvalidate(); } else { shimmerView.postInvalidateOnAnimation(); } animator = null; } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); if (animatorListener != null) { animator.addListener(animatorListener); } animator.start(); } }; if (!shimmerView.isSetUp()) { shimmerView.setAnimationSetupCallback(new ShimmerViewHelper.AnimationSetupCallback() { @Override public void onSetupAnimation(final View target) { animate.run(); } }); } else { animate.run(); } } public void cancel() { if (animator != null) { animator.cancel(); } } public boolean isAnimating() { return animator != null && animator.isRunning(); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/ShimmerViewBase.java ================================================ package io.virtualapp.widgets; public interface ShimmerViewBase { float getGradientX(); void setGradientX(float gradientX); boolean isShimmering(); void setShimmering(boolean isShimmering); boolean isSetUp(); void setAnimationSetupCallback(ShimmerViewHelper.AnimationSetupCallback callback); int getPrimaryColor(); void setPrimaryColor(int primaryColor); int getReflectionColor(); void setReflectionColor(int reflectionColor); } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/ShimmerViewHelper.java ================================================ package io.virtualapp.widgets; import android.content.res.TypedArray; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Shader; import android.util.AttributeSet; import android.view.View; import io.virtualapp.R; public class ShimmerViewHelper { private static final int DEFAULT_REFLECTION_COLOR = 0xFFFFFFFF; private View view; private Paint paint; // center position of the gradient private float gradientX; // shader applied on the text view // only null until the first global layout private LinearGradient linearGradient; // shader's local matrix // never null private Matrix linearGradientMatrix; private int primaryColor; // shimmer reflection color private int reflectionColor; // true when animating private boolean isShimmering; // true after first global layout private boolean isSetUp; // callback called after first global layout private AnimationSetupCallback callback; public ShimmerViewHelper(View view, Paint paint, AttributeSet attributeSet) { this.view = view; this.paint = paint; init(attributeSet); } public float getGradientX() { return gradientX; } public void setGradientX(float gradientX) { this.gradientX = gradientX; view.invalidate(); } public boolean isShimmering() { return isShimmering; } public void setShimmering(boolean isShimmering) { this.isShimmering = isShimmering; } public boolean isSetUp() { return isSetUp; } public void setAnimationSetupCallback(AnimationSetupCallback callback) { this.callback = callback; } public int getPrimaryColor() { return primaryColor; } public void setPrimaryColor(int primaryColor) { this.primaryColor = primaryColor; if (isSetUp) { resetLinearGradient(); } } public int getReflectionColor() { return reflectionColor; } public void setReflectionColor(int reflectionColor) { this.reflectionColor = reflectionColor; if (isSetUp) { resetLinearGradient(); } } private void init(AttributeSet attributeSet) { reflectionColor = DEFAULT_REFLECTION_COLOR; if (attributeSet != null) { TypedArray a = view.getContext().obtainStyledAttributes(attributeSet, R.styleable.ShimmerView, 0, 0); if (a != null) { try { reflectionColor = a.getColor(R.styleable.ShimmerView_reflectionColor, DEFAULT_REFLECTION_COLOR); } catch (Exception e) { android.util.Log.e("ShimmerTextView", "Error while creating the view:", e); } finally { a.recycle(); } } } linearGradientMatrix = new Matrix(); } private void resetLinearGradient() { // our gradient is a simple linear gradient from textColor to reflectionColor. its axis is at the center // when it's outside of the view, the outer color (textColor) will be repeated (Shader.TileMode.CLAMP) // initially, the linear gradient is positioned on the left side of the view linearGradient = new LinearGradient(-view.getWidth(), 0, 0, 0, new int[]{ primaryColor, reflectionColor, primaryColor, }, new float[]{ 0, 0.5f, 1 }, Shader.TileMode.CLAMP ); paint.setShader(linearGradient); } protected void onSizeChanged() { resetLinearGradient(); if (!isSetUp) { isSetUp = true; if (callback != null) { callback.onSetupAnimation(view); } } } /** * content of the wrapping view's onDraw(Canvas) * MUST BE CALLED BEFORE SUPER STATEMENT */ public void onDraw() { // only draw the shader gradient over the text while animating if (isShimmering) { // first onDraw() when shimmering if (paint.getShader() == null) { paint.setShader(linearGradient); } // translate the shader local matrix linearGradientMatrix.setTranslate(2 * gradientX, 0); // this is required in order to invalidate the shader's position linearGradient.setLocalMatrix(linearGradientMatrix); } else { // we're not animating, remove the shader from the paint paint.setShader(null); } } public interface AnimationSetupCallback { void onSetupAnimation(View target); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/TwoGearsView.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; public class TwoGearsView extends BaseView { ValueAnimator valueAnimator = null; float mAnimatedValue = 0f; float hypotenuse = 0f; float smallRingCenterX = 0f; float smallRingCenterY = 0f; float bigRingCenterX = 0f; float bigRingCenterY = 0f; private float mWidth = 0f; private Paint mPaint, mPaintAxle; private Paint mPaintRing; private float mPadding = 0f; private float mWheelLength; private int mWheelSmallSpace = 10; private int mWheelBigSpace = 8; public TwoGearsView(Context context) { super(context); } public TwoGearsView(Context context, AttributeSet attrs) { super(context, attrs); } public TwoGearsView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getMeasuredWidth() > getHeight()) mWidth = getMeasuredHeight(); else mWidth = getMeasuredWidth(); } private void drawSmallRing(Canvas canvas) { hypotenuse = (float) (mWidth * Math.sqrt(2)); smallRingCenterX = (float) ((hypotenuse / 6.f) * Math.cos(45 * Math.PI / 180f)); smallRingCenterY = (float) ((hypotenuse / 6.f) * Math.sin(45 * Math.PI / 180f)); mPaintRing.setStrokeWidth(dip2px(1.0f)); canvas.drawCircle(mPadding + smallRingCenterX, smallRingCenterY + mPadding, smallRingCenterX, mPaintRing); mPaintRing.setStrokeWidth(dip2px(1.5f)); canvas.drawCircle(mPadding + smallRingCenterX, smallRingCenterY + mPadding, smallRingCenterX / 2, mPaintRing); } private void drawSmallGear(Canvas canvas) { mPaint.setStrokeWidth(dip2px(1)); for (int i = 0; i < 360; i = i + mWheelSmallSpace) { int angle = (int) (mAnimatedValue * mWheelSmallSpace + i); float x3 = (float) ((smallRingCenterX) * Math.cos(angle * Math.PI / 180f)); float y3 = (float) ((smallRingCenterY) * Math.sin(angle * Math.PI / 180f)); float x4 = (float) ((smallRingCenterX + mWheelLength) * Math.cos(angle * Math.PI / 180f)); float y4 = (float) ((smallRingCenterY + mWheelLength) * Math.sin(angle * Math.PI / 180f)); canvas.drawLine(mPadding + smallRingCenterX - x4, smallRingCenterY + mPadding - y4, smallRingCenterX + mPadding - x3, smallRingCenterY + mPadding - y3, mPaint); } } private void drawBigGear(Canvas canvas) { bigRingCenterX = (float) ((hypotenuse / 2.f) * Math.cos(45 * Math.PI / 180f)); bigRingCenterY = (float) ((hypotenuse / 2.f) * Math.sin(45 * Math.PI / 180f)); float strokeWidth = dip2px(1.5f) / 4; mPaint.setStrokeWidth(dip2px(1.5f)); for (int i = 0; i < 360; i = i + mWheelBigSpace) { int angle = (int) (360 - (mAnimatedValue * mWheelBigSpace + i)); float x3 = (float) ((bigRingCenterX - smallRingCenterX) * Math.cos(angle * Math.PI / 180f)); float y3 = (float) ((bigRingCenterY - smallRingCenterY) * Math.sin(angle * Math.PI / 180f)); float x4 = (float) ((bigRingCenterX - smallRingCenterX + mWheelLength) * Math.cos(angle * Math.PI / 180f)); float y4 = (float) ((bigRingCenterY - smallRingCenterY + mWheelLength) * Math.sin(angle * Math.PI / 180f)); canvas.drawLine(bigRingCenterX + mPadding - x4 + mWheelLength * 2 + strokeWidth, bigRingCenterY + mPadding - y4 + mWheelLength * 2 + strokeWidth, bigRingCenterX + mPadding - x3 + mWheelLength * 2 + strokeWidth, bigRingCenterY + mPadding - y3 + mWheelLength * 2 + strokeWidth, mPaint); } } private void drawBigRing(Canvas canvas) { float strokeWidth = dip2px(1.5f) / 4; mPaintRing.setStrokeWidth(dip2px(1.5f)); canvas.drawCircle(bigRingCenterX + mPadding + mWheelLength * 2 + strokeWidth, bigRingCenterY + mPadding + mWheelLength * 2 + strokeWidth, bigRingCenterX - smallRingCenterX - strokeWidth, mPaintRing); mPaintRing.setStrokeWidth(dip2px(1.5f)); canvas.drawCircle(bigRingCenterX + mPadding + mWheelLength * 2 + strokeWidth, bigRingCenterY + mPadding + mWheelLength * 2 + strokeWidth, (bigRingCenterX - smallRingCenterX) / 2 - strokeWidth, mPaintRing); } private void drawAxle(Canvas canvas) { for (int i = 0; i < 3; i++) { float x3 = (float) ((smallRingCenterX) * Math.cos(i * (360 / 3) * Math.PI / 180f)); float y3 = (float) ((smallRingCenterY) * Math.sin(i * (360 / 3) * Math.PI / 180f)); canvas.drawLine(mPadding + smallRingCenterX, mPadding + smallRingCenterY, mPadding + smallRingCenterX - x3, mPadding + smallRingCenterY - y3, mPaintAxle); } for (int i = 0; i < 3; i++) { float x3 = (float) ((bigRingCenterX - smallRingCenterX) * Math.cos(i * (360 / 3) * Math.PI / 180f)); float y3 = (float) ((bigRingCenterY - smallRingCenterY) * Math.sin(i * (360 / 3) * Math.PI / 180f)); canvas.drawLine(bigRingCenterX + mPadding + mWheelLength * 2, bigRingCenterY + mPadding + mWheelLength * 2, bigRingCenterX + mPadding + mWheelLength * 2 - x3, bigRingCenterY + mPadding + mWheelLength * 2 - y3, mPaintAxle); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPadding = dip2px(5); canvas.save(); canvas.rotate(180, mWidth / 2, mWidth / 2); drawSmallRing(canvas); drawSmallGear(canvas); drawBigGear(canvas); drawBigRing(canvas); drawAxle(canvas); canvas.restore(); } private void initPaint() { mPaintRing = new Paint(); mPaintRing.setAntiAlias(true); mPaintRing.setStyle(Paint.Style.STROKE); mPaintRing.setColor(Color.WHITE); mPaintRing.setStrokeWidth(dip2px(1.5f)); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setColor(Color.WHITE); mPaint.setStrokeWidth(dip2px(1)); mPaintAxle = new Paint(); mPaintAxle.setAntiAlias(true); mPaintAxle.setStyle(Paint.Style.FILL); mPaintAxle.setColor(Color.WHITE); mPaintAxle.setStrokeWidth(dip2px(1.5f)); mWheelLength = dip2px(2f); } public void setViewColor(int color) { mPaint.setColor(color); mPaintAxle.setColor(color); mPaintRing.setColor(color); postInvalidate(); } @Override protected void InitPaint() { initPaint(); } @Override protected void OnAnimationUpdate(ValueAnimator valueAnimator) { mAnimatedValue = (float) valueAnimator.getAnimatedValue(); postInvalidate(); } @Override protected void OnAnimationRepeat(Animator animation) { } @Override protected int OnStopAnim() { postInvalidate(); return 1; } @Override protected int SetAnimRepeatMode() { return ValueAnimator.RESTART; } @Override protected void AnimIsRunning() { } @Override protected int SetAnimRepeatCount() { return ValueAnimator.INFINITE; } private int dip2px(float dpValue) { final float scale = getContext().getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } } ================================================ FILE: VirtualApp/app/src/main/java/io/virtualapp/widgets/ViewHelper.java ================================================ package io.virtualapp.widgets; import io.virtualapp.VApp; /** * @author Lody */ public class ViewHelper { public static int dip2px(float dpValue) { final float scale = VApp.getApp().getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } } ================================================ 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/layout/activity_clone_app.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/layout/activity_home.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/layout/activity_install.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/layout/activity_loading.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/layout/activity_location_settings.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/layout/activity_marker.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/layout/activity_splash.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/layout/activity_users.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/layout/content_toolbar.xml ================================================ ================================================ FILE: VirtualApp/app/src/main/res/layout/fragment_list_app.xml ================================================