Repository: jinkg/Style Branch: master Commit: 05796d467dae Files: 342 Total size: 868.2 KB Directory structure: gitextract_vzylt4jg/ ├── .gitignore ├── README.md ├── build.gradle ├── buildApk.bat ├── buildApk.sh ├── data/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── yalin/ │ │ └── data/ │ │ └── ExampleInstrumentedTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── cpp/ │ │ │ └── facet_id-lib.cpp │ │ ├── java/ │ │ │ └── com/ │ │ │ └── yalin/ │ │ │ └── style/ │ │ │ └── data/ │ │ │ ├── SyncConfig.java │ │ │ ├── cache/ │ │ │ │ ├── AdvanceWallpaperCache.kt │ │ │ │ ├── AdvanceWallpaperCacheImpl.kt │ │ │ │ ├── GalleryWallpaperCache.kt │ │ │ │ ├── SourcesCache.kt │ │ │ │ ├── SourcesCacheImpl.kt │ │ │ │ ├── WallpaperCache.java │ │ │ │ └── WallpaperCacheImpl.java │ │ │ ├── entity/ │ │ │ │ ├── AdvanceWallpaperEntity.java │ │ │ │ ├── DeviceInfo.java │ │ │ │ ├── GalleryWallpaperEntity.kt │ │ │ │ ├── HttpRequestBody.java │ │ │ │ ├── SourceEntity.kt │ │ │ │ ├── WallpaperEntity.java │ │ │ │ └── mapper/ │ │ │ │ ├── AdvanceWallpaperEntityMapper.java │ │ │ │ └── WallpaperEntityMapper.java │ │ │ ├── exception/ │ │ │ │ ├── LikeException.java │ │ │ │ ├── NetworkConnectionException.java │ │ │ │ ├── NoContentException.java │ │ │ │ ├── RemoteServerException.java │ │ │ │ └── ReswitchException.java │ │ │ ├── executor/ │ │ │ │ ├── JobExecutor.java │ │ │ │ └── SerialJobExecutor.java │ │ │ ├── extensions/ │ │ │ │ └── DelegateExt.kt │ │ │ ├── lock/ │ │ │ │ ├── LikeWallpaperLock.java │ │ │ │ ├── OpenInputStreamLock.java │ │ │ │ ├── ResourceLock.java │ │ │ │ └── SelectSourceLock.java │ │ │ ├── log/ │ │ │ │ └── LogUtil.java │ │ │ ├── observable/ │ │ │ │ ├── SourcesObservableImpl.kt │ │ │ │ └── WallpaperObservableImpl.kt │ │ │ ├── repository/ │ │ │ │ ├── AdvanceWallpaperDataRepository.kt │ │ │ │ ├── GalleryWallpaperDataRepository.kt │ │ │ │ ├── SourcesDataRepository.kt │ │ │ │ ├── StyleWallpaperDataRepository.java │ │ │ │ └── datasource/ │ │ │ │ ├── AdvanceWallpaperDataStore.kt │ │ │ │ ├── AdvanceWallpaperDataStoreFactory.kt │ │ │ │ ├── AdvanceWallpaperDataStoreImpl.kt │ │ │ │ ├── CacheWallpaperDataStore.java │ │ │ │ ├── DbWallpaperDataStore.java │ │ │ │ ├── GalleryWallpaperDataStore.kt │ │ │ │ ├── GalleryWallpaperDataStoreFactory.kt │ │ │ │ ├── RemoteAdvanceWallpaperDataStore.kt │ │ │ │ ├── SourcesDataStore.kt │ │ │ │ ├── SourcesDataStoreFactory.kt │ │ │ │ ├── SourcesDataStoreImpl.kt │ │ │ │ ├── StyleWallpaperDataStoreFactory.java │ │ │ │ ├── WallpaperDataStore.java │ │ │ │ ├── io/ │ │ │ │ │ ├── AdvanceWallpaperHandler.kt │ │ │ │ │ ├── GalleryWallpapersHandler.kt │ │ │ │ │ ├── JSONHandler.java │ │ │ │ │ ├── RemoveGalleryWallpapersHandler.kt │ │ │ │ │ └── WallpapersHandler.java │ │ │ │ ├── net/ │ │ │ │ │ ├── DataFetcher.java │ │ │ │ │ └── RemoteAdvanceWallpaperFetcher.java │ │ │ │ ├── provider/ │ │ │ │ │ ├── StyleContract.java │ │ │ │ │ ├── StyleContractHelper.java │ │ │ │ │ ├── StyleDatabase.java │ │ │ │ │ ├── StyleProvider.java │ │ │ │ │ ├── StyleProviderUriMatcher.java │ │ │ │ │ └── StyleUriEnum.java │ │ │ │ └── sync/ │ │ │ │ ├── RemoteStyleDataFetcher.java │ │ │ │ ├── StyleDataHandler.java │ │ │ │ ├── SyncAdapter.java │ │ │ │ ├── SyncHelper.java │ │ │ │ ├── SyncService.java │ │ │ │ ├── account/ │ │ │ │ │ ├── Account.java │ │ │ │ │ ├── Authenticator.java │ │ │ │ │ └── AuthenticatorService.java │ │ │ │ └── gallery/ │ │ │ │ └── GalleryScheduleService.kt │ │ │ └── utils/ │ │ │ ├── ChecksumUtil.java │ │ │ ├── DeviceUtil.java │ │ │ ├── FacetIdUtil.java │ │ │ ├── HttpRequestUtil.java │ │ │ ├── NativeFileHelper.kt │ │ │ ├── NetworkUtil.java │ │ │ ├── SelectionBuilder.java │ │ │ ├── TimeUtil.java │ │ │ ├── UriUtil.kt │ │ │ ├── WallpaperFileHelper.java │ │ │ └── WallpaperUtil.kt │ │ └── res/ │ │ ├── values/ │ │ │ └── strings.xml │ │ ├── values-zh-rCN/ │ │ │ └── strings.xml │ │ └── xml/ │ │ ├── authenticator.xml │ │ └── syncadapter.xml │ └── test/ │ └── java/ │ └── com/ │ └── yalin/ │ └── data/ │ └── ExampleUnitTest.java ├── domain/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── yalin/ │ └── style/ │ └── domain/ │ ├── AdvanceWallpaper.java │ ├── GalleryWallpaper.java │ ├── Source.java │ ├── Wallpaper.java │ ├── exception/ │ │ ├── DefaultErrorBundle.java │ │ └── ErrorBundle.java │ ├── executor/ │ │ ├── PostExecutionThread.java │ │ ├── SerialThreadExecutor.java │ │ └── ThreadExecutor.java │ ├── interactor/ │ │ ├── AddGalleryWallpaper.java │ │ ├── DefaultObserver.java │ │ ├── DownloadAdvanceWallpaper.java │ │ ├── ForceNow.java │ │ ├── GetAdvanceWallpapers.java │ │ ├── GetGalleryUpdateInterval.java │ │ ├── GetGalleryWallpaper.java │ │ ├── GetSelectedAdvanceWallpaper.java │ │ ├── GetSelectedSource.java │ │ ├── GetSources.java │ │ ├── GetWallpaper.java │ │ ├── GetWallpaperCount.java │ │ ├── LikeWallpaper.java │ │ ├── LoadAdvanceWallpaper.java │ │ ├── ObserverSources.java │ │ ├── ObserverWallpaper.java │ │ ├── OpenWallpaperInputStream.java │ │ ├── ReadAdvanceAd.java │ │ ├── RemoveGalleryWallpaper.java │ │ ├── SelectAdvanceWallpaper.java │ │ ├── SelectSource.java │ │ ├── SetGalleryUpdateInterval.java │ │ ├── SwitchWallpaper.java │ │ └── UseCase.java │ ├── observable/ │ │ ├── SourcesObservable.java │ │ └── WallpaperObservable.java │ └── repository/ │ ├── SourcesRepository.java │ └── WallpaperRepository.java ├── engine/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── yalin/ │ │ └── style/ │ │ └── engine/ │ │ └── ExampleInstrumentedTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ ├── com/ │ │ │ │ └── yalin/ │ │ │ │ └── style/ │ │ │ │ └── engine/ │ │ │ │ ├── GDXWallpaperServiceProxy.kt │ │ │ │ ├── GLWallpaperServiceProxy.kt │ │ │ │ ├── IProvider.java │ │ │ │ ├── ProxyApi.java │ │ │ │ ├── ProxyProvider.kt │ │ │ │ ├── WallpaperActiveCallback.kt │ │ │ │ ├── WallpaperServiceProxy.kt │ │ │ │ ├── advance/ │ │ │ │ │ ├── AnimationWallpaper.java │ │ │ │ │ ├── BokehRainbowCircle.java │ │ │ │ │ └── BokehRainbowWallpaper.java │ │ │ │ ├── component/ │ │ │ │ │ ├── ComponentContext.java │ │ │ │ │ └── StyleClassLoader.kt │ │ │ │ └── resource/ │ │ │ │ ├── BrandUtil.java │ │ │ │ ├── CompatResources.java │ │ │ │ ├── ReflectUtil.java │ │ │ │ └── ResourcesManager.java │ │ │ └── net/ │ │ │ └── rbgrn/ │ │ │ └── android/ │ │ │ └── glwallpaperservice/ │ │ │ ├── BaseConfigChooser.java │ │ │ └── GLWallpaperService.java │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ └── test/ │ └── java/ │ └── com/ │ └── yalin/ │ └── style/ │ └── engine/ │ └── ExampleUnitTest.java ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── presentation/ │ ├── .gitignore │ ├── build.gradle │ ├── component-proguard.pro │ ├── google-services.json │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── yalin/ │ │ └── style/ │ │ ├── ExampleInstrumentedTest.java │ │ ├── provider/ │ │ │ └── DatabaseTest.java │ │ └── sync/ │ │ └── SyncAdapterTest.java │ ├── demo/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── yalin/ │ │ │ └── style/ │ │ │ └── util/ │ │ │ └── AdUtil.kt │ │ └── res/ │ │ └── values/ │ │ └── ad_strings.xml │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── yalin/ │ │ │ └── style/ │ │ │ ├── LockScreenVisibleReceiver.kt │ │ │ ├── StyleApplication.kt │ │ │ ├── StyleWallpaperService.kt │ │ │ ├── StyleWallpaperServiceMirror.kt │ │ │ ├── UIThread.kt │ │ │ ├── WallpaperDetailViewport.kt │ │ │ ├── analytics/ │ │ │ │ ├── Analytics.kt │ │ │ │ ├── Event.kt │ │ │ │ └── IAnalytics.kt │ │ │ ├── engine/ │ │ │ │ └── StyleWallpaperProxy.kt │ │ │ ├── event/ │ │ │ │ ├── MainContainerInsetsChangedEvent.java │ │ │ │ ├── SeenTutorialEvent.java │ │ │ │ ├── StyleWallpaperSizeChangedEvent.java │ │ │ │ ├── SwitchWallpaperServiceEvent.java │ │ │ │ ├── SwitchingPhotosStateChangedEvent.java │ │ │ │ ├── SystemWallpaperSizeChangedEvent.java │ │ │ │ ├── WallpaperActivateEvent.java │ │ │ │ ├── WallpaperDetailOpenedEvent.java │ │ │ │ └── WallpaperSwitchEvent.java │ │ │ ├── exception/ │ │ │ │ └── ErrorMessageFactory.java │ │ │ ├── extensions/ │ │ │ │ └── DelegatesExtensions.kt │ │ │ ├── injection/ │ │ │ │ ├── HasComponent.kt │ │ │ │ ├── PerActivity.kt │ │ │ │ ├── component/ │ │ │ │ │ ├── ApplicationComponent.kt │ │ │ │ │ ├── SourceComponent.kt │ │ │ │ │ └── WallpaperComponent.kt │ │ │ │ └── modules/ │ │ │ │ ├── ApplicationModule.kt │ │ │ │ ├── SourceModule.kt │ │ │ │ └── WallpaperModule.kt │ │ │ ├── mapper/ │ │ │ │ ├── AdvanceWallpaperItemMapper.kt │ │ │ │ └── WallpaperItemMapper.kt │ │ │ ├── model/ │ │ │ │ ├── AdvanceWallpaperItem.java │ │ │ │ ├── GalleryWallpaperItem.java │ │ │ │ ├── SourceItem.kt │ │ │ │ └── WallpaperItem.java │ │ │ ├── presenter/ │ │ │ │ ├── AdvanceSettingPresenter.kt │ │ │ │ ├── GallerySettingPresenter.kt │ │ │ │ ├── Presenter.kt │ │ │ │ ├── SettingsChooseSourcePresenter.kt │ │ │ │ └── WallpaperDetailPresenter.kt │ │ │ ├── render/ │ │ │ │ ├── BitmapRegionLoader.java │ │ │ │ ├── DemoRenderController.kt │ │ │ │ ├── GLColorOverlay.java │ │ │ │ ├── GLPicture.java │ │ │ │ ├── GLTextureView.java │ │ │ │ ├── GLUtil.java │ │ │ │ ├── ImageBlurrer.java │ │ │ │ ├── ImageUtil.java │ │ │ │ ├── RenderController.kt │ │ │ │ ├── StyleBlurRenderer.java │ │ │ │ └── TickingFloatAnimator.java │ │ │ ├── settings/ │ │ │ │ └── Prefs.java │ │ │ ├── util/ │ │ │ │ ├── ImageLoader.java │ │ │ │ ├── MathUtil.java │ │ │ │ ├── MultiSelectionController.kt │ │ │ │ ├── ScrimUtil.java │ │ │ │ ├── SettingsUtil.java │ │ │ │ ├── ShareUtil.kt │ │ │ │ ├── SvgPathParser.java │ │ │ │ └── TypefaceUtil.java │ │ │ └── view/ │ │ │ ├── AdvanceSettingView.kt │ │ │ ├── GallerySettingView.kt │ │ │ ├── LoadingDataView.kt │ │ │ ├── SourceChooseView.kt │ │ │ ├── WallpaperDetailView.kt │ │ │ ├── activity/ │ │ │ │ ├── AboutActivity.kt │ │ │ │ ├── AdvanceSettingActivity.kt │ │ │ │ ├── BaseActivity.kt │ │ │ │ ├── GallerySettingActivity.kt │ │ │ │ ├── SettingsActivity.kt │ │ │ │ └── StyleActivity.kt │ │ │ ├── component/ │ │ │ │ ├── CircleProgressView.java │ │ │ │ ├── DownloadingDialog.kt │ │ │ │ ├── DrawInsetsFrameLayout.java │ │ │ │ ├── GalleryEmptyStateGraphicView.kt │ │ │ │ ├── ObservableHorizontalScrollView.kt │ │ │ │ ├── PanScaleProxyView.java │ │ │ │ ├── Scrollbar.kt │ │ │ │ ├── ShadowDipsTextView.kt │ │ │ │ ├── TintableImageButton.kt │ │ │ │ └── Zoomer.kt │ │ │ └── fragment/ │ │ │ ├── AnimatedStyleLogoFragment.kt │ │ │ ├── BaseFragment.kt │ │ │ ├── SettingsAdvanceFragment.kt │ │ │ ├── SettingsChooseSourceFragment.kt │ │ │ ├── StyleRenderFragment.java │ │ │ ├── TutorialFragment.kt │ │ │ └── WallpaperDetailFragment.kt │ │ └── res/ │ │ ├── anim/ │ │ │ └── image_fade_in.xml │ │ ├── anim-v21/ │ │ │ └── tutorial_icon_emanate_interpolator.xml │ │ ├── animator/ │ │ │ ├── fade_in.xml │ │ │ ├── fade_out.xml │ │ │ ├── tutorial_icon_emanate_wave1.xml │ │ │ ├── tutorial_icon_emanate_wave1_path.xml │ │ │ ├── tutorial_icon_emanate_wave2.xml │ │ │ ├── tutorial_icon_emanate_wave2_path.xml │ │ │ └── tutorial_icon_overlay_state_list.xml │ │ ├── color/ │ │ │ └── selector_skip_tint.xml │ │ ├── drawable/ │ │ │ ├── gallery_ic_add.xml │ │ │ ├── gallery_ic_add_folder.xml │ │ │ ├── gallery_ic_add_photo.xml │ │ │ ├── gallery_ic_folder.xml │ │ │ ├── grey_selectable_item_background_circle.xml │ │ │ ├── intro_background_protection.xml │ │ │ ├── popup_background.xml │ │ │ ├── scrubber_control_selector.xml │ │ │ ├── scrubber_progress_blur_amount.xml │ │ │ ├── scrubber_progress_dim_amount.xml │ │ │ ├── scrubber_progress_grey_amount.xml │ │ │ ├── scrubber_progress_horizontal.xml │ │ │ ├── settings_source_item_image_overlay.xml │ │ │ ├── tutorial_icon_on_overlay.xml │ │ │ └── white_circle_button.xml │ │ ├── drawable-v21/ │ │ │ ├── avd_tutorial_icon_emanate.xml │ │ │ ├── grey_selectable_item_background_circle.xml │ │ │ ├── settings_source_item_image_overlay.xml │ │ │ ├── tutorial_icon_emanate.xml │ │ │ └── tutorial_icon_on_overlay.xml │ │ ├── layout/ │ │ │ ├── activity_about.xml │ │ │ ├── activity_advance_setting.xml │ │ │ ├── activity_gallery_setting.xml │ │ │ ├── activity_main.xml │ │ │ ├── activity_settings.xml │ │ │ ├── advance_chosen_wallpaper_item.xml │ │ │ ├── animated_logo_fragment.xml │ │ │ ├── dialog_downloading.xml │ │ │ ├── gallery_chosen_photo_item.xml │ │ │ ├── gallery_chosen_photo_tree_item.xml │ │ │ ├── layout_include_about_content.xml │ │ │ ├── layout_include_active.xml │ │ │ ├── layout_include_settings_content.xml │ │ │ ├── layout_include_tutorial_content.xml │ │ │ ├── layout_include_wallpaper_detail.xml │ │ │ ├── layout_include_wallpaper_tutorial.xml │ │ │ ├── layout_settings_choose_source.xml │ │ │ ├── layout_style_settings.xml │ │ │ ├── layout_wallpaper_detail.xml │ │ │ ├── settings_ab_spinner_list_item.xml │ │ │ ├── settings_ab_spinner_list_item_dropdown.xml │ │ │ └── settings_choose_source_item.xml │ │ ├── menu/ │ │ │ ├── advance_activity.xml │ │ │ ├── gallery_activity.xml │ │ │ ├── gallery_selection.xml │ │ │ ├── menu_settings.xml │ │ │ ├── menu_settings_advanced.xml │ │ │ └── style_overflow.xml │ │ ├── values/ │ │ │ ├── attrs.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── ids.xml │ │ │ ├── integers.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-v19/ │ │ │ └── styles.xml │ │ ├── values-v21/ │ │ │ └── styles.xml │ │ ├── values-w820dp/ │ │ │ └── dimens.xml │ │ ├── values-zh-rCN/ │ │ │ └── strings.xml │ │ └── xml/ │ │ ├── shortcuts.xml │ │ └── wallpaper.xml │ ├── production/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── yalin/ │ │ │ └── style/ │ │ │ └── util/ │ │ │ └── AdUtil.kt │ │ └── res/ │ │ └── values/ │ │ └── ad_strings.xml │ └── test/ │ └── java/ │ └── com/ │ └── yalin/ │ └── style/ │ └── ExampleUnitTest.java ├── settings.gradle └── switch.properties ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures .externalNativeBuild ================================================ FILE: README.md ================================================ # Deprecated, Not maintain anymore. # Style Live Wallpaper A live wallpaper project for Android ==================== Get it on Google Play For developer ========= Style offers a sdk that allows you to build your own live wallpaper. **[Develop Doc](https://github.com/jinkg/style-develop-sdk)**  •  **[For more examples](https://github.com/jinkg/style-sdk)** ![bezier](https://github.com/jinkg/Screenshots/blob/master/Style/bezier.gif) ![boids](https://github.com/jinkg/Screenshots/blob/master/Style/boids.gif) ![botz](https://github.com/jinkg/Screenshots/blob/master/Style/botz.gif) ![flower](https://github.com/jinkg/Screenshots/blob/master/Style/flower.gif) ![rainbow](https://github.com/jinkg/Screenshots/blob/master/Style/rainbow.gif) ![shark](https://github.com/jinkg/Screenshots/blob/master/Style/shark.gif) ![stride](https://github.com/jinkg/Screenshots/blob/master/Style/stride.gif) ![yinyang](https://github.com/jinkg/Screenshots/blob/master/Style/yinyang.gif) ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.3.1' classpath 'com.google.gms:google-services:3.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { jcenter() } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: buildApk.bat ================================================ @echo off echo Start Build Apk call gradlew clean call gradlew assembleProductionRelease -Dchannel=kinglloy call gradlew assembleProductionRelease -Dchannel=yingyongbao call gradlew assembleProductionRelease -Dchannel=360 -Dapp_name=Style艺术壁纸 call gradlew assembleProductionRelease -Dchannel=vivo call gradlew assembleProductionRelease -Dchannel=flyme call gradlew assembleProductionRelease -Dchannel=wandoujia call gradlew assembleProductionRelease -Dchannel=baidu -Dapp_name=Style艺术壁纸 call gradlew assembleProductionRelease -Dchannel=google call gradlew assembleProductionRelease -Dchannel=huawei -Dapp_name=Style艺术壁纸 echo Build Apk Complete ================================================ FILE: buildApk.sh ================================================ #!/usr/bin/env bash echo "Start Build Apk" ./gradlew clean ./gradlew assembleProductionRelease -Dchannel=kinglloy ./gradlew assembleProductionRelease -Dchannel=yingyongbao ./gradlew assembleProductionRelease -Dchannel=360 -Dapp_name=Style艺术壁纸 ./gradlew assembleProductionRelease -Dchannel=vivo ./gradlew assembleProductionRelease -Dchannel=flyme ./gradlew assembleProductionRelease -Dchannel=wandoujia ./gradlew assembleProductionRelease -Dchannel=baidu -Dapp_name=Style艺术壁纸 ./gradlew assembleProductionRelease -Dchannel=google ./gradlew assembleProductionRelease -Dchannel=huawei -Dapp_name=Style艺术壁纸 echo "Build Apk Complete" ================================================ FILE: data/.gitignore ================================================ /build ================================================ FILE: data/CMakeLists.txt ================================================ # Sets the minimum version of CMake required to build the native # library. You should either keep the default value or only pass a # value of 3.4.0 or lower. cmake_minimum_required(VERSION 3.4.1) # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds it for you. # Gradle automatically packages shared libraries with your APK. add_library( # Sets the name of the library. facet_id-lib # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). # Associated headers in the same location as their source # file are automatically included. src/main/cpp/facet_id-lib.cpp ) # Searches for a specified prebuilt library and stores the path as a # variable. Because system libraries are included in the search path by # default, you only need to specify the name of the public NDK library # you want to add. CMake verifies that the library exists before # completing its build. find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log ) # Specifies libraries CMake should link to your target library. You # can link multiple libraries, such as libraries you define in the # build script, prebuilt third-party libraries, or system libraries. target_link_libraries( # Specifies the target library. facet_id-lib # Links the target library to the log library # included in the NDK. ${log-lib} ) ================================================ FILE: data/build.gradle ================================================ buildscript { repositories { mavenCentral() } dependencies { classpath 'me.tatarka:gradle-retrolambda:3.2.3' } } apply plugin: 'com.android.library' apply plugin: 'me.tatarka.retrolambda' apply plugin: 'kotlin-android' Properties properties = new Properties() properties.load(project.rootProject.file('switch.properties').newDataInputStream()) def ENABLE_EXTERNAL_LOG = properties.get('ENABLE_EXTERNAL_LOG') def LOG_ENABLE = properties.get('LOG_ENABLE') def CHANNEL = System.getProperty("channel", "default") android { compileSdkVersion COMPILE_SDK_VERSION as int buildToolsVersion BUILD_TOOLS_VERSION as String defaultConfig { minSdkVersion MIN_SDK_VERSION as int targetSdkVersion TARGET_SDK_VERSION as int versionCode APP_VERSION_CODE as int versionName APP_VERSION_NAME as String testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { cppFlags "" } } buildConfigField("boolean", "LOG_ENABLE", "${LOG_ENABLE}") buildConfigField("String", "CHANNEL", "\"${CHANNEL}\"") } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } publishNonDefault true productFlavors { demo { buildConfigField("boolean", "DEMO_MODE", "true") buildConfigField("boolean", "ENABLE_EXTERNAL_LOG", "true") buildConfigField("String", "SERVER_WALLPAPER_ENDPOINT", "\"${DEMO_API_WALLPAPER_ENDPOINT}\"") } production { buildConfigField("boolean", "DEMO_MODE", "false") buildConfigField("boolean", "ENABLE_EXTERNAL_LOG", "${ENABLE_EXTERNAL_LOG}") buildConfigField("String", "SERVER_WALLPAPER_ENDPOINT", "\"${PRODUCTION_API_WALLPAPER_ENDPOINT}\"") } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } externalNativeBuild { cmake { path "CMakeLists.txt" } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile "com.android.support:appcompat-v7:${SUPPORT_LIBRARY_VERSION}" compile "com.android.support:exifinterface:${SUPPORT_LIBRARY_VERSION}" compile "org.jetbrains.kotlin:kotlin-stdlib:${KOTLIN_VERSION}" compile "org.jetbrains.anko:anko-common:${ANKO_VESION}" compile "org.jetbrains.anko:anko-sqlite:${ANKO_VESION}" compile "com.google.code.gson:gson:${GSON_VERSION}" compile "com.squareup.okhttp3:okhttp:${OK_HTTP_VERSION}" compile project(':domain') androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) testCompile 'junit:junit:4.12' } ================================================ FILE: data/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/jinyalin/Library/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 liked 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 *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you liked the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: data/src/androidTest/java/com/yalin/data/ExampleInstrumentedTest.java ================================================ package com.yalin.data; import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; /** * Instrumentation test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() throws Exception { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); assertEquals("com.yalin.data.test", appContext.getPackageName()); } } ================================================ FILE: data/src/main/AndroidManifest.xml ================================================ ================================================ FILE: data/src/main/cpp/facet_id-lib.cpp ================================================ #include #include #include #define LOG_TAG ("facet_id_check") #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) int LOG_ENABLE = 0; const char *LOG = "LOG"; int check_facet_id(JNIEnv *, jstring); jstring get_facet_id(JNIEnv *, jobject, jint); void printLog(const char *); extern "C" jboolean Java_com_yalin_style_data_utils_FacetIdUtil_checkCurrentFacetId__Landroid_content_Context_2I( JNIEnv *env, jclass, jobject context, jint uId) { jstring facet_id = get_facet_id(env, context, uId);; int check_result = check_facet_id(env, facet_id); bool result = check_result == 0; return (jboolean) result; } extern "C" jstring Java_com_yalin_style_data_utils_FacetIdUtil_getFacetId__Landroid_content_Context_2I( JNIEnv *env, jclass, jobject context, jint uId) { return get_facet_id(env, context, uId); } jstring get_facet_id(JNIEnv *env, jobject context, jint uId) { jclass contextClazz = env->FindClass("android/content/Context"); jmethodID getPackageManagerMethodId = env->GetMethodID(contextClazz, "getPackageManager", "()Landroid/content/pm/PackageManager;"); jobject packageManager = env->CallObjectMethod(context, getPackageManagerMethodId); printLog("PackageManager obtained."); jclass packageManagerClazz = env->FindClass("android/content/pm/PackageManager"); jmethodID getPackageForUidMethodId = env->GetMethodID(packageManagerClazz, "getPackagesForUid", "(I)[Ljava/lang/String;"); jobjectArray packageNames = (jobjectArray) (jarray) env->CallObjectMethod(packageManager, getPackageForUidMethodId, uId); jstring packageName = (jstring) env->GetObjectArrayElement(packageNames, 0); printLog("packageName obtained."); jmethodID getPackageInfoMethodId = env->GetMethodID(packageManagerClazz, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;"); jfieldID flag = env->GetStaticFieldID(packageManagerClazz, "GET_SIGNATURES", "I"); jint signaturesFlag = env->GetStaticIntField(packageManagerClazz, flag); jobject packageInfo = env->CallObjectMethod(packageManager, getPackageInfoMethodId, packageName, signaturesFlag); printLog("packageInfo obtained."); jclass packageInfoClazz = env->FindClass("android/content/pm/PackageInfo"); jfieldID signatureFieldId = env->GetFieldID(packageInfoClazz, "signatures", "[Landroid/content/pm/Signature;"); jobjectArray signatures = (jobjectArray) env->GetObjectField(packageInfo, signatureFieldId); printLog("signatures obtained."); jclass signatureClazz = env->FindClass("android/content/pm/Signature"); jmethodID toByteArrayMethodId = env->GetMethodID(signatureClazz, "toByteArray", "()[B"); jbyteArray cert = (jbyteArray) env->CallObjectMethod(env->GetObjectArrayElement(signatures, 0), toByteArrayMethodId); printLog("cert obtained."); jclass inputStreamClazz = env->FindClass("java/io/ByteArrayInputStream"); jmethodID inputStreamConstructorMethodId = env->GetMethodID(inputStreamClazz, "", "([B)V"); jobject inputStream = env->NewObject(inputStreamClazz, inputStreamConstructorMethodId, cert); printLog("inputStream obtained."); jclass certificateFactoryClazz = env->FindClass("java/security/cert/CertificateFactory"); jmethodID certificateFactoryGetInstanceMethodId = env->GetStaticMethodID( certificateFactoryClazz, "getInstance", "(Ljava/lang/String;)Ljava/security/cert/CertificateFactory;"); jstring x509String = env->NewStringUTF("X509"); jobject certificateFactory = env->CallStaticObjectMethod(certificateFactoryClazz, certificateFactoryGetInstanceMethodId, x509String); printLog("certificateFactory obtained."); jmethodID generateCertificateMethodId = env->GetMethodID(certificateFactoryClazz, "generateCertificate", "(Ljava/io/InputStream;)Ljava/security/cert/Certificate;"); jclass x509CertificateClazz = env->FindClass("java/security/cert/X509Certificate"); jobject x509Certificate = env->CallObjectMethod(certificateFactory, generateCertificateMethodId, inputStream); printLog("x509Certificate obtained."); jclass messageDigestClazz = env->FindClass("java/security/MessageDigest"); jmethodID messageDigestInstanceMethodId = env->GetStaticMethodID(messageDigestClazz, "getInstance", "(Ljava/lang/String;)Ljava/security/MessageDigest;"); jstring sha1 = env->NewStringUTF("SHA1"); jobject messageDigest = env->CallStaticObjectMethod(messageDigestClazz, messageDigestInstanceMethodId, sha1); printLog("messageDigest obtained."); jmethodID certificateEncodeMethodId = env->GetMethodID(x509CertificateClazz, "getEncoded", "()[B"); jbyteArray certEncode = (jbyteArray) env->CallObjectMethod(x509Certificate, certificateEncodeMethodId); printLog("certEncode obtained."); jmethodID messageDigestMethodId = env->GetMethodID(messageDigestClazz, "digest", "([B)[B"); jbyteArray digestArray = (jbyteArray) env->CallObjectMethod(messageDigest, messageDigestMethodId, certEncode); printLog("digestArray obtained."); jclass base64Clazz = env->FindClass("android/util/Base64"); jmethodID encodeToStringMethodId = env->GetStaticMethodID(base64Clazz, "encodeToString", "([BI)Ljava/lang/String;"); jfieldID base64FlagFiledId = env->GetStaticFieldID(base64Clazz, "DEFAULT", "I"); jint base64DefaultFlag = env->GetStaticIntField(base64Clazz, base64FlagFiledId); jstring result = (jstring) env->CallStaticObjectMethod(base64Clazz, encodeToStringMethodId, digestArray, base64DefaultFlag); printLog("result obtained."); env->DeleteLocalRef(packageManager); env->DeleteLocalRef(packageNames); env->DeleteLocalRef(packageInfo); env->DeleteLocalRef(signatures); env->DeleteLocalRef(cert); env->DeleteLocalRef(inputStream); env->DeleteLocalRef(certificateFactory); env->DeleteLocalRef(x509Certificate); env->DeleteLocalRef(messageDigest); env->DeleteLocalRef(certEncode); env->DeleteLocalRef(digestArray); return result; } int check_facet_id(JNIEnv *env, jstring facet_id) { // modify to your app's facet wallpaperId jstring valid_facet_id_string = env->NewStringUTF("unkown"); const char *valid_facet_id = env->GetStringUTFChars(valid_facet_id_string, 0); char *target_facet_id = (char *) env->GetStringUTFChars(facet_id, 0); // target_facet_id[strcspn(target_facet_id, "\r\n")] = '\0'; printLog(target_facet_id); printLog(valid_facet_id); int result = strcmp(target_facet_id, valid_facet_id); char tmp[128]; sprintf(tmp, "check complete: result=%d", result); printLog(tmp); env->DeleteLocalRef(valid_facet_id_string); return result; } void printLog(const char *str) { if (LOG_ENABLE == 0) { return; } LOGI(str, LOG); } ================================================ FILE: data/src/main/java/com/yalin/style/data/SyncConfig.java ================================================ package com.yalin.style.data; import java.util.concurrent.TimeUnit; /** * YaLin 2017/1/3. */ public class SyncConfig { public static final int DEFAULT_DOWNLOAD_TIMEOUT = 120; // in seconds public static final int DEFAULT_READ_TIMEOUT = 30; // in seconds public static final int DEFAULT_CONNECT_TIMEOUT = 15; // in seconds public static final long DEBUG_AUTO_SYNC_INTERVAL_LONG = TimeUnit.MILLISECONDS.convert(5L, TimeUnit.MINUTES); public static final long AUTO_SYNC_INTERVAL_LONG = TimeUnit.MILLISECONDS.convert(16L, TimeUnit.HOURS); } ================================================ FILE: data/src/main/java/com/yalin/style/data/cache/AdvanceWallpaperCache.kt ================================================ package com.yalin.style.data.cache import com.yalin.style.data.entity.AdvanceWallpaperEntity /** * @author jinyalin * @since 2017/8/4. */ interface AdvanceWallpaperCache { fun put(wallpapers: List) fun getSelectedWallpaper(): AdvanceWallpaperEntity? fun selectWallpaper(wallpaperId: String) fun getWallpapers(): List fun getWallpaper(wallpaperId: String): AdvanceWallpaperEntity? fun readAd(wallpaperId: String) fun evictAll() fun isCached(wallpaperId: String): Boolean fun isDirty(): Boolean fun makeDirty() } ================================================ FILE: data/src/main/java/com/yalin/style/data/cache/AdvanceWallpaperCacheImpl.kt ================================================ package com.yalin.style.data.cache import android.text.TextUtils import com.yalin.style.data.entity.AdvanceWallpaperEntity import javax.inject.Inject import javax.inject.Singleton /** * @author jinyalin * @since 2017/8/4. */ @Singleton class AdvanceWallpaperCacheImpl @Inject constructor() : AdvanceWallpaperCache { private var wallpapers: List? = null @Synchronized override fun put(wallpapers: List) { this.wallpapers = wallpapers } override fun getSelectedWallpaper(): AdvanceWallpaperEntity? { if (isDirty()) { return null } return wallpapers!!.firstOrNull { it.isSelected } } override fun selectWallpaper(wallpaperId: String) { if (isDirty()) { throw IllegalStateException("Cache is dirty.") } for (wallpaper in wallpapers!!) { wallpaper.isSelected = TextUtils.equals(wallpaperId, wallpaper.wallpaperId) } } override fun getWallpapers(): List { if (isDirty()) { throw IllegalStateException("Cache is dirty.") } return ArrayList(wallpapers!!) } override fun getWallpaper(wallpaperId: String): AdvanceWallpaperEntity? { if (!isCached(wallpaperId)) { throw IllegalStateException("WallpaperId $wallpaperId is not cached.") } return wallpapers!!.firstOrNull { TextUtils.equals(wallpaperId, it.wallpaperId) } } override fun readAd(wallpaperId: String) { if (isCached(wallpaperId)) { getWallpaper(wallpaperId)?.needAd = false } } @Synchronized override fun evictAll() { wallpapers = null } override fun isCached(wallpaperId: String): Boolean { if (isDirty()) { return false } return wallpapers!!.any { TextUtils.equals(wallpaperId, it.wallpaperId) } } override fun isDirty(): Boolean { return wallpapers == null || wallpapers!!.isEmpty() } override fun makeDirty() { evictAll() } } ================================================ FILE: data/src/main/java/com/yalin/style/data/cache/GalleryWallpaperCache.kt ================================================ package com.yalin.style.data.cache import com.yalin.style.data.entity.GalleryWallpaperEntity import javax.inject.Inject import javax.inject.Singleton /** * @author jinyalin * @since 2017/5/26. */ @Singleton class GalleryWallpaperCache @Inject constructor() { private var wallpaperEntities: List? = null fun put(wallpaperEntities: List) { this.wallpaperEntities = wallpaperEntities } fun get() = wallpaperEntities fun isCached() = wallpaperEntities != null && wallpaperEntities!!.isNotEmpty() fun isDirty() = wallpaperEntities == null || wallpaperEntities!!.isEmpty() fun evictAll() { wallpaperEntities = null } } ================================================ FILE: data/src/main/java/com/yalin/style/data/cache/SourcesCache.kt ================================================ package com.yalin.style.data.cache import android.content.Context import com.yalin.style.data.entity.SourceEntity import io.reactivex.Observable /** * @author jinyalin * @since 2017/5/22. */ interface SourcesCache { fun getSources(ctx: Context): Observable> fun selectSource(selectSourceId: Int, tempSelect: Boolean): Boolean fun getUsedSourceId(): Int } ================================================ FILE: data/src/main/java/com/yalin/style/data/cache/SourcesCacheImpl.kt ================================================ package com.yalin.style.data.cache import android.content.Context import android.graphics.Color import com.yalin.style.data.R import com.yalin.style.data.entity.SourceEntity import com.yalin.style.data.extensions.DelegateExt import com.yalin.style.data.repository.datasource.provider.StyleContract import com.yalin.style.data.repository.datasource.sync.gallery.GalleryScheduleService import com.yalin.style.domain.repository.SourcesRepository.* import io.reactivex.Observable import javax.inject.Inject import javax.inject.Singleton /** * @author jinyalin * @since 2017/5/22. */ @Singleton class SourcesCacheImpl @Inject constructor(val ctx: Context) : SourcesCache { var selectedId: Int by DelegateExt.preferences(ctx, "selected_source_id", SOURCE_ID_STYLE) val advanceSource: SourceEntity val featureSource: SourceEntity val gallerySource: SourceEntity init { advanceSource = SourceEntity(SOURCE_ID_ADVANCE).apply { title = ctx.getString(R.string.advance_source_title) iconId = R.drawable.style_ic_source description = ctx.getString(R.string.advance_source_description) color = Color.WHITE isSelected = selectedId == id isHasSetting = true } featureSource = SourceEntity(SOURCE_ID_STYLE).apply { title = ctx.getString(R.string.featuredart_source_title) iconId = R.drawable.style_ic_source description = ctx.getString(R.string.featuredart_source_description) color = Color.WHITE isSelected = selectedId == id isHasSetting = false } gallerySource = SourceEntity(SOURCE_ID_CUSTOM).apply { title = ctx.getString(R.string.gallery_title) iconId = R.drawable.gallery_ic_source description = ctx.getString(R.string.gallery_description) color = 0x4db6ac isSelected = selectedId == id isHasSetting = true } if (getUsedSourceId() == SOURCE_ID_CUSTOM) { GalleryScheduleService.startUp(ctx) } } override fun getSources(ctx: Context): Observable> { return Observable.create { emitter -> val sources = ArrayList() sources.add(advanceSource) sources.add(featureSource) sources.add(gallerySource) emitter.onNext(sources) emitter.onComplete() } } override fun selectSource(selectSourceId: Int, tempSelect: Boolean): Boolean { var success = false if (featureSource.id == selectSourceId) { featureSource.isSelected = true gallerySource.isSelected = false advanceSource.isSelected = false success = true GalleryScheduleService.shutDown(ctx) } else if (gallerySource.id == selectSourceId) { featureSource.isSelected = false gallerySource.isSelected = true advanceSource.isSelected = false success = true GalleryScheduleService.startUp(ctx) } else if (advanceSource.id == selectSourceId) { featureSource.isSelected = false gallerySource.isSelected = false advanceSource.isSelected = true success = true GalleryScheduleService.shutDown(ctx) } selectedId = selectSourceId if (success) { notifyChanged() } return success } override fun getUsedSourceId(): Int { return selectedId } private fun notifyChanged() { ctx.contentResolver.notifyChange(StyleContract.Source.CONTENT_URI, null) } } ================================================ FILE: data/src/main/java/com/yalin/style/data/cache/WallpaperCache.java ================================================ package com.yalin.style.data.cache; import com.yalin.style.data.entity.WallpaperEntity; import java.util.Queue; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/4/20. */ public interface WallpaperCache { /** * return the first * * @return wallpaperEntity */ Observable get(); Observable getNext(); Observable getWallpaperCount(); void likeWallpaper(String wallpaperId); void put(Queue wallpaperEntities); boolean isCached(); boolean isCached(String wallpaperId); boolean isDirty(); void evictAll(); } ================================================ FILE: data/src/main/java/com/yalin/style/data/cache/WallpaperCacheImpl.java ================================================ package com.yalin.style.data.cache; import android.text.TextUtils; import com.fernandocejas.arrow.checks.Preconditions; import com.yalin.style.data.entity.WallpaperEntity; import java.util.Queue; import javax.inject.Inject; import javax.inject.Singleton; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/4/20. */ @Singleton public class WallpaperCacheImpl implements WallpaperCache { private Queue wallpaperEntities; @Inject public WallpaperCacheImpl() { } @Override public Observable get() { Preconditions.checkNotNull(wallpaperEntities, "There is not cached wallpaper."); Preconditions.checkArgument(!wallpaperEntities.isEmpty(), "There is not cached wallpaper."); return Observable.create(emitter -> { emitter.onNext(wallpaperEntities.peek()); emitter.onComplete(); }); } @Override public Observable getNext() { Preconditions.checkNotNull(wallpaperEntities, "There is not cached wallpaper."); Preconditions.checkArgument(!wallpaperEntities.isEmpty(), "There is not cached wallpaper."); return Observable.create(emitter -> { WallpaperEntity entity = wallpaperEntities.poll(); wallpaperEntities.offer(entity); emitter.onNext(wallpaperEntities.peek()); emitter.onComplete(); }); } @Override public Observable getWallpaperCount() { Preconditions.checkNotNull(wallpaperEntities, "There is not cached wallpaper."); return Observable.create(emitter -> { emitter.onNext(wallpaperEntities.size()); emitter.onComplete(); }); } @Override public void likeWallpaper(String wallpaperId) { Preconditions.checkNotNull(wallpaperId, "There is not cached wallpaper."); if (isCached(wallpaperId)) { WallpaperEntity entity = get(wallpaperId); if (entity != null) { entity.liked = !entity.liked; } } } @Override public synchronized void put(Queue wallpaperEntities) { this.wallpaperEntities = wallpaperEntities; } @Override public boolean isCached() { return wallpaperEntities != null && !wallpaperEntities.isEmpty(); } @Override public boolean isCached(String wallpaperId) { if (isDirty()) { return false; } for (WallpaperEntity entity : wallpaperEntities) { if (TextUtils.equals(entity.wallpaperId, wallpaperId)) { return true; } } return false; } @Override public boolean isDirty() { return wallpaperEntities == null; } @Override public synchronized void evictAll() { if (wallpaperEntities != null) { wallpaperEntities.clear(); } wallpaperEntities = null; } private WallpaperEntity get(String wallpaperId) { for (WallpaperEntity entity : wallpaperEntities) { if (TextUtils.equals(entity.wallpaperId, wallpaperId)) { return entity; } } return null; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/entity/AdvanceWallpaperEntity.java ================================================ package com.yalin.style.data.entity; import android.database.Cursor; import android.text.TextUtils; import com.yalin.style.data.log.LogUtil; import com.yalin.style.data.repository.datasource.provider.StyleContract; import java.io.File; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.List; /** * @author jinyalin * @since 2017/7/28. */ public class AdvanceWallpaperEntity { private static final String TAG = "AdvanceWallpaperEntity"; public int id; public String wallpaperId; public String link; public String name; public String author; public String iconUrl; public String downloadUrl; public String providerName; public boolean lazyDownload; public boolean needAd; public String storePath; public String checkSum; public boolean isDefault = false; public boolean isSelected = false; public static List readCursor(Cursor cursor) { List validWallpapers = new ArrayList<>(); while (cursor != null && cursor.moveToNext()) { AdvanceWallpaperEntity wallpaperEntity = readEntityFromCursor(cursor); try { if (!wallpaperEntity.lazyDownload && !new File(wallpaperEntity.storePath).exists()) { throw new FileNotFoundException("Component not found."); } validWallpapers.add(wallpaperEntity); } catch (Exception e) { LogUtil.D(TAG, "File not found with wallpaper wallpaperId : " + wallpaperEntity.wallpaperId); } } return validWallpapers; } public static AdvanceWallpaperEntity readEntityFromCursor(Cursor cursor) { AdvanceWallpaperEntity wallpaperEntity = new AdvanceWallpaperEntity(); wallpaperEntity.id = cursor.getInt(cursor.getColumnIndex( StyleContract.AdvanceWallpaper._ID)); wallpaperEntity.name = cursor.getString(cursor.getColumnIndex( StyleContract.AdvanceWallpaper.COLUMN_NAME_NAME)); wallpaperEntity.wallpaperId = cursor.getString(cursor.getColumnIndex( StyleContract.AdvanceWallpaper.COLUMN_NAME_WALLPAPER_ID)); wallpaperEntity.iconUrl = cursor.getString(cursor.getColumnIndex( StyleContract.AdvanceWallpaper.COLUMN_NAME_ICON_URL)); wallpaperEntity.link = cursor.getString(cursor.getColumnIndex( StyleContract.AdvanceWallpaper.COLUMN_NAME_LINK)); wallpaperEntity.author = cursor.getString(cursor.getColumnIndex( StyleContract.AdvanceWallpaper.COLUMN_NAME_AUTHOR)); wallpaperEntity.downloadUrl = cursor.getString(cursor.getColumnIndex( StyleContract.AdvanceWallpaper.COLUMN_NAME_DOWNLOAD_URL)); wallpaperEntity.checkSum = cursor.getString(cursor.getColumnIndex( StyleContract.AdvanceWallpaper.COLUMN_NAME_CHECKSUM)); wallpaperEntity.storePath = cursor.getString(cursor.getColumnIndex( StyleContract.AdvanceWallpaper.COLUMN_NAME_STORE_PATH)); wallpaperEntity.providerName = cursor.getString(cursor.getColumnIndex( StyleContract.AdvanceWallpaper.COLUMN_NAME_PROVIDER_NAME)); wallpaperEntity.isSelected = cursor.getInt(cursor.getColumnIndex( StyleContract.AdvanceWallpaper.COLUMN_NAME_SELECTED)) == 1; wallpaperEntity.lazyDownload = cursor.getInt(cursor.getColumnIndex( StyleContract.AdvanceWallpaper.COLUMN_NAME_LAZY_DOWNLOAD)) == 1; wallpaperEntity.needAd = cursor.getInt(cursor.getColumnIndex( StyleContract.AdvanceWallpaper.COLUMN_NAME_NEED_AD)) == 1; return wallpaperEntity; } @Override public boolean equals(Object obj) { if (obj instanceof AdvanceWallpaperEntity) { if (TextUtils.equals(((AdvanceWallpaperEntity) obj).name, name) && TextUtils.equals(((AdvanceWallpaperEntity) obj).checkSum, checkSum)) { return true; } } return false; } @Override public int hashCode() { int result = 0; result = 31 * result + name.hashCode(); result = 31 * result + checkSum.hashCode(); return result; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/entity/DeviceInfo.java ================================================ package com.yalin.style.data.entity; import com.yalin.style.data.BuildConfig; /** * @author jinyalin * @since 2017/4/25. */ public class DeviceInfo { private int sdkVersion; private String androidId; private String manufacturer; private String brand; private String model; private String type; private String versionName; private int versionCode; private String channel; public DeviceInfo(int sdkVersion, String androidId, String manufacturer, String brand, String model) { this.sdkVersion = sdkVersion; this.androidId = androidId; this.manufacturer = manufacturer; this.brand = brand; this.model = model; type = "Android"; versionName = BuildConfig.VERSION_NAME; versionCode = BuildConfig.VERSION_CODE; channel = BuildConfig.CHANNEL; } public int getSdkVersion() { return sdkVersion; } public String getAndroidId() { return androidId; } public String getManufacturer() { return manufacturer; } public String getModel() { return model; } public String getType() { return type; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/entity/GalleryWallpaperEntity.kt ================================================ package com.yalin.style.data.entity import android.content.Context import android.database.Cursor import android.net.Uri import android.os.Build import com.yalin.style.data.log.LogUtil import com.yalin.style.data.repository.datasource.provider.StyleContract import com.yalin.style.data.utils.getCacheFileForUri import com.yalin.style.data.utils.getImagesFromTreeUri import java.io.InputStream import java.util.ArrayList /** * @author jinyalin * @since 2017/5/24. */ class GalleryWallpaperEntity { var id: Long = 0 var uri: String? = null var isTreeUri: Boolean = false var dateTime: Long = 0 var location: String? = null var hasMetadata: Boolean = false companion object { private val TAG = "GalleryWallpaperEntity" fun readCursor(context: Context, cursor: Cursor?): ArrayList { val validWallpapers = ArrayList(5) while (cursor != null && cursor.moveToNext()) { val entity = readEntityFromCursor(cursor) var inputStream: InputStream? = null try { // valid input stream if (!entity.isTreeUri) { inputStream = context.contentResolver.openInputStream( Uri.parse(entity.uri)) } validWallpapers.add(entity) } catch (e: Exception) { LogUtil.D(TAG, "Cannot open inputStream for uri : " + entity.uri) val cacheFile = getCacheFileForUri(context, entity.uri!!) if ((cacheFile != null && cacheFile.exists())) { // has cache file validWallpapers.add(entity) } } finally { inputStream?.close() } } return validWallpapers; } fun readEntityFromCursor(cursor: Cursor): GalleryWallpaperEntity { val entity = GalleryWallpaperEntity() entity.id = cursor.getLong(cursor.getColumnIndex( StyleContract.GalleryWallpaper._ID)) entity.uri = cursor.getString(cursor.getColumnIndex( StyleContract.GalleryWallpaper.COLUMN_NAME_CUSTOM_URI)) entity.isTreeUri = cursor.getInt(cursor.getColumnIndex( StyleContract.GalleryWallpaper.COLUMN_NAME_IS_TREE_URI)) == 1 entity.dateTime = cursor.getLong(cursor.getColumnIndex( StyleContract.GalleryWallpaper.COLUMN_NAME_DATE_TIME)) entity.location = cursor.getString(cursor.getColumnIndex( StyleContract.GalleryWallpaper.COLUMN_NAME_LOCATION)) entity.hasMetadata = cursor.getInt(cursor.getColumnIndex( StyleContract.GalleryWallpaper.COLUMN_NAME_HAS_METADATA)) == 1 return entity } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/entity/HttpRequestBody.java ================================================ package com.yalin.style.data.entity; import android.content.Context; import com.yalin.style.data.utils.FacetIdUtil; /** * @author jinyalin * @since 2017/4/25. */ public class HttpRequestBody { private DeviceInfo deviceInfo; private String facetId; public HttpRequestBody(Context context, DeviceInfo deviceInfo) { this.deviceInfo = deviceInfo; this.facetId = FacetIdUtil.getFacetId(context); } public DeviceInfo getDeviceInfo() { return deviceInfo; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/entity/SourceEntity.kt ================================================ package com.yalin.style.data.entity /** * @author jinyalin * * * @since 2017/5/23. */ class SourceEntity(var id: Int) { var title: String? = null var description: String? = null var iconId: Int = 0 var isSelected: Boolean = false var isHasSetting: Boolean = false var color: Int = 0 } ================================================ FILE: data/src/main/java/com/yalin/style/data/entity/WallpaperEntity.java ================================================ package com.yalin.style.data.entity; import android.content.Context; import android.database.Cursor; import android.text.TextUtils; import com.yalin.style.data.log.LogUtil; import com.yalin.style.data.repository.datasource.provider.StyleContract; import com.yalin.style.data.repository.datasource.provider.StyleContract.Wallpaper; import java.io.InputStream; import java.util.ArrayList; import java.util.List; /** * @author jinyalin * @since 2017/4/18. */ public class WallpaperEntity { private static final String TAG = "WallpaperEntity"; public int id; public String wallpaperId; public String imageUri; public String title; public String byline; public String attribution; public boolean canLike; public boolean liked; public boolean isDefault; public String checksum; public WallpaperEntity() { } public static List readCursor(Context context, Cursor cursor) { List validWallpapers = new ArrayList<>(5); while (cursor != null && cursor.moveToNext()) { WallpaperEntity wallpaperEntity = WallpaperEntity.readEntityFromCursor(cursor); try { // valid input stream InputStream is = context.getContentResolver().openInputStream( StyleContract.Wallpaper.buildWallpaperUri( wallpaperEntity.wallpaperId)); validWallpapers.add(wallpaperEntity); if (is != null) { is.close(); } } catch (Exception e) { LogUtil.D(TAG, "File not found with wallpaper wallpaperId : " + wallpaperEntity.wallpaperId); } } return validWallpapers; } public static WallpaperEntity readEntityFromCursor(Cursor cursor) { WallpaperEntity wallpaperEntity = new WallpaperEntity(); wallpaperEntity.id = cursor.getInt(cursor.getColumnIndex( Wallpaper._ID)); wallpaperEntity.title = cursor.getString(cursor.getColumnIndex( Wallpaper.COLUMN_NAME_TITLE)); wallpaperEntity.wallpaperId = cursor.getString(cursor.getColumnIndex( Wallpaper.COLUMN_NAME_WALLPAPER_ID)); wallpaperEntity.imageUri = cursor.getString(cursor.getColumnIndex( Wallpaper.COLUMN_NAME_IMAGE_URI)); wallpaperEntity.byline = cursor.getString(cursor.getColumnIndex( Wallpaper.COLUMN_NAME_BYLINE)); wallpaperEntity.attribution = cursor.getString(cursor.getColumnIndex( Wallpaper.COLUMN_NAME_ATTRIBUTION)); wallpaperEntity.liked = cursor.getInt(cursor.getColumnIndex( Wallpaper.COLUMN_NAME_LIKED)) == 1; wallpaperEntity.checksum = cursor.getString(cursor.getColumnIndex( Wallpaper.COLUMN_NAME_CHECKSUM)); return wallpaperEntity; } @Override public boolean equals(Object obj) { if (obj instanceof WallpaperEntity) { if (TextUtils.equals(((WallpaperEntity) obj).title, title) && TextUtils.equals(((WallpaperEntity) obj).checksum, checksum)) { return true; } } return false; } @Override public int hashCode() { int result = 0; result = 31 * result + title.hashCode(); result = 31 * result + checksum.hashCode(); return result; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/entity/mapper/AdvanceWallpaperEntityMapper.java ================================================ package com.yalin.style.data.entity.mapper; import com.fernandocejas.arrow.checks.Preconditions; import com.yalin.style.data.entity.AdvanceWallpaperEntity; import com.yalin.style.domain.AdvanceWallpaper; import com.yalin.style.domain.Wallpaper; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; /** * @author jinyalin * @since 2017/7/28. */ @Singleton public class AdvanceWallpaperEntityMapper { @Inject public AdvanceWallpaperEntityMapper() { } public AdvanceWallpaper transform(AdvanceWallpaperEntity wallpaperEntity) { Preconditions.checkNotNull(wallpaperEntity, "Wallpaper can not be null."); AdvanceWallpaper wallpaper = new AdvanceWallpaper(); wallpaper.id = wallpaperEntity.id; wallpaper.wallpaperId = wallpaperEntity.wallpaperId; wallpaper.link = wallpaperEntity.link; wallpaper.name = wallpaperEntity.name; wallpaper.author = wallpaperEntity.author; wallpaper.iconUrl = wallpaperEntity.iconUrl; wallpaper.downloadUrl = wallpaperEntity.downloadUrl; wallpaper.providerName = wallpaperEntity.providerName; wallpaper.storePath = wallpaperEntity.storePath; wallpaper.isDefault = wallpaperEntity.isDefault; wallpaper.isSelected = wallpaperEntity.isSelected; wallpaper.lazyDownload = wallpaperEntity.lazyDownload; wallpaper.needAd = wallpaperEntity.needAd; return wallpaper; } public List transformList(List wallpaperEntities) { Preconditions.checkNotNull(wallpaperEntities, "SourceEntity can not be null."); List sources = new ArrayList<>(); for (AdvanceWallpaperEntity entity : wallpaperEntities) { sources.add(transform(entity)); } return sources; } public Wallpaper mapToWallpaper(AdvanceWallpaperEntity wallpaperEntity) { Preconditions.checkNotNull(wallpaperEntity, "Wallpaper can not be null."); Wallpaper wallpaper = new Wallpaper(); wallpaper.wallpaperId = wallpaperEntity.wallpaperId; wallpaper.byline = wallpaperEntity.author; wallpaper.title = wallpaperEntity.name; wallpaper.attribution = wallpaperEntity.link; wallpaper.canLike = false; return wallpaper; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/entity/mapper/WallpaperEntityMapper.java ================================================ package com.yalin.style.data.entity.mapper; import com.fernandocejas.arrow.checks.Preconditions; import com.yalin.style.data.entity.GalleryWallpaperEntity; import com.yalin.style.data.entity.SourceEntity; import com.yalin.style.data.entity.WallpaperEntity; import com.yalin.style.domain.GalleryWallpaper; import com.yalin.style.domain.Source; import com.yalin.style.domain.Wallpaper; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; /** * @author jinyalin * @since 2017/4/18. */ @Singleton public class WallpaperEntityMapper { @Inject public WallpaperEntityMapper() { } public Wallpaper transform(WallpaperEntity wallpaperEntity) { Preconditions.checkNotNull(wallpaperEntity, "Wallpaper can not be null."); Wallpaper wallpaper = new Wallpaper(); wallpaper.title = wallpaperEntity.title; wallpaper.attribution = wallpaperEntity.attribution; wallpaper.byline = wallpaperEntity.byline; wallpaper.imageUri = wallpaperEntity.imageUri; wallpaper.wallpaperId = wallpaperEntity.wallpaperId; wallpaper.liked = wallpaperEntity.liked; wallpaper.isDefault = wallpaperEntity.isDefault; wallpaper.canLike = wallpaperEntity.canLike; return wallpaper; } public List transformSources(List sourceEntities) { Preconditions.checkNotNull(sourceEntities, "SourceEntity can not be null."); List sources = new ArrayList<>(); for (SourceEntity entity : sourceEntities) { sources.add(transformSource(entity)); } return sources; } public Source transformSource(SourceEntity sourceEntity) { Preconditions.checkNotNull(sourceEntity, "SourceEntity can not be null."); Source source = new Source(); source.id = sourceEntity.getId(); source.title = sourceEntity.getTitle(); source.description = sourceEntity.getDescription(); source.iconId = sourceEntity.getIconId(); source.selected = sourceEntity.isSelected(); source.hasSetting = sourceEntity.isHasSetting(); source.color = sourceEntity.getColor(); return source; } public List transformGalleryWallpaper( List galleryWallpaperEntities) { Preconditions.checkNotNull(galleryWallpaperEntities, "GalleryWallpaperEntity can not be null."); List entities = new ArrayList<>(); for (GalleryWallpaperEntity entity : galleryWallpaperEntities) { entities.add(transformGalleryWallpaper(entity)); } return entities; } public GalleryWallpaper transformGalleryWallpaper( GalleryWallpaperEntity galleryWallpaperEntity) { Preconditions.checkNotNull(galleryWallpaperEntity, "GalleryWallpaperEntity can not be null."); GalleryWallpaper galleryWallpaper = new GalleryWallpaper(); galleryWallpaper.id = galleryWallpaperEntity.getId(); galleryWallpaper.isTreeUri = galleryWallpaperEntity.isTreeUri(); galleryWallpaper.uri = galleryWallpaperEntity.getUri(); galleryWallpaper.dateTime = galleryWallpaperEntity.getDateTime(); galleryWallpaper.location = galleryWallpaperEntity.getLocation(); galleryWallpaper.hasMetadata = galleryWallpaperEntity.getHasMetadata(); return galleryWallpaper; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/exception/LikeException.java ================================================ package com.yalin.style.data.exception; /** * @author jinyalin * @since 2017/4/29. */ public class LikeException extends Exception { public LikeException() { super(); } public LikeException(final Throwable cause) { super(cause); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/exception/NetworkConnectionException.java ================================================ package com.yalin.style.data.exception; /** * @author jinyalin * @since 2017/4/29. */ public class NetworkConnectionException extends Exception { public NetworkConnectionException() { super(); } public NetworkConnectionException(final Throwable cause) { super(cause); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/exception/NoContentException.java ================================================ package com.yalin.style.data.exception; /** * @author jinyalin * @since 2017/7/31. */ public class NoContentException extends Exception { } ================================================ FILE: data/src/main/java/com/yalin/style/data/exception/RemoteServerException.java ================================================ package com.yalin.style.data.exception; /** * @author jinyalin * @since 2017/8/4. */ public class RemoteServerException extends Exception { } ================================================ FILE: data/src/main/java/com/yalin/style/data/exception/ReswitchException.java ================================================ package com.yalin.style.data.exception; /** * @author jinyalin * @since 2017/4/29. */ public class ReswitchException extends Exception { public ReswitchException() { super(); } public ReswitchException(final Throwable cause) { super(cause); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/executor/JobExecutor.java ================================================ package com.yalin.style.data.executor; import android.support.annotation.NonNull; import com.yalin.style.domain.executor.ThreadExecutor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Singleton; /** * @author jinyalin * @since 2017/4/18. */ @Singleton public class JobExecutor implements ThreadExecutor { protected final ThreadPoolExecutor threadPoolExecutor; @Inject JobExecutor() { threadPoolExecutor = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new JobThreadFactory()); } @Override public void execute(@NonNull Runnable command) { threadPoolExecutor.execute(command); } private static class JobThreadFactory implements ThreadFactory { private int counter = 0; @Override public Thread newThread(@NonNull Runnable runnable) { return new Thread(runnable, "android_" + counter); } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/executor/SerialJobExecutor.java ================================================ package com.yalin.style.data.executor; import android.support.annotation.NonNull; import com.yalin.style.domain.executor.SerialThreadExecutor; import java.util.ArrayDeque; import javax.inject.Inject; import javax.inject.Singleton; /** * @author jinyalin * @since 2017/4/18. */ @Singleton public class SerialJobExecutor extends JobExecutor implements SerialThreadExecutor { final ArrayDeque mTasks = new ArrayDeque<>(); Runnable mActive; @Inject SerialJobExecutor() { super(); } @Override public synchronized void execute(@NonNull Runnable command) { mTasks.offer(() -> { try { command.run(); } finally { scheduleNext(); } }); if (mActive == null) { scheduleNext(); } } protected synchronized void scheduleNext() { if ((mActive = mTasks.poll()) != null) { threadPoolExecutor.execute(mActive); } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/extensions/DelegateExt.kt ================================================ package com.yalin.style.data.extensions import android.content.Context import android.content.SharedPreferences import java.lang.IllegalArgumentException import kotlin.reflect.KProperty /** * @author jinyalin * @since 2017/5/22. */ object DelegateExt { fun preferences(context: Context, name: String, default: T) = Preferences(context, name, default) } class Preferences(val context: Context, val name: String, val default: T) { companion object { val PREFERENCE_NAME = "style_preference" } val prefs: SharedPreferences by lazy { context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) } operator fun getValue(thisRef: Any?, property: KProperty<*>): T = findPreference(name, default) operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = putPreference(name, value) @Suppress("UNCHECKED_CAST") private fun findPreference(name: String, default: T): T = with(prefs) { val res: Any = when (default) { is Long -> getLong(name, default) is String -> getString(name, default) is Int -> getInt(name, default) is Boolean -> getBoolean(name, default) is Float -> getFloat(name, default) else -> throw IllegalArgumentException("This type can not be saved into Preferences") } res as T } private fun putPreference(name: String, value: T) = with(prefs.edit()) { when (value) { is Long -> putLong(name, value) is String -> putString(name, value) is Int -> putInt(name, value) is Boolean -> putBoolean(name, value) is Float -> putFloat(name, value) else -> throw IllegalArgumentException("This type can't be saved into Preferences") }.apply() } } ================================================ FILE: data/src/main/java/com/yalin/style/data/lock/LikeWallpaperLock.java ================================================ package com.yalin.style.data.lock; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.interactor.DefaultObserver; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import javax.inject.Inject; import javax.inject.Singleton; import io.reactivex.Observable; import io.reactivex.schedulers.Schedulers; /** * YaLin * On 2017/4/30. */ @Singleton public class LikeWallpaperLock extends ResourceLock { @Inject public LikeWallpaperLock(ThreadExecutor threadExecutor) { super(threadExecutor); } @Override protected Observable appendDelay(Observable observable) { return observable.delaySubscription(500, TimeUnit.MILLISECONDS); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/lock/OpenInputStreamLock.java ================================================ package com.yalin.style.data.lock; import com.yalin.style.domain.executor.ThreadExecutor; import io.reactivex.Observable; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Singleton; /** * YaLin * On 2017/4/30. */ @Singleton public class OpenInputStreamLock extends ResourceLock { @Inject public OpenInputStreamLock(ThreadExecutor threadExecutor) { super(threadExecutor); } @Override protected Observable appendDelay(Observable observable) { return observable.delaySubscription(1, TimeUnit.SECONDS); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/lock/ResourceLock.java ================================================ package com.yalin.style.data.lock; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.interactor.DefaultObserver; import java.util.concurrent.atomic.AtomicBoolean; import io.reactivex.Observable; import io.reactivex.schedulers.Schedulers; /** * YaLin * On 2017/4/30. */ public abstract class ResourceLock { private AtomicBoolean lock = new AtomicBoolean(false); private final ThreadExecutor threadExecutor; private DefaultObserver releaseObserver; public ResourceLock(ThreadExecutor threadExecutor) { this.threadExecutor = threadExecutor; } public synchronized boolean obtain() { if (lock.get()) { return false; } else { lock.set(true); return true; } } public synchronized void release() { if (releaseObserver != null) { releaseObserver.dispose(); } releaseObserver = new DefaultObserver<>(); Observable observable = Observable.create(e -> { lock.set(false); e.onComplete(); }); observable = appendDelay(observable); observable.subscribeOn(Schedulers.from(threadExecutor)) .subscribeWith(releaseObserver); } protected Observable appendDelay(Observable observable) { return observable; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/lock/SelectSourceLock.java ================================================ package com.yalin.style.data.lock; import com.yalin.style.domain.executor.ThreadExecutor; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Singleton; import io.reactivex.Observable; /** * YaLin * On 2017/4/30. */ @Singleton public class SelectSourceLock extends ResourceLock { @Inject public SelectSourceLock(ThreadExecutor threadExecutor) { super(threadExecutor); } @Override protected Observable appendDelay(Observable observable) { return observable.delaySubscription(1, TimeUnit.SECONDS); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/log/LogUtil.java ================================================ package com.yalin.style.data.log; import android.os.Environment; import android.os.Process; import android.util.Log; import com.yalin.style.data.BuildConfig; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.util.Calendar; import java.util.Locale; /** * YaLin 2016/11/23. */ public class LogUtil { private static final String FILE_NAME = "Style/stat_log.txt"; public static final boolean LOG_ENABLE = BuildConfig.DEMO_MODE || BuildConfig.LOG_ENABLE; public static synchronized boolean isExternalLogEnabled() { //noinspection SimplifiableIfStatement if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { return false; } return LOG_ENABLE && BuildConfig.ENABLE_EXTERNAL_LOG; } public static void F(String tag, String msg, Throwable throwable) { if (!LOG_ENABLE) { return; } String stackTraces = Log.getStackTraceString(throwable); if (msg == null) { msg = ""; } F(tag, msg + " :\n " + stackTraces); } public static void F(String tag, String msg) { if (!LOG_ENABLE) { return; } String procInfo = getProcessInfo(); Log.e(tag, procInfo + msg); if (!isExternalLogEnabled()) { return; } tag = tag + ":debug"; writeLog(tag, procInfo + msg); } public static void E(String tag, String msg, Throwable throwable) { if (!LOG_ENABLE) { return; } String stackTraces = Log.getStackTraceString(throwable); if (msg == null) { msg = ""; } E(tag, msg + " :\n " + stackTraces); } public static void E(String tag, String msg) { if (!LOG_ENABLE) { return; } String procInfo = getProcessInfo(); Log.e(tag, procInfo + msg); } public static void D(String tag, String msg) { if (!LOG_ENABLE) { return; } String procInfo = getProcessInfo(); Log.d(tag, procInfo + msg); } private static String getProcessInfo() { return "Process wallpaperId: " + Process.myPid() + " Thread wallpaperId: " + Thread.currentThread().getId() + " "; } private static synchronized void writeLog(String tag, String msg) { internalWriteLog(FILE_NAME, tag, msg); } private static synchronized void internalWriteLog(String filename, String tag, String msg) { try { if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { return; } File file = new File(Environment.getExternalStorageDirectory(), filename); //noinspection ResultOfMethodCallIgnored file.getParentFile().mkdirs(); BufferedWriter bw = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(file, true))); String time = getCurrentTime(); bw.write(time + " " + tag + " \t" + msg + "\r\n"); bw.close(); } catch (Exception e) { // ignore } } private static String getCurrentTime() { Calendar c = Calendar.getInstance(); return String.format(Locale.getDefault(), "%d-%02d-%02d %02d:%02d:%02d.%03d", c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH), c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), c.get(Calendar.SECOND), c.get(Calendar.MILLISECOND)); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/observable/SourcesObservableImpl.kt ================================================ package com.yalin.style.data.observable import android.content.Context import android.database.ContentObserver import android.net.Uri import android.os.Handler import com.yalin.style.data.cache.SourcesCache import com.yalin.style.data.log.LogUtil import com.yalin.style.data.repository.datasource.provider.StyleContract import com.yalin.style.domain.interactor.DefaultObserver import com.yalin.style.domain.observable.SourcesObservable import java.util.HashSet import javax.inject.Inject import javax.inject.Singleton /** * @author jinyalin * @since 2017/5/23. */ @Singleton class SourcesObservableImpl @Inject constructor(val context: Context, val sourcesCache: SourcesCache) : SourcesObservable { companion object { val TAG = "WallpaperObservableImpl" } private val mObserverSet = HashSet>() private val mContentObserver = object : ContentObserver(Handler()) { override fun onChange(selfChange: Boolean, uri: Uri) { LogUtil.D(TAG, "Source changed notify observer to reload.") notifyObserver() } } private var observerRegistered = false override fun registerObserver(observer: DefaultObserver) { synchronized(mObserverSet) { mObserverSet.add(observer) if (!observerRegistered) { context.contentResolver .registerContentObserver(StyleContract.Source.CONTENT_URI, true, mContentObserver) observerRegistered = true } } } override fun unregisterObserver(observer: DefaultObserver) { synchronized(mObserverSet) { mObserverSet.remove(observer) if (mObserverSet.isEmpty()) { context.contentResolver.unregisterContentObserver(mContentObserver) observerRegistered = false } } } private fun notifyObserver() { for (observer in mObserverSet) { observer.onNext(null) observer.onComplete() } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/observable/WallpaperObservableImpl.kt ================================================ package com.yalin.style.data.observable import android.content.Context import android.database.ContentObserver import android.net.Uri import android.os.Handler import android.text.TextUtils import com.yalin.style.data.cache.SourcesCache import com.yalin.style.data.log.LogUtil import com.yalin.style.data.repository.datasource.AdvanceWallpaperDataStoreFactory import com.yalin.style.data.repository.datasource.StyleWallpaperDataStoreFactory import com.yalin.style.data.repository.datasource.provider.StyleContract import com.yalin.style.domain.interactor.DefaultObserver import com.yalin.style.domain.observable.SourcesObservable import com.yalin.style.domain.observable.WallpaperObservable import com.yalin.style.domain.repository.SourcesRepository.SOURCE_ID_CUSTOM import com.yalin.style.domain.repository.SourcesRepository.SOURCE_ID_STYLE import com.yalin.style.domain.repository.SourcesRepository.SOURCE_ID_ADVANCE import java.util.HashSet import javax.inject.Inject import javax.inject.Singleton /** * @author jinyalin * @since 2017/5/23. */ @Singleton class WallpaperObservableImpl @Inject constructor(val context: Context, val sourcesCache: SourcesCache, val styleWallpaperDataStoreFactory: StyleWallpaperDataStoreFactory, val advanceWallpaperDataStoreFactory: AdvanceWallpaperDataStoreFactory, val sourcesObservable: SourcesObservable) : WallpaperObservable { companion object { val TAG = "WallpaperObservableImpl" } private val mObserverSet = HashSet>() private val mWallpaperObserver = object : ContentObserver(Handler()) { override fun onChange(selfChange: Boolean, uri: Uri) { LogUtil.D(TAG, "Wallpaper data changed notify observer to reload.") if (sourcesCache.getUsedSourceId() == SOURCE_ID_CUSTOM && TextUtils.equals(uri.toString(), StyleContract.GalleryWallpaper.CONTENT_URI.toString())) { notifyObserver() } else if (sourcesCache.getUsedSourceId() == SOURCE_ID_STYLE && TextUtils.equals(uri.toString(), StyleContract.Wallpaper.CONTENT_URI.toString())) { notifyObserver() } else if (sourcesCache.getUsedSourceId() == SOURCE_ID_ADVANCE && TextUtils.equals(uri.toString(), StyleContract.AdvanceWallpaper.CONTENT_URI.toString())) { notifyObserver() } styleWallpaperDataStoreFactory.onDataRefresh() advanceWallpaperDataStoreFactory.onDataRefresh() } } private val sourceObserver = object : DefaultObserver() { override fun onComplete() { notifyObserver() } } init { context.contentResolver .registerContentObserver(StyleContract.Wallpaper.CONTENT_URI, true, mWallpaperObserver) context.contentResolver .registerContentObserver(StyleContract.GalleryWallpaper.CONTENT_URI, true, mWallpaperObserver) context.contentResolver .registerContentObserver(StyleContract.AdvanceWallpaper.CONTENT_URI, true, mWallpaperObserver) sourcesObservable.registerObserver(sourceObserver) } override fun registerObserver(observer: DefaultObserver) { synchronized(mObserverSet) { mObserverSet.add(observer) } } override fun unregisterObserver(observer: DefaultObserver) { synchronized(mObserverSet) { mObserverSet.remove(observer) } } private fun notifyObserver() { for (observer in mObserverSet) { observer.onNext(null) observer.onComplete() } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/AdvanceWallpaperDataRepository.kt ================================================ package com.yalin.style.data.repository import android.content.Context import com.yalin.style.data.entity.mapper.AdvanceWallpaperEntityMapper import com.yalin.style.data.extensions.DelegateExt import com.yalin.style.data.repository.datasource.AdvanceWallpaperDataStoreFactory import com.yalin.style.domain.AdvanceWallpaper import com.yalin.style.domain.GalleryWallpaper import com.yalin.style.domain.Wallpaper import com.yalin.style.domain.repository.WallpaperRepository import io.reactivex.Observable import java.io.InputStream import javax.inject.Inject import javax.inject.Singleton /** * @author jinyalin * @since 2017/7/27. */ @Singleton class AdvanceWallpaperDataRepository @Inject constructor(val context: Context, val factory: AdvanceWallpaperDataStoreFactory, val wallpaperMapper: AdvanceWallpaperEntityMapper, val styleRepository: StyleWallpaperDataRepository) : WallpaperRepository { var needRollback: Boolean by DelegateExt.preferences(context, "is_need_rollback", false) override fun getWallpaper(): Observable { return Observable.create { emitter -> emitter.onNext(wallpaperMapper.mapToWallpaper(factory.create().getWallpaperEntity())) emitter.onComplete() } } override fun switchWallpaper(): Observable { return Observable.create { emitter -> emitter.onError(IllegalStateException( "StyleWallpaperRepository have not gallery wallpapers.")) } } override fun openInputStream(wallpaperId: String?): Observable { var id: String? = null styleRepository.wallpaper.subscribe( { wallpaper -> id = wallpaper.wallpaperId }) return styleRepository.openInputStream(id) } override fun getWallpaperCount(): Observable { return Observable.create { emitter -> emitter.onNext(1) emitter.onComplete() } } override fun likeWallpaper(wallpaperId: String?): Observable { return Observable.create { emitter -> emitter.onError(IllegalStateException( "StyleWallpaperRepository have not gallery wallpapers.")) } } override fun addGalleryWallpaperUris(uris: MutableList?): Observable { return Observable.create { emitter -> emitter.onError(IllegalStateException( "StyleWallpaperRepository have not gallery wallpapers.")) } } override fun removeGalleryWallpaperUris(uris: MutableList?): Observable { return Observable.create { emitter -> emitter.onError(IllegalStateException( "StyleWallpaperRepository have not gallery wallpapers.")) } } override fun getGalleryWallpapers(): Observable> { return Observable.create> { emitter -> emitter.onError(IllegalStateException( "StyleWallpaperRepository have not gallery wallpapers.")) } } override fun getAdvanceWallpapers(): Observable> { return factory.create().getAdvanceWallpapers().map(wallpaperMapper::transformList) } override fun loadAdvanceWallpapers(): Observable> { return factory.createRemoteDataStore().getAdvanceWallpapers() .map(wallpaperMapper::transformList) } override fun downloadAdvanceWallpaper(wallpaperId: String): Observable { return factory.createRemoteDataStore().downloadWallpaper(wallpaperId) } override fun selectAdvanceWallpaper(wallpaperId: String, tempSelect: Boolean): Observable { return factory.create().selectWallpaper(wallpaperId, tempSelect) } override fun getAdvanceWallpaper(): AdvanceWallpaper { return wallpaperMapper.transform(factory.create().getWallpaperEntity()) } override fun readAdvanceAd(wallpaperId: String): Observable { return factory.create().readAd(wallpaperId) } override fun foreNow(wallpaperUri: String?): Observable { return Observable.create { emitter -> emitter.onError(IllegalStateException( "StyleWallpaperRepository have not gallery wallpapers.")) } } override fun setGalleryUpdateInterval(intervalMin: Int): Observable { return Observable.create { emitter -> emitter.onError(IllegalStateException( "StyleWallpaperRepository have not gallery wallpapers.")) } } override fun getGalleryUpdateInterval(): Observable { return Observable.create { emitter -> emitter.onError(IllegalStateException( "StyleWallpaperRepository have not gallery wallpapers.")) } } fun markRollback() { needRollback = true } fun maybeRollback() { if (needRollback) { factory.create().rollback() } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/GalleryWallpaperDataRepository.kt ================================================ package com.yalin.style.data.repository import com.yalin.style.data.entity.mapper.WallpaperEntityMapper import com.yalin.style.data.repository.datasource.GalleryWallpaperDataStoreFactory import com.yalin.style.domain.AdvanceWallpaper import com.yalin.style.domain.GalleryWallpaper import com.yalin.style.domain.Wallpaper import com.yalin.style.domain.repository.WallpaperRepository import io.reactivex.Observable import java.io.InputStream import javax.inject.Inject import javax.inject.Singleton /** * @author jinyalin * @since 2017/5/24. */ @Singleton class GalleryWallpaperDataRepository @Inject constructor(val galleryWallpaperDataStoreFactory: GalleryWallpaperDataStoreFactory, val wallpaperEntityMapper: WallpaperEntityMapper) : WallpaperRepository { override fun getWallpaper(): Observable = galleryWallpaperDataStoreFactory.create() .wallPaperEntity.map(wallpaperEntityMapper::transform) override fun switchWallpaper(): Observable = galleryWallpaperDataStoreFactory.create() .switchWallPaperEntity().map(wallpaperEntityMapper::transform) override fun openInputStream(wallpaperId: String?): Observable = galleryWallpaperDataStoreFactory.create() .openInputStream(wallpaperId) override fun getWallpaperCount(): Observable = galleryWallpaperDataStoreFactory.create() .wallpaperCount override fun likeWallpaper(wallpaperId: String): Observable = galleryWallpaperDataStoreFactory.create() .likeWallpaper(wallpaperId) override fun addGalleryWallpaperUris(uris: List): Observable = galleryWallpaperDataStoreFactory.create() .addGalleryWallpaperUris(uris) override fun removeGalleryWallpaperUris(uris: List): Observable = galleryWallpaperDataStoreFactory.create() .removeGalleryWallpaperUris(uris) override fun getGalleryWallpapers(): Observable> = galleryWallpaperDataStoreFactory.create() .getGalleryWallpaperUris().map(wallpaperEntityMapper::transformGalleryWallpaper) override fun getAdvanceWallpapers(): Observable> { return Observable.create> { emitter -> emitter.onError(IllegalStateException( "GalleryWallpaperDataRepository have not advance wallpapers.")) } } override fun loadAdvanceWallpapers(): Observable> { return Observable.create> { emitter -> emitter.onError(IllegalStateException( "GalleryWallpaperDataRepository cannot load advance wallpapers.")) } } override fun downloadAdvanceWallpaper(wallpaperId: String): Observable { return Observable.create { emitter -> emitter.onError(IllegalStateException( "GalleryWallpaperDataRepository cannot download advance wallpapers.")) } } override fun selectAdvanceWallpaper(wallpaperId: String, tempSelect: Boolean): Observable { return Observable.create { emitter -> emitter.onError(IllegalStateException( "GalleryWallpaperDataRepository cannot select advance wallpapers.")) } } override fun getAdvanceWallpaper(): AdvanceWallpaper { throw IllegalStateException( "GalleryWallpaperDataRepository cannot get advance wallpapers.") } override fun readAdvanceAd(wallpaperId: String?): Observable { throw IllegalStateException( "GalleryWallpaperDataRepository cannot read advance ad.") } override fun foreNow(wallpaperUri: String): Observable = galleryWallpaperDataStoreFactory.create().forceNow(wallpaperUri) override fun setGalleryUpdateInterval(intervalMin: Int): Observable = galleryWallpaperDataStoreFactory.create().setUpdateIntervalMin(intervalMin) override fun getGalleryUpdateInterval(): Observable = galleryWallpaperDataStoreFactory.create().getUpdateIntervalMin() } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/SourcesDataRepository.kt ================================================ package com.yalin.style.data.repository import android.content.Context import com.yalin.style.data.entity.mapper.WallpaperEntityMapper import com.yalin.style.data.repository.datasource.SourcesDataStoreFactory import com.yalin.style.domain.Source import com.yalin.style.domain.repository.SourcesRepository import com.yalin.style.domain.repository.SourcesRepository.SOURCE_ID_ADVANCE import com.yalin.style.domain.repository.SourcesRepository.SOURCE_ID_CUSTOM import com.yalin.style.domain.repository.WallpaperRepository import io.reactivex.Observable import javax.inject.Inject import javax.inject.Singleton /** * @author jinyalin * @since 2017/5/23. */ @Singleton class SourcesDataRepository @Inject constructor(val context: Context, val sourcesDataStoreFactory: SourcesDataStoreFactory, val wallpaperEntityMapper: WallpaperEntityMapper, var styleWallpaperDataRepository: StyleWallpaperDataRepository, var customWallpaperDataRepository: GalleryWallpaperDataRepository, var advanceWallpaperDataRepository: AdvanceWallpaperDataRepository) : SourcesRepository { override fun getSources(): Observable> { val sourcesDataStore = sourcesDataStoreFactory.create() return sourcesDataStore.getSources().map(wallpaperEntityMapper::transformSources) } override fun selectSource(sourceId: Int, tempSelect: Boolean): Observable { val sourcesDataStore = sourcesDataStoreFactory.create() return sourcesDataStore.selectSource(sourceId, tempSelect) } override fun getWallpaperRepository(): WallpaperRepository { val sourcesDataStore = sourcesDataStoreFactory.create() when (sourcesDataStore.getUsedSourceId()) { SOURCE_ID_CUSTOM -> return customWallpaperDataRepository SOURCE_ID_ADVANCE -> return advanceWallpaperDataRepository else -> return styleWallpaperDataRepository } } override fun getSelectedSource(): Int { val sourcesDataStore = sourcesDataStoreFactory.create() return sourcesDataStore.getUsedSourceId() } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/StyleWallpaperDataRepository.java ================================================ package com.yalin.style.data.repository; import android.content.Context; import android.text.TextUtils; import com.fernandocejas.arrow.checks.Preconditions; import com.yalin.style.data.entity.mapper.WallpaperEntityMapper; import com.yalin.style.data.repository.datasource.WallpaperDataStore; import com.yalin.style.data.repository.datasource.StyleWallpaperDataStoreFactory; import com.yalin.style.data.repository.datasource.sync.SyncHelper; import com.yalin.style.data.repository.datasource.sync.account.Account; import com.yalin.style.domain.AdvanceWallpaper; import com.yalin.style.domain.GalleryWallpaper; import com.yalin.style.domain.Wallpaper; import com.yalin.style.domain.repository.WallpaperRepository; import java.io.InputStream; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/4/18. */ @Singleton public class StyleWallpaperDataRepository implements WallpaperRepository { private final Context context; private final StyleWallpaperDataStoreFactory styleWallpaperDataStoreFactory; private final WallpaperEntityMapper wallpaperEntityMapper; @Inject StyleWallpaperDataRepository(Context context, StyleWallpaperDataStoreFactory styleWallpaperDataStoreFactory, WallpaperEntityMapper wallpaperEntityMapper) { this.context = context; this.styleWallpaperDataStoreFactory = styleWallpaperDataStoreFactory; this.wallpaperEntityMapper = wallpaperEntityMapper; Account.createSyncAccount(context); SyncHelper.updateSyncInterval(context); } @Override public Observable getWallpaper() { final WallpaperDataStore dataStore = styleWallpaperDataStoreFactory.create(); return dataStore.getWallPaperEntity().map(wallpaperEntityMapper::transform); } @Override public Observable switchWallpaper() { final WallpaperDataStore dataStore = styleWallpaperDataStoreFactory.create(); return dataStore.switchWallPaperEntity().map(wallpaperEntityMapper::transform); } @Override public Observable openInputStream(String wallpaperId) { Preconditions.checkArgument(!TextUtils.isEmpty(wallpaperId), "WallpaperId cannot be null"); final WallpaperDataStore dataStore = styleWallpaperDataStoreFactory.createDbDataStore(); return dataStore.openInputStream(wallpaperId); } @Override public Observable getWallpaperCount() { final WallpaperDataStore dataStore = styleWallpaperDataStoreFactory.create(); return dataStore.getWallpaperCount(); } @Override public Observable likeWallpaper(String wallpaperId) { Preconditions.checkArgument(!TextUtils.isEmpty(wallpaperId), "WallpaperId cannot be null"); final WallpaperDataStore dataStore = styleWallpaperDataStoreFactory.createDbDataStore(); return dataStore.likeWallpaper(wallpaperId); } @Override public Observable addGalleryWallpaperUris(List uris) { return Observable.create(emitter -> emitter.onError(new IllegalStateException( "StyleWallpaperRepository can not add gallery wallpaper.")) ); } @Override public Observable removeGalleryWallpaperUris(List uris) { return Observable.create(emitter -> emitter.onError(new IllegalStateException( "StyleWallpaperRepository can not remove gallery wallpaper.")) ); } @Override public Observable> getGalleryWallpapers() { return Observable.create(emitter -> emitter.onError(new IllegalStateException( "StyleWallpaperRepository have not gallery wallpapers.")) ); } @Override public Observable> getAdvanceWallpapers() { return Observable.create(emitter -> emitter.onError(new IllegalStateException( "StyleWallpaperRepository cannot get advance wallpapers.")) ); } @Override public Observable> loadAdvanceWallpapers() { return Observable.create(emitter -> emitter.onError(new IllegalStateException( "StyleWallpaperRepository cannot load advance wallpapers.")) ); } @Override public Observable downloadAdvanceWallpaper(String wallpaperId) { return Observable.create(emitter -> emitter.onError(new IllegalStateException( "StyleWallpaperRepository cannot download advance wallpapers.")) ); } @Override public Observable selectAdvanceWallpaper(String wallpaperId, boolean tempSelect) { return Observable.create(emitter -> emitter.onError(new IllegalStateException( "StyleWallpaperRepository cannot select advance wallpapers.")) ); } public AdvanceWallpaper getAdvanceWallpaper() { throw new IllegalStateException( "StyleWallpaperRepository cannot get advance wallpapers."); } @Override public Observable readAdvanceAd(String wallpaperId) { throw new IllegalStateException( "StyleWallpaperRepository cannot read advance ad."); } @Override public Observable foreNow(String wallpaperUri) { return Observable.create(emitter -> emitter.onError(new IllegalStateException( "StyleWallpaperRepository can not foreNow gallery wallpaper.")) ); } @Override public Observable setGalleryUpdateInterval(int intervalMin) { return Observable.create(emitter -> emitter.onError(new IllegalStateException( "StyleWallpaperRepository can not setGalleryUpdateInterval gallery wallpaper.")) ); } @Override public Observable getGalleryUpdateInterval() { return Observable.create(emitter -> emitter.onError(new IllegalStateException( "StyleWallpaperRepository can not getGalleryUpdateInterval gallery wallpaper.")) ); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/AdvanceWallpaperDataStore.kt ================================================ package com.yalin.style.data.repository.datasource import com.yalin.style.data.entity.AdvanceWallpaperEntity import io.reactivex.Observable /** * @author jinyalin * @since 2017/7/28. */ interface AdvanceWallpaperDataStore { fun getWallpaperEntity(): AdvanceWallpaperEntity fun getAdvanceWallpapers(): Observable> fun selectWallpaper(wallpaperId: String, tempSelect: Boolean): Observable fun downloadWallpaper(wallpaperId: String): Observable fun readAd(wallpaperId: String): Observable fun rollback() } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/AdvanceWallpaperDataStoreFactory.kt ================================================ package com.yalin.style.data.repository.datasource import android.content.Context import com.yalin.style.data.cache.AdvanceWallpaperCache import javax.inject.Inject import javax.inject.Singleton /** * @author jinyalin * @since 2017/7/28. */ @Singleton class AdvanceWallpaperDataStoreFactory @Inject constructor(val context: Context, val advanceWallpaperCache: AdvanceWallpaperCache) { fun create(): AdvanceWallpaperDataStore { return AdvanceWallpaperDataStoreImpl(context, advanceWallpaperCache) } fun createRemoteDataStore(): AdvanceWallpaperDataStore { return RemoteAdvanceWallpaperDataStore(context, AdvanceWallpaperDataStoreImpl(context, advanceWallpaperCache)) } fun onDataRefresh() { advanceWallpaperCache.evictAll() } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/AdvanceWallpaperDataStoreImpl.kt ================================================ package com.yalin.style.data.repository.datasource import android.content.ContentValues import android.content.Context import android.database.Cursor import com.yalin.style.data.cache.AdvanceWallpaperCache import com.yalin.style.data.entity.AdvanceWallpaperEntity import com.yalin.style.data.repository.datasource.provider.StyleContract import com.yalin.style.data.utils.notifyChange import io.reactivex.Observable /** * @author jinyalin * @since 2017/7/28. */ class AdvanceWallpaperDataStoreImpl(val context: Context, val advanceWallpaperCache: AdvanceWallpaperCache) : AdvanceWallpaperDataStore { companion object { val TAG = "AdvanceDataStore" val DEFAULT_WALLPAPER_ID = "-1" } @Synchronized override fun getWallpaperEntity(): AdvanceWallpaperEntity { if (!advanceWallpaperCache.isDirty()) { val selected = advanceWallpaperCache.getSelectedWallpaper() if (selected != null) { return selected } } var cursor: Cursor? = null var entity: AdvanceWallpaperEntity? = null try { val contentResolver = context.contentResolver cursor = contentResolver.query(StyleContract.AdvanceWallpaper.CONTENT_SELECTED_URI, null, null, null, null) if (cursor != null && cursor.moveToFirst()) { entity = AdvanceWallpaperEntity.readEntityFromCursor(cursor) } } finally { if (cursor != null) { cursor.close() } } if (entity == null) { entity = buildDefaultWallpaper() } return entity } override fun getAdvanceWallpapers(): Observable> { return createAdvanceWallpapersFromDB().doOnNext(advanceWallpaperCache::put) } override fun selectWallpaper(wallpaperId: String, tempSelect: Boolean): Observable { return Observable.create { emitter -> val selectValue = ContentValues() selectValue.put(StyleContract.AdvanceWallpaper.COLUMN_NAME_SELECTED, 1) val unselectedValue = ContentValues() unselectedValue.put(StyleContract.AdvanceWallpaper.COLUMN_NAME_SELECTED, 0) // unselected old context.contentResolver.update( StyleContract.AdvanceWallpaper.CONTENT_SELECTED_URI, unselectedValue, null, null) // select new val uri = StyleContract.AdvanceWallpaper.buildWallpaperUri(wallpaperId) val selectedCount = context.contentResolver.update(uri, selectValue, null, null) if (selectedCount > 0) { emitter.onNext(true) } else { emitter.onNext(false) } synchronized(advanceWallpaperCache) { if (!advanceWallpaperCache.isDirty()) { advanceWallpaperCache.selectWallpaper(wallpaperId) } } emitter.onComplete() notifyChange(context, StyleContract.AdvanceWallpaper.CONTENT_URI) } } override fun downloadWallpaper(wallpaperId: String): Observable { throw UnsupportedOperationException("Local data store not support download wallpaper.") } fun loadWallpaperEntity(wallpaperId: String): AdvanceWallpaperEntity { var entity: AdvanceWallpaperEntity? = null synchronized(advanceWallpaperCache) { if (!advanceWallpaperCache.isDirty() && advanceWallpaperCache.isCached(wallpaperId)) { entity = advanceWallpaperCache.getWallpaper(wallpaperId) } else { entity = loadWallpaperEntityFromDB(wallpaperId) } if (entity == null) { entity = loadWallpaperEntityFromDB(wallpaperId) } } return entity!! } override fun readAd(wallpaperId: String): Observable { return Observable.create { emitter -> if (advanceWallpaperCache.isCached(wallpaperId)) { advanceWallpaperCache.readAd(wallpaperId) } val uri = StyleContract.AdvanceWallpaper.buildWallpaperUri(wallpaperId) val contentValues = ContentValues() contentValues.put(StyleContract.AdvanceWallpaper.COLUMN_NAME_NEED_AD, 0) context.contentResolver.update(uri, contentValues, null, null) emitter.onNext(true) emitter.onComplete() } } override fun rollback() { advanceWallpaperCache.evictAll() val unselectedValue = ContentValues() unselectedValue.put(StyleContract.AdvanceWallpaper.COLUMN_NAME_SELECTED, 0) // unselected all context.contentResolver.update( StyleContract.AdvanceWallpaper.CONTENT_SELECTED_URI, unselectedValue, null, null) } private fun loadWallpaperEntityFromDB(wallpaperId: String): AdvanceWallpaperEntity? { var cursor: Cursor? = null try { val contentResolver = context.contentResolver cursor = contentResolver.query(StyleContract.AdvanceWallpaper .buildWallpaperUri(wallpaperId), null, null, null, null) if (cursor != null && cursor.moveToFirst()) { val entity = AdvanceWallpaperEntity.readEntityFromCursor(cursor) return entity } return null } finally { cursor?.close() } } private fun createAdvanceWallpapersFromDB(): Observable> { return Observable.create { emitter -> var cursor: Cursor? = null val validWallpapers = ArrayList() try { val contentResolver = context.contentResolver cursor = contentResolver.query(StyleContract.AdvanceWallpaper.CONTENT_URI, null, null, null, null) validWallpapers.addAll(AdvanceWallpaperEntity.readCursor(cursor)) } finally { cursor?.close() } emitter.onNext(validWallpapers) emitter.onComplete() } } private fun buildDefaultWallpaper(): AdvanceWallpaperEntity { val entity = AdvanceWallpaperEntity() entity.isDefault = true entity.id = -1 entity.wallpaperId = DEFAULT_WALLPAPER_ID entity.author = "Yalin" entity.link = "kinglloy.com" entity.name = "Rainbow" return entity } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/CacheWallpaperDataStore.java ================================================ package com.yalin.style.data.repository.datasource; import com.yalin.style.data.cache.WallpaperCache; import com.yalin.style.data.entity.WallpaperEntity; import com.yalin.style.data.exception.ReswitchException; import com.yalin.style.data.lock.OpenInputStreamLock; import java.io.InputStream; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/4/20. */ public class CacheWallpaperDataStore implements WallpaperDataStore { private final WallpaperCache wallpaperCache; private final OpenInputStreamLock openInputStreamLock; public CacheWallpaperDataStore(WallpaperCache wallpaperCache, OpenInputStreamLock openInputStreamLock) { this.wallpaperCache = wallpaperCache; this.openInputStreamLock = openInputStreamLock; } @Override public Observable getWallPaperEntity() { return wallpaperCache.get(); } @Override public Observable switchWallPaperEntity() { if (openInputStreamLock.obtain()) { openInputStreamLock.release(); return wallpaperCache.getNext(); } else { return Observable.create(emitter -> emitter.onError(new ReswitchException())); } } @Override public Observable openInputStream(String wallpaperId) { throw new UnsupportedOperationException("Cache data store not support open input stream."); } @Override public Observable getWallpaperCount() { return wallpaperCache.getWallpaperCount(); } @Override public Observable likeWallpaper(String wallpaperId) { throw new UnsupportedOperationException("Cache data store not support open input stream."); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/DbWallpaperDataStore.java ================================================ package com.yalin.style.data.repository.datasource; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import com.yalin.style.data.cache.WallpaperCache; import com.yalin.style.data.entity.WallpaperEntity; import com.yalin.style.data.exception.LikeException; import com.yalin.style.data.lock.LikeWallpaperLock; import com.yalin.style.data.lock.OpenInputStreamLock; import com.yalin.style.data.log.LogUtil; import com.yalin.style.data.repository.datasource.provider.StyleContract; import com.yalin.style.data.repository.datasource.provider.StyleContract.Wallpaper; import java.io.IOException; import java.io.InputStream; import java.util.LinkedList; import java.util.Queue; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/4/18. */ public class DbWallpaperDataStore implements WallpaperDataStore { private static final String TAG = "DbWallpaperDataStore"; public static final String DEFAULT_WALLPAPER_ID = "-1"; private final Context context; private final WallpaperCache wallpaperCache; private final OpenInputStreamLock openInputStreamLock; private final LikeWallpaperLock likeWallpaperLock; public DbWallpaperDataStore(Context context, WallpaperCache wallpaperCache, OpenInputStreamLock openInputStreamLock, LikeWallpaperLock likeWallpaperLock) { this.context = context; this.wallpaperCache = wallpaperCache; this.openInputStreamLock = openInputStreamLock; this.likeWallpaperLock = likeWallpaperLock; } @Override public Observable getWallPaperEntity() { return createEntitiesObservable().doOnNext(wallpaperCache::put).map(Queue::peek); } @Override public Observable switchWallPaperEntity() { return getWallPaperEntity(); } @Override public Observable openInputStream(String wallpaperId) { return Observable.create(emitter -> { try { openInputStreamLock.obtain(); InputStream inputStream; if (DEFAULT_WALLPAPER_ID.equals(wallpaperId)) { inputStream = context.getAssets().open("painterly-architectonic.jpg"); } else { inputStream = context.getContentResolver().openInputStream( StyleContract.Wallpaper.buildWallpaperUri(wallpaperId)); } emitter.onNext(inputStream); emitter.onComplete(); } catch (IOException e) { LogUtil.D(TAG, "Open input stream failed for wallpaperId : " + wallpaperId); emitter.onError(e); } finally { openInputStreamLock.release(); } }); } @Override public Observable getWallpaperCount() { return Observable.create(emitter -> { Cursor cursor = null; int count = 0; ContentResolver contentResolver = context.getContentResolver(); cursor = contentResolver.query(StyleContract.Wallpaper.CONTENT_URI, null, null, null, null); if (cursor != null) { // contain default count = cursor.getCount() + 1; cursor.close(); } emitter.onNext(count); emitter.onComplete(); }); } @Override public Observable likeWallpaper(String wallpaperId) { if (!likeWallpaperLock.obtain()) { return Observable.create(emitter -> emitter.onError(new LikeException())); } wallpaperCache.likeWallpaper(wallpaperId); return Observable.create(emitter -> { Cursor cursor = null; try { ContentResolver contentResolver = context.getContentResolver(); cursor = contentResolver.query(StyleContract.Wallpaper.buildWallpaperUri(wallpaperId), null, null, null, null); if (cursor != null && cursor.moveToFirst()) { WallpaperEntity entity = WallpaperEntity.readEntityFromCursor(cursor); entity.liked = !entity.liked; int columnCount = contentResolver .update(StyleContract.Wallpaper.buildWallpaperLikeUri(wallpaperId), buildKeepContentValue(entity), null, null); if (columnCount > 0) { emitter.onNext(entity.liked); } else { throw new LikeException(); } } else { throw new LikeException(); } } catch (Exception e) { emitter.onError(e); } finally { likeWallpaperLock.release(); if (cursor != null) { cursor.close(); } } }); } private Observable> createEntitiesObservable() { return Observable.create(emitter -> { Cursor cursor = null; Queue validWallpapers = new LinkedList<>(); try { ContentResolver contentResolver = context.getContentResolver(); cursor = contentResolver.query(Wallpaper.CONTENT_URI, null, null, null, null); validWallpapers.addAll(WallpaperEntity.readCursor(context, cursor)); } finally { if (cursor != null) { cursor.close(); } } // from db all wallpaper can be liked for (WallpaperEntity entity : validWallpapers) { entity.canLike = true; } validWallpapers.add(buildDefaultWallpaper()); emitter.onNext(validWallpapers); emitter.onComplete(); }); } public static WallpaperEntity buildDefaultWallpaper() { WallpaperEntity wallpaperEntity = new WallpaperEntity(); wallpaperEntity.id = -1; wallpaperEntity.attribution = "kinglloy.com"; wallpaperEntity.byline = "Lyubov Popova, 1918"; wallpaperEntity.imageUri = "imageUri"; wallpaperEntity.title = "Painterly Architectonic"; wallpaperEntity.wallpaperId = DEFAULT_WALLPAPER_ID; wallpaperEntity.liked = false; wallpaperEntity.isDefault = true; wallpaperEntity.canLike = false; return wallpaperEntity; } private ContentValues buildKeepContentValue(WallpaperEntity entity) { ContentValues contentValues = new ContentValues(); contentValues.put(Wallpaper.COLUMN_NAME_LIKED, entity.liked ? 1 : 0); return contentValues; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/GalleryWallpaperDataStore.kt ================================================ package com.yalin.style.data.repository.datasource import android.content.ContentProviderOperation import android.content.Context import android.database.Cursor import android.net.Uri import android.os.Build import android.text.TextUtils import android.text.format.DateUtils import com.fernandocejas.arrow.checks.Preconditions import com.yalin.style.data.R import com.yalin.style.data.cache.GalleryWallpaperCache import com.yalin.style.data.entity.GalleryWallpaperEntity import com.yalin.style.data.entity.WallpaperEntity import com.yalin.style.data.exception.ReswitchException import com.yalin.style.data.extensions.DelegateExt import com.yalin.style.data.lock.OpenInputStreamLock import com.yalin.style.data.repository.datasource.io.GalleryWallpapersHandler import com.yalin.style.data.repository.datasource.io.RemoveGalleryWallpapersHandler import com.yalin.style.data.repository.datasource.provider.StyleContract import com.yalin.style.data.repository.datasource.sync.gallery.GalleryScheduleService import com.yalin.style.data.utils.getCacheFileForUri import com.yalin.style.data.utils.getImagesFromTreeUri import com.yalin.style.data.utils.notifyChange import com.yalin.style.domain.GalleryWallpaper import io.reactivex.Observable import java.io.FileInputStream import java.io.IOException import java.io.InputStream import java.util.* import kotlin.collections.ArrayList /** * @author jinyalin * @since 2017/5/22. */ class GalleryWallpaperDataStore(val context: Context, val openInputStreamLock: OpenInputStreamLock, val galleryWallpaperCache: GalleryWallpaperCache) : WallpaperDataStore { var currentGalleryWallpaperId: Long by DelegateExt.preferences(context, GalleryScheduleService.PREF_CURRENT_SHOW_WALLPAPER_ID, -1) var rotateIntervalMin: Int by DelegateExt.preferences(context, GalleryScheduleService.PREF_ROTATE_INTERVAL_MIN, GalleryScheduleService.DEFAULT_ROTATE_INTERVAL_MIN) override fun getWallPaperEntity(): Observable { if (galleryWallpaperCache.isCached()) { val entities = galleryWallpaperCache.get()!! return Observable.create { emitter -> emitter.onNext(peekValid(entities)) emitter.onComplete() } } else { return createEntitiesObservable().doOnNext(galleryWallpaperCache::put).map { entities -> return@map peekValid(entities) } } } override fun switchWallPaperEntity(): Observable { if (openInputStreamLock.obtain()) { openInputStreamLock.release() return doSwitch() } else { return Observable.create { emitter -> emitter.onError(ReswitchException()) } } } override fun openInputStream(wallpaperId: String?): Observable { Preconditions.checkArgument(!TextUtils.isEmpty(wallpaperId)) return Observable.create { emitter -> var cursor: Cursor? = null try { openInputStreamLock.obtain() var inputStream: InputStream? = null if (DbWallpaperDataStore.DEFAULT_WALLPAPER_ID == wallpaperId) { inputStream = context.assets.open("painterly-architectonic.jpg") } else { cursor = context.contentResolver.query( StyleContract.GalleryWallpaper.buildGalleryWallpaperUri( wallpaperId!!.toLong()), null, null, null, null) if (cursor != null && cursor.moveToFirst()) { val uriString = cursor.getString(cursor.getColumnIndex( StyleContract.GalleryWallpaper.COLUMN_NAME_CUSTOM_URI)) val isTreeUri = cursor.getInt(cursor.getColumnIndex( StyleContract.GalleryWallpaper.COLUMN_NAME_IS_TREE_URI)) == 1 if (isTreeUri && Build.VERSION.SDK_INT >= 21) { val images = getImagesFromTreeUri(context, Uri.parse(uriString), Int.MAX_VALUE) inputStream = context.contentResolver.openInputStream( images[Random().nextInt(images.size)]) } else { try { inputStream = context.contentResolver.openInputStream( Uri.parse(uriString)) } catch (e: Exception) { // if cached file exist then use cached file val cacheFile = getCacheFileForUri(context, uriString) if ((cacheFile != null && cacheFile.exists())) { inputStream = FileInputStream(cacheFile) } else { throw IOException("Cannot open gallery uri : " + uriString) } } } } } emitter.onNext(inputStream!!) emitter.onComplete() } catch (e: IOException) { emitter.onError(e) } finally { cursor?.close() openInputStreamLock.release() } } } override fun getWallpaperCount(): Observable { return Observable.create { emitter -> var totalCount: Int = 0 if (galleryWallpaperCache.isCached()) { totalCount = galleryWallpaperCache.get()!!.size } else { var cursor: Cursor? = null try { cursor = context.contentResolver.query(StyleContract.GalleryWallpaper.CONTENT_URI, null, null, null, null) if (cursor != null) { totalCount = cursor.count } } finally { cursor?.close() } } emitter.onNext(totalCount) emitter.onComplete() } } override fun likeWallpaper(wallpaperId: String?): Observable { throw UnsupportedOperationException("Gallery data store not support like.") } fun addGalleryWallpaperUris(uris: List): Observable { return Observable.create { emitter -> galleryWallpaperCache.evictAll() var success = true try { val wallpaperHandler = GalleryWallpapersHandler(context, uris) val contentOperators = ArrayList() wallpaperHandler.makeContentProviderOperations(contentOperators) if (contentOperators.size > 0) { context.contentResolver.applyBatch(StyleContract.AUTHORITY, contentOperators) } } catch (e: Exception) { success = false emitter.onError(e) } if (success) { emitter.onNext(true) emitter.onComplete() GalleryScheduleService.publish(context) } } } fun removeGalleryWallpaperUris(uris: List): Observable { return Observable.create { emitter -> galleryWallpaperCache.evictAll() var success = true try { val removeHandler = RemoveGalleryWallpapersHandler(context, uris) val contentOperators = ArrayList() removeHandler.makeContentProviderOperations(contentOperators) if (contentOperators.size > 0) { context.contentResolver.applyBatch(StyleContract.AUTHORITY, contentOperators) } } catch (e: Exception) { success = false emitter.onError(e) } if (success) { emitter.onNext(true) emitter.onComplete() GalleryScheduleService.publish(context) } } } fun getGalleryWallpaperUris(): Observable> { return createEntitiesObservable().doOnNext(galleryWallpaperCache::put) } fun forceNow(wallpaperUri: String): Observable { return Observable.create { emitter -> if (galleryWallpaperCache.isCached()) { for (entity in galleryWallpaperCache.get()!!) { if (TextUtils.equals(entity.uri, wallpaperUri)) { currentGalleryWallpaperId = entity.id break } } } else { var cursor: Cursor? = null try { cursor = context.contentResolver.query(StyleContract.GalleryWallpaper.CONTENT_URI, arrayOf(StyleContract.GalleryWallpaper._ID), StyleContract.GalleryWallpaper.COLUMN_NAME_CUSTOM_URI + " = ? ", arrayOf(wallpaperUri), null) if (cursor != null && cursor.moveToFirst()) { currentGalleryWallpaperId = cursor.getLong(0) } } finally { cursor?.close() } } emitter.onNext(true) emitter.onComplete() notifyChange(context, StyleContract.GalleryWallpaper.CONTENT_URI) } } fun setUpdateIntervalMin(intervalMin: Int): Observable { return Observable.create { emitter -> GalleryScheduleService.setInterval(context, intervalMin) emitter.onNext(true) emitter.onComplete() } } fun getUpdateIntervalMin(): Observable { return Observable.create { emitter -> emitter.onNext(rotateIntervalMin) emitter.onComplete() } } private fun createEntitiesObservable(): Observable> { return Observable.create> { emitter -> var cursor: Cursor? = null val validWallpapers = ArrayList() try { val contentResolver = context.contentResolver cursor = contentResolver.query(StyleContract.GalleryWallpaper.CONTENT_URI, null, null, null, null) validWallpapers.addAll(GalleryWallpaperEntity.readCursor(context, cursor)) } finally { cursor?.close() } emitter.onNext(validWallpapers) emitter.onComplete() } } private fun peekValid(entities: List): WallpaperEntity { if (entities.isNotEmpty()) { if (currentGalleryWallpaperId == -1L) { val selectedEntity: GalleryWallpaperEntity if (entities.size == 1) { selectedEntity = entities[0] currentGalleryWallpaperId = selectedEntity.id } else { val random = Random() selectedEntity = entities[random.nextInt(entities.size)] currentGalleryWallpaperId = selectedEntity.id } return switchToWallpaperEntity(selectedEntity) } else { entities.filter { it.id == currentGalleryWallpaperId } .forEach { return switchToWallpaperEntity(it) } } } return DbWallpaperDataStore.buildDefaultWallpaper() } private fun switchToWallpaperEntity(galleryWallpaperEntity: GalleryWallpaperEntity) : WallpaperEntity { val wallpaperEntity = WallpaperEntity() wallpaperEntity.isDefault = false wallpaperEntity.canLike = false wallpaperEntity.title = getTitle(galleryWallpaperEntity.dateTime) wallpaperEntity.byline = getByline(galleryWallpaperEntity.location) wallpaperEntity.attribution = "kinglloy.com" wallpaperEntity.imageUri = galleryWallpaperEntity.uri wallpaperEntity.wallpaperId = galleryWallpaperEntity.id.toString() return wallpaperEntity } private fun getTitle(dateTime: Long): String { if (dateTime > 0) return DateUtils.formatDateTime(context, dateTime, DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_SHOW_WEEKDAY) else return context.getString(R.string.gallery_from_gallery) } private fun getByline(location: String?): String { if (location.isNullOrEmpty()) return context.getString(R.string.gallery_touch_to_view) else return location!! } private fun doSwitch(): Observable { if (galleryWallpaperCache.isCached()) { return Observable.create { emitter -> val entities = galleryWallpaperCache.get() emitter.onNext(peekOne(entities!!)) emitter.onComplete() } } else { return createEntitiesObservable().doOnNext(galleryWallpaperCache::put) .map { entities -> return@map peekOne(entities) } } } private fun peekOne(entities: List): WallpaperEntity { var selectedEntity: GalleryWallpaperEntity? = null if (entities.size > 1) { val random = Random() while (true) { selectedEntity = entities[random.nextInt(entities.size)] if (selectedEntity.id != currentGalleryWallpaperId) { currentGalleryWallpaperId = selectedEntity.id break } } } else if (entities.size == 1) { if (currentGalleryWallpaperId != entities[0].id) { currentGalleryWallpaperId = entities[0].id } selectedEntity = entities[0] } else { if (currentGalleryWallpaperId != -1L) { currentGalleryWallpaperId = -1 } } if (selectedEntity != null) { return switchToWallpaperEntity(selectedEntity) } return DbWallpaperDataStore.buildDefaultWallpaper() } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/GalleryWallpaperDataStoreFactory.kt ================================================ package com.yalin.style.data.repository.datasource import android.content.Context import com.yalin.style.data.cache.GalleryWallpaperCache import com.yalin.style.data.lock.OpenInputStreamLock import javax.inject.Inject import javax.inject.Singleton /** * @author jinyalin * @since 2017/5/24. */ @Singleton class GalleryWallpaperDataStoreFactory @Inject constructor(val context: Context, val openInputStreamLock: OpenInputStreamLock, val galleryWallpaperCache: GalleryWallpaperCache) { fun create(): GalleryWallpaperDataStore { return GalleryWallpaperDataStore(context, openInputStreamLock, galleryWallpaperCache) } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/RemoteAdvanceWallpaperDataStore.kt ================================================ package com.yalin.style.data.repository.datasource import android.content.ContentProviderOperation import android.content.ContentResolver import android.content.Context import android.content.OperationApplicationException import android.database.Cursor import android.os.RemoteException import com.google.gson.JsonParser import com.yalin.style.data.R import com.yalin.style.data.entity.AdvanceWallpaperEntity import com.yalin.style.data.exception.NoContentException import com.yalin.style.data.exception.RemoteServerException import com.yalin.style.data.log.LogUtil import com.yalin.style.data.repository.datasource.io.AdvanceWallpaperHandler import com.yalin.style.data.repository.datasource.net.RemoteAdvanceWallpaperFetcher import com.yalin.style.data.repository.datasource.provider.StyleContract import com.yalin.style.data.repository.datasource.sync.account.Account import com.yalin.style.domain.interactor.DefaultObserver import io.reactivex.Observable /** * @author jinyalin * @since 2017/7/31. */ class RemoteAdvanceWallpaperDataStore(val context: Context, val localDataStore: AdvanceWallpaperDataStoreImpl) : AdvanceWallpaperDataStore { companion object { val TAG = "RemoteAdvanceWallpaper" } val wallpaperHandler = AdvanceWallpaperHandler(context) override fun getWallpaperEntity(): AdvanceWallpaperEntity { throw UnsupportedOperationException("Remote data store not support get wallpaper.") } override fun getAdvanceWallpapers(): Observable> { return Observable.create { emitter -> val account = Account.getAccount() val authority = context.getString(R.string.authority) ContentResolver.cancelSync(account, authority) val batch = ArrayList() try { val wallpapers = RemoteAdvanceWallpaperFetcher(context).fetchDataIfNewer() val parser = JsonParser() val handler = AdvanceWallpaperHandler(context) handler.process(parser.parse(wallpapers)) handler.makeContentProviderOperations(batch) } catch (e: Exception) { emitter.onError(RemoteServerException()) return@create } try { val operations = batch.size if (operations > 0) { context.contentResolver.applyBatch(StyleContract.AUTHORITY, batch) } } catch (ex: RemoteException) { LogUtil.D(TAG, "RemoteException while applying content provider operations.") throw RuntimeException("Error executing content provider batch operation", ex) } catch (ex: OperationApplicationException) { LogUtil.D(TAG, "OperationApplicationException while applying content provider operations.") throw RuntimeException("Error executing content provider batch operation", ex) } var cursor: Cursor? = null val validWallpapers = ArrayList() try { val contentResolver = context.contentResolver cursor = contentResolver.query(StyleContract.AdvanceWallpaper.CONTENT_URI, null, null, null, null) validWallpapers.addAll(AdvanceWallpaperEntity.readCursor(cursor)) } finally { if (cursor != null) { cursor.close() } } if (validWallpapers.isEmpty()) { emitter.onError(NoContentException()) } else { emitter.onNext(validWallpapers) } emitter.onComplete() } } override fun selectWallpaper(wallpaperId: String, tempSelect: Boolean): Observable { throw UnsupportedOperationException("Remote data store not support select wallpaper.") } override fun downloadWallpaper(wallpaperId: String): Observable { return Observable.create { emitter -> val entity = localDataStore.loadWallpaperEntity(wallpaperId) wallpaperHandler.downloadWallpaperComponent(entity, object : DefaultObserver() { override fun onNext(downloadedLength: Long) { emitter.onNext(downloadedLength) } override fun onComplete() { emitter.onComplete() } override fun onError(exception: Throwable) { emitter.onError(exception) } }) } } override fun readAd(wallpaperId: String): Observable { throw UnsupportedOperationException("Remote data store not support read wallpaper ad.") } override fun rollback() { // do nothing } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/SourcesDataStore.kt ================================================ package com.yalin.style.data.repository.datasource import com.yalin.style.data.entity.SourceEntity import io.reactivex.Observable /** * @author jinyalin * @since 2017/5/23. */ interface SourcesDataStore { fun getSources(): Observable> fun selectSource(sourceId: Int, tempSelect: Boolean): Observable fun getUsedSourceId(): Int } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/SourcesDataStoreFactory.kt ================================================ package com.yalin.style.data.repository.datasource import android.content.Context import com.yalin.style.data.cache.SourcesCache import com.yalin.style.data.lock.SelectSourceLock import javax.inject.Inject import javax.inject.Singleton /** * @author jinyalin * @since 2017/5/23. */ @Singleton class SourcesDataStoreFactory @Inject constructor(val context: Context, val sourcesCache: SourcesCache, val selectSourceLock: SelectSourceLock) { fun create(): SourcesDataStore { return SourcesDataStoreImpl(context, sourcesCache, selectSourceLock) } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/SourcesDataStoreImpl.kt ================================================ package com.yalin.style.data.repository.datasource import android.content.Context import com.yalin.style.data.cache.SourcesCache import com.yalin.style.data.entity.SourceEntity import com.yalin.style.data.lock.SelectSourceLock import io.reactivex.Observable /** * @author jinyalin * @since 2017/5/23. */ class SourcesDataStoreImpl(val context: Context, val sourcesCache: SourcesCache, val selectSourceLock: SelectSourceLock) : SourcesDataStore { override fun selectSource(sourceId: Int, tempSelect: Boolean): Observable { return Observable.create { emitter -> try { if ((!tempSelect && selectSourceLock.obtain()) || tempSelect) { emitter.onNext(sourcesCache.selectSource(sourceId, tempSelect)) } else { emitter.onNext(false) } emitter.onComplete() } finally { selectSourceLock.release() } } } override fun getSources(): Observable> { return sourcesCache.getSources(context) } override fun getUsedSourceId() = sourcesCache.getUsedSourceId() } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/StyleWallpaperDataStoreFactory.java ================================================ package com.yalin.style.data.repository.datasource; import android.content.Context; import com.yalin.style.data.cache.WallpaperCache; import com.yalin.style.data.lock.LikeWallpaperLock; import com.yalin.style.data.lock.OpenInputStreamLock; import javax.inject.Inject; import javax.inject.Singleton; /** * @author jinyalin * @since 2017/4/18. */ @Singleton public class StyleWallpaperDataStoreFactory { private final Context context; private final WallpaperCache wallpaperCache; private final OpenInputStreamLock openInputStreamLock; private final LikeWallpaperLock likeWallpaperLock; @Inject StyleWallpaperDataStoreFactory(Context context, WallpaperCache wallpaperCache, OpenInputStreamLock openInputStreamLock, LikeWallpaperLock likeWallpaperLock) { this.context = context; this.wallpaperCache = wallpaperCache; this.openInputStreamLock = openInputStreamLock; this.likeWallpaperLock = likeWallpaperLock; } public WallpaperDataStore create() { WallpaperDataStore wallpaperDataStore; if (!wallpaperCache.isDirty() && wallpaperCache.isCached()) { wallpaperDataStore = new CacheWallpaperDataStore(wallpaperCache, openInputStreamLock); } else { wallpaperDataStore = createDbDataStore(); } return wallpaperDataStore; } public WallpaperDataStore createDbDataStore() { return new DbWallpaperDataStore(context, wallpaperCache, openInputStreamLock, likeWallpaperLock); } public void onDataRefresh() { wallpaperCache.evictAll(); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/WallpaperDataStore.java ================================================ package com.yalin.style.data.repository.datasource; import com.yalin.style.data.entity.WallpaperEntity; import java.io.InputStream; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/4/18. */ public interface WallpaperDataStore { Observable getWallPaperEntity(); Observable switchWallPaperEntity(); Observable openInputStream(String wallpaperId); Observable getWallpaperCount(); Observable likeWallpaper(String wallpaperId); } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/io/AdvanceWallpaperHandler.kt ================================================ package com.yalin.style.data.repository.datasource.io import android.content.ContentProviderOperation import android.content.Context import android.database.Cursor import com.google.gson.Gson import com.google.gson.JsonElement import com.yalin.style.data.SyncConfig import com.yalin.style.data.entity.AdvanceWallpaperEntity import com.yalin.style.data.exception.NetworkConnectionException import com.yalin.style.data.exception.RemoteServerException import com.yalin.style.data.log.LogUtil import com.yalin.style.data.repository.datasource.provider.StyleContract import com.yalin.style.data.repository.datasource.provider.StyleContractHelper import com.yalin.style.data.utils.WallpaperFileHelper import com.yalin.style.domain.interactor.DefaultObserver import okhttp3.OkHttpClient import okhttp3.Request import java.io.* import java.net.URL import java.util.ArrayList import java.util.HashSet import java.util.concurrent.TimeUnit import java.util.concurrent.locks.ReentrantLock /** * @author jinyalin * @since 2017/7/28. */ class AdvanceWallpaperHandler(context: Context) : JSONHandler(context) { companion object { val TAG = "AdvanceWallpaperHandler" val downloadLock = ReentrantLock() } private var wallpapers: ArrayList = ArrayList() override fun makeContentProviderOperations(list: ArrayList) { val uri = StyleContractHelper.setUriAsCalledFromSyncAdapter( StyleContract.AdvanceWallpaper.CONTENT_URI) list.add(ContentProviderOperation.newDelete(uri).build()) val validFiles = HashSet() val selectedEntities = querySelectedWallpapers() validFiles.addAll(getWallpaperNameSet(selectedEntities)) for (wallpaper in this.wallpapers) { wallpaper.storePath = makeStorePath(wallpaper) if (!selectedEntities.contains(wallpaper)) { if (wallpaper.lazyDownload || (downloadWallpaperComponent(wallpaper) && WallpaperFileHelper.ensureChecksumValid(mContext, wallpaper.checkSum, wallpaper.storePath))) { LogUtil.D(TAG, "download wallpaper component " + " success, do output wallpaper.") outputWallpaper(wallpaper, list) validFiles.add(makeFilename(wallpaper)) } } } // delete old wallpapers WallpaperFileHelper.deleteOldComponent(mContext, validFiles) } override fun process(element: JsonElement) { val wallpapers = Gson().fromJson(element, Array::class.java) this.wallpapers.ensureCapacity(wallpapers.size) this.wallpapers.addAll(wallpapers) } private fun makeFilename(wallpaper: AdvanceWallpaperEntity): String { return wallpaper.hashCode().toString() + "_component.apk" } private fun makeStorePath(wallpaper: AdvanceWallpaperEntity): String { val outputDir = WallpaperFileHelper.getAdvanceWallpaperDir(mContext) return File(outputDir, makeFilename(wallpaper)).absolutePath } private fun getWallpaperNameSet(entities: List): Set { val ids = HashSet() for (entity in entities) { ids.add(makeFilename(entity)) } return ids } private fun outputWallpaper(wallpaper: AdvanceWallpaperEntity, list: ArrayList) { val uri = StyleContractHelper.setUriAsCalledFromSyncAdapter( StyleContract.AdvanceWallpaper.CONTENT_URI) val builder = ContentProviderOperation.newInsert(uri) builder.withValue(StyleContract.AdvanceWallpaper.COLUMN_NAME_WALLPAPER_ID, wallpaper.wallpaperId) builder.withValue(StyleContract.AdvanceWallpaper.COLUMN_NAME_AUTHOR, wallpaper.author) builder.withValue(StyleContract.AdvanceWallpaper.COLUMN_NAME_DOWNLOAD_URL, wallpaper.downloadUrl) builder.withValue(StyleContract.AdvanceWallpaper.COLUMN_NAME_ICON_URL, wallpaper.iconUrl) builder.withValue(StyleContract.AdvanceWallpaper.COLUMN_NAME_LINK, wallpaper.link) builder.withValue(StyleContract.AdvanceWallpaper.COLUMN_NAME_NAME, wallpaper.name) builder.withValue(StyleContract.AdvanceWallpaper.COLUMN_NAME_CHECKSUM, wallpaper.checkSum) builder.withValue(StyleContract.AdvanceWallpaper.COLUMN_NAME_STORE_PATH, wallpaper.storePath) builder.withValue(StyleContract.AdvanceWallpaper.COLUMN_NAME_PROVIDER_NAME, wallpaper.providerName) builder.withValue(StyleContract.AdvanceWallpaper.COLUMN_NAME_SELECTED, 0) builder.withValue(StyleContract.AdvanceWallpaper.COLUMN_NAME_LAZY_DOWNLOAD, if (wallpaper.lazyDownload) 1 else 0) builder.withValue(StyleContract.AdvanceWallpaper.COLUMN_NAME_NEED_AD, if (wallpaper.needAd) 1 else 0) list.add(builder.build()) } private fun querySelectedWallpapers(): List { var cursor: Cursor? = null try { cursor = mContext.contentResolver.query( StyleContract.AdvanceWallpaper.CONTENT_SELECTED_URI, null, null, null, null) return AdvanceWallpaperEntity.readCursor(cursor) } finally { if (cursor != null) { cursor.close() } } } private fun downloadWallpaperComponent(wallpaper: AdvanceWallpaperEntity): Boolean { return downloadWallpaperComponent(wallpaper, null) } fun downloadWallpaperComponent(wallpaper: AdvanceWallpaperEntity, observer: DefaultObserver?): Boolean { observer?.onNext(0) LogUtil.D(TAG, "Start download wallpaper component to " + wallpaper.storePath) val outputFile = File(wallpaper.storePath) if (outputFile.exists()) { if (WallpaperFileHelper.ensureChecksumValid(mContext, wallpaper.checkSum, wallpaper.storePath)) { observer?.onComplete() return true } } synchronized(downloadLock) { var os: OutputStream? = null var _is: InputStream? = null try { if (outputFile.exists()) { if (WallpaperFileHelper.ensureChecksumValid(mContext, wallpaper.checkSum, wallpaper.storePath)) { observer?.onComplete() return true } else { outputFile.delete() } } val storePath = outputFile.parentFile storePath.mkdirs() os = FileOutputStream(outputFile) val httpClient = OkHttpClient.Builder() .connectTimeout(SyncConfig.DEFAULT_CONNECT_TIMEOUT.toLong(), TimeUnit.SECONDS) .readTimeout(SyncConfig.DEFAULT_DOWNLOAD_TIMEOUT.toLong(), TimeUnit.SECONDS) .build() val request = Request.Builder().url(URL(wallpaper.downloadUrl)).build() val response = httpClient.newCall(request).execute() val responseCode = response.code() if (responseCode in 200..299) { _is = response.body().byteStream() val buffer = ByteArray(1024) var bytesRead: Int var writeLength = 0L bytesRead = _is.read(buffer) while (bytesRead > 0) { os.write(buffer, 0, bytesRead) writeLength += bytesRead observer?.onNext(writeLength) bytesRead = _is.read(buffer) } os.flush() observer?.onComplete() return true } else { LogUtil.E(TAG, "Download wallpaper component " + wallpaper.name + " failed.") observer?.onError(RemoteServerException()) return false } } catch (e: IOException) { e.printStackTrace() observer?.onError(NetworkConnectionException()) LogUtil.E(TAG, "Download wallpaper component" + wallpaper.name + " failed.", e) return false } finally { ensureChecksum(outputFile, wallpaper.checkSum) try { os?.close() _is?.close() } catch (e: IOException) { // ignore } } } } private fun ensureChecksum(file: File, checkSum: String) { if (file.exists()) { if (!WallpaperFileHelper.ensureChecksumValid(mContext, checkSum, file.absolutePath)) { file.delete() } } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/io/GalleryWallpapersHandler.kt ================================================ package com.yalin.style.data.repository.datasource.io import android.annotation.SuppressLint import android.content.ContentProviderOperation import android.content.Context import android.location.Address import android.location.Geocoder import android.net.Uri import android.support.media.ExifInterface import android.text.TextUtils import com.google.gson.JsonElement import com.yalin.style.data.log.LogUtil import com.yalin.style.data.repository.datasource.provider.StyleContract import com.yalin.style.data.utils.isTreeUri import com.yalin.style.data.utils.processUriPermission import com.yalin.style.domain.GalleryWallpaper import java.text.SimpleDateFormat import java.util.ArrayList /** * @author jinyalin * @since 2017/5/24. */ class GalleryWallpapersHandler(val context: Context, val uris: List) : JSONHandler(context) { companion object { private val TAG = "GalleryWallpapersHandler" @SuppressLint("SimpleDateFormat") private val sExifDateFormat = SimpleDateFormat("yyyy:MM:dd HH:mm:ss") } private var mGeocoder: Geocoder = Geocoder(context) override fun makeContentProviderOperations(list: ArrayList) { val uri = StyleContract.GalleryWallpaper.CONTENT_URI for (wallpaperEntity in uris) { if (TextUtils.isEmpty(wallpaperEntity.uri)) { continue } wallpaperEntity.isTreeUri = isTreeUri(Uri.parse(wallpaperEntity.uri)) processUriPermission(context, wallpaperEntity) val builder = ContentProviderOperation.newInsert(uri) builder.withValue(StyleContract.GalleryWallpaper.COLUMN_NAME_CUSTOM_URI, wallpaperEntity.uri) builder.withValue(StyleContract.GalleryWallpaper.COLUMN_NAME_IS_TREE_URI, if (wallpaperEntity.isTreeUri) 1 else 0) readMetaData(wallpaperEntity.uri, builder) list.add(builder.build()) } } override fun process(element: JsonElement) { } private fun readMetaData(uriString: String, builder: ContentProviderOperation.Builder) { try { val uri = Uri.parse(uriString) var hasMetadata = false context.contentResolver.openInputStream(uri).use({ `in` -> if (`in` == null) { return } val exifInterface = ExifInterface(`in`) val dateString = exifInterface.getAttribute(ExifInterface.TAG_DATETIME) if (!TextUtils.isEmpty(dateString)) { val date = sExifDateFormat.parse(dateString) builder.withValue(StyleContract.GalleryWallpaper.COLUMN_NAME_DATE_TIME, date.time) hasMetadata = true } val latlong = exifInterface.latLong if (latlong != null) { // Reverse geocode var addresses: List
? = null try { addresses = mGeocoder.getFromLocation(latlong[0], latlong[1], 1) } catch (e: IllegalArgumentException) { LogUtil.E(TAG, "Invalid latitude/longitude, skipping location metadata", e) } if (addresses != null && addresses.isNotEmpty()) { val addr = addresses[0] val locality = addr.locality val adminArea = addr.adminArea val countryCode = addr.countryCode val sb = StringBuilder() if (!TextUtils.isEmpty(locality)) { sb.append(locality) } if (!TextUtils.isEmpty(adminArea)) { if (sb.isNotEmpty()) { sb.append(", ") } sb.append(adminArea) } if (!TextUtils.isEmpty(countryCode)) { if (sb.isNotEmpty()) { sb.append(", ") } sb.append(countryCode) } builder.withValue(StyleContract.GalleryWallpaper.COLUMN_NAME_LOCATION, sb.toString()) hasMetadata = true } } }) if (hasMetadata) { builder.withValue(StyleContract.GalleryWallpaper.COLUMN_NAME_HAS_METADATA, if (hasMetadata) 1 else 0) } } catch (e: Exception) { LogUtil.E(TAG, "Couldn't read image metadata.", e) } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/io/JSONHandler.java ================================================ package com.yalin.style.data.repository.datasource.io; import android.content.ContentProviderOperation; import android.content.Context; import com.google.gson.JsonElement; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.nio.charset.Charset; import java.util.ArrayList; /** * YaLin 2017/1/3. */ public abstract class JSONHandler { protected Context mContext; public JSONHandler(Context context) { mContext = context; } public abstract void makeContentProviderOperations(ArrayList list); public abstract void process(JsonElement element); /** * Loads a raw resource and returns its content as a String. * * @throws IOException If any error was encountered, such as an incorrect resource ID, or * inaccessible file. */ public static String parseResource(Context context, int resource) throws IOException { InputStream stream = null; String data; try { stream = context.getResources().openRawResource(resource); data = parseStream(stream); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { // Ignore exceptions during stream close, other exceptions thrown earlier will // be handled by the calling methods } } } return data; } private static String parseStream(final InputStream stream) throws IOException { Reader reader = null; Writer writer = new StringWriter(); char[] buffer = new char[1024]; // IO errors are passed up to the calling method and must be caught there. try { reader = new BufferedReader(new InputStreamReader(stream, Charset.forName("UTF-8"))); int n; while ((n = reader.read(buffer)) != -1) { writer.write(buffer, 0, n); } } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { // Ignore exceptions during stream close, other exceptions thrown earlier will // be handled by the calling methods } } } return writer.toString(); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/io/RemoveGalleryWallpapersHandler.kt ================================================ package com.yalin.style.data.repository.datasource.io import android.content.ContentProviderOperation import android.content.Context import android.text.TextUtils import com.google.gson.JsonElement import com.yalin.style.data.repository.datasource.provider.StyleContract import com.yalin.style.domain.GalleryWallpaper import java.util.ArrayList /** * YaLin * On 2017/5/26. */ class RemoveGalleryWallpapersHandler(val context: Context, val uris: List) : JSONHandler(context) { override fun makeContentProviderOperations(list: ArrayList) { uris.filterNot { TextUtils.isEmpty(it.uri) } .map { StyleContract.GalleryWallpaper .buildGalleryWallpaperDeleteUri(it.uri) } .map { ContentProviderOperation.newDelete(it) } .mapTo(list) { it.build() } } override fun process(element: JsonElement) { } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/io/WallpapersHandler.java ================================================ package com.yalin.style.data.repository.datasource.io; import android.content.ContentProviderOperation; import android.content.Context; import android.database.Cursor; import android.net.Uri; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.yalin.style.data.SyncConfig; import com.yalin.style.data.entity.WallpaperEntity; import com.yalin.style.data.log.LogUtil; import com.yalin.style.data.repository.datasource.provider.StyleContract.Wallpaper; import com.yalin.style.data.repository.datasource.provider.StyleContractHelper; import com.yalin.style.data.utils.TimeUtil; import com.yalin.style.data.utils.WallpaperFileHelper; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; /** * YaLin 2017/1/3. */ public class WallpapersHandler extends JSONHandler { private static final String TAG = "WallpapersHandler"; private ArrayList mWallpapers = new ArrayList<>(); public WallpapersHandler(Context context) { super(context); } @Override public void makeContentProviderOperations(ArrayList list) { Uri uri = StyleContractHelper.setUriAsCalledFromSyncAdapter(Wallpaper.CONTENT_URI); list.add(ContentProviderOperation.newDelete(uri).build()); Set validFiles = new HashSet<>(); List keepedEntity = queryKeepedWallpapers(); validFiles.addAll(getWallpaperIdSet(keepedEntity)); for (WallpaperEntity wallpaper : mWallpapers) { Uri wallpaperUri = Wallpaper.buildWallpaperSaveUri(wallpaper.wallpaperId); if (!keepedEntity.contains(wallpaper) && downloadWallpaper(wallpaper, wallpaperUri) && WallpaperFileHelper.ensureWallpaperChecksumValid(mContext, wallpaper.checksum, wallpaper.wallpaperId)) { LogUtil.D(TAG, "download wallpaper " + wallpaperUri + " success, do output wallpaper."); outputWallpaper(wallpaper, list, wallpaperUri.toString()); validFiles.add(wallpaper.wallpaperId); } } // delete old wallpapers WallpaperFileHelper.deleteOldWallpapers(mContext, validFiles); } @Override public void process(JsonElement element) { WallpaperEntity[] wallpapers = new Gson().fromJson(element, WallpaperEntity[].class); mWallpapers.ensureCapacity(wallpapers.length); Collections.addAll(mWallpapers, wallpapers); } private Set getWallpaperIdSet(List entities) { Set ids = new HashSet<>(); for (WallpaperEntity entity : entities) { ids.add(entity.wallpaperId); } return ids; } private List queryKeepedWallpapers() { Cursor cursor = null; try { cursor = mContext.getContentResolver().query(Wallpaper.CONTENT_KEEPED_URI, null, null, null, null); return WallpaperEntity.readCursor(mContext, cursor); } finally { if (cursor != null) { cursor.close(); } } } private void outputWallpaper(WallpaperEntity wallpaper, ArrayList list, String uriString) { Uri uri = StyleContractHelper.setUriAsCalledFromSyncAdapter(Wallpaper.CONTENT_URI); ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(uri); builder.withValue(Wallpaper.COLUMN_NAME_WALLPAPER_ID, wallpaper.wallpaperId); builder.withValue(Wallpaper.COLUMN_NAME_TITLE, wallpaper.title); builder.withValue(Wallpaper.COLUMN_NAME_IMAGE_URI, uriString); builder.withValue(Wallpaper.COLUMN_NAME_BYLINE, wallpaper.byline); builder.withValue(Wallpaper.COLUMN_NAME_ATTRIBUTION, wallpaper.attribution); builder.withValue(Wallpaper.COLUMN_NAME_ADD_DATE, TimeUtil.getCurrentTime(mContext)); builder.withValue(Wallpaper.COLUMN_NAME_LIKED, wallpaper.liked ? 1 : 0); builder.withValue(Wallpaper.COLUMN_NAME_CHECKSUM, wallpaper.checksum); list.add(builder.build()); } private boolean downloadWallpaper(WallpaperEntity wallpaper, Uri uri) { LogUtil.D(TAG, "Start download wallpaper uri = " + uri); OutputStream os = null; InputStream is = null; try { os = mContext.getContentResolver().openOutputStream(uri); if (os == null) { return false; } OkHttpClient httpClient = new OkHttpClient.Builder() .connectTimeout(SyncConfig.DEFAULT_CONNECT_TIMEOUT, TimeUnit.SECONDS) .readTimeout(SyncConfig.DEFAULT_DOWNLOAD_TIMEOUT, TimeUnit.SECONDS) .build(); Request request = new Request.Builder().url(new URL(wallpaper.imageUri)).build(); Response response = httpClient.newCall(request).execute(); int responseCode = response.code(); if (responseCode >= 200 && responseCode < 300) { is = response.body().byteStream(); byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = is.read(buffer)) > 0) { os.write(buffer, 0, bytesRead); } os.flush(); return true; } else { LogUtil.E(TAG, "Download wallpaper " + wallpaper.title + " failed."); return false; } } catch (IOException e) { e.printStackTrace(); LogUtil.E(TAG, "Download wallpaper " + wallpaper.title + " failed.", e); return false; } finally { try { if (os != null) { os.close(); } if (is != null) { is.close(); } } catch (IOException e) { // ignore } } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/net/DataFetcher.java ================================================ package com.yalin.style.data.repository.datasource.net; import android.content.Context; import android.text.TextUtils; import com.yalin.style.data.BuildConfig; import com.yalin.style.data.SyncConfig; import com.yalin.style.data.log.LogUtil; import com.yalin.style.data.utils.HttpRequestUtil; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; /** * @author jinyalin * @since 2017/7/28. */ public abstract class DataFetcher { private static final String TAG = "DataFetcher"; private final Context mContext; private final String mWallpaperUrl; public DataFetcher(Context context) { mContext = context; mWallpaperUrl = getUrl(); } public String fetchDataIfNewer() throws IOException { OkHttpClient httpClient = new OkHttpClient.Builder() .connectTimeout(SyncConfig.DEFAULT_CONNECT_TIMEOUT, TimeUnit.SECONDS) .readTimeout(SyncConfig.DEFAULT_READ_TIMEOUT, TimeUnit.SECONDS) .build(); Request request = new Request.Builder() .url(new URL(mWallpaperUrl)) .post(RequestBody.create(null, HttpRequestUtil.getRequestBody(mContext))) .build(); Response response = httpClient.newCall(request).execute(); if (response == null) { LogUtil.F(TAG, "Request for wallpaper returned null response."); throw new IOException("Request for data wallpaper returned null response."); } int status = response.code(); if (status == HttpURLConnection.HTTP_OK) { LogUtil.D(TAG, "Server return HTTP_OK, so new data is available."); String body = response.body().string(); if (TextUtils.isEmpty(body)) { LogUtil.F(TAG, "Request for wallpaper returned empty data."); throw new IOException("Error fetching wallpaper data : no data."); } LogUtil.D(TAG, "Wallpaper " + mWallpaperUrl + " read, contents: " + body); return body; } else if (status == HttpURLConnection.HTTP_NOT_MODIFIED) { LogUtil.D(TAG, "HTTP_NOT_MODIFIED: data has not changed since"); return null; } else { LogUtil.D(TAG, "Error fetching conference data: HTTP status " + status + " and manifest " + mWallpaperUrl); throw new IOException("Error fetching conference data: HTTP status " + status); } } protected abstract String getUrl(); } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/net/RemoteAdvanceWallpaperFetcher.java ================================================ package com.yalin.style.data.repository.datasource.net; import android.content.Context; import com.yalin.style.data.BuildConfig; /** * @author jinyalin * @since 2017/7/28. */ public class RemoteAdvanceWallpaperFetcher extends DataFetcher { public RemoteAdvanceWallpaperFetcher(Context context) { super(context); } protected String getUrl() { return BuildConfig.SERVER_WALLPAPER_ENDPOINT + "/advance"; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/provider/StyleContract.java ================================================ package com.yalin.style.data.repository.datasource.provider; import android.net.Uri; import android.provider.BaseColumns; /** * YaLin 2016/12/30. */ public class StyleContract { public static final String AUTHORITY = "com.yalin.style"; private static final String SCHEME = "content://"; interface WallpaperColumns { /** * Type: TEXT */ String COLUMN_NAME_WALLPAPER_ID = "wallpaper_id"; /** * Type: TEXT */ String COLUMN_NAME_IMAGE_URI = "image_uri"; /** * Type: TEXT */ String COLUMN_NAME_TITLE = "title"; /** * Type: TEXT */ String COLUMN_NAME_BYLINE = "byline"; /** * Type: TEXT */ String COLUMN_NAME_ATTRIBUTION = "attribution"; /** * Type: long */ String COLUMN_NAME_ADD_DATE = "add_date"; /** * Type: SHORT */ String COLUMN_NAME_LIKED = "keep"; /** * Type: TEXT */ String COLUMN_NAME_CHECKSUM = "checksum"; } interface GalleryColumns { /** * Type: TEXT */ String COLUMN_NAME_CUSTOM_URI = "custom_wallpaper_uri"; /** * Type: INTEGER */ String COLUMN_NAME_IS_TREE_URI = "is_tree_uri"; /** * Type: INTEGER */ String COLUMN_NAME_DATE_TIME = "date_time"; /** * Type: TEXT */ String COLUMN_NAME_LOCATION = "location"; /** * Type: INTEGER */ String COLUMN_NAME_HAS_METADATA = "has_metadata"; } interface AdvanceWallpaperColumns { /** * Type: TEXT */ String COLUMN_NAME_WALLPAPER_ID = "wallpaper_id"; /** * Type: TEXT */ String COLUMN_NAME_ICON_URL = "icon_url"; /** * Type: TEXT */ String COLUMN_NAME_NAME = "name"; /** * Type: TEXT */ String COLUMN_NAME_LINK = "link"; /** * Type: TEXT */ String COLUMN_NAME_AUTHOR = "author"; /** * Type: TEXT */ String COLUMN_NAME_DOWNLOAD_URL = "download_url"; /** * Type: TEXT */ String COLUMN_NAME_CHECKSUM = "checksum"; /** * Type: TEXT */ String COLUMN_NAME_STORE_PATH = "store_path"; /** * Type: TEXT */ String COLUMN_NAME_PROVIDER_NAME = "provider_name"; /** * Type: INTEGER */ String COLUMN_NAME_SELECTED = "selected"; /** * Type: INTEGER */ String COLUMN_NAME_LAZY_DOWNLOAD = "lazy_download"; /** * Type: INTEGER */ String COLUMN_NAME_NEED_AD = "need_ad"; } public static final Uri BASE_CONTENT_URI = Uri.parse(SCHEME + AUTHORITY); private static final String PATH_SOURCE = "source"; private static final String PATH_WALLPAPER = "wallpaper"; private static final String PATH_GALLERY = "gallery"; private static final String PATH_ADVANCE_WALLPAPER = "advance_wallpaper"; public static final String[] TOP_LEVEL_PATHS = { PATH_WALLPAPER }; public static final class Source { public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon().appendPath(PATH_SOURCE).build(); } public static final class Wallpaper implements WallpaperColumns, BaseColumns { public static final String TABLE_NAME = "wallpaper"; public static final String PATH_LIKE_WALLPAPER = "like"; public static final String PATH_LIKED_WALLPAPER = "liked"; public static final String PATH_SAVE_WALLPAPER = "save"; public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon().appendPath(PATH_WALLPAPER).build(); public static final Uri CONTENT_KEEPED_URI = BASE_CONTENT_URI.buildUpon().appendPath(PATH_WALLPAPER) .appendPath(PATH_LIKED_WALLPAPER).build(); public static Uri buildWallpaperUri(String wallpaperId) { return CONTENT_URI.buildUpon().appendPath(wallpaperId).build(); } public static Uri buildWallpaperSaveUri(String wallpaperId) { return CONTENT_URI.buildUpon() .appendPath(PATH_SAVE_WALLPAPER).appendPath(wallpaperId).build(); } public static Uri buildWallpaperLikeUri(String wallpaperId) { return CONTENT_URI.buildUpon() .appendPath(PATH_LIKE_WALLPAPER).appendPath(wallpaperId).build(); } public static String getWallpaperId(Uri uri) { return uri.getPathSegments().get(1); } public static String getWallpaperSaveId(Uri uri) { return uri.getPathSegments().get(2); } public static String getWallpaperLikeId(Uri uri) { return uri.getPathSegments().get(2); } } public static final class GalleryWallpaper implements GalleryColumns, BaseColumns { public static final String TABLE_NAME = "gallery_wallpaper"; public static final String PATH_URI = "uri"; public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon().appendPath(PATH_GALLERY).build(); public static Uri buildGalleryWallpaperUri(long insertId) { return CONTENT_URI.buildUpon().appendPath(String.valueOf(insertId)).build(); } public static String getGalleryWallpaperId(Uri uri) { return uri.getPathSegments().get(1); } public static Uri buildGalleryWallpaperDeleteUri(String uri) { return CONTENT_URI.buildUpon().appendPath(PATH_URI) .appendPath(String.valueOf(uri)).build(); } public static String getGalleryWallpaperDeleteUri(Uri uri) { return uri.getPathSegments().get(2); } } public static final class AdvanceWallpaper implements AdvanceWallpaperColumns, BaseColumns { public static final String TABLE_NAME = "advance_wallpaper"; public static final String PATH_SELECTED_WALLPAPER = "selected"; public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon().appendEncodedPath(PATH_ADVANCE_WALLPAPER).build(); public static final Uri CONTENT_SELECTED_URI = BASE_CONTENT_URI.buildUpon().appendPath(PATH_ADVANCE_WALLPAPER) .appendPath(PATH_SELECTED_WALLPAPER).build(); public static Uri buildWallpaperUri(String wallpaperId) { return CONTENT_URI.buildUpon().appendPath(wallpaperId).build(); } public static String getWallpaperId(Uri uri) { return uri.getPathSegments().get(1); } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/provider/StyleContractHelper.java ================================================ package com.yalin.style.data.repository.datasource.provider; import android.net.Uri; /** * YaLin 2017/1/3. */ public class StyleContractHelper { private static final String QUERY_PARAMETER_CALLER_IS_SYNC_ADAPTER = "callerIsSyncAdapter"; public static Uri setUriAsCalledFromSyncAdapter(Uri uri) { return uri.buildUpon().appendQueryParameter(QUERY_PARAMETER_CALLER_IS_SYNC_ADAPTER, "true") .build(); } public static boolean isUriCalledFromSyncAdapter(Uri uri) { return uri.getBooleanQueryParameter(QUERY_PARAMETER_CALLER_IS_SYNC_ADAPTER, false); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/provider/StyleDatabase.java ================================================ package com.yalin.style.data.repository.datasource.provider; import android.content.ContentResolver; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; import com.yalin.style.data.R; import com.yalin.style.data.log.LogUtil; import com.yalin.style.data.repository.datasource.provider.StyleContract.AdvanceWallpaper; import com.yalin.style.data.repository.datasource.provider.StyleContract.Wallpaper; import com.yalin.style.data.repository.datasource.sync.account.Account; /** * YaLin 2016/12/30. */ public class StyleDatabase extends SQLiteOpenHelper { private static final String TAG = "StyleDatabase"; private static final String DATABASE_NAME = "style.db"; private static final int VERSION_2016_12_30 = 1; private static final int VERSION_2017_4_30 = 2; private static final int VERSION_2017_5_24 = 3; private static final int VERSION_2017_7_28 = 4; private static final int VERSION_2017_8_11 = 5; private static final int CUR_DATABASE_VERSION = VERSION_2017_8_11; private final Context mContext; interface Tables { String WALLPAPER = StyleContract.Wallpaper.TABLE_NAME; String GALLERY = StyleContract.GalleryWallpaper.TABLE_NAME; String ADVANCE_WALLPAPER = AdvanceWallpaper.TABLE_NAME; } public StyleDatabase(Context context) { super(context, DATABASE_NAME, null, CUR_DATABASE_VERSION); mContext = context; } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + Tables.WALLPAPER + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + StyleContract.Wallpaper.COLUMN_NAME_WALLPAPER_ID + " TEXT," + StyleContract.Wallpaper.COLUMN_NAME_TITLE + " TEXT," + StyleContract.Wallpaper.COLUMN_NAME_IMAGE_URI + " TEXT," + StyleContract.Wallpaper.COLUMN_NAME_ATTRIBUTION + " TEXT," + StyleContract.Wallpaper.COLUMN_NAME_BYLINE + " TEXT," + StyleContract.Wallpaper.COLUMN_NAME_ADD_DATE + " INTEGER);"); upgradeFrom20161230to20170430(db); upgradeFrom20170430to20170524(db); upgradeFrom20170525to20170728(db); upgradeFrom20170728to20170811(db); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { LogUtil.D(TAG, "onUpgrade() from " + oldVersion + " to " + newVersion); // Cancel any sync currently in progress android.accounts.Account account = Account.getAccount(); if (account != null) { LogUtil.D(TAG, "Cancelling any pending syncs for account"); ContentResolver.cancelSync(account, mContext.getString(R.string.authority)); } int version = oldVersion; if (version == VERSION_2016_12_30) { upgradeFrom20161230to20170430(db); version = VERSION_2017_4_30; } if (version == VERSION_2017_4_30) { upgradeFrom20170430to20170524(db); version = VERSION_2017_5_24; } if (version == VERSION_2017_5_24) { upgradeFrom20170525to20170728(db); version = VERSION_2017_7_28; } if (version == VERSION_2017_7_28) { upgradeFrom20170728to20170811(db); version = VERSION_2017_8_11; } if (version != CUR_DATABASE_VERSION) { LogUtil.E(TAG, "Upgrade unsuccessful -- destroying old data during upgrade"); db.execSQL("DROP TABLE IF EXISTS " + Tables.WALLPAPER); db.execSQL("DROP TABLE IF EXISTS " + Tables.GALLERY); onCreate(db); version = CUR_DATABASE_VERSION; } } private void upgradeFrom20161230to20170430(SQLiteDatabase db) { db.execSQL("ALTER TABLE " + Tables.WALLPAPER + " ADD COLUMN " + Wallpaper.COLUMN_NAME_LIKED + " INTEGER NOT NULL DEFAULT 0"); db.execSQL("ALTER TABLE " + Tables.WALLPAPER + " ADD COLUMN " + Wallpaper.COLUMN_NAME_CHECKSUM + " TEXT"); } private void upgradeFrom20170430to20170524(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + Tables.GALLERY + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + StyleContract.GalleryWallpaper.COLUMN_NAME_CUSTOM_URI + " TEXT NOT NULL," + StyleContract.GalleryWallpaper.COLUMN_NAME_IS_TREE_URI + " INTEGER," + StyleContract.GalleryWallpaper.COLUMN_NAME_DATE_TIME + " INTEGER," + StyleContract.GalleryWallpaper.COLUMN_NAME_LOCATION + " TEXT," + StyleContract.GalleryWallpaper.COLUMN_NAME_HAS_METADATA + " INTEGER DEFAULT 0," + "UNIQUE (" + StyleContract.GalleryWallpaper.COLUMN_NAME_CUSTOM_URI + ") ON CONFLICT REPLACE);"); } private void upgradeFrom20170525to20170728(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + Tables.ADVANCE_WALLPAPER + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + AdvanceWallpaper.COLUMN_NAME_WALLPAPER_ID + " TEXT," + AdvanceWallpaper.COLUMN_NAME_ICON_URL + " TEXT," + AdvanceWallpaper.COLUMN_NAME_DOWNLOAD_URL + " TEXT," + AdvanceWallpaper.COLUMN_NAME_NAME + " TEXT," + AdvanceWallpaper.COLUMN_NAME_AUTHOR + " TEXT," + AdvanceWallpaper.COLUMN_NAME_STORE_PATH + " TEXT," + AdvanceWallpaper.COLUMN_NAME_LINK + " TEXT," + AdvanceWallpaper.COLUMN_NAME_PROVIDER_NAME + " TEXT," + AdvanceWallpaper.COLUMN_NAME_CHECKSUM + " TEXT," + AdvanceWallpaper.COLUMN_NAME_SELECTED + " INTEGER DEFAULT 0);"); } private void upgradeFrom20170728to20170811(SQLiteDatabase db) { db.execSQL("ALTER TABLE " + Tables.ADVANCE_WALLPAPER + " ADD COLUMN " + AdvanceWallpaper.COLUMN_NAME_LAZY_DOWNLOAD + " INTEGER NOT NULL DEFAULT 0"); db.execSQL("ALTER TABLE " + Tables.ADVANCE_WALLPAPER + " ADD COLUMN " + AdvanceWallpaper.COLUMN_NAME_NEED_AD + " INTEGER NOT NULL DEFAULT 0"); } public static void deleteDatabase(Context context) { context.deleteDatabase(DATABASE_NAME); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/provider/StyleProvider.java ================================================ package com.yalin.style.data.repository.datasource.provider; import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.provider.BaseColumns; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.yalin.style.data.log.LogUtil; import com.yalin.style.data.repository.datasource.provider.StyleContract.AdvanceWallpaper; import com.yalin.style.data.repository.datasource.provider.StyleContract.GalleryWallpaper; import com.yalin.style.data.repository.datasource.provider.StyleContract.Wallpaper; import com.yalin.style.data.utils.SelectionBuilder; import com.yalin.style.data.utils.WallpaperFileHelper; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Arrays; /** * YaLin 2016/12/30. */ public class StyleProvider extends ContentProvider { private static final String TAG = "StyleProvider"; private StyleDatabase mOpenHelper; private StyleProviderUriMatcher mUriMatcher; @Override public boolean onCreate() { mOpenHelper = new StyleDatabase(getContext()); mUriMatcher = new StyleProviderUriMatcher(); return true; } private void deleteDatabase() { mOpenHelper.close(); Context context = getContext(); StyleDatabase.deleteDatabase(context); mOpenHelper = new StyleDatabase(context); } @NonNull @Override public ContentProviderResult[] applyBatch(@NonNull ArrayList operations) throws OperationApplicationException { final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); db.beginTransaction(); try { final int numOperations = operations.size(); final ContentProviderResult[] results = new ContentProviderResult[numOperations]; for (int i = 0; i < numOperations; i++) { results[i] = operations.get(i).apply(this, results, i); } db.setTransactionSuccessful(); return results; } finally { db.endTransaction(); } } @Nullable @Override public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); StyleUriEnum uriEnum = mUriMatcher.matchUri(uri); LogUtil.D(TAG, "uri=" + uri + " code=" + uriEnum.code + " proj=" + Arrays.toString(projection) + " selection=" + selection + " args=" + Arrays.toString(selectionArgs) + ")"); switch (uriEnum) { case WALLPAPER: case WALLPAPER_ID: case WALLPAPER_LIKED: case ADVANCE_WALLPAPER: case ADVANCE_WALLPAPER_ID: case ADVANCE_WALLPAPER_SELECTED: { final SelectionBuilder builder = buildSimpleSelection(uri); return builder.query(db, projection, BaseColumns._ID + " DESC"); } case GALLERY: case GALLERY_ID: { final SelectionBuilder builder = buildSimpleSelection(uri); return builder.query(db, projection, GalleryWallpaper._ID + " DESC"); } default: { final SelectionBuilder builder = buildExpandedSelection(uri, uriEnum.code); return builder.query(db, projection, null); } } } @Nullable @Override public String getType(@NonNull Uri uri) { return null; } @Nullable @Override public Uri insert(@NonNull Uri uri, ContentValues values) { LogUtil.D(TAG, "insert(uri=" + uri + ", values=" + values.toString() + ")"); final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); StyleUriEnum uriEnum = mUriMatcher.matchUri(uri); long id = db.insertOrThrow(uriEnum.table, null, values); switch (uriEnum) { case WALLPAPER: return StyleContract.Wallpaper.buildWallpaperUri( values.getAsString(StyleContract.Wallpaper.COLUMN_NAME_WALLPAPER_ID)); case GALLERY: return StyleContract.GalleryWallpaper.buildGalleryWallpaperUri(id); case ADVANCE_WALLPAPER: { return AdvanceWallpaper.buildWallpaperUri( values.getAsString(AdvanceWallpaper.COLUMN_NAME_WALLPAPER_ID)); } default: { throw new UnsupportedOperationException("Unknown insert uri: " + uri); } } } @Override public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { LogUtil.D(TAG, "delete(uri=" + uri + ")"); if (uri == StyleContract.BASE_CONTENT_URI) { deleteDatabase(); return 1; } final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); StyleUriEnum uriEnum = mUriMatcher.matchUri(uri); final SelectionBuilder builder = buildSimpleSelection(uri); switch (uriEnum) { case WALLPAPER: { builder.where(Wallpaper.COLUMN_NAME_LIKED + " = ?", "0"); break; } case ADVANCE_WALLPAPER: { builder.where(AdvanceWallpaper.COLUMN_NAME_SELECTED + " = ?", "0"); break; } case GALLERY_URI: { return builder.delete(db); } } return builder.where(selection, selectionArgs).delete(db); } @Override public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { LogUtil.D(TAG, "update(uri=" + uri + ")"); if (uri == StyleContract.BASE_CONTENT_URI) { deleteDatabase(); return 1; } final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); final SelectionBuilder builder = buildSimpleSelection(uri); return builder.where(selection, selectionArgs).update(db, values); } @Nullable @Override public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { LogUtil.D(TAG, "openReadFile(uri=" + uri + ",mode=" + mode + ")"); StyleUriEnum uriEnum = mUriMatcher.matchUri(uri); switch (uriEnum) { case WALLPAPER: return WallpaperFileHelper.openReadFile(getContext(), queryUriForShow(), mode); case WALLPAPER_ID: return WallpaperFileHelper.openReadFile(getContext(), uri, mode); case WALLPAPER_SAVE: return WallpaperFileHelper.openWriteFile(getContext(), uri, mode); default: throw new FileNotFoundException("Cannot match uri : " + uri); } } private Uri queryUriForShow() { Cursor cursor = query(Wallpaper.CONTENT_URI, new String[]{StyleContract.Wallpaper.COLUMN_NAME_IMAGE_URI}, null, null, null); try { if (cursor != null && cursor.moveToFirst()) { return Uri.parse(cursor.getString(0)); } return null; } finally { if (cursor != null) { cursor.close(); } } } private SelectionBuilder buildSimpleSelection(Uri uri) { final SelectionBuilder builder = new SelectionBuilder(); StyleUriEnum uriEnum = mUriMatcher.matchUri(uri); switch (uriEnum) { case WALLPAPER: case ADVANCE_WALLPAPER: { return builder.table(uriEnum.table); } case WALLPAPER_ID: { String wallpaperId = StyleContract.Wallpaper.getWallpaperId(uri); return builder.table(StyleDatabase.Tables.WALLPAPER) .where(Wallpaper.COLUMN_NAME_WALLPAPER_ID + " = ?", wallpaperId); } case WALLPAPER_LIKE: { String wallpaperId = StyleContract.Wallpaper.getWallpaperLikeId(uri); return builder.table(StyleDatabase.Tables.WALLPAPER) .where(Wallpaper.COLUMN_NAME_WALLPAPER_ID + " = ?", wallpaperId); } case WALLPAPER_LIKED: { return builder.table(StyleDatabase.Tables.WALLPAPER) .where(Wallpaper.COLUMN_NAME_LIKED + " = ?", "1"); } case GALLERY: { return builder.table(StyleDatabase.Tables.GALLERY); } case GALLERY_ID: { String galleryWallpaperId = StyleContract.GalleryWallpaper.getGalleryWallpaperId(uri); return builder.table(StyleDatabase.Tables.GALLERY) .where(GalleryWallpaper._ID + " = ?", galleryWallpaperId); } case GALLERY_URI: { String uriString = StyleContract.GalleryWallpaper.getGalleryWallpaperDeleteUri(uri); return builder.table(StyleDatabase.Tables.GALLERY) .where(GalleryWallpaper.COLUMN_NAME_CUSTOM_URI + " = ?", uriString); } case ADVANCE_WALLPAPER_ID: { String wallpaperId = AdvanceWallpaper.getWallpaperId(uri); return builder.table(StyleDatabase.Tables.ADVANCE_WALLPAPER) .where(AdvanceWallpaper.COLUMN_NAME_WALLPAPER_ID + " = ?", wallpaperId); } case ADVANCE_WALLPAPER_SELECTED: { return builder.table(StyleDatabase.Tables.ADVANCE_WALLPAPER) .where(AdvanceWallpaper.COLUMN_NAME_SELECTED + " = ?", String.valueOf(1)); } default: { throw new UnsupportedOperationException("Unknown uri for " + uri); } } } private SelectionBuilder buildExpandedSelection(Uri uri, int match) { final SelectionBuilder builder = new SelectionBuilder(); StyleUriEnum uriEnum = mUriMatcher.matchUri(uri); if (uriEnum == null) { throw new UnsupportedOperationException("Unknown uri: " + uri); } switch (uriEnum) { default: { throw new UnsupportedOperationException("Unknown uri: " + uri); } } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/provider/StyleProviderUriMatcher.java ================================================ package com.yalin.style.data.repository.datasource.provider; import android.content.UriMatcher; import android.net.Uri; import android.util.SparseArray; /** * YaLin 2016/12/30. */ public class StyleProviderUriMatcher { private UriMatcher mUriMatcher; private SparseArray mEnumsMap = new SparseArray<>(); public StyleProviderUriMatcher() { mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); buildUriMatcher(); } private void buildUriMatcher() { final String authority = StyleContract.AUTHORITY; StyleUriEnum[] uris = StyleUriEnum.values(); for (StyleUriEnum uri : uris) { mUriMatcher.addURI(authority, uri.path, uri.code); } buildEnumsMap(); } private void buildEnumsMap() { StyleUriEnum[] uris = StyleUriEnum.values(); for (StyleUriEnum uri : uris) { mEnumsMap.put(uri.code, uri); } } public StyleUriEnum matchUri(Uri uri) { final int code = mUriMatcher.match(uri); try { return matchCode(code); } catch (UnsupportedOperationException e) { throw new UnsupportedOperationException("Unknown uri " + uri); } } public StyleUriEnum matchCode(int code) { StyleUriEnum uriEnum = mEnumsMap.get(code); if (uriEnum != null) { return uriEnum; } else { throw new UnsupportedOperationException("Unknown uri with code " + code); } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/provider/StyleUriEnum.java ================================================ package com.yalin.style.data.repository.datasource.provider; /** * YaLin 2016/12/30. */ public enum StyleUriEnum { WALLPAPER(100, "wallpaper", StyleDatabase.Tables.WALLPAPER), WALLPAPER_SAVE(103, "wallpaper/save/*", null), WALLPAPER_LIKE(104, "wallpaper/like/*", null), WALLPAPER_LIKED(105, "wallpaper/liked", null), WALLPAPER_ID(102, "wallpaper/*", null), GALLERY(200, "gallery", StyleDatabase.Tables.GALLERY), GALLERY_URI(202, "gallery/uri/*", null), GALLERY_ID(201, "gallery/*", null), ADVANCE_WALLPAPER(300, "advance_wallpaper", StyleDatabase.Tables.ADVANCE_WALLPAPER), ADVANCE_WALLPAPER_SELECTED(302, "advance_wallpaper/selected", null), ADVANCE_WALLPAPER_ID(301, "advance_wallpaper/*", null); public int code; public String path; public String table; StyleUriEnum(int code, String path, String table) { this.code = code; this.path = path; this.table = table; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/sync/RemoteStyleDataFetcher.java ================================================ package com.yalin.style.data.repository.datasource.sync; import android.content.Context; import com.yalin.style.data.BuildConfig; import com.yalin.style.data.repository.datasource.net.DataFetcher; /** * YaLin 2017/1/3. */ public class RemoteStyleDataFetcher extends DataFetcher { public RemoteStyleDataFetcher(Context context) { super(context); } @Override protected String getUrl() { return BuildConfig.SERVER_WALLPAPER_ENDPOINT; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/sync/StyleDataHandler.java ================================================ package com.yalin.style.data.repository.datasource.sync; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.Context; import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; import com.google.gson.JsonParser; import com.google.gson.stream.JsonReader; import com.yalin.style.data.log.LogUtil; import com.yalin.style.data.repository.datasource.io.AdvanceWallpaperHandler; import com.yalin.style.data.repository.datasource.io.JSONHandler; import com.yalin.style.data.repository.datasource.io.WallpapersHandler; import com.yalin.style.data.repository.datasource.provider.StyleContract; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; /** * YaLin 2017/1/3. */ public class StyleDataHandler { private static final String TAG = "StyleDataHandler"; private static final String DATA_KEY_WALLPAPER = "wallpapers"; private static final String DATA_KEY_ADVANCE_WALLPAPER = "advance_wallpapers"; private static final String[] DATA_KEYS_IN_ORDER = { DATA_KEY_WALLPAPER, DATA_KEY_ADVANCE_WALLPAPER }; Context mContext = null; WallpapersHandler mWallpapersHandler = null; AdvanceWallpaperHandler mAdvanceWallpapersHandler = null; HashMap mHandlerForKey = new HashMap<>(); private int mContentProviderOperationsDone = 0; public StyleDataHandler(Context context) { mContext = context; } public void applyStyleData(String[] dataBodies) throws IOException { LogUtil.D(TAG, "Applying data from " + dataBodies.length + " files"); mHandlerForKey.put(DATA_KEY_WALLPAPER, mWallpapersHandler = new WallpapersHandler(mContext)); mHandlerForKey.put(DATA_KEY_ADVANCE_WALLPAPER, mAdvanceWallpapersHandler = new AdvanceWallpaperHandler(mContext)); LogUtil.D(TAG, "Processing " + dataBodies.length + " JSON objects."); for (int i = 0; i < dataBodies.length; i++) { LogUtil.D(TAG, "Processing json object #" + (i + 1) + " of " + dataBodies.length); processDataBody(dataBodies[i]); } ArrayList batch = new ArrayList<>(); for (String key : DATA_KEYS_IN_ORDER) { LogUtil.D(TAG, "Building content provider operations for: " + key); mHandlerForKey.get(key).makeContentProviderOperations(batch); LogUtil.D(TAG, "Content provider operations so far: " + batch.size()); } LogUtil.D(TAG, "Applying " + batch.size() + " content provider operations."); try { int operations = batch.size(); if (operations > 0) { mContext.getContentResolver().applyBatch(StyleContract.AUTHORITY, batch); } LogUtil.D(TAG, "Successfully applied " + operations + " content provider operations."); mContentProviderOperationsDone += operations; } catch (RemoteException ex) { LogUtil.D(TAG, "RemoteException while applying content provider operations."); throw new RuntimeException("Error executing content provider batch operation", ex); } catch (OperationApplicationException ex) { LogUtil.D(TAG, "OperationApplicationException while applying content provider operations."); throw new RuntimeException("Error executing content provider batch operation", ex); } LogUtil.D(TAG, "Notifying changes on all top-level paths on Content Resolver."); ContentResolver resolver = mContext.getContentResolver(); for (String path : StyleContract.TOP_LEVEL_PATHS) { Uri uri = StyleContract.BASE_CONTENT_URI.buildUpon().appendPath(path).build(); resolver.notifyChange(uri, null); } LogUtil.D(TAG, "Done applying conference data."); } private void processDataBody(String dataBody) throws IOException { JsonParser parser = new JsonParser(); try (JsonReader reader = new JsonReader(new StringReader(dataBody))) { reader.setLenient(true); // To err is human // the whole file is a single JSON object reader.beginObject(); while (reader.hasNext()) { String key = reader.nextName(); if (mHandlerForKey.containsKey(key)) { LogUtil.D(TAG, "Processing key in conference data json: " + key); mHandlerForKey.get(key).process(parser.parse(reader)); } else { LogUtil.D(TAG, "Skipping unknown key in conference data json: " + key); reader.skipValue(); } } reader.endObject(); } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/sync/SyncAdapter.java ================================================ package com.yalin.style.data.repository.datasource.sync; import android.accounts.Account; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; import android.content.Context; import android.content.SyncResult; import android.os.Bundle; import com.yalin.style.data.log.LogUtil; /** * YaLin 2017/1/3. */ public class SyncAdapter extends AbstractThreadedSyncAdapter { private static final String TAG = "SyncAdapter"; public static final String SYNC_MANUALLY = "syn_manually"; private final Context mContext; public SyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); mContext = context; Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> LogUtil.F(TAG, "Uncaught sync exception, suppressing UI in release build.", throwable)); } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { LogUtil.F(TAG, "PerformSync."); if (extras.getBoolean(SYNC_MANUALLY)) { LogUtil.D(TAG, "Manually sync."); } new SyncHelper(mContext).performSync(syncResult, extras); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/sync/SyncHelper.java ================================================ package com.yalin.style.data.repository.datasource.sync; import android.accounts.Account; import android.content.ContentResolver; import android.content.Context; import android.content.SyncResult; import android.net.ConnectivityManager; import android.os.Bundle; import android.text.TextUtils; import com.yalin.style.data.BuildConfig; import com.yalin.style.data.SyncConfig; import com.yalin.style.data.log.LogUtil; import com.yalin.style.data.repository.datasource.provider.StyleContract; import java.io.IOException; /** * YaLin 2017/1/3. */ public class SyncHelper { private static final String TAG = "SyncHelper"; private final Context mContext; private StyleDataHandler mDataHandler; public SyncHelper(Context context) { mContext = context; mDataHandler = new StyleDataHandler(mContext); } public boolean performSync(SyncResult syncResult, Bundle extras) { try { doStyleSync(); return true; } catch (IOException e) { e.printStackTrace(); } return false; } private boolean doStyleSync() throws IOException { if (!isOnline()) { LogUtil.D(TAG, "Not attempting remote sync because device is OFFLINE"); return false; } LogUtil.D(TAG, "Starting remote sync."); String data = new RemoteStyleDataFetcher(mContext).fetchDataIfNewer(); if (!TextUtils.isEmpty(data)) { mDataHandler.applyStyleData(new String[]{data}); } return true; } private boolean isOnline() { ConnectivityManager cm = (ConnectivityManager) mContext .getSystemService(Context.CONNECTIVITY_SERVICE); return cm.getActiveNetworkInfo() != null && cm.getActiveNetworkInfo().isConnectedOrConnecting(); } public static void updateSyncInterval(final Context context) { Account account = com.yalin.style.data.repository.datasource.sync.account.Account.getAccount(); LogUtil.D(TAG, "Checking sync interval"); long recommended = calculateRecommendedSyncInterval(context); LogUtil.D(TAG, "Setting up sync for account, interval " + recommended + "ms"); ContentResolver.setIsSyncable(account, StyleContract.AUTHORITY, 1); ContentResolver.setSyncAutomatically(account, StyleContract.AUTHORITY, true); ContentResolver .addPeriodicSync(account, StyleContract.AUTHORITY, new Bundle(), recommended / 1000L); } private static long calculateRecommendedSyncInterval(final Context context) { if (BuildConfig.DEMO_MODE) { return SyncConfig.DEBUG_AUTO_SYNC_INTERVAL_LONG; } else { return SyncConfig.AUTO_SYNC_INTERVAL_LONG; } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/sync/SyncService.java ================================================ package com.yalin.style.data.repository.datasource.sync; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.support.annotation.Nullable; /** * YaLin 2017/1/3. */ public class SyncService extends Service { private static final Object sSyncAdapterLock = new Object(); private static SyncAdapter sSyncAdapter = null; @Override public void onCreate() { super.onCreate(); synchronized (sSyncAdapterLock) { if (sSyncAdapter == null) { sSyncAdapter = new SyncAdapter(getApplicationContext(), false); } } } @Nullable @Override public IBinder onBind(Intent intent) { return sSyncAdapter.getSyncAdapterBinder(); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/sync/account/Account.java ================================================ package com.yalin.style.data.repository.datasource.sync.account; import android.accounts.AccountManager; import android.content.Context; import com.yalin.style.data.log.LogUtil; /** * YaLin 2017/1/3. */ public class Account { public static final String ACCOUNT_TYPE = "com.yalin.style"; public static final String ACCOUNT_NAME = "Sync Account"; private static final String TAG = "Account"; private static android.accounts.Account mAccount; public static android.accounts.Account createSyncAccount(Context context) { AccountManager accountManager = (AccountManager) context .getSystemService(Context.ACCOUNT_SERVICE); android.accounts.Account account = getAccount(); if (accountManager.addAccountExplicitly(account, null, null)) { return account; } else { LogUtil.D(TAG, "Unable to create account"); return null; } } public static android.accounts.Account getAccount() { if (mAccount == null) { mAccount = new android.accounts.Account(ACCOUNT_NAME, ACCOUNT_TYPE); } return mAccount; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/sync/account/Authenticator.java ================================================ package com.yalin.style.data.repository.datasource.sync.account; import android.accounts.*; import android.accounts.Account; import android.content.Context; import android.os.Bundle; /** * YaLin 2017/1/3. */ public class Authenticator extends AbstractAccountAuthenticator { public Authenticator(Context context) { super(context); } @Override public Bundle editProperties( AccountAuthenticatorResponse response, String accountType) { throw new UnsupportedOperationException(); } // Don't add additional accounts @Override public Bundle addAccount( AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException { return null; } // Ignore attempts to confirm credentials @Override public Bundle confirmCredentials( AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException { return null; } // Getting an authentication token is not supported @Override public Bundle getAuthToken( AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { throw new UnsupportedOperationException(); } // Getting a label for the auth token is not supported @Override public String getAuthTokenLabel(String authTokenType) { throw new UnsupportedOperationException(); } // Updating user credentials is not supported @Override public Bundle updateCredentials( AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { throw new UnsupportedOperationException(); } // Checking features for the account is not supported @Override public Bundle hasFeatures( AccountAuthenticatorResponse response, Account account, String[] options) throws NetworkErrorException { throw new UnsupportedOperationException(); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/sync/account/AuthenticatorService.java ================================================ package com.yalin.style.data.repository.datasource.sync.account; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.support.annotation.Nullable; /** * YaLin 2017/1/3. */ public class AuthenticatorService extends Service { private Authenticator mAuthenticator; @Override public void onCreate() { super.onCreate(); mAuthenticator = new Authenticator(this); } @Nullable @Override public IBinder onBind(Intent intent) { return mAuthenticator.getIBinder(); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/repository/datasource/sync/gallery/GalleryScheduleService.kt ================================================ package com.yalin.style.data.repository.datasource.sync.gallery import android.app.AlarmManager import android.app.IntentService import android.app.PendingIntent import android.content.ComponentName import android.content.Context import android.content.Intent import android.database.Cursor import com.yalin.style.data.entity.GalleryWallpaperEntity import com.yalin.style.data.extensions.DelegateExt import com.yalin.style.data.log.LogUtil import com.yalin.style.data.repository.datasource.provider.StyleContract import com.yalin.style.data.utils.notifyChange import java.util.* /** * @author jinyalin * @since 2017/6/9. */ class GalleryScheduleService : IntentService(TAG) { companion object { val TAG = "GalleryScheduleService" val PREF_ROTATE_INTERVAL_MIN = "rotate_interval_min" val PREF_CURRENT_SHOW_WALLPAPER_ID = "current_gallery_wallpaper_id" val DEFAULT_ROTATE_INTERVAL_MIN = 60 * 6 val ACTION_START_UP = "com.yalin.style.ACTION_START_UP" val ACTION_SHUT_DOWN = "com.yalin.style.ACTION_SHUT_DOWN" val ACTION_SCHEDULE = "com.yalin.style.ACTION_SCHEDULE" val ACTION_SET_INTERVAL = "com.yalin.style.ACTION_SET_INTERVAL" val INTERVAL_KEY = "interval" fun startUp(context: Context) { val intent = Intent(ACTION_START_UP).setComponent(ComponentName(context, GalleryScheduleService::class.java)) context.startService(intent) } fun shutDown(context: Context) { val intent = Intent(ACTION_SHUT_DOWN).setComponent(ComponentName(context, GalleryScheduleService::class.java)) context.startService(intent) } fun publish(context: Context) { val intent = Intent(ACTION_SCHEDULE).setComponent(ComponentName(context, GalleryScheduleService::class.java)) context.startService(intent) } fun setInterval(context: Context, intervalMin: Int) { val intent = Intent(ACTION_SET_INTERVAL).setComponent(ComponentName(context, GalleryScheduleService::class.java)) intent.putExtra(INTERVAL_KEY, intervalMin) context.startService(intent) } } var rotateIntervalMin: Int by DelegateExt.preferences(this, PREF_ROTATE_INTERVAL_MIN, DEFAULT_ROTATE_INTERVAL_MIN) var currentShowWallpaperId: Long by DelegateExt.preferences(this, PREF_CURRENT_SHOW_WALLPAPER_ID, -1) override fun onCreate() { super.onCreate() } override fun onHandleIntent(intent: Intent?) { if (intent != null) { handleCommand(intent, intent.action) } } override fun onDestroy() { super.onDestroy() } private fun handleCommand(intent: Intent, action: String) { when (action) { ACTION_START_UP -> startUp() ACTION_SCHEDULE -> scheduleNext() ACTION_SHUT_DOWN -> shutDown() ACTION_SET_INTERVAL -> setInterval(intent.getIntExtra(INTERVAL_KEY, DEFAULT_ROTATE_INTERVAL_MIN)) } } private fun startUp() { LogUtil.D(TAG, "Start up gallery schedule service.") setNextAlarm() } private fun scheduleNext() { LogUtil.D(TAG, "Schedule next gallery wallpaper.") setNextAlarm() publicNextWallpaper() } private fun shutDown() { LogUtil.D(TAG, "Shut down gallery schedule service.") cancelAlarm() stopSelf() } private fun setInterval(intervalMin: Int) { rotateIntervalMin = intervalMin LogUtil.D(TAG, "Set schedule interval $rotateIntervalMin = $intervalMin") setNextAlarm() } private fun setNextAlarm() { if (rotateIntervalMin > 0) { setUpdateAlarm(System.currentTimeMillis() + rotateIntervalMin * 60 * 1000) } else { cancelAlarm() } } private fun setUpdateAlarm(nextTimeMillis: Long) { if (nextTimeMillis < System.currentTimeMillis()) { LogUtil.D(TAG, "Refusing to schedule next artwork in the past") return } val am = getSystemService(Context.ALARM_SERVICE) as AlarmManager am.set(AlarmManager.RTC, nextTimeMillis, getHandleNextCommandPendingIntent(this)) LogUtil.D(TAG, "Scheduling next gallery at " + Date(nextTimeMillis)) } private fun cancelAlarm() { LogUtil.D(TAG, "Cancel schedule alarm") val am = getSystemService(Context.ALARM_SERVICE) as AlarmManager am.cancel(getHandleNextCommandPendingIntent(this)) } private fun getHandleNextCommandPendingIntent(context: Context): PendingIntent { return PendingIntent.getService(context, 0, Intent(ACTION_SCHEDULE).setComponent(ComponentName(context, javaClass)), PendingIntent.FLAG_UPDATE_CURRENT) } private fun publicNextWallpaper() { var cursor: Cursor? = null val validWallpapers = ArrayList() try { cursor = contentResolver.query(StyleContract.GalleryWallpaper.CONTENT_URI, null, null, null, null) validWallpapers.addAll(GalleryWallpaperEntity.readCursor(this, cursor)) } finally { if (cursor != null) { cursor.close() } } if (validWallpapers.size > 1) { val random = Random() while (true) { val selectedEntity = validWallpapers[random.nextInt(validWallpapers.size)] if (selectedEntity.id != currentShowWallpaperId) { currentShowWallpaperId = selectedEntity.id break } } } else if (validWallpapers.size == 1) { if (currentShowWallpaperId != validWallpapers[0].id) { currentShowWallpaperId = validWallpapers[0].id } } else { if (currentShowWallpaperId != -1L) { currentShowWallpaperId = -1 } } LogUtil.D(TAG, "Current select wallpaper wallpaperId : $currentShowWallpaperId ") notifyChange(this, StyleContract.GalleryWallpaper.CONTENT_URI) } } ================================================ FILE: data/src/main/java/com/yalin/style/data/utils/ChecksumUtil.java ================================================ package com.yalin.style.data.utils; import android.util.Base64; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * @author jinyalin * @since 2017/4/25. */ public class ChecksumUtil { public static String getChecksum(File file) { try { MessageDigest md = MessageDigest.getInstance("MD5"); try (InputStream is = new FileInputStream(file); DigestInputStream dis = new DigestInputStream(is, md)) { byte[] buffer = new byte[2048]; //noinspection StatementWithEmptyBody while (dis.read(buffer) > 0) { } byte[] digest = dis.getMessageDigest().digest(); return Base64.encodeToString(digest, Base64.URL_SAFE).trim(); } catch (Exception e) { return null; } } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/utils/DeviceUtil.java ================================================ package com.yalin.style.data.utils; import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; import android.provider.Settings; /** * @author jinyalin * @since 2017/4/25. */ public class DeviceUtil { private DeviceUtil() { throw new UnsupportedOperationException("u can't instantiate me..."); } public static int getSDKVersion() { return android.os.Build.VERSION.SDK_INT; } @SuppressLint("HardwareIds") public static String getAndroidID(Context context) { return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); } public static String getManufacturer() { return Build.MANUFACTURER; } public static String getModel() { String model = Build.MODEL; if (model != null) { model = model.trim().replaceAll("\\s*", ""); } else { model = ""; } return model; } public static String getBrand() { String brand = Build.BRAND; if (brand != null) { brand = brand.trim().replaceAll("\\s*", ""); } else { brand = ""; } return brand; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/utils/FacetIdUtil.java ================================================ package com.yalin.style.data.utils; import android.content.Context; /** * @author jinyalin * @since 2017/4/26. */ public class FacetIdUtil { static { System.loadLibrary("facet_id-lib"); } public static boolean checkCurrentFacetId(Context context) { return checkCurrentFacetId(context, getUid(context)); } public static String getFacetId(Context context) { return getFacetId(context, getUid(context)); } private static native boolean checkCurrentFacetId(Context context, int uId); private static native String getFacetId(Context context, int uId); private static int getUid(Context context) { if (context != null) { return context.getApplicationInfo().uid; } else { return -1; } } } ================================================ FILE: data/src/main/java/com/yalin/style/data/utils/HttpRequestUtil.java ================================================ package com.yalin.style.data.utils; import android.content.Context; import com.google.gson.Gson; import com.yalin.style.data.entity.DeviceInfo; import com.yalin.style.data.entity.HttpRequestBody; /** * @author jinyalin * @since 2017/4/25. */ public class HttpRequestUtil { private static Gson gson = new Gson(); public static String getRequestBody(Context context) { HttpRequestBody requestBody = new HttpRequestBody(context, getDeviceJson(context)); return gson.toJson(requestBody); } private static DeviceInfo getDeviceJson(Context context) { int sdkVersion = DeviceUtil.getSDKVersion(); String androidId = DeviceUtil.getAndroidID(context); String manufacturer = DeviceUtil.getManufacturer(); String brand = DeviceUtil.getBrand(); String model = DeviceUtil.getModel(); return new DeviceInfo(sdkVersion, androidId, manufacturer, brand, model); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/utils/NativeFileHelper.kt ================================================ package com.yalin.style.data.utils import android.content.Context import android.text.TextUtils import com.yalin.style.data.log.LogUtil import java.io.File /** * @author jinyalin * @since 2017/8/10. */ private val TAG = "NativeFileHelper" private var nativePath: String? = null fun getNativeDir(context: Context): File { if (TextUtils.isEmpty(nativePath)) { val cacheDir = context.cacheDir val nativeDir = File(cacheDir.parent, "lib") nativeDir.mkdirs() nativePath = nativeDir.absolutePath } return File(nativePath) } fun getNativeFileName(componentPath: String, libName: String): String { return "plugin_" + getComponentName(componentPath) + "_" + libName + ".so" } fun clearNativeFiles(context: Context, componentPath: String) { val files = getNativeFiles(context, componentPath) for (file in files) { file.delete() } } private fun getNativeFiles(context: Context, componentPath: String): Array { val nativePrefix = "plugin_" + getComponentName(componentPath) return getNativeDir(context).listFiles({ file -> file.name.contains(nativePrefix) }) } private fun getComponentName(componentPath: String): String { var componentName: String try { val tmp = componentPath.split("/".toRegex()) componentName = tmp[tmp.size - 1].split("\\.".toRegex())[0] } catch (e: Exception) { componentName = componentPath.hashCode().toString() } LogUtil.D(TAG, "getComponentName for $componentPath result :$componentName") return componentName } ================================================ FILE: data/src/main/java/com/yalin/style/data/utils/NetworkUtil.java ================================================ package com.yalin.style.data.utils; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; /** * @author jinyalin * @since 2017/4/29. */ public class NetworkUtil { public static boolean isThereInternetConnection(Context context) { boolean isConnected; ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); isConnected = (networkInfo != null && networkInfo.isConnectedOrConnecting()); return isConnected; } } ================================================ FILE: data/src/main/java/com/yalin/style/data/utils/SelectionBuilder.java ================================================ package com.yalin.style.data.utils; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.text.TextUtils; import com.yalin.style.data.log.LogUtil; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * YaLin 2016/12/30. */ public class SelectionBuilder { private static final String TAG = "SelectionBuilder"; private String mTable = null; private Map mProjectionMap = new HashMap<>(); private StringBuilder mSelection = new StringBuilder(); private ArrayList mSelectionArgs = new ArrayList<>(); private String mGroupBy = null; private String mHaving = null; /** * Reset any internal state, allowing this builder to be recycled. */ public SelectionBuilder reset() { mTable = null; mGroupBy = null; mHaving = null; mSelection.setLength(0); mSelectionArgs.clear(); return this; } /** * Append the given selection clause to the internal state. Each clause is surrounded with * parenthesis and combined using {@code AND}. */ public SelectionBuilder where(String selection, String... selectionArgs) { if (TextUtils.isEmpty(selection)) { if (selectionArgs != null && selectionArgs.length > 0) { throw new IllegalArgumentException( "Valid selection required when including arguments="); } // Shortcut when clause is empty return this; } if (mSelection.length() > 0) { mSelection.append(" AND "); } mSelection.append("(").append(selection).append(")"); if (selectionArgs != null) { Collections.addAll(mSelectionArgs, selectionArgs); } return this; } public SelectionBuilder groupBy(String groupBy) { mGroupBy = groupBy; return this; } public SelectionBuilder having(String having) { mHaving = having; return this; } public SelectionBuilder table(String table) { mTable = table; return this; } /** * Replace positional params in table. Use for JOIN ON conditions. */ public SelectionBuilder table(String table, String... tableParams) { if (tableParams != null && tableParams.length > 0) { String[] parts = table.split("[?]", tableParams.length + 1); StringBuilder sb = new StringBuilder(parts[0]); for (int i = 1; i < parts.length; i++) { sb.append('"').append(tableParams[i - 1]).append('"') .append(parts[i]); } mTable = sb.toString(); } else { mTable = table; } return this; } private void assertTable() { if (mTable == null) { throw new IllegalStateException("Table not specified"); } } public SelectionBuilder mapToTable(String column, String table) { mProjectionMap.put(column, table + "." + column); return this; } public SelectionBuilder map(String fromColumn, String toClause) { mProjectionMap.put(fromColumn, toClause + " AS " + fromColumn); return this; } /** * Return selection string for current internal state. * * @see #getSelectionArgs() */ public String getSelection() { return mSelection.toString(); } /** * Return selection arguments for current internal state. * * @see #getSelection() */ public String[] getSelectionArgs() { return mSelectionArgs.toArray(new String[mSelectionArgs.size()]); } private void mapColumns(String[] columns) { for (int i = 0; i < columns.length; i++) { final String target = mProjectionMap.get(columns[i]); if (target != null) { columns[i] = target; } } } @Override public String toString() { return "SelectionBuilder[table=" + mTable + ", selection=" + getSelection() + ", selectionArgs=" + Arrays.toString(getSelectionArgs()) + "projectionMap = " + mProjectionMap + " ]"; } /** * Execute query using the current internal state as {@code WHERE} clause. */ public Cursor query(SQLiteDatabase db, String[] columns, String orderBy) { return query(db, false, columns, orderBy, null); } /** * Execute query using the current internal state as {@code WHERE} clause. */ public Cursor query(SQLiteDatabase db, boolean distinct, String[] columns, String orderBy, String limit) { assertTable(); if (columns != null) { mapColumns(columns); } LogUtil.D(TAG, "query(columns=" + Arrays.toString(columns) + ", distinct=" + distinct + ") " + this); return db.query(distinct, mTable, columns, getSelection(), getSelectionArgs(), mGroupBy, mHaving, orderBy, limit); } /** * Execute update using the current internal state as {@code WHERE} clause. */ public int update(SQLiteDatabase db, ContentValues values) { assertTable(); LogUtil.D(TAG, "update() " + this); return db.update(mTable, values, getSelection(), getSelectionArgs()); } /** * Execute delete using the current internal state as {@code WHERE} clause. */ public int delete(SQLiteDatabase db) { assertTable(); LogUtil.D(TAG, "delete() " + this); return db.delete(mTable, getSelection(), getSelectionArgs()); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/utils/TimeUtil.java ================================================ package com.yalin.style.data.utils; import android.content.Context; /** * YaLin 2017/1/3. */ public class TimeUtil { public static long getCurrentTime(final Context context) { return System.currentTimeMillis(); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/utils/UriUtil.kt ================================================ package com.yalin.style.data.utils import android.annotation.TargetApi import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.database.Cursor import android.database.SQLException import android.net.Uri import android.os.Binder import android.os.Build import android.provider.DocumentsContract import android.text.TextUtils import com.yalin.style.data.log.LogUtil import com.yalin.style.domain.GalleryWallpaper import java.io.File import java.io.FileOutputStream import java.io.IOException import java.io.UnsupportedEncodingException import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.util.* /** * @author jinyalin * @since 2017/5/25. */ private val TAG = "UriUtil" fun isTreeUri(possibleTreeUri: Uri): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return DocumentsContract.isTreeUri(possibleTreeUri) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Prior to N we can't directly check if the URI is a tree URI, so we have to just try it try { val treeDocumentId = DocumentsContract.getTreeDocumentId(possibleTreeUri) return !TextUtils.isEmpty(treeDocumentId) } catch (e: IllegalArgumentException) { // Definitely not a tree URI return false } } // No tree URIs prior to Lollipop return false } @TargetApi(Build.VERSION_CODES.LOLLIPOP) fun getImagesFromTreeUri(context: Context, treeUri: Uri, maxImages: Int): List { val images = ArrayList() val directories = LinkedList() directories.add(DocumentsContract.getTreeDocumentId(treeUri)) while (images.size < maxImages && !directories.isEmpty()) { val parentDocumentId = directories.poll() val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, parentDocumentId) var children: Cursor? try { children = context.contentResolver.query(childrenUri, arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE), null, null, null) } catch (e: SecurityException) { // No longer can read this URI, which means no images from this URI // This a temporary state as the next onLoadFinished() will remove this item entirely children = null } if (children == null) { continue } while (children.moveToNext()) { val documentId = children.getString( children.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) val mimeType = children.getString( children.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE)) if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) { directories.add(documentId) } else if (mimeType != null && mimeType.startsWith("image/")) { // Add images to the list images.add(DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)) } if (images.size == maxImages) { break } } children.close() } return images } @TargetApi(Build.VERSION_CODES.LOLLIPOP) fun getDisplayNameForTreeUri(context: Context, treeUri: Uri): String? { val documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)) var data: Cursor? = null try { data = context.contentResolver.query(documentUri, arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME), null, null, null) } catch (e: Throwable) { LogUtil.E(TAG, "getDisplayNameForTreeUri failed.", e) } var displayName: String? = null if (data != null && data.moveToNext()) { displayName = data.getString( data.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)) } data?.close() return displayName } fun processUriPermission(context: Context, galleryWallpaper: GalleryWallpaper) { val uri = Uri.parse(galleryWallpaper.uri) if (galleryWallpaper.isTreeUri) { try { context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) } catch (e: SecurityException) { // You can't persist URI permissions from your own app, so this fails. // We'll still have access to it directly LogUtil.E(TAG, "processUriPermission exception ", e) } } else { val haveUriPermission = context.checkUriPermission(uri, Binder.getCallingPid(), Binder.getCallingUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED // If we only have permission to this URI via URI permissions (rather than directly, // such as if the URI is from our own app), it is from an external source and we need // to make sure to gain persistent access to the URI's content if (haveUriPermission) { var persistedPermission = false // Try to persist access to the URI, saving us from having to store a local copy if (DocumentsContract.isDocumentUri(context, uri)) { try { context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) persistedPermission = true // If we have a persisted URI permission, we don't need a local copy val cachedFile = getCacheFileForUri(context, galleryWallpaper.uri) if (cachedFile != null && cachedFile.exists()) { if (!cachedFile.delete()) { LogUtil.D(TAG, "Unable to delete " + cachedFile) } } } catch (e: SecurityException) { // If we don't have FLAG_GRANT_PERSISTABLE_URI_PERMISSION (such as when using ACTION_GET_CONTENT), // this will fail. We'll need to make a local copy (handled below) LogUtil.E(TAG, "processUriPermission exception ", e) } } if (!persistedPermission) { // We only need to make a local copy if we weren't able to persist the permission try { writeUriToFile(context, galleryWallpaper.uri, getCacheFileForUri(context, galleryWallpaper.uri)) } catch (e: IOException) { LogUtil.E(TAG, "Error downloading gallery image " + galleryWallpaper.uri, e) throw SQLException("Error downloading gallery image " + galleryWallpaper.uri, e) } } } else { // On API 25 and lower, we don't get URI permissions to URIs // from our own package so we manage those URI permissions manually val resolver = context.contentResolver try { resolver.call(uri, "takePersistableUriPermission", uri.toString(), null) } catch (e: Exception) { LogUtil.E(TAG, "Unable to manually persist uri permissions to " + uri, e) } } } } private fun writeUriToFile(context: Context?, uri: String, destFile: File?) { if (context == null) { return } if (destFile == null) { throw IOException("Invalid destination for " + uri) } try { val input = context.contentResolver.openInputStream(Uri.parse(uri)) ?: return val fileOutput = FileOutputStream(destFile) val buffer = ByteArray(1024) var bytesRead = input.read(buffer) while (bytesRead > 0) { fileOutput.write(buffer, 0, bytesRead) bytesRead = input.read(buffer) } fileOutput.flush() } catch (e: SecurityException) { throw IOException("Unable to read Uri: " + uri, e) } } fun getCacheFileForUri(context: Context, imageUri: String): File? { val directory = File(context.getExternalFilesDir(null), "gallery_images") if (!directory.exists() && !directory.mkdirs()) { return null } // Create a unique filename based on the imageUri val uri = Uri.parse(imageUri) val filename = StringBuilder() filename.append(uri.scheme).append("_") .append(uri.host).append("_") var encodedPath = uri.encodedPath if (!TextUtils.isEmpty(encodedPath)) { val length = encodedPath.length if (length > 60) { encodedPath = encodedPath.substring(length - 60) } encodedPath = encodedPath.replace('/', '_') filename.append(encodedPath).append("_") } try { val md = MessageDigest.getInstance("MD5") md.update(uri.toString().toByteArray(charset("UTF-8"))) val digest = md.digest() for (b in digest) { if (0xff and b.toInt() < 0x10) { filename.append("0").append(Integer.toHexString(0xFF and b.toInt())) } else { filename.append(Integer.toHexString(0xFF and b.toInt())) } } } catch (e: NoSuchAlgorithmException) { filename.append(uri.toString().hashCode()) } catch (e: UnsupportedEncodingException) { filename.append(uri.toString().hashCode()) } return File(directory, filename.toString()) } ================================================ FILE: data/src/main/java/com/yalin/style/data/utils/WallpaperFileHelper.java ================================================ package com.yalin.style.data.utils; import android.content.Context; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.text.TextUtils; import com.yalin.style.data.log.LogUtil; import com.yalin.style.data.repository.datasource.provider.StyleContract.Wallpaper; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashSet; import java.util.Set; /** * YaLin On 2017/1/2. */ public class WallpaperFileHelper { private static final String TAG = "WallpaperFileHelper"; public static final String WALLPAPER_FOLDER = "wallpaper"; public static final String ADVANCE_WALLPAPER_FOLDER = "component"; public static ParcelFileDescriptor openReadFile(Context context, Uri uri, String mode) throws FileNotFoundException { LogUtil.D(TAG, "Read file Uri=" + (uri == null ? null : uri.toString())); String wallpaperId = Wallpaper.getWallpaperId(uri); File directory = new File(context.getFilesDir(), WALLPAPER_FOLDER); if (!directory.exists()) { throw new FileNotFoundException("Wallpaper file : " + directory.toString() + " cannot found."); } File file = new File(directory, generateFileName(wallpaperId)); return ParcelFileDescriptor.open(file, parseMode(mode)); } public static ParcelFileDescriptor openWriteFile(Context context, Uri uri, String mode) throws FileNotFoundException { String wallpaperId = Wallpaper.getWallpaperSaveId(uri); File directory = new File(context.getFilesDir(), WALLPAPER_FOLDER); if (!directory.exists() && !directory.mkdir()) { throw new FileNotFoundException("Wallpaper save dir : " + directory.toString() + " cannot be create."); } File file = new File(directory, generateFileName(wallpaperId)); return ParcelFileDescriptor.open(file, parseMode(mode)); } private static String generateFileName(String wallpaperId) { return wallpaperId; } private static int parseMode(String mode) { final int modeBits; if ("r".equals(mode)) { modeBits = ParcelFileDescriptor.MODE_READ_ONLY; } else if ("w".equals(mode) || "wt".equals(mode)) { modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_TRUNCATE; } else if ("wa".equals(mode)) { modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_APPEND; } else if ("rw".equals(mode)) { modeBits = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE; } else if ("rwt".equals(mode)) { modeBits = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_TRUNCATE; } else { throw new IllegalArgumentException("Bad mode '" + mode + "'"); } return modeBits; } public static boolean copyAssets(Context context, String name, File output) { InputStream is = null; FileOutputStream fos = null; try { is = context.getAssets().open(name); fos = new FileOutputStream(output); byte[] buffer = new byte[2048]; int len; while ((len = is.read(buffer)) > 0) { fos.write(buffer, 0, len); } fos.flush(); return true; } catch (IOException e) { return false; } finally { try { if (is != null) { is.close(); } if (fos != null) { fos.close(); } } catch (IOException e) { e.printStackTrace(); } } } public static void deleteOldWallpapers(Context context, Set excludeIds) { File directory = new File(context.getFilesDir(), WALLPAPER_FOLDER); if (!directory.exists()) { return; } Set namesSet = new HashSet<>(); for (String wallpaperId : excludeIds) { namesSet.add(generateFileName(wallpaperId)); } File[] files = directory.listFiles(fileName -> !namesSet.contains(fileName.getName())); for (File file : files) { //noinspection ResultOfMethodCallIgnored file.delete(); } } public static void deleteOldComponent(Context context, Set excludeNames) { File dir = getAdvanceWallpaperDir(context); if (!dir.exists()) { return; } File[] files = dir.listFiles(fileName -> !excludeNames.contains(fileName.getName())); for (File file : files) { //noinspection ResultOfMethodCallIgnored file.delete(); NativeFileHelperKt.clearNativeFiles(context, file.getAbsolutePath()); } } public static void deleteOldWallpapers(Context context, String... excludeIds) { File directory = new File(context.getFilesDir(), WALLPAPER_FOLDER); if (!directory.exists()) { return; } Set namesSet = new HashSet<>(); for (String wallpaperId : excludeIds) { namesSet.add(generateFileName(wallpaperId)); } File[] files = directory.listFiles(fileName -> !namesSet.contains(fileName.getName())); for (File file : files) { //noinspection ResultOfMethodCallIgnored file.delete(); } } public static boolean ensureWallpaperChecksumValid(Context context, String checksum, String wallpaperId) { File directory = new File(context.getFilesDir(), WALLPAPER_FOLDER); if (!directory.exists()) { return false; } File file = new File(directory, generateFileName(wallpaperId)); String computedChecksum = ChecksumUtil.getChecksum(file); if (TextUtils.equals(checksum, computedChecksum)) { return true; } //noinspection ResultOfMethodCallIgnored file.delete(); return false; } public static boolean ensureChecksumValid(Context context, String checksum, String filePath) { File file = new File(filePath); if (!file.exists()) { return false; } String computedChecksum = ChecksumUtil.getChecksum(file); if (TextUtils.equals(checksum, computedChecksum)) { return true; } //noinspection ResultOfMethodCallIgnored file.delete(); return false; } public static File getAdvanceWallpaperDir(Context context) { return new File(context.getFilesDir(), ADVANCE_WALLPAPER_FOLDER); } public static boolean isNeedDownloadAdvanceComponent(boolean lazy, String storePath) { return lazy && !new File(storePath).exists(); } } ================================================ FILE: data/src/main/java/com/yalin/style/data/utils/WallpaperUtil.kt ================================================ package com.yalin.style.data.utils import android.content.Context import android.net.Uri import com.yalin.style.data.repository.datasource.provider.StyleContractHelper /** * YaLin * On 2017/5/26. */ fun notifyChange(context: Context, uri: Uri) { if (!StyleContractHelper.isUriCalledFromSyncAdapter(uri)) { context.contentResolver.notifyChange(uri, null) } } ================================================ FILE: data/src/main/res/values/strings.xml ================================================ Style com.yalin.style Advance Wallpaper Advance animation component Featured art A new painting every day My Photos Random camera photos From your gallery Touch to view more ================================================ FILE: data/src/main/res/values-zh-rCN/strings.xml ================================================ Style com.yalin.style 特效动态壁纸 富含各种炫酷效果 Style 每天自动更新艺术作品 我的照片 随机更新照片 来自你的相册 点击查看更多 ================================================ FILE: data/src/main/res/xml/authenticator.xml ================================================ ================================================ FILE: data/src/main/res/xml/syncadapter.xml ================================================ ================================================ FILE: data/src/test/java/com/yalin/data/ExampleUnitTest.java ================================================ package com.yalin.data; import org.junit.Test; import static org.junit.Assert.*; /** * Example local unit test, which will execute on the development machine (host). * * @see Testing documentation */ public class ExampleUnitTest { @Test public void addition_isCorrect() throws Exception { assertEquals(4, 2 + 2); } } ================================================ FILE: domain/.gitignore ================================================ /build ================================================ FILE: domain/build.gradle ================================================ apply plugin: 'java' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) } sourceCompatibility = "1.7" targetCompatibility = "1.7" dependencies { compile "io.reactivex.rxjava2:rxjava:${RXJAVA_VERSION}" compile "com.fernandocejas:arrow:${ARROW_VERSION}" compile "javax.inject:javax.inject:${JAVAX_INJECT_VERSION}" } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/AdvanceWallpaper.java ================================================ package com.yalin.style.domain; /** * @author jinyalin * @since 2017/7/28. */ public class AdvanceWallpaper { public long id; public String wallpaperId; public String link; public String name; public String author; public String iconUrl; public String downloadUrl; public boolean lazyDownload; public boolean needAd; public String providerName; public String storePath; public boolean isDefault; public boolean isSelected; } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/GalleryWallpaper.java ================================================ package com.yalin.style.domain; /** * @author jinyalin * @since 2017/5/24. */ public class GalleryWallpaper { public long id; public String uri; public boolean isTreeUri; public long dateTime; public String location; public boolean hasMetadata; } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/Source.java ================================================ package com.yalin.style.domain; /** * @author jinyalin * @since 2017/5/22. */ public class Source { public int id; public String title; public String description; public int iconId; public boolean selected; public boolean hasSetting; public int color; } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/Wallpaper.java ================================================ package com.yalin.style.domain; /** * @author jinyalin * @since 2017/4/18. */ public class Wallpaper { public String wallpaperId; public String imageUri; public String title; public String byline; public String attribution; public boolean canLike; public boolean liked; public boolean isDefault; } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/exception/DefaultErrorBundle.java ================================================ package com.yalin.style.domain.exception; /** * @author jinyalin * @since 2017/4/18. */ public class DefaultErrorBundle implements ErrorBundle { private static final String DEFAULT_ERROR_MSG = "Unknown error"; private final Exception exception; public DefaultErrorBundle(Exception exception) { this.exception = exception; } @Override public Exception getException() { return exception; } @Override public String getErrorMessage() { return (exception != null) ? this.exception.getMessage() : DEFAULT_ERROR_MSG; } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/exception/ErrorBundle.java ================================================ package com.yalin.style.domain.exception; /** * @author jinyalin * @since 2017/4/18. */ public interface ErrorBundle { Exception getException(); String getErrorMessage(); } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/executor/PostExecutionThread.java ================================================ package com.yalin.style.domain.executor; import io.reactivex.Scheduler; /** * @author jinyalin * @since 2017/4/18. */ public interface PostExecutionThread { Scheduler getScheduler(); } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/executor/SerialThreadExecutor.java ================================================ package com.yalin.style.domain.executor; import java.util.concurrent.Executor; /** * @author jinyalin * @since 2017/4/18. */ public interface SerialThreadExecutor extends Executor { } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/executor/ThreadExecutor.java ================================================ package com.yalin.style.domain.executor; import java.util.concurrent.Executor; /** * @author jinyalin * @since 2017/4/18. */ public interface ThreadExecutor extends Executor { } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/AddGalleryWallpaper.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.GalleryWallpaper; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import java.util.List; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/5/24. */ public class AddGalleryWallpaper extends UseCase { private SourcesRepository sourcesRepository; @Inject public AddGalleryWallpaper(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(Params params) { return sourcesRepository.getWallpaperRepository() .addGalleryWallpaperUris(params.galleryWallpaperUris); } public static final class Params { private final List galleryWallpaperUris; private Params(List galleryWallpapers) { this.galleryWallpaperUris = galleryWallpapers; } public static Params addGalleryWallpaperUris(List galleryWallpapers) { return new Params(galleryWallpapers); } } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/DefaultObserver.java ================================================ package com.yalin.style.domain.interactor; import io.reactivex.observers.DisposableObserver; /** * @author jinyalin * @since 2017/4/18. */ public class DefaultObserver extends DisposableObserver { @Override public void onNext(T needDownload) { // no-op by default. } @Override public void onComplete() { // no-op by default. } @Override public void onError(Throwable exception) { // no-op by default. } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/DownloadAdvanceWallpaper.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/8/11. */ public class DownloadAdvanceWallpaper extends UseCase { private SourcesRepository sourcesRepository; @Inject public DownloadAdvanceWallpaper(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(Params params) { return sourcesRepository.getWallpaperRepository() .downloadAdvanceWallpaper(params.wallpaperId); } public static final class Params { private final String wallpaperId; private Params(String wallpaperId) { this.wallpaperId = wallpaperId; } public static Params download(String wallpaperId) { return new Params(wallpaperId); } } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/ForceNow.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/6/9. */ public class ForceNow extends UseCase { private SourcesRepository sourcesRepository; @Inject public ForceNow(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(Params params) { return sourcesRepository.getWallpaperRepository().foreNow(params.galleryWallpaperUri); } public static final class Params { private final String galleryWallpaperUri; private Params(String galleryWallpaperUri) { this.galleryWallpaperUri = galleryWallpaperUri; } public static Params fromUri(String galleryWallpaperUri) { return new Params(galleryWallpaperUri); } } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/GetAdvanceWallpapers.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.AdvanceWallpaper; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import java.util.List; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/7/28. */ public class GetAdvanceWallpapers extends UseCase, Void> { private SourcesRepository sourcesRepository; @Inject public GetAdvanceWallpapers(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable> buildUseCaseObservable(Void aVoid) { return sourcesRepository.getWallpaperRepository().getAdvanceWallpapers(); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/GetGalleryUpdateInterval.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/6/9. */ public class GetGalleryUpdateInterval extends UseCase { private SourcesRepository sourcesRepository; @Inject public GetGalleryUpdateInterval(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(Void aVoid) { return sourcesRepository.getWallpaperRepository().getGalleryUpdateInterval(); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/GetGalleryWallpaper.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.GalleryWallpaper; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import java.util.List; import io.reactivex.Observable; import javax.inject.Inject; /** * @author jinyalin * @since 2017/5/24. */ public class GetGalleryWallpaper extends UseCase, Void> { private SourcesRepository sourcesRepository; @Inject public GetGalleryWallpaper(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable> buildUseCaseObservable(Void aVoid) { return sourcesRepository.getWallpaperRepository().getGalleryWallpapers(); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/GetSelectedAdvanceWallpaper.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.AdvanceWallpaper; import com.yalin.style.domain.repository.SourcesRepository; import javax.inject.Inject; /** * @author jinyalin * @since 2017/7/28. */ public class GetSelectedAdvanceWallpaper { private SourcesRepository sourcesRepository; @Inject public GetSelectedAdvanceWallpaper(SourcesRepository sourcesRepository) { this.sourcesRepository = sourcesRepository; } public AdvanceWallpaper getSelected() { return sourcesRepository.getWallpaperRepository().getAdvanceWallpaper(); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/GetSelectedSource.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.repository.SourcesRepository; import javax.inject.Inject; /** * @author jinyalin * @since 2017/7/27. */ public class GetSelectedSource { private SourcesRepository sourcesRepository; @Inject public GetSelectedSource(SourcesRepository sourcesRepository) { this.sourcesRepository = sourcesRepository; } public int getSelectedSourceId() { return sourcesRepository.getSelectedSource(); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/GetSources.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.Source; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import java.util.List; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/5/22. */ public class GetSources extends UseCase, Void> { private SourcesRepository sourcesRepository; @Inject public GetSources(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable> buildUseCaseObservable(Void a) { return sourcesRepository.getSources(); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/GetWallpaper.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.Wallpaper; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/4/18. */ public class GetWallpaper extends UseCase { private SourcesRepository sourcesRepository; @Inject public GetWallpaper(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(Void a) { return sourcesRepository.getWallpaperRepository().getWallpaper(); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/GetWallpaperCount.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/4/28. */ public class GetWallpaperCount extends UseCase { private SourcesRepository sourcesRepository; @Inject public GetWallpaperCount(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(Void aVoid) { return sourcesRepository.getWallpaperRepository().getWallpaperCount(); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/LikeWallpaper.java ================================================ package com.yalin.style.domain.interactor; import com.fernandocejas.arrow.checks.Preconditions; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.interactor.LikeWallpaper.Params; import com.yalin.style.domain.repository.SourcesRepository; import io.reactivex.Observable; import javax.inject.Inject; /** * YaLin * On 2017/4/30. */ public class LikeWallpaper extends UseCase { private SourcesRepository sourcesRepository; @Inject public LikeWallpaper(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(LikeWallpaper.Params params) { Preconditions.checkNotNull(params); return sourcesRepository.getWallpaperRepository().likeWallpaper(params.wallpaperId); } public static final class Params { private final String wallpaperId; private Params(String wallpaperId) { this.wallpaperId = wallpaperId; } public static LikeWallpaper.Params likeWallpaper(String wallpaperId) { return new LikeWallpaper.Params(wallpaperId); } } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/LoadAdvanceWallpaper.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.AdvanceWallpaper; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import java.util.List; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/7/31. */ public class LoadAdvanceWallpaper extends UseCase, Void> { private SourcesRepository sourcesRepository; @Inject public LoadAdvanceWallpaper(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable> buildUseCaseObservable(Void aVoid) { return sourcesRepository.getWallpaperRepository().loadAdvanceWallpapers(); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/ObserverSources.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.observable.SourcesObservable; import javax.inject.Inject; /** * @author jinyalin * @since 2017/5/23. */ public class ObserverSources { private SourcesObservable sourceObservable; @Inject public ObserverSources(SourcesObservable sourceObservable) { this.sourceObservable = sourceObservable; } public void registerObserver(DefaultObserver observer) { sourceObservable.registerObserver(observer); } public void unregisterObserver(DefaultObserver observer) { sourceObservable.unregisterObserver(observer); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/ObserverWallpaper.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.observable.WallpaperObservable; import javax.inject.Inject; /** * @author jinyalin * @since 2017/5/23. */ public class ObserverWallpaper { private WallpaperObservable wallpaperObservable; @Inject public ObserverWallpaper(WallpaperObservable wallpaperObservable) { this.wallpaperObservable = wallpaperObservable; } public void registerObserver(DefaultObserver observer) { wallpaperObservable.registerObserver(observer); } public void unregisterObserver(DefaultObserver observer) { wallpaperObservable.unregisterObserver(observer); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/OpenWallpaperInputStream.java ================================================ package com.yalin.style.domain.interactor; import com.fernandocejas.arrow.checks.Preconditions; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import java.io.InputStream; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/4/21. */ public class OpenWallpaperInputStream extends UseCase { private SourcesRepository sourcesRepository; @Inject public OpenWallpaperInputStream(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(Params params) { Preconditions.checkNotNull(params); return sourcesRepository.getWallpaperRepository().openInputStream(params.wallpaperId); } public static final class Params { private final String wallpaperId; private Params(String wallpaperId) { this.wallpaperId = wallpaperId; } public static Params openInputStream(String wallpaperId) { return new Params(wallpaperId); } } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/ReadAdvanceAd.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/8/16. */ public class ReadAdvanceAd extends UseCase { private SourcesRepository sourcesRepository; @Inject public ReadAdvanceAd(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(Params params) { return sourcesRepository.getWallpaperRepository() .readAdvanceAd(params.wallpaperId); } public static final class Params { private final String wallpaperId; private Params(String wallpaperId) { this.wallpaperId = wallpaperId; } public static Params read(String wallpaperId) { return new Params(wallpaperId); } } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/RemoveGalleryWallpaper.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.GalleryWallpaper; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.interactor.RemoveGalleryWallpaper.Params; import com.yalin.style.domain.repository.SourcesRepository; import io.reactivex.Observable; import java.util.List; import javax.inject.Inject; /** * YaLin * On 2017/5/26. */ public class RemoveGalleryWallpaper extends UseCase { private SourcesRepository sourcesRepository; @Inject public RemoveGalleryWallpaper(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(Params params) { return sourcesRepository.getWallpaperRepository() .removeGalleryWallpaperUris(params.galleryWallpaperUris); } public static final class Params { private final List galleryWallpaperUris; private Params(List galleryWallpaperUris) { this.galleryWallpaperUris = galleryWallpaperUris; } public static RemoveGalleryWallpaper.Params removeGalleryWallpaperUris( List customWallpapers) { return new RemoveGalleryWallpaper.Params(customWallpapers); } } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/SelectAdvanceWallpaper.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/7/31. */ public class SelectAdvanceWallpaper extends UseCase { private SourcesRepository sourcesRepository; @Inject public SelectAdvanceWallpaper(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(SelectAdvanceWallpaper.Params params) { return sourcesRepository.getWallpaperRepository() .selectAdvanceWallpaper(params.wallpaperId, params.tempSelect); } public static final class Params { private final String wallpaperId; private final boolean tempSelect; private Params(String wallpaperId, boolean tempSelect) { this.wallpaperId = wallpaperId; this.tempSelect = tempSelect; } public static Params selectWallpaper(String wallpaperId, boolean tempSelect) { return new Params(wallpaperId, tempSelect); } } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/SelectSource.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/5/23. */ public class SelectSource extends UseCase { private SourcesRepository sourcesRepository; @Inject public SelectSource(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(Params params) { return sourcesRepository.selectSource(params.sourceId, params.tempSelect); } public static final class Params { private final int sourceId; private final boolean tempSelect; public Params(int sourceId, boolean tempSelect) { this.sourceId = sourceId; this.tempSelect = tempSelect; } public static Params selectSource(int sourceId, boolean tempSelect) { return new Params(sourceId, tempSelect); } } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/SetGalleryUpdateInterval.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/6/9. */ public class SetGalleryUpdateInterval extends UseCase { private SourcesRepository sourcesRepository; @Inject public SetGalleryUpdateInterval(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(Params params) { return sourcesRepository.getWallpaperRepository() .setGalleryUpdateInterval(params.intervalMin); } public static final class Params { private final int intervalMin; private Params(int intervalMin) { this.intervalMin = intervalMin; } public static Params interval(int intervalMin) { return new Params(intervalMin); } } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/SwitchWallpaper.java ================================================ package com.yalin.style.domain.interactor; import com.yalin.style.domain.Wallpaper; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import com.yalin.style.domain.repository.SourcesRepository; import javax.inject.Inject; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/4/28. */ public class SwitchWallpaper extends UseCase { private SourcesRepository sourcesRepository; @Inject public SwitchWallpaper(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread, SourcesRepository sourcesRepository) { super(threadExecutor, serialThreadExecutor, postExecutionThread); this.sourcesRepository = sourcesRepository; } @Override Observable buildUseCaseObservable(Void aVoid) { return sourcesRepository.getWallpaperRepository().switchWallpaper(); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/interactor/UseCase.java ================================================ package com.yalin.style.domain.interactor; import com.fernandocejas.arrow.checks.Preconditions; import com.yalin.style.domain.executor.PostExecutionThread; import com.yalin.style.domain.executor.SerialThreadExecutor; import com.yalin.style.domain.executor.ThreadExecutor; import io.reactivex.Observable; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.observers.DisposableObserver; import io.reactivex.schedulers.Schedulers; /** * @author jinyalin * @since 2017/4/18. */ public abstract class UseCase { private final ThreadExecutor threadExecutor; private final SerialThreadExecutor serialThreadExecutor; private final PostExecutionThread postExecutionThread; private final CompositeDisposable disposables; public UseCase(ThreadExecutor threadExecutor, SerialThreadExecutor serialThreadExecutor, PostExecutionThread postExecutionThread) { this.threadExecutor = threadExecutor; this.serialThreadExecutor = serialThreadExecutor; this.postExecutionThread = postExecutionThread; this.disposables = new CompositeDisposable(); } abstract Observable buildUseCaseObservable(Params params); public void execute(DisposableObserver observer, Params params) { Preconditions.checkNotNull(observer); final Observable observable = this.buildUseCaseObservable(params) .subscribeOn(Schedulers.from(threadExecutor)) .observeOn(postExecutionThread.getScheduler()); addDisposable(observable.subscribeWith(observer)); } public void executeSerial(DisposableObserver observer, Params params) { Preconditions.checkNotNull(observer); final Observable observable = this.buildUseCaseObservable(params) .subscribeOn(Schedulers.from(serialThreadExecutor)) .observeOn(postExecutionThread.getScheduler()); addDisposable(observable.subscribeWith(observer)); } public void dispose() { if (!disposables.isDisposed()) { disposables.dispose(); } } private void addDisposable(Disposable disposable) { Preconditions.checkNotNull(disposable); Preconditions.checkNotNull(disposables); disposables.add(disposable); } } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/observable/SourcesObservable.java ================================================ package com.yalin.style.domain.observable; import com.yalin.style.domain.interactor.DefaultObserver; /** * @author jinyalin * @since 2017/5/23. */ public interface SourcesObservable { void registerObserver(DefaultObserver observer); void unregisterObserver(DefaultObserver observer); } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/observable/WallpaperObservable.java ================================================ package com.yalin.style.domain.observable; import com.yalin.style.domain.interactor.DefaultObserver; /** * @author jinyalin * @since 2017/5/23. */ public interface WallpaperObservable { void registerObserver(DefaultObserver observer); void unregisterObserver(DefaultObserver observer); } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/repository/SourcesRepository.java ================================================ package com.yalin.style.domain.repository; import com.yalin.style.domain.Source; import java.util.List; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/5/23. */ public interface SourcesRepository { int SOURCE_ID_STYLE = 0; int SOURCE_ID_CUSTOM = 1; int SOURCE_ID_ADVANCE = 2; Observable> getSources(); Observable selectSource(int sourceId, boolean tempSelect); WallpaperRepository getWallpaperRepository(); int getSelectedSource(); } ================================================ FILE: domain/src/main/java/com/yalin/style/domain/repository/WallpaperRepository.java ================================================ package com.yalin.style.domain.repository; import com.yalin.style.domain.AdvanceWallpaper; import com.yalin.style.domain.GalleryWallpaper; import com.yalin.style.domain.Wallpaper; import java.io.InputStream; import java.util.List; import io.reactivex.Observable; /** * @author jinyalin * @since 2017/4/18. */ public interface WallpaperRepository { Observable getWallpaper(); Observable switchWallpaper(); Observable openInputStream(String wallpaperId); Observable getWallpaperCount(); Observable likeWallpaper(String wallpaperId); Observable addGalleryWallpaperUris(List uris); Observable removeGalleryWallpaperUris(List uris); Observable> getGalleryWallpapers(); Observable> getAdvanceWallpapers(); Observable> loadAdvanceWallpapers(); Observable downloadAdvanceWallpaper(String wallpaperId); Observable selectAdvanceWallpaper(String wallpaperId, boolean tempSelect); AdvanceWallpaper getAdvanceWallpaper(); Observable readAdvanceAd(String wallpaperId); Observable foreNow(String wallpaperUri); Observable setGalleryUpdateInterval(int intervalMin); Observable getGalleryUpdateInterval(); } ================================================ FILE: engine/.gitignore ================================================ /build ================================================ FILE: engine/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { compileSdkVersion COMPILE_SDK_VERSION as int buildToolsVersion BUILD_TOOLS_VERSION as String defaultConfig { minSdkVersion MIN_SDK_VERSION as int targetSdkVersion TARGET_SDK_VERSION as int versionCode APP_VERSION_CODE as int versionName APP_VERSION_NAME as String testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } publishNonDefault true productFlavors { demo { } production { } } sourceSets { main { jniLibs.srcDirs = ['libs'] } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile "com.android.support:appcompat-v7:${SUPPORT_LIBRARY_VERSION}" compile "org.jetbrains.kotlin:kotlin-stdlib:${KOTLIN_VERSION}" compile "com.badlogicgames.gdx:gdx-backend-android:$GDX_VERSION" compile project(':domain') demoProvided project(path: ':data', configuration: 'demoDebug') productionProvided project(path: ':data', configuration: 'productionRelease') testCompile 'junit:junit:4.12' } ================================================ FILE: engine/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/jinyalin/Library/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 *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: engine/src/androidTest/java/com/yalin/style/engine/ExampleInstrumentedTest.java ================================================ package com.yalin.style.engine; import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; /** * Instrumentation test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() throws Exception { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); assertEquals("com.yalin.style.engine.test", appContext.getPackageName()); } } ================================================ FILE: engine/src/main/AndroidManifest.xml ================================================ ================================================ FILE: engine/src/main/java/com/yalin/style/engine/GDXWallpaperServiceProxy.kt ================================================ package com.yalin.style.engine import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build import android.service.wallpaper.WallpaperService import android.support.v4.os.UserManagerCompat import android.view.SurfaceHolder import com.badlogic.gdx.backends.android.AndroidLiveWallpaperService /** * @author jinyalin * * * @since 2017/7/28. */ open class GDXWallpaperServiceProxy(host: Context) : AndroidLiveWallpaperService() { private val activateCallback: WallpaperActiveCallback? init { @Suppress("LeakingThis") attachBaseContext(host) activateCallback = if (host is WallpaperActiveCallback) host else null } override fun onCreateEngine(): WallpaperService.Engine { return GDXActiveEngine() } inner class GDXActiveEngine : AndroidWallpaperEngine() { private var mWallpaperActivate = false private var mEngineUnlockReceiver: BroadcastReceiver? = null override fun onCreate(surfaceHolder: SurfaceHolder?) { if (!isPreview) { if (UserManagerCompat.isUserUnlocked(this@GDXWallpaperServiceProxy)) { activateWallpaper() } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { mEngineUnlockReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { activateWallpaper() context.unregisterReceiver(this) } } val filter = IntentFilter(Intent.ACTION_USER_UNLOCKED) this@GDXWallpaperServiceProxy.registerReceiver(mEngineUnlockReceiver, filter) } } } override fun onDestroy() { super.onDestroy() deactivateWallpaper() } private fun activateWallpaper() { mWallpaperActivate = true activateCallback?.onWallpaperActivate() } private fun deactivateWallpaper() { if (mWallpaperActivate) { activateCallback?.onWallpaperDeactivate() } } } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/GLWallpaperServiceProxy.kt ================================================ package com.yalin.style.engine import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build import android.support.v4.os.UserManagerCompat import android.view.SurfaceHolder import net.rbgrn.android.glwallpaperservice.GLWallpaperService /** * @author jinyalin * * * @since 2017/7/27. */ open class GLWallpaperServiceProxy(var host: Context) : GLWallpaperService() { private val activateCallback: WallpaperActiveCallback? init { @Suppress("LeakingThis") attachBaseContext(host) activateCallback = if (host is WallpaperActiveCallback) host as WallpaperActiveCallback else null } override fun onCreate() { } override fun onDestroy() { } open inner class GLActiveEngine : GLEngine() { private var mWallpaperActivate = false private var mEngineUnlockReceiver: BroadcastReceiver? = null override fun onCreate(surfaceHolder: SurfaceHolder?) { if (!isPreview) { if (UserManagerCompat.isUserUnlocked(this@GLWallpaperServiceProxy)) { activateWallpaper() } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { mEngineUnlockReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { activateWallpaper() context.unregisterReceiver(this) } } val filter = IntentFilter(Intent.ACTION_USER_UNLOCKED) this@GLWallpaperServiceProxy.registerReceiver(mEngineUnlockReceiver, filter) } } } override fun onDestroy() { super.onDestroy() deactivateWallpaper() } private fun activateWallpaper() { mWallpaperActivate = true activateCallback?.onWallpaperActivate() } private fun deactivateWallpaper() { if (mWallpaperActivate) { activateCallback?.onWallpaperDeactivate() } } } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/IProvider.java ================================================ package com.yalin.style.engine; import android.content.Context; import android.service.wallpaper.WallpaperService; /** * YaLin * On 2017/7/27. */ public interface IProvider { WallpaperService provideProxy(Context host); } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/ProxyApi.java ================================================ package com.yalin.style.engine; import android.content.Context; import android.service.wallpaper.WallpaperService; import com.yalin.style.engine.component.ComponentContext; /** * YaLin * On 2017/7/27. */ public class ProxyApi { private static IProvider getProvider(ComponentContext context, String providerName) throws Exception { synchronized (ProxyApi.class) { IProvider provider; Class providerClazz = context.getClassLoader().loadClass(providerName); if (providerClazz != null) { provider = (IProvider) providerClazz.newInstance(); return provider; } else { throw new IllegalStateException("Load Provider error."); } } } public static WallpaperService getProxy(Context context, String componentPath, String providerName) { try { ComponentContext componentContext = new ComponentContext(context, componentPath); IProvider provider = getProvider(componentContext, providerName); return provider.provideProxy(componentContext); } catch (Exception e) { e.printStackTrace(); } return null; } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/ProxyProvider.kt ================================================ package com.yalin.style.engine import android.content.Context import android.service.wallpaper.WallpaperService import com.yalin.style.domain.interactor.GetSelectedAdvanceWallpaper import com.yalin.style.domain.interactor.GetSelectedSource import com.yalin.style.domain.repository.SourcesRepository import com.yalin.style.engine.advance.BokehRainbowWallpaper import javax.inject.Inject /** * @author jinyalin * * * @since 2017/7/27. */ class ProxyProvider @Inject constructor(val getSelectedSourceUseCase: GetSelectedSource, val getAdvanceWallpaper: GetSelectedAdvanceWallpaper) { val NORMAL_PROXY_CLASS = "com.yalin.style.engine.StyleWallpaperProxy" fun provideProxy(host: Context): WallpaperService { if (getSelectedSourceUseCase.selectedSourceId == SourcesRepository.SOURCE_ID_ADVANCE) { val selected = getAdvanceWallpaper.selected if (selected.isDefault) { return BokehRainbowWallpaper(host) } else { val proxy = ProxyApi.getProxy(host, selected.storePath, selected.providerName) if (proxy != null) { return proxy } } } val constructor = Class.forName(NORMAL_PROXY_CLASS).getConstructor(Context::class.java) return constructor.newInstance(host) as WallpaperService } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/WallpaperActiveCallback.kt ================================================ package com.yalin.style.engine /** * @author jinyalin * @since 2017/8/1. */ interface WallpaperActiveCallback { fun onWallpaperActivate() fun onWallpaperDeactivate() } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/WallpaperServiceProxy.kt ================================================ package com.yalin.style.engine import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build import android.service.wallpaper.WallpaperService import android.support.v4.os.UserManagerCompat import android.view.SurfaceHolder /** * @author jinyalin * * * @since 2017/7/27. */ open class WallpaperServiceProxy(var host: Context) : WallpaperService() { private val activateCallback: WallpaperActiveCallback? init { @Suppress("LeakingThis") attachBaseContext(host) activateCallback = if (host is WallpaperActiveCallback) host as WallpaperActiveCallback else null } override fun onCreateEngine(): WallpaperService.Engine? { return null } override fun onCreate() { } override fun onDestroy() { } open inner class ActiveEngine : Engine() { private var mWallpaperActivate = false private var mEngineUnlockReceiver: BroadcastReceiver? = null override fun onCreate(surfaceHolder: SurfaceHolder?) { if (!isPreview) { if (UserManagerCompat.isUserUnlocked(this@WallpaperServiceProxy)) { activateWallpaper() } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { mEngineUnlockReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { activateWallpaper() context.unregisterReceiver(this) } } val filter = IntentFilter(Intent.ACTION_USER_UNLOCKED) this@WallpaperServiceProxy.registerReceiver(mEngineUnlockReceiver, filter) } } } override fun onDestroy() { super.onDestroy() deactivateWallpaper() } private fun activateWallpaper() { mWallpaperActivate = true activateCallback?.onWallpaperActivate() } private fun deactivateWallpaper() { if (mWallpaperActivate) { activateCallback?.onWallpaperDeactivate() } } } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/advance/AnimationWallpaper.java ================================================ /* * Copyright (C) 2007 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yalin.style.engine.advance; import android.content.Context; import android.os.Handler; import android.view.SurfaceHolder; import com.yalin.style.engine.WallpaperServiceProxy; public abstract class AnimationWallpaper extends WallpaperServiceProxy { public AnimationWallpaper(Context host) { super(host); } protected abstract class AnimationEngine extends ActiveEngine { private Handler mHandler = new Handler(); private Runnable mIteration = new Runnable() { public void run() { iteration(); drawFrame(); } }; private boolean mVisible; @Override public void onDestroy() { super.onDestroy(); // stop the animation mHandler.removeCallbacks(mIteration); } @Override public void onVisibilityChanged(boolean visible) { mVisible = visible; if (visible) { iteration(); drawFrame(); } else { // stop the animation mHandler.removeCallbacks(mIteration); } } @Override public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { iteration(); drawFrame(); } @Override public void onSurfaceDestroyed(SurfaceHolder holder) { super.onSurfaceDestroyed(holder); mVisible = false; // stop the animation mHandler.removeCallbacks(mIteration); } @Override public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset) { iteration(); drawFrame(); } protected abstract void drawFrame(); protected void iteration() { // Reschedule the next redraw in 40ms mHandler.removeCallbacks(mIteration); if (mVisible) { mHandler.postDelayed(mIteration, 1000 / 25); } } } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/advance/BokehRainbowCircle.java ================================================ /* * Copyright (C) 2007 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yalin.style.engine.advance; import android.graphics.Bitmap; public class BokehRainbowCircle { float origRadius; float deltaRadius; float radius; float origX; float deltaX; float x; float origY; float deltaY; float y; int color; int alpha; int steps; int currentStep; Bitmap bitmap; public BokehRainbowCircle(float xCenter, float yCenter, float radius, int color, int steps) { this.x = xCenter; this.origX = xCenter; this.deltaX = (float) (40.0 * Math.random() - 20.0); this.y = yCenter; this.origY = yCenter; this.deltaY = (float) (40.0 * Math.random() - 20.0); this.origRadius = radius; this.radius = radius; this.deltaRadius = 0.5f * radius; this.color = color; this.alpha = 0; this.steps = steps; } void tick() { this.currentStep++; float fraction = (float) this.currentStep / (float) this.steps; this.radius = this.origRadius + fraction * this.deltaRadius; this.x = this.origX + fraction * this.deltaX; this.y = this.origY + fraction * this.deltaY; if (fraction <= 0.25f) { this.alpha = (int) (128 * 4.0f * fraction); } else { this.alpha = (int) (-128 * (fraction - 1) / 0.75f); } } boolean isDone() { return this.currentStep > this.steps; } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/advance/BokehRainbowWallpaper.java ================================================ /* * Copyright (C) 2007 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yalin.style.engine.advance; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.Bundle; import android.view.SurfaceHolder; import java.util.HashSet; import java.util.Iterator; import java.util.Set; public class BokehRainbowWallpaper extends AnimationWallpaper { public BokehRainbowWallpaper(Context host) { super(host); } @Override public Engine onCreateEngine() { return new BokehEngine(); } class BokehEngine extends AnimationEngine { int offsetX; int offsetY; int height; int width; int visibleWidth; Set circles = new HashSet(); int iterationCount = 0; Paint paint = new Paint(); @Override public void onCreate(SurfaceHolder surfaceHolder) { super.onCreate(surfaceHolder); // By default we don't get touch events, so enable them. setTouchEventsEnabled(true); } @Override public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { this.height = height; if (this.isPreview()) { this.width = width; } else { this.width = 2 * width; } this.visibleWidth = width; for (int i = 0; i < 20; i++) { this.createRandomCircle(); } super.onSurfaceChanged(holder, format, width, height); } @Override public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset) { // store the offsets this.offsetX = xPixelOffset; this.offsetY = yPixelOffset; super.onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset); } @Override public Bundle onCommand(String action, int x, int y, int z, Bundle extras, boolean resultRequested) { if ("android.wallpaper.tap".equals(action)) { createCircle(x - this.offsetX, y - this.offsetY); } return super.onCommand(action, x, y, z, extras, resultRequested); } @Override protected void drawFrame() { SurfaceHolder holder = getSurfaceHolder(); Canvas c = null; try { c = holder.lockCanvas(); if (c != null) { draw(c); } } finally { if (c != null) holder.unlockCanvasAndPost(c); } } void draw(Canvas c) { c.save(); c.drawColor(0xff000000); synchronized (circles) { for (BokehRainbowCircle circle : circles) { if (circle.alpha == 0) continue; // intersects with the screen? float minX = circle.x - circle.radius; if (minX > (-this.offsetX + this.visibleWidth)) { continue; } float maxX = circle.x + circle.radius; if (maxX < -this.offsetX) { continue; } paint.setAntiAlias(true); // paint the fill paint.setColor(Color.argb(circle.alpha, Color .red(circle.color), Color.green(circle.color), Color.blue(circle.color))); paint.setStyle(Paint.Style.FILL_AND_STROKE); c.drawCircle(circle.x + this.offsetX, circle.y + this.offsetY, circle.radius, paint); // paint the contour paint.setColor(Color.argb(circle.alpha, 63 + 3 * Color .red(circle.color) / 4, 63 + 3 * Color .green(circle.color) / 4, 63 + 3 * Color .blue(circle.color) / 4)); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(3.0f); c.drawCircle(circle.x + this.offsetX, circle.y + this.offsetY, circle.radius, paint); } } c.restore(); } @Override protected void iteration() { synchronized (circles) { for (Iterator it = circles.iterator(); it .hasNext();) { BokehRainbowCircle circle = it.next(); circle.tick(); if (circle.isDone()) it.remove(); } iterationCount++; if (isPreview() || iterationCount % 2 == 0) createRandomCircle(); } super.iteration(); } void createRandomCircle() { int x = (int) (width * Math.random()); int y = (int) (height * Math.random()); createCircle(x, y); } int getColor(float yFraction) { return Color.HSVToColor(new float[] { 360.0f * yFraction, 1.0f, 1.0f }); } void createCircle(int x, int y) { float radius = (float) (40 + 20 * Math.random()); float yFraction = (float) y / (float) height; yFraction = yFraction + 0.05f - (float) (0.1f * (Math.random())); if (yFraction < 0.0f) yFraction += 1.0f; if (yFraction > 1.0f) yFraction -= 1.0f; int color = getColor(yFraction); int steps = 40 + (int) (20 * Math.random()); BokehRainbowCircle circle = new BokehRainbowCircle(x, y, radius, color, steps); synchronized (this.circles) { this.circles.add(circle); } } } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/component/ComponentContext.java ================================================ package com.yalin.style.engine.component; import android.content.Context; import android.content.ContextWrapper; import android.content.res.AssetManager; import android.content.res.Resources; import com.yalin.style.engine.WallpaperActiveCallback; import com.yalin.style.engine.resource.ResourcesManager; import dalvik.system.DexClassLoader; /** * @author jinyalin * @since 2017/7/3. */ public class ComponentContext extends ContextWrapper implements WallpaperActiveCallback { private String componentPath; private WallpaperActiveCallback origin; private Resources mResources; private ClassLoader mClassLoader; public ComponentContext(Context base, String componentPath) { super(base.getApplicationContext()); this.componentPath = componentPath; if (base instanceof WallpaperActiveCallback) { origin = (WallpaperActiveCallback) base; } } @Override public Resources getResources() { if (mResources == null) { mResources = ResourcesManager.createResources(getBaseContext(), componentPath); } return mResources; } @Override public ClassLoader getClassLoader() { return getClassLoader(componentPath); } private ClassLoader getClassLoader(String componentFilePath) { if (mClassLoader == null) { mClassLoader = new DexClassLoader(componentFilePath, getCacheDir().getAbsolutePath(), null, getBaseContext().getClassLoader()); } return mClassLoader; } @Override public AssetManager getAssets() { return getResources().getAssets(); } @Override public Context getApplicationContext() { return this; } @Override public void onWallpaperActivate() { if (origin != null) { origin.onWallpaperActivate(); } } @Override public void onWallpaperDeactivate() { if (origin != null) { origin.onWallpaperDeactivate(); } } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/component/StyleClassLoader.kt ================================================ package com.yalin.style.engine.component import android.content.Context import android.os.Build import android.util.Log import com.yalin.style.data.utils.getNativeDir import com.yalin.style.data.utils.getNativeFileName import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.File import java.io.FileOutputStream import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.util.Enumeration import java.util.zip.ZipEntry import java.util.zip.ZipFile import dalvik.system.DexClassLoader /** * @author jinyalin * * * @since 2017/8/9. */ class StyleClassLoader(private val context: Context, private val dexPath: String, optimizedDirectory: String?, librarySearchPath: String?, parent: ClassLoader?) : DexClassLoader(dexPath, optimizedDirectory, librarySearchPath, parent) { override fun findLibrary(name: String): String? { val soName = getNativeFileName(dexPath, name) maybeCopyNativeLib(soName) val targetFile = File(getNativeDir(context), soName) return if (targetFile.exists()) targetFile.absolutePath else null } private fun maybeCopyNativeLib(libName: String) { try { val soFile = File(getNativeDir(context), libName) if (soFile.exists()) { return } val cpuArch: String if (Build.VERSION.SDK_INT >= 21) { cpuArch = Build.SUPPORTED_ABIS[0] } else { cpuArch = Build.CPU_ABI } var findSo = false val zipfile = ZipFile(dexPath) var entry: ZipEntry var e: Enumeration<*> = zipfile.entries() while (e.hasMoreElements()) { entry = e.nextElement() as ZipEntry if (entry.isDirectory) continue if (entry.name.endsWith(".so") && entry.name.contains("lib/" + cpuArch)) { findSo = true break } } e = zipfile.entries() while (e.hasMoreElements()) { entry = e.nextElement() if (entry.isDirectory || !entry.name.endsWith(".so")) continue if (findSo && entry.name.contains("lib/" + cpuArch) || !findSo && entry.name.contains("lib/armeabi/")) { val libFile = File(getNativeDir(context).absolutePath + File.separator + libName) if (libFile.exists()) { // check version } val fos = FileOutputStream(libFile) Log.d(TAG, "copy so " + entry.name + " of " + cpuArch) copySo(zipfile.getInputStream(entry), fos) break } } zipfile.close() } catch (e: IOException) { e.printStackTrace() } } @Throws(IOException::class) private fun copySo(input: InputStream, output: OutputStream) { val bufferedInput = BufferedInputStream(input) val bufferedOutput = BufferedOutputStream(output) var count: Int val data = ByteArray(8192) count = bufferedInput.read(data, 0, 8192) while (count != -1) { bufferedOutput.write(data, 0, count) count = bufferedInput.read(data, 0, 8192) } bufferedOutput.flush() bufferedOutput.close() output.close() bufferedInput.close() input.close() } companion object { private val TAG = "StyleClassLoader" } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/resource/BrandUtil.java ================================================ package com.yalin.style.engine.resource; import android.content.res.Resources; /** * @author jinyalin * @since 2017/7/1. */ public class BrandUtil { public static boolean isMiUi(Resources resources) { return resources.getClass().getName().equals("android.content.res.MiuiResources"); } public static boolean isVivo(Resources resources) { return resources.getClass().getName().equals("android.content.res.VivoResources"); } public static boolean isNubia(Resources resources) { return resources.getClass().getName().equals("android.content.res.NubiaResources"); } public static boolean isNotRawResources(Resources resources) { return !resources.getClass().getName().equals("android.content.res.Resources"); } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/resource/CompatResources.java ================================================ package com.yalin.style.engine.resource; import android.content.res.AssetFileDescriptor; import android.content.res.AssetManager; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.Movie; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.annotation.AnimRes; import android.support.annotation.AnyRes; import android.support.annotation.ArrayRes; import android.support.annotation.BoolRes; import android.support.annotation.ColorRes; import android.support.annotation.DimenRes; import android.support.annotation.DrawableRes; import android.support.annotation.FractionRes; import android.support.annotation.IntegerRes; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.PluralsRes; import android.support.annotation.RawRes; import android.support.annotation.RequiresApi; import android.support.annotation.StringRes; import android.support.annotation.XmlRes; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.TypedValue; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.InputStream; /** * @author jinyalin * @since 2017/7/17. */ public class CompatResources extends Resources { private Resources mHostResources; public CompatResources(Resources host, AssetManager assets, DisplayMetrics metrics, Configuration config) { super(assets, metrics, config); mHostResources = host; } @NonNull @Override public CharSequence getText(@StringRes int id) throws NotFoundException { try { return super.getText(id); } catch (Exception e) { return mHostResources.getText(id); } } @NonNull @Override public CharSequence getQuantityText(@PluralsRes int id, int quantity) throws NotFoundException { try { return super.getQuantityText(id, quantity); } catch (Exception e) { return mHostResources.getQuantityText(id, quantity); } } @NonNull @Override public String getString(@StringRes int id) throws NotFoundException { try { return super.getString(id); } catch (Exception e) { return mHostResources.getString(id); } } @NonNull @Override public String getString(@StringRes int id, Object... formatArgs) throws NotFoundException { try { return super.getString(id, formatArgs); } catch (Exception e) { return mHostResources.getString(id, formatArgs); } } @NonNull @Override public String getQuantityString(@PluralsRes int id, int quantity, Object... formatArgs) throws NotFoundException { try { return super.getQuantityString(id, quantity, formatArgs); } catch (Exception e) { return mHostResources.getQuantityString(id, quantity, formatArgs); } } @NonNull @Override public String getQuantityString(@PluralsRes int id, int quantity) throws NotFoundException { try { return super.getQuantityString(id, quantity); } catch (Exception e) { return mHostResources.getQuantityString(id, quantity); } } @Override public CharSequence getText(@StringRes int id, CharSequence def) { try { return super.getText(id, def); } catch (Exception e) { return mHostResources.getText(id, def); } } @NonNull @Override public CharSequence[] getTextArray(@ArrayRes int id) throws NotFoundException { try { return super.getTextArray(id); } catch (Exception e) { return mHostResources.getTextArray(id); } } @NonNull @Override public String[] getStringArray(@ArrayRes int id) throws NotFoundException { try { return super.getStringArray(id); } catch (Exception e) { return mHostResources.getStringArray(id); } } @NonNull @Override public int[] getIntArray(@ArrayRes int id) throws NotFoundException { try { return super.getIntArray(id); } catch (Exception e) { return mHostResources.getIntArray(id); } } @NonNull @Override public TypedArray obtainTypedArray(@ArrayRes int id) throws NotFoundException { try { return super.obtainTypedArray(id); } catch (Exception e) { return mHostResources.obtainTypedArray(id); } } @Override public float getDimension(@DimenRes int id) throws NotFoundException { try { return super.getDimension(id); } catch (Exception e) { return mHostResources.getDimension(id); } } @Override public int getDimensionPixelOffset(@DimenRes int id) throws NotFoundException { try { return super.getDimensionPixelOffset(id); } catch (Exception e) { return mHostResources.getDimensionPixelOffset(id); } } @Override public int getDimensionPixelSize(@DimenRes int id) throws NotFoundException { try { return super.getDimensionPixelSize(id); } catch (Exception e) { return mHostResources.getDimensionPixelSize(id); } } @Override public float getFraction(@FractionRes int id, int base, int pbase) { try { return super.getFraction(id, base, pbase); } catch (Exception e) { return mHostResources.getFraction(id, base, pbase); } } @Override public Drawable getDrawable(@DrawableRes int id) throws NotFoundException { try { return super.getDrawable(id); } catch (Exception e) { return mHostResources.getDrawable(id); } } @RequiresApi(api = 21) @Override public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException { try { return super.getDrawable(id, theme); } catch (Exception e) { return mHostResources.getDrawable(id, theme); } } @RequiresApi(api = 15) @Override public Drawable getDrawableForDensity(@DrawableRes int id, int density) throws NotFoundException { try { return super.getDrawableForDensity(id, density); } catch (Exception e) { return mHostResources.getDrawableForDensity(id, density); } } @RequiresApi(api = 21) @Override public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) { try { return super.getDrawableForDensity(id, density, theme); } catch (Exception e) { return mHostResources.getDrawableForDensity(id, density, theme); } } @Override public Movie getMovie(@RawRes int id) throws NotFoundException { try { return super.getMovie(id); } catch (Exception e) { return mHostResources.getMovie(id); } } @Override public int getColor(@ColorRes int id) throws NotFoundException { try { return super.getColor(id); } catch (Exception e) { return mHostResources.getColor(id); } } @RequiresApi(api = 23) @Override public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException { try { return super.getColor(id, theme); } catch (Exception e) { return mHostResources.getColor(id, theme); } } @Nullable @Override public ColorStateList getColorStateList(@ColorRes int id) throws NotFoundException { try { return super.getColorStateList(id); } catch (Exception e) { return mHostResources.getColorStateList(id); } } @RequiresApi(api = 23) @Nullable @Override public ColorStateList getColorStateList(@ColorRes int id, @Nullable Theme theme) throws NotFoundException { try { return super.getColorStateList(id, theme); } catch (Exception e) { return mHostResources.getColorStateList(id, theme); } } @Override public boolean getBoolean(@BoolRes int id) throws NotFoundException { try { return super.getBoolean(id); } catch (Exception e) { return mHostResources.getBoolean(id); } } @Override public int getInteger(@IntegerRes int id) throws NotFoundException { try { return super.getInteger(id); } catch (Exception e) { return mHostResources.getInteger(id); } } @Override public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException { try { return super.getLayout(id); } catch (Exception e) { return mHostResources.getLayout(id); } } @Override public XmlResourceParser getAnimation(@AnimRes int id) throws NotFoundException { try { return super.getAnimation(id); } catch (Exception e) { return mHostResources.getAnimation(id); } } @Override public XmlResourceParser getXml(@XmlRes int id) throws NotFoundException { try { return super.getXml(id); } catch (Exception e) { return mHostResources.getXml(id); } } @Override public InputStream openRawResource(@RawRes int id) throws NotFoundException { try { return super.openRawResource(id); } catch (Exception e) { return mHostResources.openRawResource(id); } } @Override public InputStream openRawResource(@RawRes int id, TypedValue value) throws NotFoundException { try { return super.openRawResource(id, value); } catch (Exception e) { return mHostResources.openRawResource(id, value); } } @Override public AssetFileDescriptor openRawResourceFd(@RawRes int id) throws NotFoundException { try { return super.openRawResourceFd(id); } catch (Exception e) { return mHostResources.openRawResourceFd(id); } } @Override public void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException { try { super.getValue(id, outValue, resolveRefs); } catch (Exception e) { mHostResources.getValue(id, outValue, resolveRefs); } } @RequiresApi(api = 15) @Override public void getValueForDensity(@AnyRes int id, int density, TypedValue outValue, boolean resolveRefs) throws NotFoundException { try { super.getValueForDensity(id, density, outValue, resolveRefs); } catch (Exception e) { mHostResources.getValueForDensity(id, density, outValue, resolveRefs); } } @Override public void getValue(String name, TypedValue outValue, boolean resolveRefs) throws NotFoundException { try { super.getValue(name, outValue, resolveRefs); } catch (Exception e) { mHostResources.getValue(name, outValue, resolveRefs); } } @Override public TypedArray obtainAttributes(AttributeSet set, int[] attrs) { try { return super.obtainAttributes(set, attrs); } catch (Exception e) { return mHostResources.obtainAttributes(set, attrs); } } @Override public void updateConfiguration(Configuration config, DisplayMetrics metrics) { try { super.updateConfiguration(config, metrics); } catch (Exception e) { // mHostResources.updateConfiguration(config, metrics); } } @Override public DisplayMetrics getDisplayMetrics() { try { return super.getDisplayMetrics(); } catch (Exception e) { return mHostResources.getDisplayMetrics(); } } @Override public Configuration getConfiguration() { try { return super.getConfiguration(); } catch (Exception e) { return mHostResources.getConfiguration(); } } @Override public int getIdentifier(String name, String defType, String defPackage) { try { return super.getIdentifier(name, defType, defPackage); } catch (Exception e) { return mHostResources.getIdentifier(name, defType, defPackage); } } @Override public String getResourceName(@AnyRes int resid) throws NotFoundException { try { return super.getResourceName(resid); } catch (Exception e) { return mHostResources.getResourceName(resid); } } @Override public String getResourcePackageName(@AnyRes int resid) throws NotFoundException { try { return super.getResourcePackageName(resid); } catch (Exception e) { return mHostResources.getResourcePackageName(resid); } } @Override public String getResourceTypeName(@AnyRes int resid) throws NotFoundException { try { return super.getResourceTypeName(resid); } catch (Exception e) { return mHostResources.getResourceTypeName(resid); } } @Override public String getResourceEntryName(@AnyRes int resid) throws NotFoundException { try { return super.getResourceEntryName(resid); } catch (Exception e) { return mHostResources.getResourceEntryName(resid); } } @Override public void parseBundleExtras(XmlResourceParser parser, Bundle outBundle) throws XmlPullParserException, IOException { try { super.parseBundleExtras(parser, outBundle); } catch (Exception e) { // mHostResources.parseBundleExtras(parser, outBundle); } } @Override public void parseBundleExtra(String tagName, AttributeSet attrs, Bundle outBundle) throws XmlPullParserException { try { super.parseBundleExtra(tagName, attrs, outBundle); } catch (Exception e) { // mHostResources.parseBundleExtra(tagName, attrs, outBundle); } } public static TypedArray obtainAttributes( Resources res, Theme theme, AttributeSet set, int[] attrs) { try { if (theme == null) { return res.obtainAttributes(set, attrs); } return theme.obtainStyledAttributes(set, attrs, 0, 0); } catch (Exception e) { return (TypedArray) ReflectUtil.invokeNoException( Resources.class, null, "obtainAttributes", new Class[]{Resources.class, Theme.class, AttributeSet.class, int[].class}, res, theme, set, attrs); } } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/resource/ReflectUtil.java ================================================ package com.yalin.style.engine.resource; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; /** * @author jinyalin * @since 2017/7/1. */ public class ReflectUtil { public static Object getField(Class clazz, Object target, String name) throws Exception { Field field = clazz.getDeclaredField(name); field.setAccessible(true); return field.get(target); } public static Object getFieldNoException(Class clazz, Object target, String name) { try { return ReflectUtil.getField(clazz, target, name); } catch (Exception e) { //ignored. } return null; } public static void setField(Class clazz, Object target, String name, Object value) throws Exception { Field field = clazz.getDeclaredField(name); field.setAccessible(true); field.set(target, value); } public static void setFieldNoException(Class clazz, Object target, String name, Object value) { try { ReflectUtil.setField(clazz, target, name, value); } catch (Exception e) { //ignored. } } @SuppressWarnings("unchecked") public static Object invoke(Class clazz, Object target, String name, Object... args) throws Exception { Class[] parameterTypes = null; if (args != null) { parameterTypes = new Class[args.length]; for (int i = 0; i < args.length; i++) { parameterTypes[i] = args[i].getClass(); } } Method method = clazz.getDeclaredMethod(name, parameterTypes); method.setAccessible(true); return method.invoke(target, args); } @SuppressWarnings("unchecked") public static Object invoke(Class clazz, Object target, String name, Class[] parameterTypes, Object... args) throws Exception { Method method = clazz.getDeclaredMethod(name, parameterTypes); method.setAccessible(true); return method.invoke(target, args); } @SuppressWarnings("unchecked") public static Object invokeNoException(Class clazz, Object target, String name, Class[] parameterTypes, Object... args) { try { return invoke(clazz, target, name, parameterTypes, args); } catch (Exception ignore) { } return null; } @SuppressWarnings("unchecked") public static Object invokeConstructor(Class clazz, Class[] parameterTypes, Object... args) throws Exception { Constructor constructor = clazz.getDeclaredConstructor(parameterTypes); constructor.setAccessible(true); return constructor.newInstance(args); } } ================================================ FILE: engine/src/main/java/com/yalin/style/engine/resource/ResourcesManager.java ================================================ package com.yalin.style.engine.resource; import android.content.Context; import android.content.ContextWrapper; import android.content.res.AssetManager; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Build; import android.util.DisplayMetrics; import java.lang.ref.WeakReference; import java.util.Map; /** * @author jinyalin * @since 2017/7/1. */ public class ResourcesManager { public static synchronized Resources createCompatResources(Context context, String apkFile) { Resources hostResources = context.getResources(); AssetManager assetManager = createAssetManager(context, apkFile); return new CompatResources(hostResources, assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); } public static synchronized Resources createResources(Context context, String apkFile) { Resources hostResources = context.getResources(); AssetManager assetManager = createAssetManager(context, apkFile); return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); } private static AssetManager createAssetManager(Context context, String apkFile) { try { AssetManager am = AssetManager.class.newInstance(); ReflectUtil.invoke(AssetManager.class, am, "addAssetPath", apkFile); return am; } catch (Exception e) { e.printStackTrace(); return null; } } private static ResourcesCompat createResourcesCompat(Resources hostResources) { if (BrandUtil.isMiUi(hostResources)) { return new MiUiResourcesCompat(); } else if (BrandUtil.isVivo(hostResources)) { return new VivoResourcesCompat(); } else if (BrandUtil.isNubia(hostResources)) { return new NubiaResourcesCompat(); } else if (BrandUtil.isNotRawResources(hostResources)) { return new AdaptationResourcesCompat(); } else { // is raw android resources return new AndroidResourcesCompat(); } } private static void hookResources(Context context, Resources resources) { if (Build.VERSION.SDK_INT >= 24) { return; } try { context = getContextImpl(context); ReflectUtil.setField(context.getClass(), context, "mResources", resources); Object loadedApk = ReflectUtil.getField(context.getClass(), context, "mPackageInfo"); ReflectUtil.setField(loadedApk.getClass(), loadedApk, "mResources", resources); Object activityThread = getActivityThread(context); Object resManager = ReflectUtil.getField(activityThread.getClass(), activityThread, "mResourcesManager"); //noinspection unchecked Map> map = (Map>) ReflectUtil.getField(resManager.getClass(), resManager, "mActiveResources"); Object key = map.keySet().iterator().next(); map.put(key, new WeakReference<>(resources)); } catch (Exception e) { e.printStackTrace(); } } private static Context getContextImpl(Context base) throws Exception { Context impl; if (base instanceof ContextWrapper) { impl = (Context) ReflectUtil.getField(ContextWrapper.class, base, "mBase"); } else { impl = base; } return impl; } private static Object getActivityThread(Context base) throws Exception { Class activityThreadClazz = Class.forName("android.app.ActivityThread"); Object activityThread = null; try { activityThread = ReflectUtil.getField(activityThreadClazz, null, "sCurrentActivityThread"); } catch (Exception e) { // ignored } if (activityThread == null) { activityThread = ((ThreadLocal) ReflectUtil.getField(activityThreadClazz, null, "sThreadLocal")).get(); } return activityThread; } private static abstract class ResourcesCompat { abstract Resources createResources(Context context, Resources hostResources, AssetManager assetManager) throws Exception; } private static final class MiUiResourcesCompat extends ResourcesCompat { @Override Resources createResources(Context context, Resources hostResources, AssetManager assetManager) throws Exception { Class resourcesClazz = Class.forName("android.content.res.MiuiResources"); return (Resources) ReflectUtil.invokeConstructor(resourcesClazz, new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class}, assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); } } private static final class VivoResourcesCompat extends ResourcesCompat { @Override Resources createResources(Context hostContext, Resources hostResources, AssetManager assetManager) throws Exception { Class resourcesClazz = Class.forName("android.content.res.VivoResources"); Resources newResources = (Resources) ReflectUtil.invokeConstructor(resourcesClazz, new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class}, assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); ReflectUtil.invokeNoException(resourcesClazz, newResources, "init", new Class[]{String.class}, hostContext.getPackageName()); Object themeValues = ReflectUtil.getFieldNoException(resourcesClazz, hostResources, "mThemeValues"); ReflectUtil.setFieldNoException(resourcesClazz, newResources, "mThemeValues", themeValues); return newResources; } } private static final class NubiaResourcesCompat extends ResourcesCompat { @Override Resources createResources(Context context, Resources hostResources, AssetManager assetManager) throws Exception { Class resourcesClazz = Class.forName("android.content.res.NubiaResources"); return (Resources) ReflectUtil.invokeConstructor(resourcesClazz, new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class}, assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); } } private static final class AdaptationResourcesCompat extends ResourcesCompat { @Override Resources createResources(Context context, Resources hostResources, AssetManager assetManager) throws Exception { Resources newResources; try { Class resourcesClazz = hostResources.getClass(); newResources = (Resources) ReflectUtil.invokeConstructor(resourcesClazz, new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class}, assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); } catch (Exception e) { newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); } return newResources; } } private static final class AndroidResourcesCompat extends ResourcesCompat { @Override Resources createResources(Context context, Resources hostResources, AssetManager assetManager) throws Exception { return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); } } } ================================================ FILE: engine/src/main/java/net/rbgrn/android/glwallpaperservice/BaseConfigChooser.java ================================================ /* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.rbgrn.android.glwallpaperservice; import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLDisplay; /** * Created by romannurik on 11/6/13. */ abstract class BaseConfigChooser implements GLSurfaceView.EGLConfigChooser { private int eglContextClientVersion; public BaseConfigChooser(int[] configSpec, int eglContextClientVersion) { this.eglContextClientVersion = eglContextClientVersion; mConfigSpec = filterConfigSpec(configSpec); } public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { int[] num_config = new int[1]; if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config)) { throw new IllegalArgumentException("eglChooseConfig failed"); } int numConfigs = num_config[0]; if (numConfigs <= 0) { throw new IllegalArgumentException( "No configs match configSpec"); } EGLConfig[] configs = new EGLConfig[numConfigs]; if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, num_config)) { throw new IllegalArgumentException("eglChooseConfig#2 failed"); } EGLConfig config = chooseConfig(egl, display, configs); if (config == null) { throw new IllegalArgumentException("No config chosen"); } return config; } abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs); protected int[] mConfigSpec; private int[] filterConfigSpec(int[] configSpec) { if (eglContextClientVersion != 2) { return configSpec; } /* We know none of the subclasses define EGL_RENDERABLE_TYPE. * And we know the configSpec is well formed. */ int len = configSpec.length; int[] newConfigSpec = new int[len + 2]; System.arraycopy(configSpec, 0, newConfigSpec, 0, len-1); newConfigSpec[len-1] = EGL10.EGL_RENDERABLE_TYPE; newConfigSpec[len] = 4; /* EGL_OPENGL_ES2_BIT */ newConfigSpec[len+1] = EGL10.EGL_NONE; return newConfigSpec; } public static class ComponentSizeChooser extends BaseConfigChooser { public ComponentSizeChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize, int eglContextClientVersion) { super(new int[] { EGL10.EGL_RED_SIZE, redSize, EGL10.EGL_GREEN_SIZE, greenSize, EGL10.EGL_BLUE_SIZE, blueSize, EGL10.EGL_ALPHA_SIZE, alphaSize, EGL10.EGL_DEPTH_SIZE, depthSize, EGL10.EGL_STENCIL_SIZE, stencilSize, EGL10.EGL_NONE }, eglContextClientVersion); mValue = new int[1]; mRedSize = redSize; mGreenSize = greenSize; mBlueSize = blueSize; mAlphaSize = alphaSize; mDepthSize = depthSize; mStencilSize = stencilSize; } @Override public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { EGLConfig closestConfig = null; int closestDistance = 1000; for (EGLConfig config : configs) { int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE); int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE); if (d >= mDepthSize && s >= mStencilSize) { int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE); int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE); int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE); int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE); int distance = Math.abs(r - mRedSize) + Math.abs(g - mGreenSize) + Math.abs(b - mBlueSize) + Math.abs(a - mAlphaSize); if (distance < closestDistance) { closestDistance = distance; closestConfig = config; } } } return closestConfig; } private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute) { if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { return mValue[0]; } return 0; } private int[] mValue; // Subclasses can adjust these values: protected int mRedSize; protected int mGreenSize; protected int mBlueSize; protected int mAlphaSize; protected int mDepthSize; protected int mStencilSize; } /** * This class will choose a supported surface as close to RGB565 as possible, with or without a depth buffer. * */ public static class SimpleEGLConfigChooser extends ComponentSizeChooser { public SimpleEGLConfigChooser(boolean withDepthBuffer, int eglContextClientVersion) { super(4, 4, 4, 0, withDepthBuffer ? 16 : 0, 0, eglContextClientVersion); // Adjust target values. This way we'll accept a 4444 or // 555 buffer if there's no 565 buffer available. mRedSize = 5; mGreenSize = 6; mBlueSize = 5; } } } ================================================ FILE: engine/src/main/java/net/rbgrn/android/glwallpaperservice/GLWallpaperService.java ================================================ /* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.rbgrn.android.glwallpaperservice; import android.opengl.GLSurfaceView; import android.service.wallpaper.WallpaperService; import android.util.Log; import android.view.SurfaceHolder; import net.rbgrn.android.glwallpaperservice.BaseConfigChooser.ComponentSizeChooser; import net.rbgrn.android.glwallpaperservice.BaseConfigChooser.SimpleEGLConfigChooser; import java.io.Writer; import java.util.ArrayList; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGL11; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLContext; import javax.microedition.khronos.egl.EGLDisplay; import javax.microedition.khronos.egl.EGLSurface; import javax.microedition.khronos.opengles.GL; import javax.microedition.khronos.opengles.GL10; // Original code provided by Robert Green // http://www.rbgrn.net/content/354-glsurfaceview-adapted-3d-live-wallpapers public class GLWallpaperService extends WallpaperService { private static final String TAG = "GLWallpaperService"; @Override public Engine onCreateEngine() { return new GLEngine(); } public class GLEngine extends Engine { public final static int RENDERMODE_WHEN_DIRTY = 0; public final static int RENDERMODE_CONTINUOUSLY = 1; private GLThread mGLThread; private GLSurfaceView.EGLConfigChooser mEGLConfigChooser; private GLSurfaceView.EGLContextFactory mEGLContextFactory; private GLSurfaceView.EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; private GLSurfaceView.GLWrapper mGLWrapper; private int mDebugFlags; private int mEGLContextClientVersion; public GLEngine() { super(); } @Override public void onVisibilityChanged(boolean visible) { if (visible) { onResume(); } else { onPause(); } super.onVisibilityChanged(visible); } @Override public void onCreate(SurfaceHolder surfaceHolder) { super.onCreate(surfaceHolder); // Log.D(TAG, "GLEngine.onCreate()"); } @Override public void onDestroy() { super.onDestroy(); // Log.D(TAG, "GLEngine.onDestroy()"); mGLThread.requestExitAndWait(); } @Override public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Log.D(TAG, "onSurfaceChanged()"); mGLThread.onWindowResize(width, height); super.onSurfaceChanged(holder, format, width, height); } @Override public void onSurfaceCreated(SurfaceHolder holder) { Log.d(TAG, "onSurfaceCreated()"); mGLThread.surfaceCreated(holder); super.onSurfaceCreated(holder); } @Override public void onSurfaceDestroyed(SurfaceHolder holder) { Log.d(TAG, "onSurfaceDestroyed()"); mGLThread.surfaceDestroyed(); super.onSurfaceDestroyed(holder); } /** * An EGL helper class. */ public void setGLWrapper(GLSurfaceView.GLWrapper glWrapper) { mGLWrapper = glWrapper; } public void setDebugFlags(int debugFlags) { mDebugFlags = debugFlags; } public int getDebugFlags() { return mDebugFlags; } public void setRenderer(GLSurfaceView.Renderer renderer) { checkRenderThreadState(); if (mEGLConfigChooser == null) { mEGLConfigChooser = new SimpleEGLConfigChooser(true, mEGLContextClientVersion); } if (mEGLContextFactory == null) { mEGLContextFactory = new DefaultContextFactory(mEGLContextClientVersion); } if (mEGLWindowSurfaceFactory == null) { mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); } mGLThread = new GLThread(renderer, mEGLConfigChooser, mEGLContextFactory, mEGLWindowSurfaceFactory, mGLWrapper); mGLThread.start(); } public void setEGLContextFactory(GLSurfaceView.EGLContextFactory factory) { checkRenderThreadState(); mEGLContextFactory = factory; } public void setEGLWindowSurfaceFactory(GLSurfaceView.EGLWindowSurfaceFactory factory) { checkRenderThreadState(); mEGLWindowSurfaceFactory = factory; } public void setEGLConfigChooser(GLSurfaceView.EGLConfigChooser configChooser) { checkRenderThreadState(); mEGLConfigChooser = configChooser; } public void setEGLConfigChooser(boolean needDepth) { setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth, mEGLContextClientVersion)); } public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) { setEGLConfigChooser( new ComponentSizeChooser(redSize, greenSize, blueSize, alphaSize, depthSize, stencilSize, mEGLContextClientVersion)); } public void setEGLContextClientVersion(int version) { checkRenderThreadState(); mEGLContextClientVersion = version; } public void setRenderMode(int renderMode) { mGLThread.setRenderMode(renderMode); } public int getRenderMode() { return mGLThread.getRenderMode(); } public void requestRender() { mGLThread.requestRender(); } public void onPause() { mGLThread.onPause(); } public void onResume() { mGLThread.onResume(); } public void queueEvent(Runnable r) { mGLThread.queueEvent(r); } private void checkRenderThreadState() { if (mGLThread != null) { throw new IllegalStateException("setRenderer has already been called for this instance."); } } } /** * Empty wrapper for {@link GLSurfaceView.Renderer}. * * @deprecated Use {@link GLSurfaceView.Renderer} instead. */ @Deprecated public interface Renderer extends GLSurfaceView.Renderer { } } class LogWriter extends Writer { private StringBuilder mBuilder = new StringBuilder(); @Override public void close() { flushBuilder(); } @Override public void flush() { flushBuilder(); } @Override public void write(char[] buf, int offset, int count) { for (int i = 0; i < count; i++) { char c = buf[offset + i]; if (c == '\n') { flushBuilder(); } else { mBuilder.append(c); } } } private void flushBuilder() { if (mBuilder.length() > 0) { Log.v("GLSurfaceView", mBuilder.toString()); mBuilder.delete(0, mBuilder.length()); } } } // ---------------------------------------------------------------------- /** * Empty wrapper for {@link GLSurfaceView.EGLContextFactory}. * * @deprecated Use {@link GLSurfaceView.EGLContextFactory} instead. */ @Deprecated interface EGLContextFactory extends GLSurfaceView.EGLContextFactory { } class DefaultContextFactory implements GLSurfaceView.EGLContextFactory { private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; private int eglContextClientVersion; DefaultContextFactory(int eglContextClientVersion) { this.eglContextClientVersion = eglContextClientVersion; } public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, eglContextClientVersion, EGL10.EGL_NONE }; return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, eglContextClientVersion != 0 ? attrib_list : null); } public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { egl.eglDestroyContext(display, context); } } /** * Empty wrapper for {@link GLSurfaceView.EGLWindowSurfaceFactory}. * * @deprecated Use {@link GLSurfaceView.EGLWindowSurfaceFactory} instead. */ @Deprecated interface EGLWindowSurfaceFactory extends GLSurfaceView.EGLWindowSurfaceFactory { } class DefaultWindowSurfaceFactory implements GLSurfaceView.EGLWindowSurfaceFactory { public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) { // this is a bit of a hack to work around Droid init problems - if you don't have this, it'll get hung up on orientation changes EGLSurface eglSurface = null; while (eglSurface == null) { try { eglSurface = egl.eglCreateWindowSurface(display, config, nativeWindow, null); } catch (Throwable t) { } finally { if (eglSurface == null) { try { Thread.sleep(10); } catch (InterruptedException t) { } } } } return eglSurface; } public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) { egl.eglDestroySurface(display, surface); } } /** * Empty wrapper for {@link GLSurfaceView.GLWrapper}. * * @deprecated Use {@link GLSurfaceView.GLWrapper} instead. */ @Deprecated interface GLWrapper extends GLSurfaceView.GLWrapper { } class EglHelper { private EGL10 mEgl; private EGLDisplay mEglDisplay; private EGLSurface mEglSurface; private EGLContext mEglContext; EGLConfig mEglConfig; private GLSurfaceView.EGLConfigChooser mEGLConfigChooser; private GLSurfaceView.EGLContextFactory mEGLContextFactory; private GLSurfaceView.EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; private GLSurfaceView.GLWrapper mGLWrapper; public EglHelper(GLSurfaceView.EGLConfigChooser chooser, GLSurfaceView.EGLContextFactory contextFactory, GLSurfaceView.EGLWindowSurfaceFactory surfaceFactory, GLSurfaceView.GLWrapper wrapper) { this.mEGLConfigChooser = chooser; this.mEGLContextFactory = contextFactory; this.mEGLWindowSurfaceFactory = surfaceFactory; this.mGLWrapper = wrapper; } /** * Initialize EGL for a given configuration spec. * * @param configSpec */ public void start() { // Log.D("EglHelper" + instanceId, "start()"); if (mEgl == null) { // Log.D("EglHelper" + instanceId, "getting new EGL"); /* * Get an EGL instance */ mEgl = (EGL10) EGLContext.getEGL(); } else { // Log.D("EglHelper" + instanceId, "reusing EGL"); } if (mEglDisplay == null) { // Log.D("EglHelper" + instanceId, "getting new display"); /* * Get to the default display. */ mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); } else { // Log.D("EglHelper" + instanceId, "reusing display"); } if (mEglConfig == null) { // Log.D("EglHelper" + instanceId, "getting new config"); /* * We can now initialize EGL for that display */ int[] version = new int[2]; mEgl.eglInitialize(mEglDisplay, version); mEglConfig = mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay); } else { // Log.D("EglHelper" + instanceId, "reusing config"); } if (mEglContext == null) { // Log.D("EglHelper" + instanceId, "creating new context"); /* * Create an OpenGL ES context. This must be done only once, an OpenGL context is a somewhat heavy object. */ mEglContext = mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig); if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { throw new RuntimeException("createContext failed"); } } else { // Log.D("EglHelper" + instanceId, "reusing context"); } mEglSurface = null; } /* * React to the creation of a new surface by creating and returning an OpenGL interface that renders to that * surface. */ public GL createSurface(SurfaceHolder holder) { /* * The window size has changed, so we need to create a new surface. */ if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { /* * Unbind and destroy the old EGL surface, if there is one. */ mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); } /* * Create an EGL surface we can render into. */ mEglSurface = mEGLWindowSurfaceFactory.createWindowSurface(mEgl, mEglDisplay, mEglConfig, holder); if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { throw new RuntimeException("createWindowSurface failed"); } /* * Before we can issue GL commands, we need to make sure the context is current and bound to a surface. */ if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { throw new RuntimeException("eglMakeCurrent failed."); } GL gl = mEglContext.getGL(); if (mGLWrapper != null) { gl = mGLWrapper.wrap(gl); } /* * if ((mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS))!= 0) { int configFlags = 0; Writer log = * null; if ((mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) { configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; } * if ((mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) { log = new LogWriter(); } gl = GLDebugHelper.wrap(gl, * configFlags, log); } */ return gl; } /** * Display the current render surface. * * @return false if the context has been lost. */ public boolean swap() { mEgl.eglSwapBuffers(mEglDisplay, mEglSurface); /* * Always check for EGL_CONTEXT_LOST, which means the context and all associated data were lost (For instance * because the device went to sleep). We need to sleep until we get a new surface. */ return mEgl.eglGetError() != EGL11.EGL_CONTEXT_LOST; } public void destroySurface() { if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); mEglSurface = null; } } public void finish() { if (mEglContext != null) { mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext); mEglContext = null; } if (mEglDisplay != null) { mEgl.eglTerminate(mEglDisplay); mEglDisplay = null; } } } class GLThread extends Thread { private final static boolean LOG_THREADS = false; public final static int DEBUG_CHECK_GL_ERROR = 1; public final static int DEBUG_LOG_GL_CALLS = 2; private final GLThreadManager sGLThreadManager = new GLThreadManager(); private GLThread mEglOwner; private GLSurfaceView.EGLConfigChooser mEGLConfigChooser; private GLSurfaceView.EGLContextFactory mEGLContextFactory; private GLSurfaceView.EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; private GLSurfaceView.GLWrapper mGLWrapper; public SurfaceHolder mHolder; private boolean mSizeChanged = true; // Once the thread is started, all accesses to the following member // variables are protected by the sGLThreadManager monitor public boolean mDone; private boolean mPaused; private boolean mHasSurface; private boolean mWaitingForSurface; private boolean mHaveEgl; private int mWidth; private int mHeight; private int mRenderMode; private boolean mRequestRender; private boolean mEventsWaiting; // End of member variables protected by the sGLThreadManager monitor. private GLSurfaceView.Renderer mRenderer; private ArrayList mEventQueue = new ArrayList(); private EglHelper mEglHelper; GLThread(GLSurfaceView.Renderer renderer, GLSurfaceView.EGLConfigChooser chooser, GLSurfaceView.EGLContextFactory contextFactory, GLSurfaceView.EGLWindowSurfaceFactory surfaceFactory, GLSurfaceView.GLWrapper wrapper) { super(); mDone = false; mWidth = 0; mHeight = 0; mRequestRender = true; mRenderMode = GLWallpaperService.GLEngine.RENDERMODE_CONTINUOUSLY; mRenderer = renderer; this.mEGLConfigChooser = chooser; this.mEGLContextFactory = contextFactory; this.mEGLWindowSurfaceFactory = surfaceFactory; this.mGLWrapper = wrapper; } @Override public void run() { setName("GLThread " + getId()); if (LOG_THREADS) { Log.i("GLThread", "starting tid=" + getId()); } try { guardedRun(); } catch (InterruptedException e) { // fall thru and exit normally } finally { sGLThreadManager.threadExiting(this); } } /* * This private method should only be called inside a synchronized(sGLThreadManager) block. */ private void stopEglLocked() { if (mHaveEgl) { mHaveEgl = false; mEglHelper.destroySurface(); sGLThreadManager.releaseEglSurface(this); } } private void guardedRun() throws InterruptedException { mEglHelper = new EglHelper(mEGLConfigChooser, mEGLContextFactory, mEGLWindowSurfaceFactory, mGLWrapper); try { GL10 gl = null; boolean tellRendererSurfaceCreated = true; boolean tellRendererSurfaceChanged = true; /* * This is our main activity thread's loop, we go until asked to quit. */ while (!isDone()) { /* * Update the asynchronous state (window size) */ int w = 0; int h = 0; boolean changed = false; boolean needStart = false; boolean eventsWaiting = false; synchronized (sGLThreadManager) { while (true) { // Manage acquiring and releasing the SurfaceView // surface and the EGL surface. if (mPaused) { stopEglLocked(); } if (!mHasSurface) { if (!mWaitingForSurface) { stopEglLocked(); mWaitingForSurface = true; sGLThreadManager.notifyAll(); } } else { if (!mHaveEgl) { if (sGLThreadManager.tryAcquireEglSurface(this)) { mHaveEgl = true; mEglHelper.start(); mRequestRender = true; needStart = true; } } } // Check if we need to wait. If not, update any state // that needs to be updated, copy any state that // needs to be copied, and use "break" to exit the // wait loop. if (mDone) { return; } if (mEventsWaiting) { eventsWaiting = true; mEventsWaiting = false; break; } if ((!mPaused) && mHasSurface && mHaveEgl && (mWidth > 0) && (mHeight > 0) && (mRequestRender || (mRenderMode == GLWallpaperService.GLEngine.RENDERMODE_CONTINUOUSLY))) { changed = mSizeChanged; w = mWidth; h = mHeight; mSizeChanged = false; mRequestRender = false; if (mHasSurface && mWaitingForSurface) { changed = true; mWaitingForSurface = false; sGLThreadManager.notifyAll(); } break; } // By design, this is the only place where we wait(). if (LOG_THREADS) { Log.i("GLThread", "waiting tid=" + getId()); } sGLThreadManager.wait(); } } // end of synchronized(sGLThreadManager) /* * Handle queued events */ if (eventsWaiting) { Runnable r; while ((r = getEvent()) != null) { r.run(); if (isDone()) { return; } } // Go back and see if we need to wait to render. continue; } if (needStart) { tellRendererSurfaceCreated = true; changed = true; } if (changed) { gl = (GL10) mEglHelper.createSurface(mHolder); tellRendererSurfaceChanged = true; } if (tellRendererSurfaceCreated) { mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig); tellRendererSurfaceCreated = false; } if (tellRendererSurfaceChanged) { mRenderer.onSurfaceChanged(gl, w, h); tellRendererSurfaceChanged = false; } if ((w > 0) && (h > 0)) { /* draw a frame here */ mRenderer.onDrawFrame(gl); /* * Once we're done with GL, we need to call swapBuffers() to instruct the system to display the * rendered frame */ mEglHelper.swap(); Thread.sleep(10); } } } finally { /* * clean-up everything... */ synchronized (sGLThreadManager) { stopEglLocked(); mEglHelper.finish(); } } } private boolean isDone() { synchronized (sGLThreadManager) { return mDone; } } public void setRenderMode(int renderMode) { if (!((GLWallpaperService.GLEngine.RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= GLWallpaperService.GLEngine.RENDERMODE_CONTINUOUSLY))) { throw new IllegalArgumentException("renderMode"); } synchronized (sGLThreadManager) { mRenderMode = renderMode; if (renderMode == GLWallpaperService.GLEngine.RENDERMODE_CONTINUOUSLY) { sGLThreadManager.notifyAll(); } } } public int getRenderMode() { synchronized (sGLThreadManager) { return mRenderMode; } } public void requestRender() { synchronized (sGLThreadManager) { mRequestRender = true; sGLThreadManager.notifyAll(); } } public void surfaceCreated(SurfaceHolder holder) { mHolder = holder; synchronized (sGLThreadManager) { if (LOG_THREADS) { Log.i("GLThread", "surfaceCreated tid=" + getId()); } mHasSurface = true; sGLThreadManager.notifyAll(); } } public void surfaceDestroyed() { synchronized (sGLThreadManager) { if (LOG_THREADS) { Log.i("GLThread", "surfaceDestroyed tid=" + getId()); } mHasSurface = false; sGLThreadManager.notifyAll(); while (!mWaitingForSurface && isAlive() && !mDone) { try { sGLThreadManager.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } public void onPause() { synchronized (sGLThreadManager) { mPaused = true; sGLThreadManager.notifyAll(); } } public void onResume() { synchronized (sGLThreadManager) { mPaused = false; mRequestRender = true; sGLThreadManager.notifyAll(); } } public void onWindowResize(int w, int h) { synchronized (sGLThreadManager) { mWidth = w; mHeight = h; mSizeChanged = true; sGLThreadManager.notifyAll(); } } public void requestExitAndWait() { // don't call this from GLThread thread or it is a guaranteed // deadlock! synchronized (sGLThreadManager) { mDone = true; sGLThreadManager.notifyAll(); } try { join(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } /** * Queue an "event" to be run on the GL rendering thread. * * @param r * the runnable to be run on the GL rendering thread. */ public void queueEvent(Runnable r) { synchronized (this) { mEventQueue.add(r); synchronized (sGLThreadManager) { mEventsWaiting = true; sGLThreadManager.notifyAll(); } } } private Runnable getEvent() { synchronized (this) { if (mEventQueue.size() > 0) { return mEventQueue.remove(0); } } return null; } private class GLThreadManager { public synchronized void threadExiting(GLThread thread) { if (LOG_THREADS) { Log.i("GLThread", "exiting tid=" + thread.getId()); } thread.mDone = true; if (mEglOwner == thread) { mEglOwner = null; } notifyAll(); } /* * Tries once to acquire the right to use an EGL surface. Does not block. * * @return true if the right to use an EGL surface was acquired. */ public synchronized boolean tryAcquireEglSurface(GLThread thread) { if (mEglOwner == thread || mEglOwner == null) { mEglOwner = thread; notifyAll(); return true; } return false; } public synchronized void releaseEglSurface(GLThread thread) { if (mEglOwner == thread) { mEglOwner = null; } notifyAll(); } } } /** * Empty wrapper for {@link GLSurfaceView.EGLConfigChooser}. * * @deprecated Use {@link GLSurfaceView.EGLConfigChooser} instead. */ @Deprecated interface EGLConfigChooser extends GLSurfaceView.EGLConfigChooser { } ================================================ FILE: engine/src/main/res/values/strings.xml ================================================ engine ================================================ FILE: engine/src/test/java/com/yalin/style/engine/ExampleUnitTest.java ================================================ package com.yalin.style.engine; import org.junit.Test; import static org.junit.Assert.*; /** * Example local unit test, which will execute on the development machine (host). * * @see Testing documentation */ public class ExampleUnitTest { @Test public void addition_isCorrect() throws Exception { assertEquals(4, 2 + 2); } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Tue Apr 18 15:48:48 CST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip ================================================ FILE: gradle.properties ================================================ COMPILE_SDK_VERSION=25 BUILD_TOOLS_VERSION=25.0.2 MIN_SDK_VERSION=19 TARGET_SDK_VERSION=25 APP_VERSION_NAME=2.7.1 APP_VERSION_CODE=25 KOTLIN_VERSION = 1.1.3-2 ANKO_VESION = 0.8.2 SUPPORT_LIBRARY_VERSION=25.3.1 OK_HTTP_VERSION=3.5.0 GSON_VERSION=2.8.0 RXJAVA_VERSION=2.0.8 RXANDROID_VERSION=2.0.1 ARROW_VERSION=1.0.0 DAGGER_VERSION=2.10 GLIDE_VERSION=3.7.0 EVENT_BUS_VERSION=3.0.0 BUGLY_VERSION=2.5.0 FLURRY_VERSION=7.0.0@aar GDX_VERSION = 1.7.1 FIRE_BASE_VERSION=10.2.1 JAVAX_INJECT_VERSION=1 CONF_VERSION = 20170102 DEMO_API_WALLPAPER_ENDPOINT = http://api.kinglloy.com:6060/style PRODUCTION_API_WALLPAPER_ENDPOINT = http://api.kinglloy.com:6060/style ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: presentation/.gitignore ================================================ /build ================================================ FILE: presentation/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-android-extensions' Properties properties = new Properties() properties.load(project.rootProject.file('local.properties').newDataInputStream()) def alias = properties.get('alias') def _keyPassword = properties.get('keyPassword') def _storePassword = properties.get('storePassword') def keystorePath = properties.get('keystorePath') def buglyAppId = properties.get('buglyAppId') def flurryApiKey = properties.get('flurryApiKey') def CHANNEL = System.getProperty("channel", "default") def APP_NAME = System.getProperty("app_name", "Style") android { compileSdkVersion COMPILE_SDK_VERSION as int buildToolsVersion BUILD_TOOLS_VERSION as String defaultConfig { applicationId "com.yalin.style" minSdkVersion MIN_SDK_VERSION as int targetSdkVersion TARGET_SDK_VERSION as int versionCode APP_VERSION_CODE as int versionName APP_VERSION_NAME as String testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" multiDexEnabled true buildConfigField("String", "BUGLY_APP_ID", "\"${buglyAppId}\"") buildConfigField("String", "FLURRY_API_KEY", "\"${flurryApiKey}\"") resValue("string", "app_name_ext", "${APP_NAME}") ndk { abiFilters 'x86', 'armeabi', 'armeabi-v7a' } } signingConfigs { release { keyAlias alias keyPassword _keyPassword storeFile file(keystorePath) storePassword _storePassword } } buildTypes { debug { minifyEnabled false signingConfig signingConfigs.release } release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'component-proguard.pro' signingConfig signingConfigs.release } } productFlavors { demo { buildConfigField("boolean", "DEMO_MODE", "true") } production { buildConfigField("boolean", "DEMO_MODE", "false") } } dexOptions { javaMaxHeapSize "4g" } } dependencies { compile "com.android.support:appcompat-v7:${SUPPORT_LIBRARY_VERSION}" compile "com.android.support:exifinterface:${SUPPORT_LIBRARY_VERSION}" compile "com.android.support:design:${SUPPORT_LIBRARY_VERSION}" compile "com.android.support:recyclerview-v7:${SUPPORT_LIBRARY_VERSION}" compile "com.android.support:percent:${SUPPORT_LIBRARY_VERSION}" compile "com.google.code.gson:gson:${GSON_VERSION}" compile "com.squareup.okhttp3:okhttp:${OK_HTTP_VERSION}" compile "com.google.dagger:dagger:${DAGGER_VERSION}" compile "io.reactivex.rxjava2:rxjava:${RXJAVA_VERSION}" compile "io.reactivex.rxjava2:rxandroid:${RXANDROID_VERSION}" compile "com.github.bumptech.glide:glide:${GLIDE_VERSION}" compile "org.greenrobot:eventbus:${EVENT_BUS_VERSION}" compile "com.tencent.bugly:crashreport:${BUGLY_VERSION}" compile "com.flurry.android:analytics:${FLURRY_VERSION}" compile "com.google.firebase:firebase-core:${FIRE_BASE_VERSION}" compile "com.google.firebase:firebase-crash:${FIRE_BASE_VERSION}" compile "com.google.firebase:firebase-ads:${FIRE_BASE_VERSION}" // annotationProcessor "com.google.dagger:dagger-compiler:${DAGGER_VERSION}" kapt "com.google.dagger:dagger-compiler:${DAGGER_VERSION}" compile 'com.afollestad.material-dialogs:core:0.9.4.5' compile project(':domain') demoCompile project(path: ':engine', configuration: 'demoDebug') productionCompile project(path: ':engine', configuration: 'productionRelease') demoCompile project(path: ':data', configuration: 'demoDebug') productionCompile project(path: ':data', configuration: 'productionRelease') demoCompile 'com.facebook.stetho:stetho:1.5.0' compile 'com.android.support:multidex:1.0.1' testCompile 'junit:junit:4.12' androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) } project.afterEvaluate { android.applicationVariants.all { variant -> variant.outputs.each { output -> def oldFile = output.outputFile output.outputFile = new File(oldFile.parent, "${APP_NAME}-${CHANNEL}.apk") } } } buildscript { repositories { jcenter() } dependencies { classpath "org.jetbrains.kotlin:kotlin-android-extensions:${KOTLIN_VERSION}" } } apply plugin: 'com.google.gms.google-services' ================================================ FILE: presentation/component-proguard.pro ================================================ # for component -keep public class com.yalin.style.engine.StyleWallpaperProxy{ *; } -keep public class com.yalin.style.engine.GLWallpaperServiceProxy{ *; } -keep public class com.yalin.style.engine.GLWallpaperServiceProxy$GLActiveEngine{ *; } -keep public class com.yalin.style.engine.WallpaperServiceProxy{ *; } -keep public class com.yalin.style.engine.WallpaperServiceProxy$ActiveEngine{ *; } -keep public class com.yalin.style.engine.GDXWallpaperServiceProxy{ *; } -keep public class com.yalin.style.engine.GDXWallpaperServiceProxy$GDXActiveEngine{ *; } -keep interface com.yalin.style.engine.IProvider{ *; } -keep class android.support.v4.content.ContextCompat{ *; } -keep class android.support.v4.content.res.ResourcesCompat{ *; } -keep class android.support.v4.graphics.drawable.DrawableCompat{ *; } -keep class android.support.graphics.drawable.VectorDrawableCompat{ *; } -dontwarn com.badlogic.gdx.backends.android.AndroidFragmentApplication -dontwarn com.badlogic.gdx.utils.GdxBuild -dontwarn com.badlogic.gdx.physics.box2d.utils.Box2DBuild -dontwarn com.badlogic.gdx.jnigen.BuildTarget* -keep interface com.badlogic.gdx.**{ *; } -keep class com.badlogic.gdx.**{ *; } ================================================ FILE: presentation/google-services.json ================================================ { "project_info": { "project_number": "938703193302", "firebase_url": "https://style-997fc.firebaseio.com", "project_id": "style-997fc", "storage_bucket": "style-997fc.appspot.com" }, "client": [ { "client_info": { "mobilesdk_app_id": "1:938703193302:android:08f0f10e5a95f84f", "android_client_info": { "package_name": "com.yalin.style" } }, "oauth_client": [ { "client_id": "938703193302-vutiiimvcf6kl6p8ea7klbapa0c4kkq1.apps.googleusercontent.com", "client_type": 3 } ], "api_key": [ { "current_key": "AIzaSyAXgpHrzEXNQSROL4OHn9ZYU41ijhUd5vc" } ], "services": { "analytics_service": { "status": 1 }, "appinvite_service": { "status": 1, "other_platform_oauth_client": [] }, "ads_service": { "status": 2 } } } ], "configuration_version": "1" } ================================================ FILE: presentation/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in D:\DevTools\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 liked 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 *; #} -dontpreverify -repackageclasses '' -allowaccessmodification -optimizations !code/simplification/arithmetic -keepattributes *Annotation* -keep public class * extends android.app.Activity -keep public class * extends android.app.Application -keep public class * extends android.app.Service -keep public class * extends android.content.BroadcastReceiver -keep public class * extends android.content.ContentProvider -keep public class * extends android.view.View { public (android.content.Context); public (android.content.Context, android.util.AttributeSet); public (android.content.Context, android.util.AttributeSet, int); public void set*(...); } -keepclasseswithmembers class * { public (android.content.Context, android.util.AttributeSet); } -keepclasseswithmembers class * { public (android.content.Context, android.util.AttributeSet, int); } -keepclassmembers class * extends android.content.Context { public void *(android.view.View); public void *(android.view.MenuItem); } -keepclassmembers class * implements android.os.Parcelable { static ** CREATOR; } -keepclassmembers class **.R$* { public static ; } -keepclassmembers class * { @android.webkit.JavascriptInterface ; } #Gson -keepattributes Signature # For using GSON @Expose annotation -keepattributes *Annotation* # Gson specific classes -keep class sun.misc.Unsafe { *; } # Application classes that will be serialized/deserialized over Gson -keep class com.yalin.style.data.entity.** { *; } # Prevent proguard from stripping interface information from TypeAdapterFactory, # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) -keep class * implements com.google.gson.TypeAdapterFactory -keep class * implements com.google.gson.JsonSerializer -keep class * implements com.google.gson.JsonDeserializer -dontwarn okio.** #Glide -keep public class * implements com.bumptech.glide.module.GlideModule -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { **[] $VALUES; public *; } # for DexGuard only #-keepresourcexmlelements manifest/application/meta-data@value=GlideModule #EventBus -keepattributes *Annotation* -keepclassmembers class ** { @org.greenrobot.eventbus.Subscribe ; } -keep enum org.greenrobot.eventbus.ThreadMode { *; } # Only required if you use AsyncExecutor -keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent { (java.lang.Throwable); } #Bugly -dontwarn com.tencent.bugly.** -keep public class com.tencent.bugly.**{*;} #Retrolambda -dontwarn java.lang.invoke** # Required to preserve the Flurry SDK -keep class com.flurry.** { *; } -dontwarn com.flurry.** -keepattributes *Annotation*,EnclosingMethod,Signature -keepclasseswithmembers class * { public (android.content.Context, android.util.AttributeSet, int); } -dontwarn kotlin.** ================================================ FILE: presentation/src/androidTest/java/com/yalin/style/ExampleInstrumentedTest.java ================================================ package com.yalin.style; import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; /** * Instrumentation test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() throws Exception { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); assertEquals("com.yalin.style", appContext.getPackageName()); } } ================================================ FILE: presentation/src/androidTest/java/com/yalin/style/provider/DatabaseTest.java ================================================ package com.yalin.style.provider; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import com.yalin.style.data.repository.datasource.provider.StyleContract.Wallpaper; import java.io.IOException; import java.io.InputStream; import junit.framework.Assert; import org.junit.Test; import org.junit.runner.RunWith; /** * YaLin 2016/12/30. */ @RunWith(AndroidJUnit4.class) public class DatabaseTest { @Test public void testInsertWallpaper() { Context context = InstrumentationRegistry.getTargetContext(); ContentValues contentValues = new ContentValues(); contentValues.put(Wallpaper.COLUMN_NAME_WALLPAPER_ID, "wallpaperId"); contentValues.put(Wallpaper.COLUMN_NAME_TITLE, "title111"); contentValues.put(Wallpaper.COLUMN_NAME_ATTRIBUTION, "yalin..."); contentValues.put(Wallpaper.COLUMN_NAME_BYLINE, "byline111"); contentValues.put(Wallpaper.COLUMN_NAME_IMAGE_URI, "xxx"); contentValues.put(Wallpaper.COLUMN_NAME_ADD_DATE, System.currentTimeMillis()); Uri uri = context.getContentResolver().insert(Wallpaper.CONTENT_URI, contentValues); try { Cursor cursor = context.getContentResolver() .query(Wallpaper.CONTENT_URI, null, null, null, null); Assert.assertNotNull(uri); Assert.assertNotNull(cursor); Assert.assertTrue(cursor.getCount() == 1); if (cursor.moveToFirst()) { String title = cursor.getString(cursor.getColumnIndex(Wallpaper.COLUMN_NAME_TITLE)); Assert.assertEquals(title, "title111"); } cursor.close(); } finally { if (uri != null) { int retVal = context.getContentResolver().delete(uri, null, null); Assert.assertEquals(retVal, 1); } } } @Test public void testOpenFile() { Context context = InstrumentationRegistry.getTargetContext(); try { InputStream is = context.getContentResolver().openInputStream(Wallpaper.CONTENT_URI); Assert.assertNotNull(is); is.close(); } catch (IOException e) { e.printStackTrace(); } } } ================================================ FILE: presentation/src/androidTest/java/com/yalin/style/sync/SyncAdapterTest.java ================================================ package com.yalin.style.sync; import android.accounts.Account; import android.content.ContentResolver; import android.os.Bundle; import android.support.test.runner.AndroidJUnit4; import com.yalin.style.data.repository.datasource.provider.StyleContract; import org.junit.Test; import org.junit.runner.RunWith; /** * YaLin 2017/1/3. */ @RunWith(AndroidJUnit4.class) public class SyncAdapterTest { @Test public void syncAdapterTest() { // ContentResolver // .setSyncAutomatically(null, StyleContract.AUTHORITY, true); // ContentResolver.setIsSyncable(account, ScheduleContract.CONTENT_AUTHORITY, 1); Account account = com.yalin.style.data.repository.datasource.sync.account.Account.getAccount(); // ContentResolver.setIsSyncable(account, StyleContract.AUTHORITY, 1); int syncable = ContentResolver.getIsSyncable(account, StyleContract.AUTHORITY); Bundle b = new Bundle(); b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); ContentResolver.requestSync(account, StyleContract.AUTHORITY, b); } } ================================================ FILE: presentation/src/demo/java/com/yalin/style/util/AdUtil.kt ================================================ package com.yalin.style.util import android.app.Activity import com.google.android.gms.ads.* import com.yalin.style.R import com.yalin.style.view.activity.AdvanceSettingActivity /** * @author jinyalin * @since 2017/8/10. */ fun maybeAttachAd(activity: Activity) { MobileAds.initialize(activity.applicationContext, activity.getString(R.string.app_ad_id)) val adView = activity.findViewById(R.id.adView) as AdView val adRequest = AdRequest.Builder().build() adView.loadAd(adRequest) } private var mInterstitialAd: InterstitialAd? = null fun maybeAttachInterstitialAd(activity: AdvanceSettingActivity, listener: AdListener) { mInterstitialAd = InterstitialAd(activity) mInterstitialAd!!.adUnitId = activity.getString(R.string.advance_insert_ad_unit_id) mInterstitialAd!!.adListener = object : AdListener() { override fun onAdLeftApplication() { listener.onAdLeftApplication() } override fun onAdFailedToLoad(p0: Int) { listener.onAdFailedToLoad(p0) } override fun onAdClosed() { mInterstitialAd!!.loadAd(AdRequest.Builder().build()) listener.onAdClosed() } override fun onAdOpened() { listener.onAdOpened() } override fun onAdLoaded() { listener.onAdLoaded() } } mInterstitialAd!!.loadAd(AdRequest.Builder().build()) } fun maybeShowInterstitialAd(): Boolean { if (mInterstitialAd != null) { if (mInterstitialAd!!.isLoaded) { mInterstitialAd!!.show() return true } } return false } ================================================ FILE: presentation/src/demo/res/values/ad_strings.xml ================================================ ca-app-pub-3940256099942544/6300978111 @string/test_id @string/test_id @string/test_id @string/test_id ================================================ FILE: presentation/src/main/AndroidManifest.xml ================================================ ================================================ FILE: presentation/src/main/java/com/yalin/style/LockScreenVisibleReceiver.kt ================================================ package com.yalin.style import android.content.BroadcastReceiver import android.content.Context import android.content.Intent /** * YaLin 2016/12/30. */ class LockScreenVisibleReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { } } ================================================ FILE: presentation/src/main/java/com/yalin/style/StyleApplication.kt ================================================ package com.yalin.style //import com.facebook.stetho.Stetho import android.content.Context import android.support.multidex.MultiDex import android.support.multidex.MultiDexApplication import com.tencent.bugly.crashreport.CrashReport import com.yalin.style.analytics.Analytics import com.yalin.style.data.log.LogUtil import com.yalin.style.data.repository.AdvanceWallpaperDataRepository import com.yalin.style.injection.component.ApplicationComponent import com.yalin.style.injection.component.DaggerApplicationComponent import com.yalin.style.injection.modules.ApplicationModule import com.yalin.style.extensions.DelegatesExt import javax.inject.Inject /** * @author jinyalin * * * @since 2017/4/18. */ class StyleApplication : MultiDexApplication() { companion object { private val TAG = "StyleApplication" var instance: StyleApplication by DelegatesExt.notNullSingleValue() } val applicationComponent: ApplicationComponent by lazy { initializeInjector() } @Inject lateinit var advanceWallpaperRepository: AdvanceWallpaperDataRepository override fun attachBaseContext(base: Context) { super.attachBaseContext(base) MultiDex.install(this) } override fun onCreate() { super.onCreate() instance = this applicationComponent.inject(this) resetExceptionHandler() Analytics.init(this) CrashReport.initCrashReport(applicationContext, BuildConfig.BUGLY_APP_ID, LogUtil.LOG_ENABLE) CrashReport.setAppChannel(applicationContext, com.yalin.style.data.BuildConfig.CHANNEL) // if (BuildConfig.DEMO_MODE) { // Stetho.initialize( // Stetho.newInitializerBuilder(this) // .enableDumpapp( // Stetho.defaultDumperPluginsProvider(this)) // .enableWebKitInspector( // Stetho.defaultInspectorModulesProvider(this)) // .build()) // } } private fun initializeInjector() = DaggerApplicationComponent.builder() .applicationModule(ApplicationModule(this)) .build() private fun resetExceptionHandler() { val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { t, e -> advanceWallpaperRepository.markRollback() LogUtil.F(TAG, "exception", e) exceptionHandler.uncaughtException(t, e) } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/StyleWallpaperService.kt ================================================ package com.yalin.style import android.app.WallpaperManager import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Intent import android.service.wallpaper.WallpaperService import com.yalin.style.analytics.Analytics import com.yalin.style.analytics.Event import com.yalin.style.domain.interactor.GetSelectedSource import com.yalin.style.domain.interactor.ObserverSources import com.yalin.style.engine.ProxyProvider import com.yalin.style.engine.WallpaperActiveCallback import com.yalin.style.event.SwitchWallpaperServiceEvent import com.yalin.style.event.WallpaperActivateEvent import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.jetbrains.anko.toast import javax.inject.Inject /** * YaLin 2016/12/30. */ open class StyleWallpaperService : WallpaperService(), WallpaperActiveCallback { @Inject lateinit var proxyProvider: ProxyProvider @Inject lateinit var sourcesObserverUseCase: ObserverSources @Inject lateinit var getSelectedSourceUseCase: GetSelectedSource private var proxy: WallpaperService? = null private var currentSelectedSource: Int init { StyleApplication.instance.applicationComponent.inject(this) currentSelectedSource = getSelectedSourceUseCase.selectedSourceId } override fun onCreateEngine(): WallpaperService.Engine { return proxy!!.onCreateEngine() } override fun onCreate() { super.onCreate() proxy = proxyProvider.provideProxy(this) proxy?.onCreate() EventBus.getDefault().register(this) } override fun onDestroy() { super.onDestroy() proxy?.onDestroy() EventBus.getDefault().unregister(this) } override fun onWallpaperActivate() { Analytics.logEvent(this, Event.WALLPAPER_CREATED) EventBus.getDefault().postSticky(WallpaperActivateEvent(true)) } override fun onWallpaperDeactivate() { Analytics.logEvent(this, Event.WALLPAPER_DESTROYED) EventBus.getDefault().postSticky(WallpaperActivateEvent(false)) } open fun getWallpaperTargetClass(): Class<*> { return StyleWallpaperServiceMirror::class.java } private fun pickWallpaper() { try { startActivity(Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER) .putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, ComponentName(this, getWallpaperTargetClass())) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } catch (e: ActivityNotFoundException) { try { startActivity(Intent(WallpaperManager.ACTION_LIVE_WALLPAPER_CHOOSER) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } catch (e2: ActivityNotFoundException) { toast(R.string.exception_message_device_unsupported) Analytics.logEvent(this, Event.DEVICE_UNSUPPORTED) } } } @Subscribe fun onEventMainThread(e: SwitchWallpaperServiceEvent) { pickWallpaper() } } ================================================ FILE: presentation/src/main/java/com/yalin/style/StyleWallpaperServiceMirror.kt ================================================ package com.yalin.style /** * YaLin 2016/12/30. */ class StyleWallpaperServiceMirror : StyleWallpaperService() { override fun getWallpaperTargetClass(): Class<*> { return StyleWallpaperService::class.java } } ================================================ FILE: presentation/src/main/java/com/yalin/style/UIThread.kt ================================================ package com.yalin.style import com.yalin.style.domain.executor.PostExecutionThread import javax.inject.Inject import javax.inject.Singleton import io.reactivex.Scheduler import io.reactivex.android.schedulers.AndroidSchedulers /** * @author jinyalin * * * @since 2017/4/18. */ @Singleton class UIThread @Inject constructor() : PostExecutionThread { override fun getScheduler(): Scheduler { return AndroidSchedulers.mainThread() } } ================================================ FILE: presentation/src/main/java/com/yalin/style/WallpaperDetailViewport.kt ================================================ /* * Copyright 2014 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yalin.style import android.graphics.RectF import org.greenrobot.eventbus.EventBus // Singleton that also behaves as an event class WallpaperDetailViewport private constructor() { companion object { val instance = WallpaperDetailViewport() } private val mViewport0 = RectF() private val mViewport1 = RectF() var isFromUser: Boolean = false private set fun getViewport(id: Int): RectF { return if (id == 0) mViewport0 else mViewport1 } fun setViewport(id: Int, viewport: RectF, fromUser: Boolean) { setViewport(id, viewport.left, viewport.top, viewport.right, viewport.bottom, fromUser) } fun setViewport(id: Int, left: Float, top: Float, right: Float, bottom: Float, fromUser: Boolean) { isFromUser = fromUser getViewport(id).set(left, top, right, bottom) EventBus.getDefault().post(this) } fun setDefaultViewport(id: Int, bitmapAspectRatio: Float, screenAspectRatio: Float): WallpaperDetailViewport { isFromUser = false if (bitmapAspectRatio > screenAspectRatio) { getViewport(id).set( 0.5f - screenAspectRatio / bitmapAspectRatio / 2f, 0f, 0.5f + screenAspectRatio / bitmapAspectRatio / 2f, 1f) } else { getViewport(id).set( 0f, 0.5f - bitmapAspectRatio / screenAspectRatio / 2f, 1f, 0.5f + bitmapAspectRatio / screenAspectRatio / 2f) } EventBus.getDefault().post(this) return this } } ================================================ FILE: presentation/src/main/java/com/yalin/style/analytics/Analytics.kt ================================================ package com.yalin.style.analytics import android.content.Context import android.os.Bundle import com.flurry.android.FlurryAgent import com.google.firebase.analytics.FirebaseAnalytics import com.yalin.style.BuildConfig import com.yalin.style.data.log.LogUtil import java.util.HashMap /** * @author jinyalin * * * @since 2017/5/3. */ object Analytics : IAnalytics { override fun init(context: Context) { FlurryAgent.Builder().withLogEnabled(LogUtil.LOG_ENABLE) .build(context, BuildConfig.FLURRY_API_KEY) } override fun setUserProperty(context: Context, key: String, value: String) { FirebaseAnalytics.getInstance(context) .setUserProperty(key, value) } override fun onStartSession(context: Context) { FlurryAgent.onStartSession(context) } override fun onEndSession(context: Context) { FlurryAgent.onEndSession(context) } override fun logEvent(context: Context, event: String) { FlurryAgent.logEvent(event) FirebaseAnalytics.getInstance(context).logEvent(event, null) } override fun logEvent(context: Context, event: String, vararg params: String) { val paramsMap = HashMap() paramsMap.put(event, params[0]) FlurryAgent.logEvent(event, paramsMap) val bundle = Bundle() bundle.putString(FirebaseAnalytics.Param.ITEM_NAME, params[0]) FirebaseAnalytics.getInstance(context).logEvent(event, bundle) } } ================================================ FILE: presentation/src/main/java/com/yalin/style/analytics/Event.kt ================================================ package com.yalin.style.analytics /** * @author jinyalin * * * @since 2017/5/3. */ object Event { val SETTINGS_OPEN = "settings_open" val WALLPAPER_CREATED = "wallpaper_created" val WALLPAPER_DESTROYED = "wallpaper_destroyed" val ACTIVATE = "activate" val ABOUT_OPEN = "about_open" val LIKE = "like" val UN_LIKE = "unlike" val SHARE = "share" val SWITCH = "switch" val TUTORIAL_BEGIN = "tutorial_begin" val TUTORIAL_COMPLETE = "tutorial_complete" val PRESENT_WALLPAPER = "present_wallpaper" val DEVICE_UNSUPPORTED = "device_unsupported" val VIEW_WALLPAPER_DETAIL = "wallpaper_detail" val VIEW_WALLPAPER_DETAIL_FAILED = "wallpaper_detail_failed" // custom settings val SELECT_WALLPAPER_SOURCE = "select_wallpaper_source" val SELECT_ADVANCE_WALLPAPER_SOURCE = "select_advance_wallpaper_source" val CUSTOM_WALLPAPER_SETTINGS = "custom_wallpaper_settings" val SETUP_UPDATE_INTERVAL = "setup_update_interval" val IMPORT_FROM_GALLERY = "import_from_gallery" val ADD_PHOTO_CLICK = "add_photo_click" val CLEAR_WALLPAPER = "clear_wallpaper" // shortcuts val SHORTCUTS_SETTINGS = "shortcuts_settings" val SHORTCUTS_ADVANCE_SETTINGS = "shortcuts_advance_settings" val SHORTCUTS_ABOUT = "shortcuts_about" val LOAD_ADVANCES = "load_advance_wallpapers" val DOWNLOAD_COMPONENT = "download_component" } ================================================ FILE: presentation/src/main/java/com/yalin/style/analytics/IAnalytics.kt ================================================ package com.yalin.style.analytics import android.content.Context /** * @author jinyalin * * * @since 2017/8/4. */ interface IAnalytics { fun init(context: Context) fun setUserProperty(context: Context, key: String, value: String) fun onStartSession(context: Context) fun onEndSession(context: Context) fun logEvent(context: Context, event: String) fun logEvent(context: Context, event: String, vararg params: String) } ================================================ FILE: presentation/src/main/java/com/yalin/style/engine/StyleWallpaperProxy.kt ================================================ package com.yalin.style.engine import android.app.KeyguardManager import android.app.WallpaperManager import android.content.* import android.os.Build import android.os.Bundle import android.os.Handler import android.support.v4.os.UserManagerCompat import android.support.v4.view.GestureDetectorCompat import android.view.GestureDetector import android.view.MotionEvent import android.view.SurfaceHolder import android.view.ViewConfiguration import com.yalin.style.StyleApplication import com.yalin.style.WallpaperDetailViewport import com.yalin.style.event.SystemWallpaperSizeChangedEvent import com.yalin.style.event.WallpaperDetailOpenedEvent import com.yalin.style.event.WallpaperSwitchEvent import com.yalin.style.render.RenderController import com.yalin.style.render.StyleBlurRenderer import com.yalin.style.settings.Prefs import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import javax.inject.Inject /** * @author jinyalin * @since 2017/7/27. */ class StyleWallpaperProxy(host: Context) : GLWallpaperServiceProxy(host) { companion object { private val TEMPORARY_FOCUS_DURATION_MILLIS: Long = 3000 } private var mInitialized = false private var mUnlockReceiver: BroadcastReceiver? = null override fun onCreateEngine(): Engine { return StyleWallpaperEngine() } override fun onCreate() { super.onCreate() if (UserManagerCompat.isUserUnlocked(this)) { initialize() } else if (Build.VERSION.SDK_INT >= 24) { mUnlockReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { initialize() unregisterReceiver(this) } } val filter = IntentFilter(Intent.ACTION_USER_UNLOCKED) registerReceiver(mUnlockReceiver, filter) } } private fun initialize() { mInitialized = true } override fun onDestroy() { super.onDestroy() if (mInitialized) { //todo } else { unregisterReceiver(mUnlockReceiver) } } inner class StyleWallpaperEngine : GLActiveEngine(), StyleBlurRenderer.Callbacks, RenderController.Callbacks { private var mRenderer: StyleBlurRenderer? = null @Inject lateinit var mRenderController: RenderController internal val mGestureDetector: GestureDetectorCompat = GestureDetectorCompat(host, object : GestureDetector.SimpleOnGestureListener() { override fun onDown(e: MotionEvent): Boolean { return true } override fun onDoubleTap(e: MotionEvent): Boolean { if (mWallpaperDetailMode) { return true } mValidDoubleTap = true mMainThreadHandler.removeCallbacks(mDoubleTapTimeoutRunnable) val timeout = ViewConfiguration.getTapTimeout() mMainThreadHandler.postDelayed(mDoubleTapTimeoutRunnable, timeout.toLong()) return true } }) private val mMainThreadHandler = Handler() private var mVisible = true // is MainActivity visible private var mWallpaperDetailMode = false // is last double tab valid private var mValidDoubleTap = false private var mIsLockScreenVisibleReceiverRegistered = false private val mLockScreenPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sp, key -> if (Prefs.PREF_DISABLE_BLUR_WHEN_LOCKED == key) { if (sp.getBoolean(Prefs.PREF_DISABLE_BLUR_WHEN_LOCKED, false)) { val intentFilter = IntentFilter() intentFilter.addAction(Intent.ACTION_USER_PRESENT) intentFilter.addAction(Intent.ACTION_SCREEN_OFF) intentFilter.addAction(Intent.ACTION_SCREEN_ON) host.registerReceiver(mLockScreenVisibleReceiver, intentFilter) mIsLockScreenVisibleReceiverRegistered = true // If the user is not yet unlocked (i.e., using Direct Boot), we should // immediately send the lock screen visible callback if (!UserManagerCompat.isUserUnlocked(host)) { lockScreenVisibleChanged(true) } } else if (mIsLockScreenVisibleReceiverRegistered) { host.unregisterReceiver(mLockScreenVisibleReceiver) mIsLockScreenVisibleReceiverRegistered = false } } } private val mLockScreenVisibleReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { if (intent != null) { if (Intent.ACTION_USER_PRESENT == intent.action) { lockScreenVisibleChanged(false) } else if (Intent.ACTION_SCREEN_OFF == intent.action) { lockScreenVisibleChanged(true) } else if (Intent.ACTION_SCREEN_ON == intent.action) { val kgm = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager if (!kgm.inKeyguardRestrictedInputMode()) { lockScreenVisibleChanged(false) } } } } } override fun onCreate(surfaceHolder: SurfaceHolder?) { super.onCreate(surfaceHolder) mRenderer = StyleBlurRenderer(host, this).apply { setIsPreview(isPreview) } setEGLContextClientVersion(2) setEGLConfigChooser(8, 8, 8, 0, 0, 0) setRenderer(mRenderer) renderMode = GLEngine.RENDERMODE_WHEN_DIRTY requestRender() StyleApplication.instance.applicationComponent.inject(this) mRenderController.setComponent(mRenderer!!, this) val sp = Prefs.getSharedPreferences(host) sp.registerOnSharedPreferenceChangeListener(mLockScreenPreferenceChangeListener) // Trigger the initial registration if needed mLockScreenPreferenceChangeListener.onSharedPreferenceChanged(sp, Prefs.PREF_DISABLE_BLUR_WHEN_LOCKED) setTouchEventsEnabled(true) setOffsetNotificationsEnabled(true) EventBus.getDefault().register(this) } override fun onDestroy() { EventBus.getDefault().unregister(this) if (mIsLockScreenVisibleReceiverRegistered) { host.unregisterReceiver(mLockScreenVisibleReceiver) } Prefs.getSharedPreferences(host) .unregisterOnSharedPreferenceChangeListener( mLockScreenPreferenceChangeListener) queueEvent { mRenderer?.destroy() } mRenderController.destroy() super.onDestroy() } @Subscribe fun onEventMainThread(e: WallpaperDetailOpenedEvent) { if (e.isWallpaperDetailOpened == mWallpaperDetailMode) { return } mWallpaperDetailMode = e.isWallpaperDetailOpened cancelDelayedBlur() queueEvent { mRenderer?.setIsBlurred(!e.isWallpaperDetailOpened, true) } } @Subscribe fun onEventMainThread(e: WallpaperDetailViewport) { requestRender() } @Subscribe fun onEventMainThread(e: WallpaperSwitchEvent) { mRenderController.reloadCurrentWallpaper() } override fun onSurfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { super.onSurfaceChanged(holder, format, width, height) if (!isPreview) { EventBus.getDefault().postSticky( SystemWallpaperSizeChangedEvent(width, height)) } mRenderController.reloadCurrentWallpaper() } override fun onVisibilityChanged(visible: Boolean) { mVisible = visible mRenderController.setVisible(visible) } override fun onOffsetsChanged(xOffset: Float, yOffset: Float, xOffsetStep: Float, yOffsetStep: Float, xPixelOffset: Int, yPixelOffset: Int) { super.onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset) mRenderer?.setNormalOffsetX(xOffset) } override fun onTouchEvent(event: MotionEvent) { super.onTouchEvent(event) mGestureDetector.onTouchEvent(event) delayBlur() } override fun onCommand(action: String, x: Int, y: Int, z: Int, extras: Bundle?, resultRequested: Boolean): Bundle? { if (WallpaperManager.COMMAND_TAP == action && mValidDoubleTap) { queueEvent { mRenderer?.setIsBlurred(!mRenderer!!.isBlurred, false) delayBlur() } mValidDoubleTap = false } return super.onCommand(action, x, y, z, extras, resultRequested) } override fun queueEventOnGlThread(runnable: Runnable) { queueEvent(runnable) } override fun requestRender() { if (mVisible) { super.requestRender() } } private fun lockScreenVisibleChanged(isLockScreenVisible: Boolean) { cancelDelayedBlur() queueEvent { mRenderer?.setIsBlurred(!isLockScreenVisible, false) } } private fun cancelDelayedBlur() { mMainThreadHandler.removeCallbacks(mBlurRunnable) } private fun delayBlur() { if (mWallpaperDetailMode || mRenderer!!.isBlurred) { return } cancelDelayedBlur() mMainThreadHandler.postDelayed(mBlurRunnable, TEMPORARY_FOCUS_DURATION_MILLIS) } private val mBlurRunnable = Runnable { queueEvent { mRenderer?.setIsBlurred(true, false) } } private val mDoubleTapTimeoutRunnable = Runnable { mValidDoubleTap = false } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/event/MainContainerInsetsChangedEvent.java ================================================ package com.yalin.style.event; import android.graphics.Rect; /** * @author jinyalin * @since 2017/4/25. */ public class MainContainerInsetsChangedEvent { private Rect insets; public MainContainerInsetsChangedEvent(Rect insets) { this.insets = insets; } public Rect getInsets() { return insets; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/event/SeenTutorialEvent.java ================================================ package com.yalin.style.event; /** * @author jinyalin * @since 2017/5/2. */ public class SeenTutorialEvent { } ================================================ FILE: presentation/src/main/java/com/yalin/style/event/StyleWallpaperSizeChangedEvent.java ================================================ package com.yalin.style.event; /** * @author jinyalin * @since 2017/4/25. */ public class StyleWallpaperSizeChangedEvent { private int mWidth; private int mHeight; public StyleWallpaperSizeChangedEvent(int width, int height) { mWidth = width; mHeight = height; } public int getWidth() { return mWidth; } public int getHeight() { return mHeight; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/event/SwitchWallpaperServiceEvent.java ================================================ package com.yalin.style.event; /** * @author jinyalin * @since 2017/7/31. */ public class SwitchWallpaperServiceEvent { } ================================================ FILE: presentation/src/main/java/com/yalin/style/event/SwitchingPhotosStateChangedEvent.java ================================================ package com.yalin.style.event; /** * @author jinyalin * @since 2017/4/25. */ public class SwitchingPhotosStateChangedEvent { private boolean mSwitchingPhotos; private int mId; public SwitchingPhotosStateChangedEvent(int id, boolean switchingPhotos) { mId = id; mSwitchingPhotos = switchingPhotos; } public int getCurrentId() { return mId; } public boolean isSwitchingPhotos() { return mSwitchingPhotos; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/event/SystemWallpaperSizeChangedEvent.java ================================================ package com.yalin.style.event; /** * @author jinyalin * @since 2017/4/25. */ public class SystemWallpaperSizeChangedEvent { private int mWidth; private int mHeight; public SystemWallpaperSizeChangedEvent(int width, int height) { mWidth = width; mHeight = height; } public int getWidth() { return mWidth; } public int getHeight() { return mHeight; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/event/WallpaperActivateEvent.java ================================================ package com.yalin.style.event; /** * @author jinyalin * @since 2017/4/21. */ public class WallpaperActivateEvent { private final boolean wallpaperActivate; public WallpaperActivateEvent(boolean wallpaperActivate) { this.wallpaperActivate = wallpaperActivate; } public boolean isWallpaperActivate() { return wallpaperActivate; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/event/WallpaperDetailOpenedEvent.java ================================================ package com.yalin.style.event; /** * @author jinyalin * @since 2017/4/21. */ public class WallpaperDetailOpenedEvent { private final boolean wallpaperDetailOpened; public WallpaperDetailOpenedEvent(boolean wallpaperDetailOpened) { this.wallpaperDetailOpened = wallpaperDetailOpened; } public boolean isWallpaperDetailOpened() { return wallpaperDetailOpened; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/event/WallpaperSwitchEvent.java ================================================ package com.yalin.style.event; /** * @author jinyalin * @since 2017/4/28. */ public class WallpaperSwitchEvent { } ================================================ FILE: presentation/src/main/java/com/yalin/style/exception/ErrorMessageFactory.java ================================================ package com.yalin.style.exception; import android.content.Context; import com.yalin.style.R; import com.yalin.style.data.exception.NetworkConnectionException; import com.yalin.style.data.exception.RemoteServerException; import com.yalin.style.data.exception.ReswitchException; import java.net.SocketTimeoutException; /** * @author jinyalin * @since 2017/4/29. */ public class ErrorMessageFactory { private ErrorMessageFactory() { //empty } /** * Creates a String representing an error message. * * @param context Context needed to retrieve string resources. * @param exception An exception used as a condition to retrieve the correct error message. * @return {@link String} an error message. */ public static String create(Context context, Exception exception) { String message = context.getString(R.string.exception_message_generic); if (exception instanceof NetworkConnectionException) { message = context.getString(R.string.exception_message_no_connection); } else if (exception instanceof ReswitchException) { message = context.getString(R.string.exception_message_resync); } else if (exception instanceof SocketTimeoutException) { message = context.getString(R.string.exception_message_remote_service); } else if (exception instanceof RemoteServerException) { message = context.getString(R.string.exception_message_remote_service); } return message; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/extensions/DelegatesExtensions.kt ================================================ package com.yalin.style.extensions import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty /** * @author jinyalin * @since 2017/5/8. */ object DelegatesExt { fun notNullSingleValue(): ReadWriteProperty = NotNullSingleValueVar() } private class NotNullSingleValueVar : ReadWriteProperty { private var value: T? = null override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { this.value = if (this.value == null) value else throw IllegalStateException("${property.name} already initialized") } override fun getValue(thisRef: Any?, property: KProperty<*>): T { return value ?: throw IllegalStateException("${property.name} " + "not initialized") } } ================================================ FILE: presentation/src/main/java/com/yalin/style/injection/HasComponent.kt ================================================ package com.yalin.style.injection /** * @author jinyalin * * * @since 2017/4/20. */ interface HasComponent { val component: C } ================================================ FILE: presentation/src/main/java/com/yalin/style/injection/PerActivity.kt ================================================ package com.yalin.style.injection import javax.inject.Scope /** * @author jinyalin * * * @since 2017/4/20. */ @Scope @Retention(AnnotationRetention.RUNTIME) annotation class PerActivity ================================================ FILE: presentation/src/main/java/com/yalin/style/injection/component/ApplicationComponent.kt ================================================ package com.yalin.style.injection.component import android.content.Context import com.yalin.style.StyleApplication import com.yalin.style.StyleWallpaperService import com.yalin.style.StyleWallpaperServiceMirror import com.yalin.style.domain.executor.PostExecutionThread import com.yalin.style.domain.executor.SerialThreadExecutor import com.yalin.style.domain.executor.ThreadExecutor import com.yalin.style.domain.observable.SourcesObservable import com.yalin.style.domain.repository.SourcesRepository import com.yalin.style.domain.observable.WallpaperObservable import com.yalin.style.domain.repository.WallpaperRepository import com.yalin.style.engine.StyleWallpaperProxy import com.yalin.style.injection.modules.ApplicationModule import com.yalin.style.view.activity.AdvanceSettingActivity import com.yalin.style.view.activity.GallerySettingActivity import com.yalin.style.view.activity.StyleActivity import com.yalin.style.view.fragment.StyleRenderFragment import javax.inject.Singleton import dagger.Component /** * @author jinyalin * * * @since 2017/4/18. */ @Singleton @Component(modules = arrayOf(ApplicationModule::class)) interface ApplicationComponent { fun inject(styleApplication: StyleApplication) fun inject(styleActivity: StyleActivity) fun inject(styleStyleWallpaperEngine: StyleWallpaperProxy.StyleWallpaperEngine) fun inject(styleView: StyleRenderFragment.StyleView) fun inject(gallerySettingActivity: GallerySettingActivity) fun inject(advanceSettingActivity: AdvanceSettingActivity) fun inject(styleWallpaperService: StyleWallpaperService) fun inject(styleWallpaperService: StyleWallpaperServiceMirror) //Exposed to sub-graphs. fun context(): Context fun threadExecutor(): ThreadExecutor fun serialThreadExecutor(): SerialThreadExecutor fun postExecutionThread(): PostExecutionThread fun wallpaperRepository(): WallpaperRepository fun sourcesRepository(): SourcesRepository fun wallpaperObservable(): WallpaperObservable fun sourceObservable(): SourcesObservable } ================================================ FILE: presentation/src/main/java/com/yalin/style/injection/component/SourceComponent.kt ================================================ package com.yalin.style.injection.component import com.yalin.style.injection.PerActivity import com.yalin.style.injection.modules.SourceModule import com.yalin.style.view.fragment.SettingsChooseSourceFragment import dagger.Component /** * @author jinyalin * * * @since 2017/4/18. */ @PerActivity @Component(dependencies = arrayOf(ApplicationComponent::class), modules = arrayOf(SourceModule::class)) interface SourceComponent { fun inject(settingsChooseSourceFragment: SettingsChooseSourceFragment) } ================================================ FILE: presentation/src/main/java/com/yalin/style/injection/component/WallpaperComponent.kt ================================================ package com.yalin.style.injection.component import com.yalin.style.injection.PerActivity import com.yalin.style.injection.modules.WallpaperModule import com.yalin.style.view.fragment.WallpaperDetailFragment import javax.inject.Singleton import dagger.Component /** * @author jinyalin * * * @since 2017/4/18. */ @PerActivity @Component(dependencies = arrayOf(ApplicationComponent::class), modules = arrayOf(WallpaperModule::class)) interface WallpaperComponent { fun inject(wallpaperDetailFragment: WallpaperDetailFragment) } ================================================ FILE: presentation/src/main/java/com/yalin/style/injection/modules/ApplicationModule.kt ================================================ package com.yalin.style.injection.modules import android.content.Context import com.yalin.style.UIThread import com.yalin.style.data.cache.* import com.yalin.style.data.executor.JobExecutor import com.yalin.style.data.executor.SerialJobExecutor import com.yalin.style.data.observable.SourcesObservableImpl import com.yalin.style.data.repository.SourcesDataRepository import com.yalin.style.data.repository.StyleWallpaperDataRepository import com.yalin.style.data.observable.WallpaperObservableImpl import com.yalin.style.domain.executor.PostExecutionThread import com.yalin.style.domain.executor.SerialThreadExecutor import com.yalin.style.domain.executor.ThreadExecutor import com.yalin.style.domain.observable.SourcesObservable import com.yalin.style.domain.repository.SourcesRepository import com.yalin.style.domain.observable.WallpaperObservable import com.yalin.style.domain.repository.WallpaperRepository import javax.inject.Singleton import dagger.Module import dagger.Provides /** * @author jinyalin * * * @since 2017/4/18. */ @Module class ApplicationModule(context: Context) { private val applicationContext: Context = context.applicationContext @Provides @Singleton internal fun provideApplicationContext(): Context { return applicationContext } @Provides @Singleton internal fun provideThreadExecutor(jobExecutor: JobExecutor): ThreadExecutor { return jobExecutor } @Provides @Singleton internal fun provideSerialThreadExecutor(jobExecutor: SerialJobExecutor): SerialThreadExecutor { return jobExecutor } @Provides @Singleton internal fun providePostExecutionThread(uiThread: UIThread): PostExecutionThread { return uiThread } @Provides @Singleton internal fun provideWallpaperRepository(styleWallpaperDataRepository: StyleWallpaperDataRepository): WallpaperRepository { return styleWallpaperDataRepository } @Provides @Singleton internal fun provideSourcesRepository(sourcesDataRepository: SourcesDataRepository): SourcesRepository { return sourcesDataRepository } @Provides @Singleton internal fun provideWallpaperCache(wallpaperCache: WallpaperCacheImpl): WallpaperCache { return wallpaperCache } @Provides @Singleton internal fun provideSourceCache(sourceCache: SourcesCacheImpl): SourcesCache { return sourceCache } @Provides @Singleton internal fun provideAdvanceWallpaperCache(cache: AdvanceWallpaperCacheImpl): AdvanceWallpaperCache { return cache } @Provides @Singleton internal fun provideWallpaperObservable(observable: WallpaperObservableImpl): WallpaperObservable { return observable } @Provides @Singleton internal fun provideSourceObservable(observable: SourcesObservableImpl): SourcesObservable { return observable } } ================================================ FILE: presentation/src/main/java/com/yalin/style/injection/modules/SourceModule.kt ================================================ package com.yalin.style.injection.modules import dagger.Module /** * @author jinyalin * * * @since 2017/4/18. */ @Module class SourceModule ================================================ FILE: presentation/src/main/java/com/yalin/style/injection/modules/WallpaperModule.kt ================================================ package com.yalin.style.injection.modules import dagger.Module /** * @author jinyalin * * * @since 2017/4/18. */ @Module class WallpaperModule ================================================ FILE: presentation/src/main/java/com/yalin/style/mapper/AdvanceWallpaperItemMapper.kt ================================================ package com.yalin.style.mapper import com.fernandocejas.arrow.checks.Preconditions import com.yalin.style.domain.AdvanceWallpaper import com.yalin.style.model.AdvanceWallpaperItem import java.util.ArrayList import javax.inject.Inject /** * @author jinyalin * @since 2017/7/28. */ class AdvanceWallpaperItemMapper @Inject constructor() { fun transform(wallpaper: AdvanceWallpaper): AdvanceWallpaperItem { Preconditions.checkNotNull(wallpaper, "Wallpaper can not be null.") val wallpaperItem = AdvanceWallpaperItem() wallpaperItem.id = wallpaper.id wallpaperItem.wallpaperId = wallpaper.wallpaperId wallpaperItem.link = wallpaper.link wallpaperItem.name = wallpaper.name wallpaperItem.author = wallpaper.author wallpaperItem.iconUrl = wallpaper.iconUrl wallpaperItem.downloadUrl = wallpaper.downloadUrl wallpaperItem.providerName = wallpaper.providerName wallpaperItem.storePath = wallpaper.storePath wallpaperItem.isSelected = wallpaper.isSelected wallpaperItem.lazyDownload = wallpaper.lazyDownload wallpaperItem.needAd = wallpaper.needAd return wallpaperItem } fun transformList(wallpaperEntities: List): List { Preconditions.checkNotNull(wallpaperEntities, "SourceEntity can not be null.") val sources = ArrayList() for (entity in wallpaperEntities) { sources.add(transform(entity)) } return sources } } ================================================ FILE: presentation/src/main/java/com/yalin/style/mapper/WallpaperItemMapper.kt ================================================ package com.yalin.style.mapper import com.fernandocejas.arrow.checks.Preconditions import com.yalin.style.domain.GalleryWallpaper import com.yalin.style.domain.Source import com.yalin.style.domain.Wallpaper import com.yalin.style.model.GalleryWallpaperItem import com.yalin.style.model.SourceItem import com.yalin.style.model.WallpaperItem import javax.inject.Inject import kotlin.collections.ArrayList /** * @author jinyalin * * * @since 2017/4/18. */ class WallpaperItemMapper @Inject constructor() { fun transform(wallpaper: Wallpaper): WallpaperItem { Preconditions.checkNotNull(wallpaper, "Wallpaper can not be null.") val wallpaperItem = WallpaperItem() wallpaperItem.title = wallpaper.title wallpaperItem.attribution = wallpaper.attribution wallpaperItem.byline = wallpaper.byline wallpaperItem.imageUri = wallpaper.imageUri wallpaperItem.wallpaperId = wallpaper.wallpaperId wallpaperItem.liked = wallpaper.liked wallpaperItem.isDefault = wallpaper.isDefault wallpaperItem.canLike = wallpaper.canLike return wallpaperItem } fun transformSources(sourceEntities: List): List { Preconditions.checkNotNull(sourceEntities, "SourceEntity can not be null.") val sources = ArrayList() for (entity in sourceEntities) { sources.add(transformSource(entity)) } return sources } fun transformSource(source: Source): SourceItem { Preconditions.checkNotNull(source, "SourceEntity can not be null.") val sourceItem = SourceItem() sourceItem.id = source.id sourceItem.title = source.title sourceItem.description = source.description sourceItem.iconId = source.iconId sourceItem.selected = source.selected sourceItem.hasSetting = source.hasSetting sourceItem.color = source.color return sourceItem } fun transformGalleryWallpaper( galleryWallpapers: List): List { Preconditions.checkNotNull(galleryWallpapers, "GalleryWallpaperEntity can not be null.") val items = ArrayList() for (wallpaper in galleryWallpapers) { items.add(transformGalleryWallpaper(wallpaper)) } return items } fun transformGalleryWallpaper( galleryWallpaper: GalleryWallpaper): GalleryWallpaperItem { Preconditions.checkNotNull(galleryWallpaper, "GalleryWallpaperEntity can not be null.") val galleryWallpaperItem = GalleryWallpaperItem() galleryWallpaperItem.id = galleryWallpaper.id galleryWallpaperItem.isTreeUri = galleryWallpaper.isTreeUri galleryWallpaperItem.uri = galleryWallpaper.uri galleryWallpaperItem.dateTime = galleryWallpaper.dateTime galleryWallpaperItem.location = galleryWallpaper.location galleryWallpaperItem.hasMetadata = galleryWallpaper.hasMetadata return galleryWallpaperItem } } ================================================ FILE: presentation/src/main/java/com/yalin/style/model/AdvanceWallpaperItem.java ================================================ package com.yalin.style.model; import android.os.Parcel; import android.os.Parcelable; /** * @author jinyalin * @since 2017/7/28. */ public class AdvanceWallpaperItem implements Parcelable { public long id; public String wallpaperId; public String link; public String name; public String author; public String iconUrl; public String downloadUrl; public boolean lazyDownload; public boolean needAd; public String providerName; public String storePath; public boolean isSelected; public AdvanceWallpaperItem() { } protected AdvanceWallpaperItem(Parcel in) { id = in.readLong(); wallpaperId = in.readString(); link = in.readString(); name = in.readString(); author = in.readString(); iconUrl = in.readString(); downloadUrl = in.readString(); lazyDownload = in.readByte() != 0; needAd = in.readByte() != 0; providerName = in.readString(); storePath = in.readString(); isSelected = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public AdvanceWallpaperItem createFromParcel(Parcel in) { return new AdvanceWallpaperItem(in); } @Override public AdvanceWallpaperItem[] newArray(int size) { return new AdvanceWallpaperItem[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(id); dest.writeString(wallpaperId); dest.writeString(link); dest.writeString(name); dest.writeString(author); dest.writeString(iconUrl); dest.writeString(downloadUrl); dest.writeByte((byte) (lazyDownload ? 1 : 0)); dest.writeByte((byte) (needAd ? 1 : 0)); dest.writeString(providerName); dest.writeString(storePath); dest.writeByte((byte) (isSelected ? 1 : 0)); } } ================================================ FILE: presentation/src/main/java/com/yalin/style/model/GalleryWallpaperItem.java ================================================ package com.yalin.style.model; import android.os.Parcel; import android.os.Parcelable; /** * @author jinyalin * @since 2017/5/25. */ public class GalleryWallpaperItem implements Parcelable { public long id; public String uri; public boolean isTreeUri; public long dateTime; public String location; public boolean hasMetadata; public GalleryWallpaperItem() { } protected GalleryWallpaperItem(Parcel in) { id = in.readLong(); uri = in.readString(); isTreeUri = in.readByte() != 0; dateTime = in.readLong(); location = in.readString(); hasMetadata = in.readByte() != 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(id); dest.writeString(uri); dest.writeByte((byte) (isTreeUri ? 1 : 0)); dest.writeLong(dateTime); dest.writeString(location); dest.writeByte((byte) (hasMetadata ? 1 : 0)); } @Override public int describeContents() { return 0; } public static final Creator CREATOR = new Creator() { @Override public GalleryWallpaperItem createFromParcel(Parcel in) { return new GalleryWallpaperItem(in); } @Override public GalleryWallpaperItem[] newArray(int size) { return new GalleryWallpaperItem[size]; } }; @Override public boolean equals(Object obj) { if (obj instanceof GalleryWallpaperItem) { GalleryWallpaperItem objItem = (GalleryWallpaperItem) obj; return objItem.id == id; } return false; } @Override public int hashCode() { int result = (int) id; result = 31 * result + uri.hashCode(); return result; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/model/SourceItem.kt ================================================ package com.yalin.style.model import com.yalin.style.domain.repository.SourcesRepository /** * @author jinyalin * @since 2017/5/22. */ class SourceItem { var id: Int = 0 var title: String? = null var description: String? = null var iconId: Int = 0 var selected: Boolean = false var hasSetting: Boolean = false var color: Int = 0 val needPermission: Boolean get() = id == SourcesRepository.SOURCE_ID_CUSTOM } ================================================ FILE: presentation/src/main/java/com/yalin/style/model/WallpaperItem.java ================================================ package com.yalin.style.model; import android.os.Parcel; import android.os.Parcelable; /** * YaLin 2017/1/3. */ public class WallpaperItem implements Parcelable { public String wallpaperId; public String imageUri; public String title; public String byline; public String attribution; public boolean canLike; public boolean liked; public boolean isDefault; public WallpaperItem() { } protected WallpaperItem(Parcel in) { wallpaperId = in.readString(); imageUri = in.readString(); title = in.readString(); byline = in.readString(); attribution = in.readString(); liked = in.readByte() != 0; isDefault = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public WallpaperItem createFromParcel(Parcel in) { return new WallpaperItem(in); } @Override public WallpaperItem[] newArray(int size) { return new WallpaperItem[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(wallpaperId); dest.writeString(imageUri); dest.writeString(title); dest.writeString(byline); dest.writeString(attribution); dest.writeByte((byte) (liked ? 1 : 0)); dest.writeByte((byte) (isDefault ? 1 : 0)); } } ================================================ FILE: presentation/src/main/java/com/yalin/style/presenter/AdvanceSettingPresenter.kt ================================================ package com.yalin.style.presenter import android.os.Bundle import android.text.TextUtils import com.yalin.style.data.log.LogUtil import com.yalin.style.data.utils.WallpaperFileHelper import com.yalin.style.domain.AdvanceWallpaper import com.yalin.style.domain.interactor.* import com.yalin.style.event.SwitchWallpaperServiceEvent import com.yalin.style.event.WallpaperActivateEvent import com.yalin.style.exception.ErrorMessageFactory import com.yalin.style.mapper.AdvanceWallpaperItemMapper import com.yalin.style.model.AdvanceWallpaperItem import com.yalin.style.view.AdvanceSettingView import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import javax.inject.Inject /** * @author jinyalin * @since 2017/7/28. */ class AdvanceSettingPresenter @Inject constructor(val getAdvanceWallpapers: GetAdvanceWallpapers, val loadAdvanceWallpaper: LoadAdvanceWallpaper, val selectAdvanceWallpaper: SelectAdvanceWallpaper, val getSelectedAdvanceWallpaper: GetSelectedAdvanceWallpaper, val advanceWallpaperItemMapper: AdvanceWallpaperItemMapper, val downloadAdvanceWallpaper: DownloadAdvanceWallpaper, val readAdvanceAd: ReadAdvanceAd) : Presenter { companion object { val DOWNLOAD_STATE = "download_state" val DOWNLOADING_ITEM = "download_item" val DOWNLOAD_NONE = 0 val DOWNLOADING = 1 val DOWNLOAD_ERROR = 2 } private val wallpaperObserver = WallpapersObserver() private var view: AdvanceSettingView? = null private var mLastSelectedItemId: String? = null private var selecting = false private var downloadingWallpaper: AdvanceWallpaperItem? = null private var downloadState = DOWNLOAD_NONE fun setView(view: AdvanceSettingView) { this.view = view } fun initialize() { view?.showLoading() getAdvanceWallpapers.execute(wallpaperObserver, null) mLastSelectedItemId = getSelectedAdvanceWallpaper.selected.wallpaperId EventBus.getDefault().register(this) } fun loadAdvanceWallpaper() { view?.showLoading() loadAdvanceWallpaper.execute(object : DefaultObserver>() { override fun onNext(needDownload: List) { view?.renderWallpapers(advanceWallpaperItemMapper.transformList(needDownload)) } override fun onComplete() { } override fun onError(exception: Throwable) { view?.showError( ErrorMessageFactory.create(view!!.context(), exception as Exception)) view?.showRetry() } }, null) } fun selectAdvanceWallpaper(item: AdvanceWallpaperItem) { if (WallpaperFileHelper.isNeedDownloadAdvanceComponent(item.lazyDownload, item.storePath) || (downloadingWallpaper != null && TextUtils.equals(downloadingWallpaper!!.wallpaperId, item.wallpaperId))) { view?.showDownloadHintDialog(item) } else if (item.needAd) { view?.showAd(item) } else { selectAdvanceWallpaper(item.wallpaperId, false) } } fun requestDownload(item: AdvanceWallpaperItem) { view?.showDownloadingDialog(item) downloadingWallpaper = item downloadState = DOWNLOADING downloadAdvanceWallpaper.execute(object : DefaultObserver() { override fun onNext(progress: Long) { view?.updateDownloadingProgress(progress) } override fun onComplete() { view?.downloadComplete(item) downloadingWallpaper = null downloadState = DOWNLOAD_NONE } override fun onError(exception: Throwable) { view?.showDownloadError(item, exception as Exception) downloadingWallpaper = null downloadState = DOWNLOAD_ERROR } }, DownloadAdvanceWallpaper.Params.download(item.wallpaperId)) } fun adViewed(item: AdvanceWallpaperItem) { readAdvanceAd.execute(object : DefaultObserver() { override fun onNext(success: Boolean) { if (success) { view?.adViewed(item) } } }, ReadAdvanceAd.Params.read(item.wallpaperId)) } fun onSaveInstanceState(outState: Bundle) { outState.putInt(DOWNLOAD_STATE, downloadState) if (downloadingWallpaper != null) { outState.putParcelable(DOWNLOADING_ITEM, downloadingWallpaper!!) } } fun onRestoreInstanceState(savedInstanceState: Bundle) { downloadState = savedInstanceState.getInt(DOWNLOAD_STATE) downloadingWallpaper = savedInstanceState.getParcelable(DOWNLOADING_ITEM) if (downloadingWallpaper != null) { if (downloadState == DOWNLOADING) { requestDownload(downloadingWallpaper!!) } else if (downloadState == DOWNLOAD_ERROR) { } } } fun getDownloadingItem(): AdvanceWallpaperItem? { return downloadingWallpaper } private fun selectAdvanceWallpaper(wallpaperId: String, rollback: Boolean) { if (!rollback && TextUtils.equals(wallpaperId, mLastSelectedItemId)) { view?.complete() return } selecting = false var tempSelect = false if (!rollback) { selecting = true tempSelect = true } selectAdvanceWallpaper.execute(object : DefaultObserver() { override fun onNext(success: Boolean) { if (!rollback && success) { EventBus.getDefault().post(SwitchWallpaperServiceEvent()) } } override fun onComplete() { view?.wallpaperSelected(wallpaperId) } }, SelectAdvanceWallpaper.Params.selectWallpaper(wallpaperId, tempSelect)) } override fun resume() { maybeResetWallpaper() } private fun maybeResetWallpaper() { if (selecting && !TextUtils.isEmpty(mLastSelectedItemId)) { LogUtil.D(SettingsChooseSourcePresenter.TAG, "restore wallpaper to $mLastSelectedItemId") selectAdvanceWallpaper(mLastSelectedItemId!!, true) } } override fun pause() { } override fun destroy() { EventBus.getDefault().unregister(this) getAdvanceWallpapers.dispose() loadAdvanceWallpaper.dispose() selectAdvanceWallpaper.dispose() downloadAdvanceWallpaper.dispose() downloadingWallpaper = null view = null } private inner class WallpapersObserver : DefaultObserver>() { override fun onNext(needDownload: List) { if (needDownload.isEmpty()) { view?.showEmpty() } else { view?.renderWallpapers(advanceWallpaperItemMapper.transformList(needDownload)) } } override fun onComplete() { } override fun onError(exception: Throwable?) { } } @Subscribe fun onEventMainThread(e: WallpaperActivateEvent) { // we cannot known if the user cancel the wallpaper set // so we need listen wallpaper be reactivated if (!e.isWallpaperActivate) { selecting = false } else { mLastSelectedItemId = getSelectedAdvanceWallpaper.selected.wallpaperId } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/presenter/GallerySettingPresenter.kt ================================================ package com.yalin.style.presenter import android.net.Uri import com.yalin.style.domain.GalleryWallpaper import com.yalin.style.domain.interactor.* import com.yalin.style.mapper.WallpaperItemMapper import com.yalin.style.model.GalleryWallpaperItem import com.yalin.style.view.GallerySettingView import javax.inject.Inject /** * @author jinyalin * @since 2017/5/25. */ class GallerySettingPresenter @Inject constructor(val wallpaperItemMapper: WallpaperItemMapper, val addGalleryWallpaperUseCase: AddGalleryWallpaper, val removeGalleryWallpaperUseCase: RemoveGalleryWallpaper, val getGalleryWallpaperUseCase: GetGalleryWallpaper, val forceNowUseCase: ForceNow, val setIntervalUseCase: SetGalleryUpdateInterval, val getIntervalUseCase: GetGalleryUpdateInterval) : Presenter { var gallerySettingView: GallerySettingView? = null val mWallpapers = ArrayList() fun setView(gallerySettingView: GallerySettingView) { this.gallerySettingView = gallerySettingView } fun initialize() { refreshGalleryWallpaper() getUpdateInterval() } fun addGalleryWallpaper(uris: Set) { val galleryWallpapers = ArrayList() for (uri in uris) { val wallpaper = GalleryWallpaper() wallpaper.uri = uri.toString() galleryWallpapers.add(wallpaper) } addGalleryWallpaperUseCase.execute( GalleryWallpaperChangedObserver(), AddGalleryWallpaper.Params.addGalleryWallpaperUris(galleryWallpapers)) } fun removeGalleryWallpaper(items: List) { val galleryWallpapers = ArrayList() for (item in items) { val wallpaper = GalleryWallpaper() wallpaper.uri = item.uri galleryWallpapers.add(wallpaper) } removeGalleryWallpaperUseCase.execute( GalleryWallpaperChangedObserver(), RemoveGalleryWallpaper.Params.removeGalleryWallpaperUris(galleryWallpapers)) } fun forceNow(wallpaperUri: String) { forceNowUseCase.execute(DefaultObserver(), ForceNow.Params.fromUri(wallpaperUri)) } fun setUpdateInterval(intervalMin: Int) { setIntervalUseCase.execute(DefaultObserver(), SetGalleryUpdateInterval.Params.interval(intervalMin)) } override fun resume() { } override fun pause() { } override fun destroy() { addGalleryWallpaperUseCase.dispose() getGalleryWallpaperUseCase.dispose() removeGalleryWallpaperUseCase.dispose() getGalleryWallpaperUseCase.dispose() forceNowUseCase.dispose() setIntervalUseCase.dispose() getIntervalUseCase.dispose() gallerySettingView = null } private fun refreshGalleryWallpaper() { getGalleryWallpaperUseCase.execute(object : DefaultObserver>() { override fun onNext(sources: List) { val itemSet = wallpaperItemMapper.transformGalleryWallpaper(sources) mWallpapers.clear() mWallpapers.addAll(itemSet) gallerySettingView?.renderGalleryWallpapers(mWallpapers) } }, null) } private fun getUpdateInterval() { getIntervalUseCase.execute(object : DefaultObserver() { override fun onNext(intervalMin: Int) { gallerySettingView?.renderUpdateInterval(intervalMin) } }, null) } private inner class GalleryWallpaperChangedObserver : DefaultObserver() { override fun onNext(success: Boolean) { super.onNext(success) if (success) { refreshGalleryWallpaper() } } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/presenter/Presenter.kt ================================================ package com.yalin.style.presenter /** * Interface representing a Presenter in a model view presenter (MVP) pattern. * @author jinyalin * * * @since 2017/4/20. */ interface Presenter { /** * Method that control the lifecycle of the view. It should be called in the view's * (Activity or Fragment) onResume() method. */ fun resume() /** * Method that control the lifecycle of the view. It should be called in the view's * (Activity or Fragment) onPause() method. */ fun pause() /** * Method that control the lifecycle of the view. It should be called in the view's * (Activity or Fragment) onDestroy() method. */ fun destroy() } ================================================ FILE: presentation/src/main/java/com/yalin/style/presenter/SettingsChooseSourcePresenter.kt ================================================ package com.yalin.style.presenter import android.os.Bundle import com.yalin.style.data.log.LogUtil import com.yalin.style.domain.Source import com.yalin.style.domain.interactor.DefaultObserver import com.yalin.style.domain.interactor.GetSources import com.yalin.style.domain.interactor.SelectSource import com.yalin.style.domain.repository.SourcesRepository import com.yalin.style.event.SwitchWallpaperServiceEvent import com.yalin.style.event.WallpaperActivateEvent import com.yalin.style.mapper.WallpaperItemMapper import com.yalin.style.model.SourceItem import com.yalin.style.view.SourceChooseView import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import java.util.ArrayList import javax.inject.Inject /** * @author jinyalin * @since 2017/5/23. */ class SettingsChooseSourcePresenter @Inject constructor(val getSourcesUseCase: GetSources, val selectSourceUseCae: SelectSource, val wallpaperMapper: WallpaperItemMapper) : Presenter { companion object { val TAG = "SettingsChooseSource" var LAST_SELECTED_ID = "last_selected_id" var SELECTING = "selecting" } private val mSources = ArrayList() private var mSourceChooseView: SourceChooseView? = null private var mSelectedSource: SourceItem? = null private var mLastSelectedItemId = -1 private var selecting = false fun setView(sourceChooseView: SourceChooseView) { mSourceChooseView = sourceChooseView } fun initialize() { updateSources() EventBus.getDefault().register(this) } fun selectSource(sourceId: Int) { selectSource(sourceId, false) } private fun selectSource(sourceId: Int, force: Boolean) { selecting = false mLastSelectedItemId = mSelectedSource!!.id var tempSelect = false if (!force && needSwitchWallpaper(sourceId)) { selecting = true tempSelect = true LogUtil.D(TAG, "Select source Last Selected $mLastSelectedItemId") } selectSourceUseCae.executeSerial(object : DefaultObserver() { override fun onNext(success: Boolean) { LogUtil.D(TAG, "onNext " + success) if (success) { for (source in mSources) { source.selected = source.id == sourceId if (source.selected) { mSelectedSource = source } } LogUtil.D(TAG, "selected wallpaper id ${mSelectedSource!!.id}") mSourceChooseView?.sourceSelected(mSources, mSelectedSource!!) if (!force && needSwitchWallpaper(sourceId)) { EventBus.getDefault().post(SwitchWallpaperServiceEvent()) } } } }, SelectSource.Params.selectSource(sourceId, tempSelect)) } private fun needSwitchWallpaper(sourceId: Int): Boolean { return mLastSelectedItemId == SourcesRepository.SOURCE_ID_ADVANCE || sourceId == SourcesRepository.SOURCE_ID_ADVANCE } override fun resume() { maybeResetSource() } private fun maybeResetSource() { if (selecting) { LogUtil.D(TAG, "restore wallpaper to $mLastSelectedItemId") selectSource(mLastSelectedItemId, true) } } override fun pause() { } override fun destroy() { EventBus.getDefault().unregister(this) getSourcesUseCase.dispose() selectSourceUseCae.dispose() mSourceChooseView = null } fun restoreInstanceState(instanceState: Bundle?) { if (instanceState != null && instanceState.containsKey(LAST_SELECTED_ID)) { mLastSelectedItemId = instanceState.getInt(LAST_SELECTED_ID) selecting = instanceState.getBoolean(SELECTING) } } fun saveInstanceState(outState: Bundle) { outState.putInt(LAST_SELECTED_ID, mLastSelectedItemId) outState.putBoolean(SELECTING, selecting) } fun updateSources() { getSourcesUseCase.executeSerial(object : DefaultObserver>() { override fun onNext(sources: List) { super.onNext(sources) mSources.clear() mSources.addAll(wallpaperMapper.transformSources(sources)) for (source in mSources) { if (source.selected) { mSelectedSource = source break } } mSourceChooseView?.renderSources(mSources) mSourceChooseView?.sourceSelected(mSources, mSelectedSource!!) } }, null) } @Subscribe fun onEventMainThread(e: WallpaperActivateEvent) { // we cannot known if the user cancel the wallpaper set // so we need listen wallpaper be reactivated if (!e.isWallpaperActivate) { selecting = false } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/presenter/WallpaperDetailPresenter.kt ================================================ package com.yalin.style.presenter import android.os.Bundle import com.yalin.style.analytics.Analytics import com.yalin.style.analytics.Event import com.yalin.style.data.exception.ReswitchException import com.yalin.style.domain.Wallpaper import com.yalin.style.domain.interactor.* import com.yalin.style.event.WallpaperSwitchEvent import com.yalin.style.exception.ErrorMessageFactory import com.yalin.style.injection.PerActivity import com.yalin.style.mapper.WallpaperItemMapper import com.yalin.style.model.WallpaperItem import com.yalin.style.util.ShareUtil import com.yalin.style.view.WallpaperDetailView import org.greenrobot.eventbus.EventBus import javax.inject.Inject /** * @author jinyalin * * * @since 2017/4/20. */ @PerActivity class WallpaperDetailPresenter @Inject constructor(private val getWallpaperUseCase: GetWallpaper, private val observerWallpaper: ObserverWallpaper, private val getWallpaperCountUseCase: GetWallpaperCount, private val switchWallpaperUseCase: SwitchWallpaper, private val likeWallpaperUseCase: LikeWallpaper, private val wallpaperItemMapper: WallpaperItemMapper) : Presenter { companion object { private val CURRENT_ITEM = "current_item" private val WALLPAPER_COUNT = "wallpaper_count" } private var currentShowItem: WallpaperItem? = null private var wallpaperCount: Int = 0 private var wallpaperDetailView: WallpaperDetailView? = null private val wallpaperRefreshObserver: WallpaperRefreshObserver init { wallpaperRefreshObserver = WallpaperRefreshObserver() observerWallpaper.registerObserver(wallpaperRefreshObserver) } fun setView(wallpaperDetailView: WallpaperDetailView) { this.wallpaperDetailView = wallpaperDetailView } fun initialize() { getWallpaperUseCase.execute(WallpaperObserver(), null) getWallpaperCountUseCase.execute(WallpaperCountObserver(), null) } fun getNextWallpaper() { switchWallpaperUseCase.execute(WallpaperObserver(true), null) } fun likeWallpaper() { currentShowItem?.apply { likeWallpaperUseCase.execute(WallpaperLikeObserver(), LikeWallpaper.Params.likeWallpaper(wallpaperId)) Analytics.logEvent(wallpaperDetailView!!.context(), if (!liked) Event.LIKE else Event.UN_LIKE, title) } } fun shareWallpaper() { currentShowItem?.apply { wallpaperDetailView!!.shareWallpaper( ShareUtil.createShareIntent(wallpaperDetailView!!.context(), this)) } } fun restoreInstanceState(instanceState: Bundle?) { if (instanceState != null && instanceState.containsKey(CURRENT_ITEM)) { currentShowItem = instanceState.getParcelable(CURRENT_ITEM) wallpaperCount = instanceState.getInt(WALLPAPER_COUNT) showWallpaperDetailInView(currentShowItem!!) showOrHideNextView(wallpaperCount) } else { initialize() } } fun saveInstanceState(outState: Bundle) { if (currentShowItem != null) { outState.putParcelable(CURRENT_ITEM, currentShowItem) outState.putInt(WALLPAPER_COUNT, wallpaperCount) } } override fun resume() { } override fun pause() { } override fun destroy() { getWallpaperUseCase.dispose() getWallpaperCountUseCase.dispose() observerWallpaper.unregisterObserver(wallpaperRefreshObserver) wallpaperDetailView = null } private fun showWallpaperDetailInView(wallpaperItem: WallpaperItem) { with(wallpaperDetailView!!) { renderWallpaper(wallpaperItem) validLikeAction(wallpaperItem.canLike) if (wallpaperItem.canLike) { updateLikeState(wallpaperItem, wallpaperItem.liked) } } } private fun showOrHideNextView(count: Int) { wallpaperDetailView?.showNextButton(count > 1) } private inner class WallpaperObserver @JvmOverloads constructor(private val isSwitch: Boolean = false) : DefaultObserver() { override fun onNext(wallpaper: Wallpaper) { val wallpaperItem = wallpaperItemMapper.transform(wallpaper) currentShowItem = wallpaperItem showWallpaperDetailInView(wallpaperItem) } override fun onComplete() { if (isSwitch) { EventBus.getDefault().post(WallpaperSwitchEvent()) } } override fun onError(exception: Throwable) { if (exception is ReswitchException) { return } wallpaperDetailView?. showError(ErrorMessageFactory.create(wallpaperDetailView!!.context(), exception as Exception)) } } private inner class WallpaperCountObserver : DefaultObserver() { override fun onNext(count: Int) { wallpaperCount = count showOrHideNextView(count) } override fun onComplete() {} override fun onError(exception: Throwable) { showOrHideNextView(0) } } private inner class WallpaperLikeObserver : DefaultObserver() { override fun onNext(liked: Boolean) { wallpaperDetailView!!.updateLikeState(currentShowItem!!, liked) } } private inner class WallpaperRefreshObserver : DefaultObserver() { override fun onComplete() { initialize() } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/render/BitmapRegionLoader.java ================================================ /* * Copyright 2014 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yalin.style.render; import static android.graphics.BitmapFactory.Options; import android.graphics.Bitmap; import android.graphics.BitmapRegionDecoder; import android.graphics.Matrix; import android.graphics.Rect; import java.io.IOException; import java.io.InputStream; /** * Wrapper for {@link BitmapRegionDecoder} with some extra functionality. */ public class BitmapRegionLoader { private boolean mValid = false; private int mRotation = 0; private int mOriginalWidth; private int mOriginalHeight; private Rect mTempRect = new Rect(); private InputStream mInputStream; private volatile BitmapRegionDecoder mBitmapRegionDecoder; private Matrix mRotateMatrix; public static BitmapRegionLoader newInstance(InputStream in) throws IOException { return newInstance(in, 0); } public static BitmapRegionLoader newInstance(InputStream in, int rotation) throws IOException { if (in == null) { return null; } BitmapRegionLoader loader = new BitmapRegionLoader(in); if (loader.mValid) { loader.mRotation = rotation; if (loader.mRotation != 0) { loader.mRotateMatrix = new Matrix(); loader.mRotateMatrix.postRotate(rotation); } return loader; } return null; } private BitmapRegionLoader(InputStream in) throws IOException { mInputStream = in; mBitmapRegionDecoder = BitmapRegionDecoder.newInstance(in, false); if (mBitmapRegionDecoder != null) { mOriginalWidth = mBitmapRegionDecoder.getWidth(); mOriginalHeight = mBitmapRegionDecoder.getHeight(); if (mOriginalWidth > 0 && mOriginalHeight > 0) { mValid = true; } } } /** * Key difference, aside from support for rotation, from * {@link BitmapRegionDecoder#decodeRegion(Rect, Options)} in this implementation is that even * if inBitmap is given, a sub-bitmap might be returned. */ public synchronized Bitmap decodeRegion(Rect rect, Options options) { int unsampledInBitmapWidth = -1; int unsampledInBitmapHeight = -1; int sampleSize = Math.max(1, options != null ? options.inSampleSize : 1); if (options != null && options.inBitmap != null) { unsampledInBitmapWidth = options.inBitmap.getWidth() * sampleSize; unsampledInBitmapHeight = options.inBitmap.getHeight() * sampleSize; } // Decode with rotation switch (mRotation) { case 90: mTempRect.set( rect.top, mOriginalHeight - rect.right, rect.bottom, mOriginalHeight - rect.left); break; case 180: mTempRect.set( mOriginalWidth - rect.right, mOriginalHeight - rect.bottom, mOriginalWidth - rect.left, mOriginalHeight - rect.top); break; case 270: mTempRect.set( mOriginalWidth - rect.bottom, rect.left, mOriginalWidth - rect.top, rect.right); break; default: mTempRect.set(rect); } Bitmap bitmap = mBitmapRegionDecoder.decodeRegion(mTempRect, options); if (bitmap == null) { return null; } if (options != null && options.inBitmap != null && ((mTempRect.width() != unsampledInBitmapWidth || mTempRect.height() != unsampledInBitmapHeight))) { // Need to extract the sub-bitmap Bitmap subBitmap = Bitmap.createBitmap( bitmap, 0, 0, mTempRect.width() / sampleSize, mTempRect.height() / sampleSize); if (bitmap != options.inBitmap && bitmap != subBitmap) { bitmap.recycle(); } bitmap = subBitmap; } if (mRotateMatrix != null) { // Rotate decoded bitmap Bitmap rotatedBitmap = Bitmap.createBitmap( bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), mRotateMatrix, true); if ((options == null || bitmap != options.inBitmap) && bitmap != rotatedBitmap) { bitmap.recycle(); } bitmap = rotatedBitmap; } return bitmap; } public synchronized int getWidth() { return (mRotation == 90 || mRotation == 270) ? mOriginalHeight : mOriginalWidth; } public synchronized int getHeight() { return (mRotation == 90 || mRotation == 270) ? mOriginalWidth : mOriginalHeight; } public synchronized void destroy() { mBitmapRegionDecoder.recycle(); mBitmapRegionDecoder = null; try { mInputStream.close(); } catch (IOException ignored) { } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/render/DemoRenderController.kt ================================================ package com.yalin.style.render import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.content.Context import android.os.Handler import com.yalin.style.domain.interactor.GetWallpaper import com.yalin.style.domain.interactor.ObserverWallpaper import com.yalin.style.domain.interactor.OpenWallpaperInputStream import com.yalin.style.mapper.WallpaperItemMapper import javax.inject.Inject /** * @author jinyalin * * * @since 2017/4/19. */ class DemoRenderController @Inject constructor(context: Context, getWallpaperUseCase: GetWallpaper, observerWallpaper: ObserverWallpaper, openWallpaperInputStream: OpenWallpaperInputStream, wallpaperItemMapper: WallpaperItemMapper) : RenderController(context, getWallpaperUseCase, observerWallpaper, openWallpaperInputStream, wallpaperItemMapper) { companion object { private val ANIMATION_CYCLE_TIME_MILLIS: Long = 35000 private val FOCUS_DELAY_TIME_MILLIS: Long = 2000 private val FOCUS_TIME_MILLIS: Long = 6000 private val mHandler = Handler() } private var mCurrentScrollAnimator: Animator? = null private var mReverseDirection = false private var mAllowFocus = false fun start(allowFocus: Boolean) { mAllowFocus = allowFocus runAnimation() } private fun runAnimation() { if (mCurrentScrollAnimator != null) { mCurrentScrollAnimator!!.cancel() } mCurrentScrollAnimator = ObjectAnimator .ofFloat(mRenderer, "normalOffsetX", if (mReverseDirection) 1f else 0f, if (mReverseDirection) 0f else 1f) .setDuration(ANIMATION_CYCLE_TIME_MILLIS) mCurrentScrollAnimator!!.start() mCurrentScrollAnimator!!.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) mReverseDirection = !mReverseDirection runAnimation() } }) if (mAllowFocus) { mHandler.postDelayed({ mRenderer!!.setIsBlurred(false, false) mHandler.postDelayed({ mRenderer!!.setIsBlurred(true, false) }, FOCUS_TIME_MILLIS) }, FOCUS_DELAY_TIME_MILLIS) } } override fun destroy() { super.destroy() if (mCurrentScrollAnimator != null) { mCurrentScrollAnimator!!.cancel() mCurrentScrollAnimator!!.removeAllListeners() } mHandler.removeCallbacksAndMessages(null) } } ================================================ FILE: presentation/src/main/java/com/yalin/style/render/GLColorOverlay.java ================================================ /* * Copyright 2014 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yalin.style.render; import android.graphics.Color; import android.opengl.GLES20; import java.nio.FloatBuffer; class GLColorOverlay { private static final String VERTEX_SHADER_CODE = "" + // This matrix member variable provides a hook to manipulate // the coordinates of the objects that use this vertex shader "uniform mat4 uMVPMatrix;" + "attribute vec4 aPosition;" + "void main(){" + " gl_Position = uMVPMatrix * aPosition;" + "}"; private static final String FRAGMENT_SHADER_CODE = "" + "precision mediump float;" + "uniform sampler2D uTexture;" + "uniform vec4 uColor;" + "void main(){" + " gl_FragColor = uColor;" + "}"; // number of coordinates per vertex in this array private static final int COORDS_PER_VERTEX = 3; private static final int VERTEX_STRIDE_BYTES = COORDS_PER_VERTEX * GLUtil.BYTES_PER_FLOAT; private float mVertices[] = { -1, 1, 0, // top left -1, -1, 0, // bottom left 1, -1, 0, // bottom right -1, 1, 0, // top left 1, -1, 0, // bottom right 1, 1, 0, // top right }; private int mColor = 0; private FloatBuffer mVertexBuffer; private static int sProgramHandle; private static int sAttribPositionHandle; private static int sUniformColorHandle; private static int sUniformMVPMatrixHandle; public GLColorOverlay() { mVertexBuffer = GLUtil.asFloatBuffer(mVertices); } public static void initGl() { // Initialize shaders and create/link program int vertexShaderHandle = GLUtil.loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_CODE); int fragShaderHandle = GLUtil.loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_CODE); sProgramHandle = GLUtil.createAndLinkProgram(vertexShaderHandle, fragShaderHandle, null); sAttribPositionHandle = GLES20.glGetAttribLocation(sProgramHandle, "aPosition"); sUniformMVPMatrixHandle = GLES20.glGetUniformLocation(sProgramHandle, "uMVPMatrix"); sUniformColorHandle = GLES20.glGetUniformLocation(sProgramHandle, "uColor"); } public void draw(float[] mvpMatrix) { // Add program to OpenGL ES environment GLES20.glUseProgram(sProgramHandle); // Pass in the vertex information GLES20.glEnableVertexAttribArray(sAttribPositionHandle); GLES20.glVertexAttribPointer(sAttribPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, VERTEX_STRIDE_BYTES, mVertexBuffer); // Apply the projection and view transformation GLES20.glUniformMatrix4fv(sUniformMVPMatrixHandle, 1, false, mvpMatrix, 0); GLUtil.checkGlError("glUniformMatrix4fv"); // Set the alpha float r = Color.red(mColor) * 1f / 255; float g = Color.green(mColor) * 1f / 255; float b = Color.blue(mColor) * 1f / 255; float a = Color.alpha(mColor) * 1f / 255; GLES20.glUniform4f(sUniformColorHandle, r, g, b, a); // Draw the triangle GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, mVertices.length / COORDS_PER_VERTEX); GLES20.glDisableVertexAttribArray(sAttribPositionHandle); } public void setColor(int color) { mColor = color; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/render/GLPicture.java ================================================ /* * Copyright 2014 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yalin.style.render; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Rect; import android.opengl.GLES20; import com.yalin.style.util.MathUtil; import java.nio.FloatBuffer; class GLPicture { private static final String VERTEX_SHADER_CODE = "" + // This matrix member variable provides a hook to manipulate // the coordinates of the objects that use this vertex shader "uniform mat4 uMVPMatrix;" + "attribute vec4 aPosition;" + "attribute vec2 aTexCoords;" + "varying vec2 vTexCoords;" + "void main(){" + " vTexCoords = aTexCoords;" + " gl_Position = uMVPMatrix * aPosition;" + "}"; private static final String FRAGMENT_SHADER_CODE = "" + "precision mediump float;" + "uniform sampler2D uTexture;" + "uniform float uAlpha;" + "varying vec2 vTexCoords;" + "void main(){" + " gl_FragColor = texture2D(uTexture, vTexCoords);" + " gl_FragColor.a = uAlpha;" + "}"; // number of coordinates per vertex in this array private static final int COORDS_PER_VERTEX = 3; private static final int VERTEX_STRIDE_BYTES = COORDS_PER_VERTEX * GLUtil.BYTES_PER_FLOAT; private static final int VERTICES = 6; // TL, BL, BR, TL, BR, TR // S, T (or X, Y) private static final int COORDS_PER_TEXTURE_VERTEX = 2; private static final int TEXTURE_VERTEX_STRIDE_BYTES = COORDS_PER_TEXTURE_VERTEX * GLUtil.BYTES_PER_FLOAT; private static final float[] SQUARE_TEXTURE_VERTICES = { 0, 0, // top left 0, 1, // bottom left 1, 1, // bottom right 0, 0, // top left 1, 1, // bottom right 1, 0, // top right }; private boolean mHasContent = false; private float[] mVertices = new float[COORDS_PER_VERTEX * VERTICES]; private FloatBuffer mVertexBuffer; private FloatBuffer mTextureCoordsBuffer; private static int sMaxTextureSize; private static int sProgramHandle; private static int sAttribPositionHandle; private static int sAttribTextureCoordsHandle; private static int sUniformAlphaHandle; private static int sUniformTextureHandle; private static int sUniformMVPMatrixHandle; private int mCols = 1; private int mRows = 1; private int mWidth = 0; private int mHeight = 0; private int mTileSize = sMaxTextureSize; private int[] mTextureHandles; public static void initGl() { // Initialize shaders and create/link program int vertexShaderHandle = GLUtil.loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_CODE); int fragShaderHandle = GLUtil.loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_CODE); sProgramHandle = GLUtil.createAndLinkProgram(vertexShaderHandle, fragShaderHandle, null); sAttribPositionHandle = GLES20.glGetAttribLocation(sProgramHandle, "aPosition"); sAttribTextureCoordsHandle = GLES20.glGetAttribLocation(sProgramHandle, "aTexCoords"); sUniformMVPMatrixHandle = GLES20.glGetUniformLocation(sProgramHandle, "uMVPMatrix"); sUniformTextureHandle = GLES20.glGetUniformLocation(sProgramHandle, "uTexture"); sUniformAlphaHandle = GLES20.glGetUniformLocation(sProgramHandle, "uAlpha"); // Compute max texture size int[] maxTextureSize = new int[1]; GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSize, 0); sMaxTextureSize = maxTextureSize[0]; } public GLPicture(BitmapRegionLoader bitmapRegionLoader, int maxHeight) { if (bitmapRegionLoader == null || maxHeight == 0) { return; } mHasContent = true; mVertexBuffer = GLUtil.newFloatBuffer(mVertices.length); mTextureCoordsBuffer = GLUtil.asFloatBuffer(SQUARE_TEXTURE_VERTICES); int originalWidth = bitmapRegionLoader.getWidth(); int originalHeight = bitmapRegionLoader.getHeight(); int sampleSize = 1; while (originalHeight / (sampleSize << 1) > maxHeight) { sampleSize <<= 1; } mWidth = originalWidth / sampleSize; mHeight = originalHeight / sampleSize; mTileSize = Math.min(512, sMaxTextureSize); int unsampledTileSize = mTileSize * sampleSize; int leftoverHeight = originalHeight % unsampledTileSize; // Load m x n textures mCols = MathUtil.intDivideRoundUp(mWidth, mTileSize); mRows = MathUtil.intDivideRoundUp(mHeight, mTileSize); mTextureHandles = new int[mCols * mRows]; Bitmap tileBitmap = Bitmap.createBitmap(mTileSize, mTileSize, Bitmap.Config.ARGB_8888); Rect rect = new Rect(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = sampleSize; options.inBitmap = tileBitmap; for (int y = 0; y < mRows; y++) { for (int x = 0; x < mCols; x++) { rect.set(x * unsampledTileSize, (mRows - y - 1) * unsampledTileSize, (x + 1) * unsampledTileSize, (mRows - y) * unsampledTileSize); // The bottom tiles must be full tiles for drawing, so only allow edge tiles // at the top if (leftoverHeight > 0) { rect.offset(0, -unsampledTileSize + leftoverHeight); } rect.intersect(0, 0, originalWidth, originalHeight); Bitmap useBitmap = bitmapRegionLoader.decodeRegion(rect, options); if (useBitmap != null) { mTextureHandles[y * mCols + x] = GLUtil.loadTexture(useBitmap); if (useBitmap != tileBitmap) { useBitmap.recycle(); } } } } } public GLPicture(Bitmap bitmap) { if (bitmap == null) { return; } mTileSize = Math.min(512, sMaxTextureSize); mHasContent = true; mVertexBuffer = GLUtil.newFloatBuffer(mVertices.length); mTextureCoordsBuffer = GLUtil.asFloatBuffer(SQUARE_TEXTURE_VERTICES); mWidth = bitmap.getWidth(); mHeight = bitmap.getHeight(); int leftoverHeight = mHeight % mTileSize; // Load m x n textures mCols = MathUtil.intDivideRoundUp(mWidth, mTileSize); mRows = MathUtil.intDivideRoundUp(mHeight, mTileSize); mTextureHandles = new int[mCols * mRows]; if (mCols == 1 && mRows == 1) { mTextureHandles[0] = GLUtil.loadTexture(bitmap); } else { Rect rect = new Rect(); for (int y = 0; y < mRows; y++) { for (int x = 0; x < mCols; x++) { rect.set(x * mTileSize, (mRows - y - 1) * mTileSize, (x + 1) * mTileSize, (mRows - y) * mTileSize); // The bottom tiles must be full tiles for drawing, so only allow edge tiles // at the top if (leftoverHeight > 0) { rect.offset(0, -mTileSize + leftoverHeight); } rect.intersect(0, 0, mWidth, mHeight); Bitmap subBitmap = Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height()); mTextureHandles[y * mCols + x] = GLUtil.loadTexture(subBitmap); subBitmap.recycle(); } } } } public void draw(float[] mvpMatrix, float alpha) { if (!mHasContent) { return; } // Add program to OpenGL ES environment GLES20.glUseProgram(sProgramHandle); // Apply the projection and view transformation GLES20.glUniformMatrix4fv(sUniformMVPMatrixHandle, 1, false, mvpMatrix, 0); GLUtil.checkGlError("glUniformMatrix4fv"); // Set up vertex buffer GLES20.glEnableVertexAttribArray(sAttribPositionHandle); GLES20.glVertexAttribPointer(sAttribPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, VERTEX_STRIDE_BYTES, mVertexBuffer); // Set up texture stuff GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glUniform1i(sUniformTextureHandle, 0); GLES20.glVertexAttribPointer(sAttribTextureCoordsHandle, COORDS_PER_TEXTURE_VERTEX, GLES20.GL_FLOAT, false, TEXTURE_VERTEX_STRIDE_BYTES, mTextureCoordsBuffer); GLES20.glEnableVertexAttribArray(sAttribTextureCoordsHandle); // Set the alpha GLES20.glUniform1f(sUniformAlphaHandle, alpha); // Draw tiles for (int y = 0; y < mRows; y++) { for (int x = 0; x < mCols; x++) { // Pass in the vertex information mVertices[0] = mVertices[3] = mVertices[9] = Math.min(-1 + 2f * x * mTileSize / mWidth, 1); // left mVertices[1] = mVertices[10] = mVertices[16] = Math.min(-1 + 2f * (y + 1) * mTileSize / mHeight, 1); // top mVertices[6] = mVertices[12] = mVertices[15] = Math.min(-1 + 2f * (x + 1) * mTileSize / mWidth, 1); // right mVertices[4] = mVertices[7] = mVertices[13] = Math.min(-1 + 2f * y * mTileSize / mHeight, 1); // bottom mVertexBuffer.put(mVertices); mVertexBuffer.position(0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureHandles[y * mCols + x]); GLUtil.checkGlError("glBindTexture"); // Draw the two triangles GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, mVertices.length / COORDS_PER_VERTEX); } } GLES20.glDisableVertexAttribArray(sAttribPositionHandle); GLES20.glDisableVertexAttribArray(sAttribTextureCoordsHandle); } public void destroy() { if (mTextureHandles != null) { GLES20.glDeleteTextures(mTextureHandles.length, mTextureHandles, 0); GLUtil.checkGlError("Destroy picture"); mTextureHandles = null; } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/render/GLTextureView.java ================================================ /* * Copyright 2014 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yalin.style.render; import android.content.Context; import android.graphics.SurfaceTexture; import android.opengl.GLDebugHelper; import android.opengl.GLSurfaceView; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; import android.view.TextureView; import java.io.Writer; import java.lang.ref.WeakReference; import java.util.ArrayList; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGL11; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLContext; import javax.microedition.khronos.egl.EGLDisplay; import javax.microedition.khronos.egl.EGLSurface; import javax.microedition.khronos.opengles.GL; import javax.microedition.khronos.opengles.GL10; /** * See http://stackoverflow.com/questions/12061419/converting-from-glsurfaceview-to-textureview-via-gltextureview */ public class GLTextureView extends TextureView implements TextureView.SurfaceTextureListener { private final static String TAG = "GLTextureView"; private final static boolean LOG_ATTACH_DETACH = false; private final static boolean LOG_THREADS = false; private final static boolean LOG_PAUSE_RESUME = false; private final static boolean LOG_SURFACE = false; private final static boolean LOG_RENDERER = false; private final static boolean LOG_RENDERER_DRAW_FRAME = false; private final static boolean LOG_EGL = false; /** * The renderer only renders * when the surface is created, or when {@link #requestRender} is called. * * @see #getRenderMode() * @see #setRenderMode(int) * @see #requestRender() */ public final static int RENDERMODE_WHEN_DIRTY = 0; /** * The renderer is called * continuously to re-render the scene. * * @see #getRenderMode() * @see #setRenderMode(int) */ public final static int RENDERMODE_CONTINUOUSLY = 1; /** * Check glError() after every GL call and throw an exception if glError indicates * that an error has occurred. This can be used to help track down which OpenGL ES call * is causing an error. * * @see #getDebugFlags * @see #setDebugFlags */ public final static int DEBUG_CHECK_GL_ERROR = 1; /** * Log GL calls to the system log at "verbose" level with tag "GLSurfaceView". * * @see #getDebugFlags * @see #setDebugFlags */ public final static int DEBUG_LOG_GL_CALLS = 2; /** * Standard View constructor. In order to render something, you * must call {@link #setRenderer} to register a renderer. */ public GLTextureView(Context context) { super(context); init(); } /** * Standard View constructor. In order to render something, you * must call {@link #setRenderer} to register a renderer. */ public GLTextureView(Context context, AttributeSet attrs) { super(context, attrs); init(); } @Override protected void finalize() throws Throwable { try { if (mGLThread != null) { // GLThread may still be running if this view was never // attached to a window. mGLThread.requestExitAndWait(); } } finally { super.finalize(); } } private void init() { setSurfaceTextureListener(this); } /** * Set the glWrapper. If the glWrapper is not null, its * {@link GLWrapper#wrap(GL)} method is called * whenever a surface is created. A GLWrapper can be used to wrap * the GL object that's passed to the renderer. Wrapping a GL * object enables examining and modifying the behavior of the * GL calls made by the renderer. *

* Wrapping is typically used for debugging purposes. *

* The default value is null. * * @param glWrapper the new GLWrapper */ public void setGLWrapper(GLWrapper glWrapper) { mGLWrapper = glWrapper; } /** * Set the debug flags to a new value. The value is * constructed by OR-together zero or more * of the DEBUG_CHECK_* constants. The debug flags take effect * whenever a surface is created. The default value is zero. * * @param debugFlags the new debug flags * @see #DEBUG_CHECK_GL_ERROR * @see #DEBUG_LOG_GL_CALLS */ public void setDebugFlags(int debugFlags) { mDebugFlags = debugFlags; } /** * Get the current value of the debug flags. * * @return the current value of the debug flags. */ public int getDebugFlags() { return mDebugFlags; } /** * Control whether the EGL context is preserved when the GLSurfaceView is paused and * resumed. *

* If set to true, then the EGL context may be preserved when the GLSurfaceView is paused. * Whether the EGL context is actually preserved or not depends upon whether the * Android device that the program is running on can support an arbitrary number of EGL * contexts or not. Devices that can only support a limited number of EGL contexts must * release the EGL context in order to allow multiple applications to share the GPU. *

* If set to false, the EGL context will be released when the GLSurfaceView is paused, * and recreated when the GLSurfaceView is resumed. *

*

* The default is false. * * @param preserveOnPause preserve the EGL context when paused */ public void setPreserveEGLContextOnPause(boolean preserveOnPause) { mPreserveEGLContextOnPause = preserveOnPause; } /** * @return true if the EGL context will be preserved when paused */ public boolean getPreserveEGLContextOnPause() { return mPreserveEGLContextOnPause; } /** * Set the renderer associated with this view. Also starts the thread that * will call the renderer, which in turn causes the rendering to start. *

This method should be called once and only once in the life-cycle of * a GLSurfaceView. *

The following GLSurfaceView methods can only be called before * setRenderer is called: *

    *
  • {@link #setEGLConfigChooser(boolean)} *
  • {@link #setEGLConfigChooser(EGLConfigChooser)} *
  • {@link #setEGLConfigChooser(int, int, int, int, int, int)} *
*

* The following GLSurfaceView methods can only be called after * setRenderer is called: *

    *
  • {@link #getRenderMode()} *
  • {@link #onPause()} *
  • {@link #onResume()} *
  • {@link #queueEvent(Runnable)} *
  • {@link #requestRender()} *
  • {@link #setRenderMode(int)} *
* * @param renderer the renderer to use to perform OpenGL drawing. */ public void setRenderer(GLSurfaceView.Renderer renderer) { checkRenderThreadState(); if (mEGLConfigChooser == null) { mEGLConfigChooser = new SimpleEGLConfigChooser(true); } if (mEGLContextFactory == null) { mEGLContextFactory = new DefaultContextFactory(); } if (mEGLWindowSurfaceFactory == null) { mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); } mRenderer = renderer; mGLThread = new GLThread(mThisWeakRef); mGLThread.start(); } /** * Install a custom EGLContextFactory. *

If this method is * called, it must be called before {@link #setRenderer(Renderer)} * is called. *

* If this method is not called, then by default * a context will be created with no shared context and * with a null attribute list. */ public void setEGLContextFactory(EGLContextFactory factory) { checkRenderThreadState(); mEGLContextFactory = factory; } /** * Install a custom EGLWindowSurfaceFactory. *

If this method is * called, it must be called before {@link #setRenderer(Renderer)} * is called. *

* If this method is not called, then by default * a window surface will be created with a null attribute list. */ public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { checkRenderThreadState(); mEGLWindowSurfaceFactory = factory; } /** * Install a custom EGLConfigChooser. *

If this method is * called, it must be called before {@link #setRenderer(Renderer)} * is called. *

* If no setEGLConfigChooser method is called, then by default the * view will choose an EGLConfig that is compatible with the current * android.view.Surface, with a depth buffer depth of * at least 16 bits. * * @param configChooser */ public void setEGLConfigChooser(EGLConfigChooser configChooser) { checkRenderThreadState(); mEGLConfigChooser = configChooser; } /** * Install a config chooser which will choose a config * as close to 16-bit RGB as possible, with or without an optional depth * buffer as close to 16-bits as possible. *

If this method is * called, it must be called before {@link #setRenderer(Renderer)} * is called. *

* If no setEGLConfigChooser method is called, then by default the * view will choose an RGB_888 surface with a depth buffer depth of * at least 16 bits. * * @param needDepth */ public void setEGLConfigChooser(boolean needDepth) { setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth)); } /** * Install a config chooser which will choose a config * with at least the specified depthSize and stencilSize, * and exactly the specified redSize, greenSize, blueSize and alphaSize. *

If this method is * called, it must be called before {@link #setRenderer(Renderer)} * is called. *

* If no setEGLConfigChooser method is called, then by default the * view will choose an RGB_888 surface with a depth buffer depth of * at least 16 bits. */ public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) { setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, blueSize, alphaSize, depthSize, stencilSize)); } /** * Inform the default EGLContextFactory and default EGLConfigChooser * which EGLContext client version to pick. *

Use this method to create an OpenGL ES 2.0-compatible context. * Example: *

     * public MyView(Context context) {
     * super(context);
     * setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context.
     * setRenderer(new MyRenderer());
     * }
     * 
*

Note: Activities which require OpenGL ES 2.0 should indicate this by * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's * AndroidManifest.xml file. *

If this method is called, it must be called before {@link #setRenderer(Renderer)} * is called. *

This method only affects the behavior of the default EGLContexFactory and the * default EGLConfigChooser. If * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context. * If * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config. * * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0 */ public void setEGLContextClientVersion(int version) { checkRenderThreadState(); mEGLContextClientVersion = version; } /** * Set the rendering mode. When renderMode is * RENDERMODE_CONTINUOUSLY, the renderer is called * repeatedly to re-render the scene. When renderMode * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. *

* Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance * by allowing the GPU and CPU to idle when the view does not need to be updated. *

* This method can only be called after {@link #setRenderer(Renderer)} * * @param renderMode one of the RENDERMODE_X constants * @see #RENDERMODE_CONTINUOUSLY * @see #RENDERMODE_WHEN_DIRTY */ public void setRenderMode(int renderMode) { mGLThread.setRenderMode(renderMode); } /** * Get the current rendering mode. May be called * from any thread. Must not be called before a renderer has been set. * * @return the current rendering mode. * @see #RENDERMODE_CONTINUOUSLY * @see #RENDERMODE_WHEN_DIRTY */ public int getRenderMode() { return mGLThread.getRenderMode(); } /** * Request that the renderer render a frame. * This method is typically used when the render mode has been set to * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand. * May be called * from any thread. Must not be called before a renderer has been set. */ public void requestRender() { mGLThread.requestRender(); } @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { mGLThread.surfaceCreated(); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { mGLThread.onWindowResize(width, height); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mGLThread.onWindowResize(w, h); } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { mGLThread.surfaceDestroyed(); return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { requestRender(); } /** * This method is part of the SurfaceHolder.Callback interface, and is * not normally called or subclassed by clients of GLSurfaceView. */ public void on(SurfaceHolder holder) { mGLThread.surfaceCreated(); } /** * Inform the view that the activity is paused. The owner of this view must * call this method when the activity is paused. Calling this method will * pause the rendering thread. * Must not be called before a renderer has been set. */ public void onPause() { mGLThread.onPause(); } /** * Inform the view that the activity is resumed. The owner of this view must * call this method when the activity is resumed. Calling this method will * recreate the OpenGL display and resume the rendering * thread. * Must not be called before a renderer has been set. */ public void onResume() { mGLThread.onResume(); } /** * Queue a runnable to be run on the GL rendering thread. This can be used * to communicate with the Renderer on the rendering thread. * Must not be called before a renderer has been set. * * @param r the runnable to be run on the GL rendering thread. */ public void queueEvent(Runnable r) { mGLThread.queueEvent(r); } /** * This method is used as part of the View class and is not normally * called or subclassed by clients of GLSurfaceView. */ @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (LOG_ATTACH_DETACH) { Log.d(TAG, "onAttachedToWindow reattach =" + mDetached); } if (mDetached && (mRenderer != null)) { int renderMode = RENDERMODE_CONTINUOUSLY; if (mGLThread != null) { renderMode = mGLThread.getRenderMode(); } mGLThread = new GLThread(mThisWeakRef); if (renderMode != RENDERMODE_CONTINUOUSLY) { mGLThread.setRenderMode(renderMode); } mGLThread.start(); } mDetached = false; } /** * This method is used as part of the View class and is not normally * called or subclassed by clients of GLSurfaceView. * Must not be called before a renderer has been set. */ @Override protected void onDetachedFromWindow() { if (LOG_ATTACH_DETACH) { Log.d(TAG, "onDetachedFromWindow"); } if (mGLThread != null) { mGLThread.requestExitAndWait(); } mDetached = true; super.onDetachedFromWindow(); } // ---------------------------------------------------------------------- /** * An interface used to wrap a GL interface. *

Typically * used for implementing debugging and tracing on top of the default * GL interface. You would typically use this by creating your own class * that implemented all the GL methods by delegating to another GL instance. * Then you could add your own behavior before or after calling the * delegate. All the GLWrapper would do was instantiate and return the * wrapper GL instance: *

     * class MyGLWrapper implements GLWrapper {
     * GL wrap(GL gl) {
     * return new MyGLImplementation(gl);
     * }
     * static class MyGLImplementation implements GL,GL10,GL11,... {
     * ...
     * }
     * }
     * 
* * @see #setGLWrapper(GLWrapper) */ public interface GLWrapper { /** * Wraps a gl interface in another gl interface. * * @param gl a GL interface that is to be wrapped. * @return either the input argument or another GL object that wraps the input argument. */ GL wrap(GL gl); } /** * An interface for customizing the eglCreateContext and eglDestroyContext calls. *

* This interface must be implemented by clients wishing to call * {@link GLSurfaceView#setEGLContextFactory(EGLContextFactory)} */ public interface EGLContextFactory { EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig); void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context); } private class DefaultContextFactory implements EGLContextFactory { private int EGL_CONTEXT_CLIENT_VERSION = 0x3098; public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, EGL10.EGL_NONE}; return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, mEGLContextClientVersion != 0 ? attrib_list : null); } public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { if (!egl.eglDestroyContext(display, context)) { Log.e("DefaultContextFactory", "display:" + display + " context: " + context); if (LOG_THREADS) { Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId()); } EglHelper.throwEglException("eglDestroyContex", egl.eglGetError()); } } } /** * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls. *

* This interface must be implemented by clients wishing to call * {@link GLSurfaceView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)} */ public interface EGLWindowSurfaceFactory { /** * @return null if the surface cannot be constructed. */ EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow); void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface); } private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory { public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) { EGLSurface result = null; try { result = egl.eglCreateWindowSurface(display, config, nativeWindow, null); } catch (IllegalArgumentException e) { // This exception indicates that the surface flinger surface // is not valid. This can happen if the surface flinger surface has // been torn down, but the application has not yet been // notified via SurfaceHolder.Callback.surfaceDestroyed. // In theory the application should be notified first, // but in practice sometimes it is not. See b/4588890 Log.e(TAG, "eglCreateWindowSurface", e); } return result; } public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) { egl.eglDestroySurface(display, surface); } } /** * An interface for choosing an EGLConfig configuration from a list of * potential configurations. *

* This interface must be implemented by clients wishing to call * {@link GLSurfaceView#setEGLConfigChooser(EGLConfigChooser)} */ public interface EGLConfigChooser { /** * Choose a configuration from the list. Implementors typically * implement this method by calling * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. * * @param egl the EGL10 for the current display. * @param display the current display. * @return the chosen configuration. */ EGLConfig chooseConfig(EGL10 egl, EGLDisplay display); } private abstract class BaseConfigChooser implements EGLConfigChooser { public BaseConfigChooser(int[] configSpec) { mConfigSpec = filterConfigSpec(configSpec); } public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { int[] num_config = new int[1]; if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config)) { throw new IllegalArgumentException("eglChooseConfig failed"); } int numConfigs = num_config[0]; if (numConfigs <= 0) { throw new IllegalArgumentException( "No configs match configSpec"); } EGLConfig[] configs = new EGLConfig[numConfigs]; if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, num_config)) { throw new IllegalArgumentException("eglChooseConfig#2 failed"); } EGLConfig config = chooseConfig(egl, display, configs); if (config == null) { throw new IllegalArgumentException("No config chosen"); } return config; } abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs); protected int[] mConfigSpec; private int[] filterConfigSpec(int[] configSpec) { if (mEGLContextClientVersion != 2) { return configSpec; } /* We know none of the subclasses define EGL_RENDERABLE_TYPE. * And we know the configSpec is well formed. */ int len = configSpec.length; int[] newConfigSpec = new int[len + 2]; System.arraycopy(configSpec, 0, newConfigSpec, 0, len - 1); newConfigSpec[len - 1] = EGL10.EGL_RENDERABLE_TYPE; newConfigSpec[len] = 4; /* EGL_OPENGL_ES2_BIT */ newConfigSpec[len + 1] = EGL10.EGL_NONE; return newConfigSpec; } } /** * Choose a configuration with exactly the specified r,g,b,a sizes, * and at least the specified depth and stencil sizes. */ private class ComponentSizeChooser extends BaseConfigChooser { public ComponentSizeChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) { super(new int[]{ EGL10.EGL_RED_SIZE, redSize, EGL10.EGL_GREEN_SIZE, greenSize, EGL10.EGL_BLUE_SIZE, blueSize, EGL10.EGL_ALPHA_SIZE, alphaSize, EGL10.EGL_DEPTH_SIZE, depthSize, EGL10.EGL_STENCIL_SIZE, stencilSize, EGL10.EGL_NONE}); mValue = new int[1]; mRedSize = redSize; mGreenSize = greenSize; mBlueSize = blueSize; mAlphaSize = alphaSize; mDepthSize = depthSize; mStencilSize = stencilSize; } @Override public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { for (EGLConfig config : configs) { int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0); int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0); if ((d >= mDepthSize) && (s >= mStencilSize)) { int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0); int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0); int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0); int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0); if ((r == mRedSize) && (g == mGreenSize) && (b == mBlueSize) && (a == mAlphaSize)) { return config; } } } return null; } private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue) { if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { return mValue[0]; } return defaultValue; } private int[] mValue; // Subclasses can adjust these values: protected int mRedSize; protected int mGreenSize; protected int mBlueSize; protected int mAlphaSize; protected int mDepthSize; protected int mStencilSize; } /** * This class will choose a RGB_888 surface with * or without a depth buffer. */ private class SimpleEGLConfigChooser extends ComponentSizeChooser { public SimpleEGLConfigChooser(boolean withDepthBuffer) { super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0); } } /** * An EGL helper class. */ private static class EglHelper { public EglHelper(WeakReference glTextureViewWeakRef) { mGLTextureViewWeakRef = glTextureViewWeakRef; } /** * Initialize EGL for a given configuration spec. * * @param configSpec */ public void start() { if (LOG_EGL) { Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId()); } /* * Get an EGL instance */ mEgl = (EGL10) EGLContext.getEGL(); /* * Get to the default display. */ mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { throw new RuntimeException("eglGetDisplay failed"); } /* * We can now initialize EGL for that display */ int[] version = new int[2]; if (!mEgl.eglInitialize(mEglDisplay, version)) { throw new RuntimeException("eglInitialize failed"); } GLTextureView view = mGLTextureViewWeakRef.get(); if (view == null) { mEglConfig = null; mEglContext = null; } else { mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay); /* * Create an EGL context. We want to do this as rarely as we can, because an * EGL context is a somewhat heavy object. */ mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig); } if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { mEglContext = null; throwEglException("createContext"); } if (LOG_EGL) { Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId()); } mEglSurface = null; } /** * Create an egl surface for the current SurfaceHolder surface. If a surface * already exists, destroy it before creating the new surface. * * @return true if the surface was created successfully. */ public boolean createSurface() { if (LOG_EGL) { Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId()); } /* * Check preconditions. */ if (mEgl == null) { throw new RuntimeException("egl not initialized"); } if (mEglDisplay == null) { throw new RuntimeException("eglDisplay not initialized"); } if (mEglConfig == null) { throw new RuntimeException("mEglConfig not initialized"); } /* * The window size has changed, so we need to create a new * surface. */ destroySurfaceImp(); /* * Create an EGL surface we can render into. */ GLTextureView view = mGLTextureViewWeakRef.get(); if (view != null) { mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl, mEglDisplay, mEglConfig, view.getSurfaceTexture()); } else { mEglSurface = null; } if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { int error = mEgl.eglGetError(); if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); } return false; } /* * Before we can issue GL commands, we need to make sure * the context is current and bound to a surface. */ if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { /* * Could not make the context current, probably because the underlying * SurfaceView surface has been destroyed. */ logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError()); return false; } return true; } /** * Create a GL object for the current EGL context. * * @return */ GL createGL() { GL gl = mEglContext.getGL(); GLTextureView view = mGLTextureViewWeakRef.get(); if (view != null) { if (view.mGLWrapper != null) { gl = view.mGLWrapper.wrap(gl); } if ((view.mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) { int configFlags = 0; Writer log = null; if ((view.mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) { configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; } if ((view.mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) { log = new LogWriter(); } gl = GLDebugHelper.wrap(gl, configFlags, log); } } return gl; } /** * Display the current render surface. * * @return the EGL error code from eglSwapBuffers. */ public int swap() { if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { return mEgl.eglGetError(); } return EGL10.EGL_SUCCESS; } public void destroySurface() { if (LOG_EGL) { Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId()); } destroySurfaceImp(); } private void destroySurfaceImp() { if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); GLTextureView view = mGLTextureViewWeakRef.get(); if (view != null) { view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); } mEglSurface = null; } } public void finish() { if (LOG_EGL) { Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId()); } if (mEglContext != null) { GLTextureView view = mGLTextureViewWeakRef.get(); if (view != null) { view.mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext); } mEglContext = null; } if (mEglDisplay != null) { mEgl.eglTerminate(mEglDisplay); mEglDisplay = null; } } private void throwEglException(String function) { throwEglException(function, mEgl.eglGetError()); } public static void throwEglException(String function, int error) { String message = formatEglError(function, error); if (LOG_THREADS) { Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + message); } throw new RuntimeException(message); } public static void logEglErrorAsWarning(String tag, String function, int error) { Log.w(tag, formatEglError(function, error)); } public static String formatEglError(String function, int error) { return function + " failed"; // + EGLLogWrapper.getErrorString(error); } private WeakReference mGLTextureViewWeakRef; EGL10 mEgl; EGLDisplay mEglDisplay; EGLSurface mEglSurface; EGLConfig mEglConfig; EGLContext mEglContext; } /** * A generic GL Thread. Takes care of initializing EGL and GL. Delegates * to a Renderer instance to do the actual drawing. Can be configured to * render continuously or on request. *

* All potentially blocking synchronization is done through the * sGLThreadManager object. This avoids multiple-lock ordering issues. */ static class GLThread extends Thread { GLThread(WeakReference glTextureViewWeakRef) { super(); mWidth = 0; mHeight = 0; mRequestRender = true; mRenderMode = RENDERMODE_CONTINUOUSLY; mGLTextureViewWeakRef = glTextureViewWeakRef; } @Override public void run() { setName("GLThread " + getId()); if (LOG_THREADS) { Log.i("GLThread", "starting tid=" + getId()); } try { guardedRun(); } catch (InterruptedException e) { // fall thru and exit normally } finally { sGLThreadManager.threadExiting(this); } } /* * This private method should only be called inside a * synchronized(sGLThreadManager) block. */ private void stopEglSurfaceLocked() { if (mHaveEglSurface) { mHaveEglSurface = false; mEglHelper.destroySurface(); } } /* * This private method should only be called inside a * synchronized(sGLThreadManager) block. */ private void stopEglContextLocked() { if (mHaveEglContext) { mEglHelper.finish(); mHaveEglContext = false; sGLThreadManager.releaseEglContextLocked(this); } } private void guardedRun() throws InterruptedException { mEglHelper = new EglHelper(mGLTextureViewWeakRef); mHaveEglContext = false; mHaveEglSurface = false; try { GL10 gl = null; boolean createEglContext = false; boolean createEglSurface = false; boolean createGlInterface = false; boolean lostEglContext = false; boolean sizeChanged = false; boolean wantRenderNotification = false; boolean doRenderNotification = false; boolean askedToReleaseEglContext = false; int w = 0; int h = 0; Runnable event = null; while (true) { synchronized (sGLThreadManager) { while (true) { if (mShouldExit) { return; } if (!mEventQueue.isEmpty()) { event = mEventQueue.remove(0); break; } // Update the pause state. boolean pausing = false; if (mPaused != mRequestPaused) { pausing = mRequestPaused; mPaused = mRequestPaused; sGLThreadManager.notifyAll(); if (LOG_PAUSE_RESUME) { Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId()); } } // Do we need to give up the EGL context? if (mShouldReleaseEglContext) { if (LOG_SURFACE) { Log.i("GLThread", "releasing EGL context because asked to tid=" + getId()); } stopEglSurfaceLocked(); stopEglContextLocked(); mShouldReleaseEglContext = false; askedToReleaseEglContext = true; } // Have we lost the EGL context? if (lostEglContext) { stopEglSurfaceLocked(); stopEglContextLocked(); lostEglContext = false; } // When pausing, release the EGL surface: if (pausing && mHaveEglSurface) { if (LOG_SURFACE) { Log.i("GLThread", "releasing EGL surface because paused tid=" + getId()); } stopEglSurfaceLocked(); } // When pausing, optionally release the EGL Context: if (pausing && mHaveEglContext) { GLTextureView view = mGLTextureViewWeakRef.get(); boolean preserveEglContextOnPause = view == null ? false : view.mPreserveEGLContextOnPause; if (!preserveEglContextOnPause || sGLThreadManager.shouldReleaseEGLContextWhenPausing()) { stopEglContextLocked(); if (LOG_SURFACE) { Log.i("GLThread", "releasing EGL context because paused tid=" + getId()); } } } // When pausing, optionally terminate EGL: if (pausing) { if (sGLThreadManager.shouldTerminateEGLWhenPausing()) { mEglHelper.finish(); if (LOG_SURFACE) { Log.i("GLThread", "terminating EGL because paused tid=" + getId()); } } } // Have we lost the SurfaceView surface? if ((!mHasSurface) && (!mWaitingForSurface)) { if (LOG_SURFACE) { Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId()); } if (mHaveEglSurface) { stopEglSurfaceLocked(); } mWaitingForSurface = true; mSurfaceIsBad = false; sGLThreadManager.notifyAll(); } // Have we acquired the surface view surface? if (mHasSurface && mWaitingForSurface) { if (LOG_SURFACE) { Log.i("GLThread", "noticed surfaceView surface acquired tid=" + getId()); } mWaitingForSurface = false; sGLThreadManager.notifyAll(); } if (doRenderNotification) { if (LOG_SURFACE) { Log.i("GLThread", "sending render notification tid=" + getId()); } wantRenderNotification = false; doRenderNotification = false; mRenderComplete = true; sGLThreadManager.notifyAll(); } // Ready to draw? if (readyToDraw()) { // If we don't have an EGL context, try to acquire one. if (!mHaveEglContext) { if (askedToReleaseEglContext) { askedToReleaseEglContext = false; } else if (sGLThreadManager.tryAcquireEglContextLocked(this)) { try { mEglHelper.start(); } catch (RuntimeException t) { sGLThreadManager.releaseEglContextLocked(this); throw t; } mHaveEglContext = true; createEglContext = true; sGLThreadManager.notifyAll(); } } if (mHaveEglContext && !mHaveEglSurface) { mHaveEglSurface = true; createEglSurface = true; createGlInterface = true; sizeChanged = true; } if (mHaveEglSurface) { if (mSizeChanged) { sizeChanged = true; w = mWidth; h = mHeight; wantRenderNotification = true; if (LOG_SURFACE) { Log.i("GLThread", "noticing that we want render notification tid=" + getId()); } // Destroy and recreate the EGL surface. createEglSurface = true; mSizeChanged = false; } mRequestRender = false; sGLThreadManager.notifyAll(); break; } } // By design, this is the only place in a GLThread thread where we wait(). if (LOG_THREADS) { Log.i("GLThread", "waiting tid=" + getId() + " mHaveEglContext: " + mHaveEglContext + " mHaveEglSurface: " + mHaveEglSurface + " mFinishedCreatingEglSurface: " + mFinishedCreatingEglSurface + " mPaused: " + mPaused + " mHasSurface: " + mHasSurface + " mSurfaceIsBad: " + mSurfaceIsBad + " mWaitingForSurface: " + mWaitingForSurface + " mWidth: " + mWidth + " mHeight: " + mHeight + " mRequestRender: " + mRequestRender + " mRenderMode: " + mRenderMode); } sGLThreadManager.wait(); } } // end of synchronized(sGLThreadManager) if (event != null) { event.run(); event = null; continue; } if (createEglSurface) { if (LOG_SURFACE) { Log.w("GLThread", "egl createSurface"); } if (mEglHelper.createSurface()) { synchronized (sGLThreadManager) { mFinishedCreatingEglSurface = true; sGLThreadManager.notifyAll(); } } else { synchronized (sGLThreadManager) { mFinishedCreatingEglSurface = true; mSurfaceIsBad = true; sGLThreadManager.notifyAll(); } continue; } createEglSurface = false; } if (createGlInterface) { gl = (GL10) mEglHelper.createGL(); sGLThreadManager.checkGLDriver(gl); createGlInterface = false; } if (createEglContext) { if (LOG_RENDERER) { Log.w("GLThread", "onSurfaceCreated"); } GLTextureView view = mGLTextureViewWeakRef.get(); if (view != null) { view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig); } createEglContext = false; } if (sizeChanged) { if (LOG_RENDERER) { Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")"); } GLTextureView view = mGLTextureViewWeakRef.get(); if (view != null) { view.mRenderer.onSurfaceChanged(gl, w, h); } sizeChanged = false; } if (LOG_RENDERER_DRAW_FRAME) { Log.w("GLThread", "onDrawFrame tid=" + getId()); } { GLTextureView view = mGLTextureViewWeakRef.get(); if (view != null) { view.mRenderer.onDrawFrame(gl); } } int swapError = mEglHelper.swap(); switch (swapError) { case EGL10.EGL_SUCCESS: break; case EGL11.EGL_CONTEXT_LOST: if (LOG_SURFACE) { Log.i("GLThread", "egl context lost tid=" + getId()); } lostEglContext = true; break; default: // Other errors typically mean that the current surface is bad, // probably because the SurfaceView surface has been destroyed, // but we haven't been notified yet. // Log the error to help developers understand why rendering stopped. EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError); synchronized (sGLThreadManager) { mSurfaceIsBad = true; sGLThreadManager.notifyAll(); } break; } if (wantRenderNotification) { doRenderNotification = true; } } } finally { /* * clean-up everything... */ synchronized (sGLThreadManager) { stopEglSurfaceLocked(); stopEglContextLocked(); } } } public boolean ableToDraw() { return mHaveEglContext && mHaveEglSurface && readyToDraw(); } private boolean readyToDraw() { return (!mPaused) && mHasSurface && (!mSurfaceIsBad) && (mWidth > 0) && (mHeight > 0) && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY)); } public void setRenderMode(int renderMode) { if (!((RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= RENDERMODE_CONTINUOUSLY))) { throw new IllegalArgumentException("renderMode"); } synchronized (sGLThreadManager) { mRenderMode = renderMode; sGLThreadManager.notifyAll(); } } public int getRenderMode() { synchronized (sGLThreadManager) { return mRenderMode; } } public void requestRender() { synchronized (sGLThreadManager) { mRequestRender = true; sGLThreadManager.notifyAll(); } } public void surfaceCreated() { synchronized (sGLThreadManager) { if (LOG_THREADS) { Log.i("GLThread", "surfaceCreated tid=" + getId()); } mHasSurface = true; mFinishedCreatingEglSurface = false; sGLThreadManager.notifyAll(); while (mWaitingForSurface && !mFinishedCreatingEglSurface && !mExited) { try { sGLThreadManager.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } public void surfaceDestroyed() { synchronized (sGLThreadManager) { if (LOG_THREADS) { Log.i("GLThread", "surfaceDestroyed tid=" + getId()); } mHasSurface = false; sGLThreadManager.notifyAll(); while ((!mWaitingForSurface) && (!mExited)) { try { sGLThreadManager.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } public void onPause() { synchronized (sGLThreadManager) { if (LOG_PAUSE_RESUME) { Log.i("GLThread", "onPause tid=" + getId()); } mRequestPaused = true; sGLThreadManager.notifyAll(); while ((!mExited) && (!mPaused)) { if (LOG_PAUSE_RESUME) { Log.i("Main thread", "onPause waiting for mPaused."); } try { sGLThreadManager.wait(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } } public void onResume() { synchronized (sGLThreadManager) { if (LOG_PAUSE_RESUME) { Log.i("GLThread", "onResume tid=" + getId()); } mRequestPaused = false; mRequestRender = true; mRenderComplete = false; sGLThreadManager.notifyAll(); while ((!mExited) && mPaused && (!mRenderComplete)) { if (LOG_PAUSE_RESUME) { Log.i("Main thread", "onResume waiting for !mPaused."); } try { sGLThreadManager.wait(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } } public void onWindowResize(int w, int h) { synchronized (sGLThreadManager) { mWidth = w; mHeight = h; mSizeChanged = true; mRequestRender = true; mRenderComplete = false; sGLThreadManager.notifyAll(); // Wait for thread to react to resize and render a frame while (!mExited && !mPaused && !mRenderComplete && ableToDraw()) { if (LOG_SURFACE) { Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); } try { sGLThreadManager.wait(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } } public void requestExitAndWait() { // don't call this from GLThread thread or it is a guaranteed // deadlock! synchronized (sGLThreadManager) { mShouldExit = true; sGLThreadManager.notifyAll(); while (!mExited) { try { sGLThreadManager.wait(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } } public void requestReleaseEglContextLocked() { mShouldReleaseEglContext = true; sGLThreadManager.notifyAll(); } /** * Queue an "event" to be run on the GL rendering thread. * * @param r the runnable to be run on the GL rendering thread. */ public void queueEvent(Runnable r) { if (r == null) { throw new IllegalArgumentException("r must not be null"); } synchronized (sGLThreadManager) { mEventQueue.add(r); sGLThreadManager.notifyAll(); } } // Once the thread is started, all accesses to the following member // variables are protected by the sGLThreadManager monitor private boolean mShouldExit; private boolean mExited; private boolean mRequestPaused; private boolean mPaused; private boolean mHasSurface; private boolean mSurfaceIsBad; private boolean mWaitingForSurface; private boolean mHaveEglContext; private boolean mHaveEglSurface; private boolean mFinishedCreatingEglSurface; private boolean mShouldReleaseEglContext; private int mWidth; private int mHeight; private int mRenderMode; private boolean mRequestRender; private boolean mRenderComplete; private ArrayList mEventQueue = new ArrayList(); private boolean mSizeChanged = true; // End of member variables protected by the sGLThreadManager monitor. private EglHelper mEglHelper; /** * Set once at thread construction time, nulled out when the parent view is garbage * called. This weak reference allows the GLSurfaceView to be garbage collected while * the GLThread is still alive. */ private WeakReference mGLTextureViewWeakRef; } static class LogWriter extends Writer { @Override public void close() { flushBuilder(); } @Override public void flush() { flushBuilder(); } @Override public void write(char[] buf, int offset, int count) { for (int i = 0; i < count; i++) { char c = buf[offset + i]; if (c == '\n') { flushBuilder(); } else { mBuilder.append(c); } } } private void flushBuilder() { if (mBuilder.length() > 0) { Log.v("GLSurfaceView", mBuilder.toString()); mBuilder.delete(0, mBuilder.length()); } } private StringBuilder mBuilder = new StringBuilder(); } private void checkRenderThreadState() { if (mGLThread != null) { throw new IllegalStateException( "setRenderer has already been called for this instance."); } } private static class GLThreadManager { private static String TAG = "GLThreadManager"; public synchronized void threadExiting(GLThread thread) { if (LOG_THREADS) { Log.i("GLThread", "exiting tid=" + thread.getId()); } thread.mExited = true; if (mEglOwner == thread) { mEglOwner = null; } notifyAll(); } /* * Tries once to acquire the right to use an EGL * context. Does not block. Requires that we are already * in the sGLThreadManager monitor when this is called. * * @return true if the right to use an EGL context was acquired. */ public boolean tryAcquireEglContextLocked(GLThread thread) { if (mEglOwner == thread || mEglOwner == null) { mEglOwner = thread; notifyAll(); return true; } checkGLESVersion(); if (mMultipleGLESContextsAllowed) { return true; } // Notify the owning thread that it should release the context. // TODO: implement a fairness policy. Currently // if the owning thread is drawing continuously it will just // reacquire the EGL context. if (mEglOwner != null) { mEglOwner.requestReleaseEglContextLocked(); } return false; } /* * Releases the EGL context. Requires that we are already in the * sGLThreadManager monitor when this is called. */ public void releaseEglContextLocked(GLThread thread) { if (mEglOwner == thread) { mEglOwner = null; } notifyAll(); } public synchronized boolean shouldReleaseEGLContextWhenPausing() { // Release the EGL context when pausing even if // the hardware supports multiple EGL contexts. // Otherwise the device could run out of EGL contexts. return mLimitedGLESContexts; } public synchronized boolean shouldTerminateEGLWhenPausing() { checkGLESVersion(); return !mMultipleGLESContextsAllowed; } public synchronized void checkGLDriver(GL10 gl) { if (!mGLESDriverCheckComplete) { checkGLESVersion(); String renderer = gl.glGetString(GL10.GL_RENDERER); if (mGLESVersion < kGLES_20) { mMultipleGLESContextsAllowed = !renderer.startsWith(kMSM7K_RENDERER_PREFIX); notifyAll(); } mLimitedGLESContexts = !mMultipleGLESContextsAllowed; if (LOG_SURFACE) { Log.w(TAG, "checkGLDriver renderer = \"" + renderer + "\" multipleContextsAllowed = " + mMultipleGLESContextsAllowed + " mLimitedGLESContexts = " + mLimitedGLESContexts); } mGLESDriverCheckComplete = true; } } private void checkGLESVersion() { if (!mGLESVersionCheckComplete) { // mGLESVersion = SystemProperties.getInt( // "ro.opengles.version", // ConfigurationInfo.GL_ES_VERSION_UNDEFINED); // if (mGLESVersion >= kGLES_20) { mMultipleGLESContextsAllowed = true; // } if (LOG_SURFACE) { Log.w(TAG, "checkGLESVersion mGLESVersion =" + " " + mGLESVersion + " mMultipleGLESContextsAllowed = " + mMultipleGLESContextsAllowed); } mGLESVersionCheckComplete = true; } } /** * This check was required for some pre-Android-3.0 hardware. Android 3.0 provides * support for hardware-accelerated views, therefore multiple EGL contexts are * supported on all Android 3.0+ EGL drivers. */ private boolean mGLESVersionCheckComplete; private int mGLESVersion; private boolean mGLESDriverCheckComplete; private boolean mMultipleGLESContextsAllowed; private boolean mLimitedGLESContexts; private static final int kGLES_20 = 0x20000; private static final String kMSM7K_RENDERER_PREFIX = "Q3Dimension MSM7500 "; private GLThread mEglOwner; } private static final GLThreadManager sGLThreadManager = new GLThreadManager(); private final WeakReference mThisWeakRef = new WeakReference(this); private GLThread mGLThread; private GLSurfaceView.Renderer mRenderer; private boolean mDetached; private EGLConfigChooser mEGLConfigChooser; private EGLContextFactory mEGLContextFactory; private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; private GLWrapper mGLWrapper; private int mDebugFlags; private int mEGLContextClientVersion; private boolean mPreserveEGLContextOnPause; } ================================================ FILE: presentation/src/main/java/com/yalin/style/render/GLUtil.java ================================================ package com.yalin.style.render; import android.graphics.Bitmap; import android.opengl.GLES20; import android.opengl.GLUtils; import android.util.Log; import com.yalin.style.BuildConfig; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; /** * YaLin 2016/12/30. */ public class GLUtil { private static final String TAG = "GLUtil"; public static final int BYTES_PER_FLOAT = 4; public static int loadShader(int type, String shaderCode) { // create a vertex shader type (GLES20.GL_VERTEX_SHADER) // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) int shaderHandle = GLES20.glCreateShader(type); // add the source code to the shader and compile it GLES20.glShaderSource(shaderHandle, shaderCode); GLES20.glCompileShader(shaderHandle); checkGlError("glCompileShader"); return shaderHandle; } public static int createAndLinkProgram(int vertexShaderHandle, int fragShaderHandle, String[] attributes) { int programHandle = GLES20.glCreateProgram(); GLUtil.checkGlError("glCreateProgram"); GLES20.glAttachShader(programHandle, vertexShaderHandle); GLES20.glAttachShader(programHandle, fragShaderHandle); if (attributes != null) { final int size = attributes.length; for (int i = 0; i < size; i++) { GLES20.glBindAttribLocation(programHandle, i, attributes[i]); } } GLES20.glLinkProgram(programHandle); GLUtil.checkGlError("glLinkProgram"); GLES20.glDeleteShader(vertexShaderHandle); GLES20.glDeleteShader(fragShaderHandle); return programHandle; } public static int loadTexture(Bitmap bitmap) { final int[] textureHandle = new int[1]; GLES20.glGenTextures(1, textureHandle, 0); GLUtil.checkGlError("glGenTextures"); if (textureHandle[0] != 0) { // Bind to the texture in OpenGL GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0]); // Set filtering GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); // Load the bitmap into the bound texture. GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); GLUtil.checkGlError("texImage2D"); } if (textureHandle[0] == 0) { Log.e(TAG, "Error loading texture (empty texture handle)"); if (BuildConfig.DEBUG) { throw new RuntimeException("Error loading texture (empty texture handle)."); } } return textureHandle[0]; } public static void checkGlError(String glOperation) { int error; while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { Log.e(TAG, glOperation + ": glError " + error); if (BuildConfig.DEBUG) { throw new RuntimeException(glOperation + ": glError " + error); } } } public static FloatBuffer asFloatBuffer(float[] array) { FloatBuffer buffer = newFloatBuffer(array.length); buffer.put(array); buffer.position(0); return buffer; } public static FloatBuffer newFloatBuffer(int size) { FloatBuffer buffer = ByteBuffer.allocateDirect(size * BYTES_PER_FLOAT) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); buffer.position(0); return buffer; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/render/ImageBlurrer.java ================================================ /* * Copyright 2014 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yalin.style.render; import android.content.Context; import android.graphics.Bitmap; import android.renderscript.Allocation; import android.renderscript.Element; import android.renderscript.Matrix3f; import android.renderscript.RenderScript; import android.renderscript.ScriptIntrinsicBlur; import android.renderscript.ScriptIntrinsicColorMatrix; import com.yalin.style.util.MathUtil; public class ImageBlurrer { public static final int MAX_SUPPORTED_BLUR_PIXELS = 25; private final RenderScript mRS; private final ScriptIntrinsicBlur mSIBlur; private final ScriptIntrinsicColorMatrix mSIGrey; private final Bitmap mSourceBitmap; private final Allocation mAllocationSrc; public ImageBlurrer(Context context, Bitmap src) { mRS = RenderScript.create(context); mSIBlur = ScriptIntrinsicBlur.create(mRS, Element.U8_4(mRS)); mSIGrey = ScriptIntrinsicColorMatrix.create(mRS); mSourceBitmap = src; mAllocationSrc = src != null ? Allocation.createFromBitmap(mRS, src) : null; } public Bitmap blurBitmap(float radius, float desaturateAmount) { if (mSourceBitmap == null) { return null; } Bitmap dest = mSourceBitmap.copy(mSourceBitmap.getConfig(), true); if (radius == 0f && desaturateAmount == 0f) { return dest; } Allocation allocationDest = Allocation.createFromBitmap(mRS, dest); if (radius > 0f && desaturateAmount > 0f) { doBlur(radius, mAllocationSrc, allocationDest); doDesaturate(MathUtil.constrain(0, 1, desaturateAmount), allocationDest, mAllocationSrc); mAllocationSrc.copyTo(dest); } else if (radius > 0f) { doBlur(radius, mAllocationSrc, allocationDest); allocationDest.copyTo(dest); } else { doDesaturate(MathUtil.constrain(0, 1, desaturateAmount), mAllocationSrc, allocationDest); allocationDest.copyTo(dest); } allocationDest.destroy(); return dest; } private void doBlur(float amount, Allocation input, Allocation output) { mSIBlur.setRadius(amount); mSIBlur.setInput(input); mSIBlur.forEach(output); } private void doDesaturate(float normalizedAmount, Allocation input, Allocation output) { Matrix3f m = new Matrix3f(new float[]{ MathUtil.interpolate(1, 0.299f, normalizedAmount), MathUtil.interpolate(0, 0.299f, normalizedAmount), MathUtil.interpolate(0, 0.299f, normalizedAmount), MathUtil.interpolate(0, 0.587f, normalizedAmount), MathUtil.interpolate(1, 0.587f, normalizedAmount), MathUtil.interpolate(0, 0.587f, normalizedAmount), MathUtil.interpolate(0, 0.114f, normalizedAmount), MathUtil.interpolate(0, 0.114f, normalizedAmount), MathUtil.interpolate(1, 0.114f, normalizedAmount), }); mSIGrey.setColorMatrix(m); mSIGrey.forEach(input, output); } public void destroy() { mSIBlur.destroy(); mSIGrey.destroy(); if (mAllocationSrc != null) { mAllocationSrc.destroy(); } mRS.destroy(); } } ================================================ FILE: presentation/src/main/java/com/yalin/style/render/ImageUtil.java ================================================ /* * Copyright 2014 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yalin.style.render; import android.graphics.Bitmap; import android.graphics.Color; public class ImageUtil { // Make sure input images are very small! public static float calculateDarkness(Bitmap bitmap) { if (bitmap == null) { return 0; } int width = bitmap.getWidth(); int height = bitmap.getHeight(); int totalLum = 0; int n = 0; int x, y, color; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { ++n; color = bitmap.getPixel(x, y); totalLum += (0.21f * Color.red(color) + 0.71f * Color.green(color) + 0.07f * Color.blue(color)); } } return (totalLum / n) / 256f; } private ImageUtil() { } public static int calculateSampleSize(int rawSize, int targetSize) { int sampleSize = 1; while (rawSize / (sampleSize << 1) > targetSize) { sampleSize <<= 1; } return sampleSize; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/render/RenderController.kt ================================================ package com.yalin.style.render import android.content.Context import android.content.SharedPreferences import android.os.Handler import android.os.Message import com.yalin.style.data.log.LogUtil import com.yalin.style.domain.Wallpaper import com.yalin.style.domain.interactor.DefaultObserver import com.yalin.style.domain.interactor.GetWallpaper import com.yalin.style.domain.interactor.ObserverWallpaper import com.yalin.style.domain.interactor.OpenWallpaperInputStream import com.yalin.style.mapper.WallpaperItemMapper import com.yalin.style.settings.Prefs import java.io.InputStream import javax.inject.Inject /** * YaLin 2016/12/30. */ open class RenderController @Inject constructor(protected var mContext: Context, private val getWallpaperUseCase: GetWallpaper, private val observerWallpaper: ObserverWallpaper, private val openWallpaperInputStreamUseCase: OpenWallpaperInputStream, private val wallpaperItemMapper: WallpaperItemMapper) { protected var mRenderer: StyleBlurRenderer? = null protected var mCallbacks: Callbacks? = null protected var mVisible: Boolean = false private var mQueuedBitmapRegionLoader: BitmapRegionLoader? = null private val wallpaperRefreshObserver: WallpaperRefreshObserver private val mOnSharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> mRenderer?.apply { if (Prefs.PREF_BLUR_AMOUNT == key) { recomputeMaxPrescaledBlurPixels() throttledForceReloadCurrentArtwork() } else if (Prefs.PREF_DIM_AMOUNT == key) { recomputeMaxDimAmount() throttledForceReloadCurrentArtwork() } else if (Prefs.PREF_GREY_AMOUNT == key) { recomputeGreyAmount() throttledForceReloadCurrentArtwork() } } } init { wallpaperRefreshObserver = WallpaperRefreshObserver() observerWallpaper.registerObserver(wallpaperRefreshObserver) Prefs.getSharedPreferences(mContext) .registerOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener) } fun setComponent(renderer: StyleBlurRenderer, callbacks: Callbacks) { this.mRenderer = renderer this.mCallbacks = callbacks reloadCurrentWallpaper() } open fun destroy() { if (mQueuedBitmapRegionLoader != null) { mQueuedBitmapRegionLoader!!.destroy() } Prefs.getSharedPreferences(mContext) .unregisterOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener) observerWallpaper.unregisterObserver(wallpaperRefreshObserver) } @Throws(Exception::class) private fun createBitmapRegionLoader(inputStream: InputStream): BitmapRegionLoader { val bitmapRegionLoader = BitmapRegionLoader.newInstance(inputStream) ?: throw IllegalStateException("Bitmap region loader create failed.") return bitmapRegionLoader } fun reloadCurrentWallpaper() { getWallpaperUseCase.execute(WallpaperItemObserver(), null) } fun setVisible(visible: Boolean) { mVisible = visible if (visible) { mCallbacks?.apply { queueEventOnGlThread(Runnable { if (mQueuedBitmapRegionLoader != null) { mRenderer!!.setAndConsumeBitmapRegionLoader(mQueuedBitmapRegionLoader) mQueuedBitmapRegionLoader = null } }) requestRender() } } } private fun setBitmapRegionLoader(bitmapRegionLoader: BitmapRegionLoader) { mCallbacks!!.queueEventOnGlThread(Runnable { if (mVisible) { mRenderer!!.setAndConsumeBitmapRegionLoader(bitmapRegionLoader) } else { mQueuedBitmapRegionLoader = bitmapRegionLoader } }) } private fun throttledForceReloadCurrentArtwork() { mThrottledForceReloadHandler.removeMessages(0) mThrottledForceReloadHandler.sendEmptyMessageDelayed(0, 250) } private val mThrottledForceReloadHandler = object : Handler() { override fun handleMessage(msg: Message) { reloadCurrentWallpaper() } } private inner class WallpaperItemObserver : DefaultObserver() { override fun onNext(wallpaper: Wallpaper) { val wallpaperItem = wallpaperItemMapper.transform(wallpaper) openWallpaperInputStreamUseCase.execute(WallpaperInputStreamObserver(), OpenWallpaperInputStream.Params.openInputStream(wallpaperItem.wallpaperId)) } override fun onError(exception: Throwable) { LogUtil.E(TAG, "Load wallpaper failed.", exception) } } private inner class WallpaperInputStreamObserver : DefaultObserver() { override fun onNext(inputStream: InputStream) { try { val bitmapRegionLoader = createBitmapRegionLoader(inputStream) LogUtil.D(TAG, "Create bitmap region loader success.") setBitmapRegionLoader(bitmapRegionLoader) } catch (e: Exception) { onError(e) } } override fun onError(exception: Throwable) { LogUtil.E(TAG, "Open input stream failed. ", exception) } } private inner class WallpaperRefreshObserver : DefaultObserver() { override fun onComplete() { LogUtil.D(TAG, "Wallpaper update,reload wallpaper.") reloadCurrentWallpaper() } } interface Callbacks { fun queueEventOnGlThread(runnable: Runnable) fun requestRender() } companion object { private val TAG = "RenderController" } } ================================================ FILE: presentation/src/main/java/com/yalin/style/render/StyleBlurRenderer.java ================================================ package com.yalin.style.render; import android.app.ActivityManager; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.Rect; import android.graphics.RectF; import android.opengl.GLES20; import android.opengl.GLSurfaceView; import android.opengl.Matrix; import android.util.DisplayMetrics; import android.util.Log; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import com.yalin.style.WallpaperDetailViewport; import com.yalin.style.event.StyleWallpaperSizeChangedEvent; import com.yalin.style.event.SwitchingPhotosStateChangedEvent; import com.yalin.style.settings.Prefs; import com.yalin.style.util.MathUtil; import org.greenrobot.eventbus.EventBus; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** * YaLin 2016/12/30. */ public class StyleBlurRenderer implements GLSurfaceView.Renderer { private static final String TAG = "StyleBlurRenderer"; private static final int CROSSFADE_ANIMATION_DURATION = 750; private static final int BLUR_ANIMATION_DURATION = 750; public static final int DEFAULT_BLUR = 150; // max 500 public static final int DEFAULT_GREY = 0; // max 500 public static final int DEMO_BLUR = 250; public static final int DEMO_DIM = 64; public static final int DEMO_GREY = 0; public static final int DEFAULT_MAX_DIM = 128; // technical max 255 public static final float DIM_RANGE = 0.5f; // percent of max dim private boolean mDemoMode; private boolean mPreview; private int mMaxPrescaledBlurPixels; private int mBlurKeyframes; private int mBlurredSampleSize; private int mMaxDim; private int mMaxGrey; // Model and view matrices. Projection and MVP stored in picture set private final float[] mMMatrix = new float[16]; private final float[] mVMatrix = new float[16]; private Callbacks mCallbacks; private float mAspectRatio; private int mHeight; private GLPictureSet mCurrentGLPictureSet; private GLPictureSet mNextGLPictureSet; private GLColorOverlay mColorOverlay; private BitmapRegionLoader mQueuedNextBitmapRegionLoader; private boolean mSurfaceCreated; private volatile float mNormalOffsetX; private volatile RectF mCurrentViewport = new RectF(); // [-1, -1] to [1, 1], flipped private Context mContext; private boolean mIsBlurred = true; private boolean mBlurRelatedToArtDetailMode = false; private Interpolator mBlurInterpolator = new AccelerateDecelerateInterpolator(); private TickingFloatAnimator mBlurAnimator; private TickingFloatAnimator mCrossfadeAnimator = TickingFloatAnimator.create().from(0); public StyleBlurRenderer(Context context, Callbacks callbacks) { mContext = context; mCallbacks = callbacks; mBlurKeyframes = getNumberOfKeyframes(); mBlurAnimator = TickingFloatAnimator.create().from(mBlurKeyframes); mCurrentGLPictureSet = new GLPictureSet(0); mNextGLPictureSet = new GLPictureSet(1); // for transitioning to next pictures setNormalOffsetX(0); recomputeMaxPrescaledBlurPixels(); recomputeMaxDimAmount(); recomputeGreyAmount(); } private int getNumberOfKeyframes() { ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); return activityManager.isLowRamDevice() ? 1 : 2; } public void recomputeMaxPrescaledBlurPixels() { // Compute blur sizes int blurAmount = mDemoMode ? DEMO_BLUR : Prefs.getSharedPreferences(mContext) .getInt(Prefs.PREF_BLUR_AMOUNT, DEFAULT_BLUR); float maxBlurRadiusOverScreenHeight = blurAmount * 0.0001f; DisplayMetrics dm = mContext.getResources().getDisplayMetrics(); int maxBlurPx = (int) (dm.heightPixels * maxBlurRadiusOverScreenHeight); mBlurredSampleSize = 4; while (maxBlurPx / mBlurredSampleSize > ImageBlurrer.MAX_SUPPORTED_BLUR_PIXELS) { mBlurredSampleSize <<= 1; } mMaxPrescaledBlurPixels = maxBlurPx / mBlurredSampleSize; } public void recomputeMaxDimAmount() { mMaxDim = Prefs.getSharedPreferences(mContext).getInt( Prefs.PREF_DIM_AMOUNT, DEFAULT_MAX_DIM); } public void recomputeGreyAmount() { mMaxGrey = mDemoMode ? DEMO_GREY : Prefs.getSharedPreferences(mContext) .getInt(Prefs.PREF_GREY_AMOUNT, DEFAULT_GREY); } public void onSurfaceCreated(GL10 unused, EGLConfig config) { mSurfaceCreated = false; GLES20.glEnable(GLES20.GL_BLEND); // GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); GLES20.glBlendFuncSeparate(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA, GLES20.GL_ONE, GLES20.GL_ONE); GLES20.glClearColor(0, 0, 0, 0); // Set the camera position (View matrix) Matrix.setLookAtM(mVMatrix, 0, 0, 0, 1, 0, 0, -1, 0, 1, 0); GLColorOverlay.initGl(); GLPicture.initGl(); mColorOverlay = new GLColorOverlay(); mSurfaceCreated = true; if (mQueuedNextBitmapRegionLoader != null) { BitmapRegionLoader loader = mQueuedNextBitmapRegionLoader; mQueuedNextBitmapRegionLoader = null; setAndConsumeBitmapRegionLoader(loader); } } public void onSurfaceChanged(GL10 unused, int width, int height) { GLES20.glViewport(0, 0, width, height); hintViewportSize(width, height); if (!mDemoMode && !mPreview) { // Reset art detail viewports WallpaperDetailViewport.Companion.getInstance().setViewport(0, 0, 0, 0, 0, false); WallpaperDetailViewport.Companion.getInstance().setViewport(1, 0, 0, 0, 0, false); } mCurrentGLPictureSet.recomputeTransformMatrices(); mNextGLPictureSet.recomputeTransformMatrices(); recomputeMaxPrescaledBlurPixels(); } public void hintViewportSize(int width, int height) { mHeight = height; mAspectRatio = width * 1f / height; } public void onDrawFrame(GL10 unused) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); Matrix.setIdentityM(mMMatrix, 0); boolean stillAnimating = mCrossfadeAnimator.tick(); stillAnimating |= mBlurAnimator.tick(); if (mBlurRelatedToArtDetailMode) { mCurrentGLPictureSet.recomputeTransformMatrices(); mNextGLPictureSet.recomputeTransformMatrices(); } float dimAmount = mCurrentGLPictureSet.mDimAmount; mCurrentGLPictureSet.drawFrame(1); if (mCrossfadeAnimator.isRunning()) { dimAmount = MathUtil.interpolate(dimAmount, mNextGLPictureSet.mDimAmount, mCrossfadeAnimator.currentValue()); mNextGLPictureSet.drawFrame(mCrossfadeAnimator.currentValue()); } mColorOverlay.setColor(Color.argb((int) (dimAmount * mBlurAnimator.currentValue() / mBlurKeyframes), 0, 0, 0)); mColorOverlay.draw(mMMatrix); // don't need any perspective or anything for color overlay if (stillAnimating) { mCallbacks.requestRender(); } } public void setNormalOffsetX(float x) { mNormalOffsetX = MathUtil.constrain(0, 1, x); onViewportChanged(); } private void onViewportChanged() { mCurrentGLPictureSet.recomputeTransformMatrices(); mNextGLPictureSet.recomputeTransformMatrices(); if (mSurfaceCreated) { mCallbacks.requestRender(); } } private float blurRadiusAtFrame(float f) { return mMaxPrescaledBlurPixels * mBlurInterpolator.getInterpolation(f / mBlurKeyframes); } public void setAndConsumeBitmapRegionLoader(final BitmapRegionLoader bitmapRegionLoader) { if (!mSurfaceCreated) { mQueuedNextBitmapRegionLoader = bitmapRegionLoader; return; } if (mCrossfadeAnimator.isRunning()) { if (mQueuedNextBitmapRegionLoader != null) { mQueuedNextBitmapRegionLoader.destroy(); } mQueuedNextBitmapRegionLoader = bitmapRegionLoader; return; } if (!mDemoMode && !mPreview) { EventBus.getDefault().postSticky(new SwitchingPhotosStateChangedEvent( mNextGLPictureSet.mId, true)); EventBus.getDefault().postSticky(new StyleWallpaperSizeChangedEvent( bitmapRegionLoader.getWidth(), bitmapRegionLoader.getHeight())); WallpaperDetailViewport.Companion.getInstance() .setDefaultViewport(mNextGLPictureSet.mId, bitmapRegionLoader.getWidth() * 1f / bitmapRegionLoader.getHeight(), mAspectRatio); } mNextGLPictureSet.load(bitmapRegionLoader); mCrossfadeAnimator .from(0).to(1) .withDuration(CROSSFADE_ANIMATION_DURATION) .withEndListener(new Runnable() { @Override public void run() { // swap current and next picturesets final GLPictureSet oldGLPictureSet = mCurrentGLPictureSet; mCurrentGLPictureSet = mNextGLPictureSet; mNextGLPictureSet = new GLPictureSet(oldGLPictureSet.mId); mCallbacks.requestRender(); oldGLPictureSet.destroyPictures(); if (!mDemoMode) { EventBus.getDefault().postSticky(new SwitchingPhotosStateChangedEvent( mCurrentGLPictureSet.mId, false)); } System.gc(); if (mQueuedNextBitmapRegionLoader != null) { BitmapRegionLoader queuedNextBitmapRegionLoader = mQueuedNextBitmapRegionLoader; mQueuedNextBitmapRegionLoader = null; setAndConsumeBitmapRegionLoader(queuedNextBitmapRegionLoader); } } }) .start(); mCallbacks.requestRender(); } public void setDemoMode(boolean demoMode) { mDemoMode = demoMode; recomputeGreyAmount(); } public void setIsPreview(boolean preview) { mPreview = preview; } private class GLPictureSet { private int mId; private volatile float[] mPMatrix = new float[16]; private final float[] mMVPMatrix = new float[16]; private GLPicture[] mPictures = new GLPicture[mBlurKeyframes + 1]; private boolean mHasBitmap = false; private float mBitmapAspectRatio = 1f; private int mDimAmount = 0; public GLPictureSet(int id) { mId = id; } public void load(BitmapRegionLoader bitmapRegionLoader) { mHasBitmap = bitmapRegionLoader != null && bitmapRegionLoader.getWidth() != 0 && bitmapRegionLoader.getHeight() != 0; mBitmapAspectRatio = mHasBitmap ? bitmapRegionLoader.getWidth() * 1f / bitmapRegionLoader.getHeight() : 1f; mDimAmount = DEFAULT_MAX_DIM; destroyPictures(); if (mHasBitmap) { BitmapFactory.Options options = new BitmapFactory.Options(); Rect rect = new Rect(); int originalWidth = bitmapRegionLoader.getWidth(); int originalHeight = bitmapRegionLoader.getHeight(); // Calculate image darkness to determine dim amount rect.set(0, 0, originalWidth, originalHeight); options.inSampleSize = ImageUtil.calculateSampleSize(originalHeight, 64); Bitmap tempBitmap = bitmapRegionLoader.decodeRegion(rect, options); float darkness = ImageUtil.calculateDarkness(tempBitmap); mDimAmount = mDemoMode ? DEMO_DIM : (int) (mMaxDim * ((1 - DIM_RANGE) + DIM_RANGE * Math.sqrt(darkness))); if (tempBitmap != null) { tempBitmap.recycle(); } // Create the GLPicture objects mPictures[0] = new GLPicture(bitmapRegionLoader, mHeight); if (mMaxPrescaledBlurPixels == 0 && mMaxGrey == 0) { for (int f = 1; f <= mBlurKeyframes; f++) { mPictures[f] = mPictures[0]; } } else { int sampleSizeTargetHeight, scaledHeight, scaledWidth; if (mMaxPrescaledBlurPixels > 0) { sampleSizeTargetHeight = mHeight / mBlurredSampleSize; } else { sampleSizeTargetHeight = mHeight; } // Note that image width should be a multiple of 4 to avoid // issues with RenderScript allocations. scaledHeight = Math.max(2, MathUtil.floorEven( sampleSizeTargetHeight)); scaledWidth = Math.max(4, MathUtil.roundMult4( (int) (scaledHeight * mBitmapAspectRatio))); // To blur, first load the entire bitmap region, but at a very large // sample size that's appropriate for the final blurred image options.inSampleSize = ImageUtil.calculateSampleSize( originalHeight, sampleSizeTargetHeight); rect.set(0, 0, originalWidth, originalHeight); tempBitmap = bitmapRegionLoader.decodeRegion(rect, options); if (tempBitmap != null && tempBitmap.getWidth() != 0 && tempBitmap.getHeight() != 0) { // Next, create a scaled down version of the bitmap so that the blur radius // looks appropriate (tempBitmap will likely be bigger than the final // blurred bitmap, and thus the blur may look smaller if we just used // tempBitmap as the final blurred bitmap). // Note that image width should be a multiple of 4 to avoid // issues with RenderScript allocations. Bitmap scaledBitmap = Bitmap.createScaledBitmap( tempBitmap, scaledWidth, scaledHeight, true); if (tempBitmap != scaledBitmap) { tempBitmap.recycle(); } // And finally, create a blurred copy for each keyframe. ImageBlurrer blurrer = new ImageBlurrer(mContext, scaledBitmap); for (int f = 1; f <= mBlurKeyframes; f++) { float desaturateAmount = mMaxGrey / 500f * f / mBlurKeyframes; float blurRadius = 0f; if (mMaxPrescaledBlurPixels > 0) { blurRadius = blurRadiusAtFrame(f); } Bitmap blurredBitmap = blurrer.blurBitmap(blurRadius, desaturateAmount); mPictures[f] = new GLPicture(blurredBitmap); if (blurredBitmap != null) { blurredBitmap.recycle(); } } blurrer.destroy(); scaledBitmap.recycle(); } else { Log.e(TAG, "BitmapRegionLoader failed to decode the region, rect=" + rect.toShortString()); for (int f = 1; f <= mBlurKeyframes; f++) { mPictures[f] = null; } } } } recomputeTransformMatrices(); mCallbacks.requestRender(); } private void recomputeTransformMatrices() { float screenToBitmapAspectRatio = mAspectRatio / mBitmapAspectRatio; if (screenToBitmapAspectRatio == 0) { return; } // Ensure the bitmap is wider than the screen relatively by applying zoom // if necessary. Vary width but liked height the same. float zoom = Math.max(1f, 1.15f * screenToBitmapAspectRatio); // Total scale factors in both zoom and scale due to aspect ratio. float scaledBitmapToScreenAspectRatio = zoom / screenToBitmapAspectRatio; // At most pan across 1.8 screenfuls (2 screenfuls + some parallax) // TODO: if we know the number of home screen pages, use that number here float maxPanScreenWidths = Math.min(1.8f, scaledBitmapToScreenAspectRatio); mCurrentViewport.left = MathUtil.interpolate(-1f, 1f, MathUtil.interpolate( (1 - maxPanScreenWidths / scaledBitmapToScreenAspectRatio) / 2, (1 + (maxPanScreenWidths - 2) / scaledBitmapToScreenAspectRatio) / 2, mNormalOffsetX)); mCurrentViewport.right = mCurrentViewport.left + 2f / scaledBitmapToScreenAspectRatio; mCurrentViewport.bottom = -1f / zoom; mCurrentViewport.top = 1f / zoom; float focusAmount = (mBlurKeyframes - mBlurAnimator.currentValue()) / mBlurKeyframes; if (mBlurRelatedToArtDetailMode && focusAmount > 0) { RectF artDetailViewport = WallpaperDetailViewport.Companion .getInstance().getViewport(mId); if (artDetailViewport.width() == 0 || artDetailViewport.height() == 0) { if (!mDemoMode && !mPreview) { // reset art detail viewport WallpaperDetailViewport.Companion .getInstance().setViewport(mId, MathUtil.uninterpolate(-1, 1, mCurrentViewport.left), MathUtil.uninterpolate(1, -1, mCurrentViewport.top), MathUtil.uninterpolate(-1, 1, mCurrentViewport.right), MathUtil.uninterpolate(1, -1, mCurrentViewport.bottom), false); } } else { // interpolate mCurrentViewport.left = MathUtil.interpolate( mCurrentViewport.left, MathUtil.interpolate(-1, 1, artDetailViewport.left), focusAmount); mCurrentViewport.top = MathUtil.interpolate( mCurrentViewport.top, MathUtil.interpolate(1, -1, artDetailViewport.top), focusAmount); mCurrentViewport.right = MathUtil.interpolate( mCurrentViewport.right, MathUtil.interpolate(-1, 1, artDetailViewport.right), focusAmount); mCurrentViewport.bottom = MathUtil.interpolate( mCurrentViewport.bottom, MathUtil.interpolate(1, -1, artDetailViewport.bottom), focusAmount); } } Matrix.orthoM(mPMatrix, 0, mCurrentViewport.left, mCurrentViewport.right, mCurrentViewport.bottom, mCurrentViewport.top, 1, 10); } public void drawFrame(float globalAlpha) { if (!mHasBitmap) { return; } Matrix.multiplyMM(mMVPMatrix, 0, mVMatrix, 0, mMMatrix, 0); Matrix.multiplyMM(mMVPMatrix, 0, mPMatrix, 0, mMVPMatrix, 0); float blurFrame = mBlurAnimator.currentValue(); int lo = (int) Math.floor(blurFrame); int hi = (int) Math.ceil(blurFrame); float localHiAlpha = (blurFrame - lo); if (globalAlpha <= 0) { // Nothing to draw } else if (lo == hi) { // Just draw one if (mPictures[lo] == null) { return; } mPictures[lo].draw(mMVPMatrix, globalAlpha); } else if (globalAlpha == 1) { // Simple drawing if (mPictures[lo] == null || mPictures[hi] == null) { return; } mPictures[lo].draw(mMVPMatrix, 1); mPictures[hi].draw(mMVPMatrix, localHiAlpha); } else { // If there's both a global and local alpha, re-compose alphas, to // effectively compose hi and lo before composing the result // with the background. // // The math, where a1,a2 are previous alphas and b1,b2 are new alphas: // b1 = a1 * (a2 - 1) / (a1 * a2 - 1) // b2 = a1 * a2 if (mPictures[lo] == null || mPictures[hi] == null) { return; } float newLocalLoAlpha = globalAlpha * (localHiAlpha - 1) / (globalAlpha * localHiAlpha - 1); float newLocalHiAlpha = globalAlpha * localHiAlpha; mPictures[lo].draw(mMVPMatrix, newLocalLoAlpha); mPictures[hi].draw(mMVPMatrix, newLocalHiAlpha); } } public void destroyPictures() { for (int i = 0; i < mPictures.length; i++) { if (mPictures[i] != null) { mPictures[i].destroy(); mPictures[i] = null; } } } } public void destroy() { mCurrentGLPictureSet.destroyPictures(); mNextGLPictureSet.destroyPictures(); } public boolean isBlurred() { return mIsBlurred; } public void setIsBlurred(final boolean isBlurred, final boolean artDetailMode) { if (artDetailMode && !isBlurred && !mDemoMode && !mPreview) { // Reset art detail viewport WallpaperDetailViewport.Companion.getInstance().setViewport(0, 0, 0, 0, 0, false); WallpaperDetailViewport.Companion.getInstance().setViewport(1, 0, 0, 0, 0, false); } mBlurRelatedToArtDetailMode = artDetailMode; mIsBlurred = isBlurred; mBlurAnimator.cancel(); mBlurAnimator .to(isBlurred ? mBlurKeyframes : 0) .withDuration(BLUR_ANIMATION_DURATION * (mDemoMode ? 5 : 1)) .withEndListener(new Runnable() { @Override public void run() { if (isBlurred && artDetailMode) { System.gc(); } } }) .start(); mCallbacks.requestRender(); } public interface Callbacks { void requestRender(); } } ================================================ FILE: presentation/src/main/java/com/yalin/style/render/TickingFloatAnimator.java ================================================ /* * Copyright 2014 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yalin.style.render; import android.animation.TimeInterpolator; import android.os.SystemClock; import android.view.animation.AccelerateDecelerateInterpolator; // Non thread-safe public class TickingFloatAnimator { private float mStartValue = 0; private float mCurrentValue; private float mEndValue; private boolean mRunning = false; private long mStartTime; private int mDuration = 1000; private Runnable mEndCallback; private TimeInterpolator mInterpolator = new AccelerateDecelerateInterpolator(); public static TickingFloatAnimator create() { return new TickingFloatAnimator(); } public TickingFloatAnimator from(float startValue) { cancel(); mStartValue = startValue; mCurrentValue = startValue; return this; } public TickingFloatAnimator to(float endValue) { mEndValue = endValue; return this; } public TickingFloatAnimator withDuration(int duration) { mDuration = Math.max(0, duration); return this; } public TickingFloatAnimator withInterpolator(TimeInterpolator interpolator) { mInterpolator = interpolator; return this; } public TickingFloatAnimator withEndListener(Runnable listener) { mEndCallback = listener; return this; } public void cancel() { mRunning = false; mEndValue = mCurrentValue; } public boolean tick() { if (!mRunning) { return false; } float t; if (mDuration <= 0) { t = 1; } else { t = (float) (SystemClock.elapsedRealtime() - mStartTime) * 1f / mDuration; if (t >= 1) { t = 1; } } if (t >= 1) { // Ended mRunning = false; mCurrentValue = mEndValue; if (mEndCallback != null) { mEndCallback.run(); } return false; } // Still running; compute value mCurrentValue = mStartValue + mInterpolator.getInterpolation(t) * (mEndValue - mStartValue); mRunning = true; return true; } public void start() { mRunning = true; mStartValue = mCurrentValue; mStartTime = SystemClock.elapsedRealtime(); tick(); } public boolean isRunning() { return mRunning; } public float currentValue() { return mCurrentValue; } private TickingFloatAnimator() { } } ================================================ FILE: presentation/src/main/java/com/yalin/style/settings/Prefs.java ================================================ package com.yalin.style.settings; import android.content.Context; import android.content.SharedPreferences; import android.support.v4.content.ContextCompat; /** * @author jinyalin * @since 2017/5/2. */ public class Prefs { public static final String PREF_GREY_AMOUNT = "grey_amount"; public static final String PREF_DIM_AMOUNT = "dim_amount"; public static final String PREF_BLUR_AMOUNT = "blur_amount"; public static final String PREF_DISABLE_BLUR_WHEN_LOCKED = "disable_blur_when_screen_locked_enabled"; private static final String WALLPAPER_PREFERENCES_NAME = "wallpaper_preferences"; private static final String PREF_MIGRATED = "migrated_from_default"; public synchronized static SharedPreferences getSharedPreferences(Context context) { Context deviceProtectedContext = ContextCompat.createDeviceProtectedStorageContext(context.getApplicationContext()); Context contextToUse = deviceProtectedContext != null ? deviceProtectedContext : context; return contextToUse.getSharedPreferences(WALLPAPER_PREFERENCES_NAME, Context.MODE_PRIVATE); } private Prefs() { } } ================================================ FILE: presentation/src/main/java/com/yalin/style/util/ImageLoader.java ================================================ package com.yalin.style.util; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.support.annotation.DrawableRes; import android.widget.ImageView; import com.bumptech.glide.BitmapRequestBuilder; import com.bumptech.glide.BitmapTypeRequest; import com.bumptech.glide.Glide; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelCache; import com.bumptech.glide.load.model.stream.BaseGlideUrlLoader; import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.bumptech.glide.request.RequestListener; import com.yalin.style.R; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author jinyalin * @since 2017/4/19. */ public class ImageLoader { private static final String TAG = "ImageLoader"; private static final ModelCache urlCache = new ModelCache<>(150); private final BitmapTypeRequest mGlideModelRequest; private final CenterCrop mCenterCrop; private int mPlaceHolderResId = -1; public ImageLoader(Context context) { VariableWidthImageLoader imageLoader = new VariableWidthImageLoader(context); mGlideModelRequest = Glide.with(context).using(imageLoader).from(String.class).asBitmap(); mCenterCrop = new CenterCrop(Glide.get(context).getBitmapPool()); } /** * Construct an ImageLoader with a default placeholder drawable. */ public ImageLoader(Context context, int placeHolderResId) { this(context); mPlaceHolderResId = placeHolderResId; } /** * Load an image from a url into an ImageView using the default placeholder * drawable if available. * * @param url The web URL of an image. * @param imageView The target ImageView to load the image into. * @param requestListener A listener to monitor the request result. */ public void loadImage(String url, ImageView imageView, RequestListener requestListener) { loadImage(url, imageView, requestListener, null, false); } /** * Load an image from a url into an ImageView using the given placeholder drawable. * * @param url The web URL of an image. * @param imageView The target ImageView to load the image into. * @param requestListener A listener to monitor the request result. * @param placeholderOverride A placeholder to use in place of the default placholder. */ public void loadImage(String url, ImageView imageView, RequestListener requestListener, Drawable placeholderOverride) { loadImage(url, imageView, requestListener, placeholderOverride, false /*crop*/); } /** * Load an image from a url into an ImageView using the default placeholder * drawable if available. * * @param url The web URL of an image. * @param imageView The target ImageView to load the image into. * @param requestListener A listener to monitor the request result. * @param placeholderOverride A drawable to use as a placeholder for this specific image. * If this parameter is present, {@link #mPlaceHolderResId} * if ignored for this request. */ public void loadImage(String url, ImageView imageView, RequestListener requestListener, Drawable placeholderOverride, boolean crop) { BitmapRequestBuilder request = beginImageLoad(url, requestListener, crop) .animate(R.anim.image_fade_in); if (placeholderOverride != null) { request.placeholder(placeholderOverride); } else if (mPlaceHolderResId != -1) { request.placeholder(mPlaceHolderResId); } request.into(imageView); } public BitmapRequestBuilder beginImageLoad(String url, RequestListener requestListener, boolean crop) { if (crop) { return mGlideModelRequest.load(url) .listener(requestListener) .transform(mCenterCrop); } else { return mGlideModelRequest.load(url) .listener(requestListener); } } /** * Load an image from a url into the given image view using the default placeholder if * available. * * @param url The web URL of an image. * @param imageView The target ImageView to load the image into. */ public void loadImage(String url, ImageView imageView) { loadImage(url, imageView, false /*crop*/); } /** * Load an image from a url into an ImageView using the default placeholder * drawable if available. * * @param url The web URL of an image. * @param imageView The target ImageView to load the image into. * @param crop True to apply a center crop to the image. */ public void loadImage(String url, ImageView imageView, boolean crop) { loadImage(url, imageView, null, null, crop); } public void loadImage(Context context, @DrawableRes int drawableResId, ImageView imageView) { Glide.with(context).load(drawableResId).into(imageView); } private static class VariableWidthImageLoader extends BaseGlideUrlLoader { private static final Pattern PATTERN = Pattern.compile("__w-((?:-?\\d+)+)__"); public VariableWidthImageLoader(Context context) { super(context, urlCache); } /** * If the URL contains a special variable width indicator (eg "__w-200-400-800__") * we get the buckets from the URL (200, 400 and 800 in the example) and replace * the URL with the best bucket for the requested width (the bucket immediately * larger than the requested width). */ @Override protected String getUrl(String model, int width, int height) { Matcher m = PATTERN.matcher(model); int bestBucket = 0; if (m.find()) { String[] found = m.group(1).split("-"); for (String bucketStr : found) { bestBucket = Integer.parseInt(bucketStr); if (bestBucket >= width) { // the best bucket is the first immediately bigger than the requested width break; } } if (bestBucket > 0) { model = m.replaceFirst("w" + bestBucket); } } return model; } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/util/MathUtil.java ================================================ package com.yalin.style.util; /** * YaLin 2016/12/30. */ public class MathUtil { public static float constrain(float min, float max, float v) { return Math.max(min, Math.min(max, v)); } public static float interpolate(float x1, float x2, float f) { return x1 + (x2 - x1) * f; } public static float uninterpolate(float x1, float x2, float v) { if (x2 - x1 == 0) { throw new IllegalArgumentException("Can't reverse interpolate with domain size of 0"); } return (v - x1) / (x2 - x1); } public static int floorEven(int num) { return num & ~0x01; } public static int roundMult4(int num) { return (num + 2) & ~0x03; } // divide two integers but round up // see http://stackoverflow.com/a/7446742/102703 public static int intDivideRoundUp(int num, int divisor) { int sign = (num > 0 ? 1 : -1) * (divisor > 0 ? 1 : -1); return sign * (Math.abs(num) + Math.abs(divisor) - 1) / Math.abs(divisor); } } ================================================ FILE: presentation/src/main/java/com/yalin/style/util/MultiSelectionController.kt ================================================ package com.yalin.style.util import android.os.Bundle import android.os.Parcelable import java.util.HashSet /** * @author jinyalin * @since 2017/5/25. */ class MultiSelectionController(val stateKey: String) { private val mSelection = HashSet() private val DUMMY_CALLBACKS: Callbacks = object : Callbacks { override fun onSelectionChanged(restored: Boolean, fromUser: Boolean) {} } private var mCallbacks: Callbacks? = DUMMY_CALLBACKS @Suppress("UNCHECKED_CAST") fun restoreInstanceState(savedInstanceState: Bundle?) { if (savedInstanceState != null) { mSelection.clear() val selection = savedInstanceState.getParcelableArray(stateKey) if (selection != null && selection.isNotEmpty()) { selection.mapTo(mSelection) { it as T } } } mCallbacks?.onSelectionChanged(true, false) } fun saveInstanceState(outBundle: Bundle) { val selection = arrayOfNulls(mSelection.size) for ((i, item) in mSelection.withIndex()) { selection[i] = item } outBundle.putParcelableArray(stateKey, selection) } fun setCallbacks(callbacks: Callbacks) { mCallbacks = callbacks if (mCallbacks == null) { mCallbacks = DUMMY_CALLBACKS } } fun getSelection(): Set { return HashSet(mSelection) } fun getSelectedCount(): Int { return mSelection.size } fun isSelecting(): Boolean { return mSelection.size > 0 } fun toggle(item: T, fromUser: Boolean) { if (mSelection.contains(item)) { mSelection.remove(item) } else { mSelection.add(item) } mCallbacks?.onSelectionChanged(false, fromUser) } fun reset(fromUser: Boolean) { mSelection.clear() mCallbacks?.onSelectionChanged(false, fromUser) } fun isSelected(item: T): Boolean { return mSelection.contains(item) } interface Callbacks { fun onSelectionChanged(restored: Boolean, fromUser: Boolean) } } ================================================ FILE: presentation/src/main/java/com/yalin/style/util/ScrimUtil.java ================================================ package com.yalin.style.util; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RectShape; import android.util.LruCache; import android.view.Gravity; /** * @author jinyalin * @since 2017/4/21. * Utility methods for creating prettier gradient scrims. */ public class ScrimUtil { private static final LruCache cubicGradientScrimCache = new LruCache<>(10); private ScrimUtil() { } /** * Creates an approximated cubic gradient using a multi-stop linear gradient. See * this post for more * details. */ public static Drawable makeCubicGradientScrimDrawable(int baseColor, int numStops, int gravity) { // Generate a cache key by hashing together the inputs, // based on the method described in the Effective Java book int cacheKeyHash = baseColor; cacheKeyHash = 31 * cacheKeyHash + numStops; cacheKeyHash = 31 * cacheKeyHash + gravity; Drawable cachedGradient = cubicGradientScrimCache.get(cacheKeyHash); if (cachedGradient != null) { return cachedGradient; } numStops = Math.max(numStops, 2); PaintDrawable paintDrawable = new PaintDrawable(); paintDrawable.setShape(new RectShape()); final int[] stopColors = new int[numStops]; int red = Color.red(baseColor); int green = Color.green(baseColor); int blue = Color.blue(baseColor); int alpha = Color.alpha(baseColor); for (int i = 0; i < numStops; i++) { float x = i * 1f / (numStops - 1); float opacity = MathUtil.constrain(0, 1, (float) Math.pow(x, 3)); stopColors[i] = Color.argb((int) (alpha * opacity), red, green, blue); } final float x0, x1, y0, y1; switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: x0 = 1; x1 = 0; break; case Gravity.RIGHT: x0 = 0; x1 = 1; break; default: x0 = 0; x1 = 0; break; } switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.TOP: y0 = 1; y1 = 0; break; case Gravity.BOTTOM: y0 = 0; y1 = 1; break; default: y0 = 0; y1 = 0; break; } paintDrawable.setShaderFactory(new ShapeDrawable.ShaderFactory() { @Override public Shader resize(int width, int height) { return new LinearGradient( width * x0, height * y0, width * x1, height * y1, stopColors, null, Shader.TileMode.CLAMP); } }); cubicGradientScrimCache.put(cacheKeyHash, paintDrawable); return paintDrawable; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/util/SettingsUtil.java ================================================ package com.yalin.style.util; /** * YaLin On 2017/1/2. */ public class SettingsUtil { } ================================================ FILE: presentation/src/main/java/com/yalin/style/util/ShareUtil.kt ================================================ package com.yalin.style.util import android.content.Context import android.content.Intent import com.yalin.style.R import com.yalin.style.model.WallpaperItem /** * @author jinyalin * @since 2017/5/14. */ class ShareUtil { companion object { fun getShareString(context: Context, wallpaperItem: WallpaperItem) = with(wallpaperItem) { val detailUrl = "www.kinglloy.com" val artist = byline.replaceFirst("\\.\\s*($|\\n).*".toRegex(), "").trim() val result = String.format( context.getString(R.string.share_text), title, artist, detailUrl) result } fun createShareIntent(context: Context, wallpaperItem: WallpaperItem): Intent { var shareIntent = Intent(Intent.ACTION_SEND) shareIntent.type = "text/plain" shareIntent.putExtra(Intent.EXTRA_TEXT, ShareUtil.Companion .getShareString(context, wallpaperItem)) shareIntent = Intent.createChooser(shareIntent, context.getString(R.string.share_title)) return shareIntent; } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/util/SvgPathParser.java ================================================ package com.yalin.style.util; import android.graphics.Path; import android.graphics.PointF; import java.text.ParseException; /** * YaLin 2017/1/4. */ public class SvgPathParser { private static final int TOKEN_ABSOLUTE_COMMAND = 1; private static final int TOKEN_RELATIVE_COMMAND = 2; private static final int TOKEN_VALUE = 3; private static final int TOKEN_EOF = 4; private int mCurrentToken; private PointF mCurrentPoint = new PointF(); private int mLength; private int mIndex; private String mPathString; protected float transformX(float x) { return x; } protected float transformY(float y) { return y; } public Path parsePath(String s) throws ParseException { mCurrentPoint.set(Float.NaN, Float.NaN); mPathString = s; mIndex = 0; mLength = mPathString.length(); PointF tempPoint1 = new PointF(); PointF tempPoint2 = new PointF(); PointF tempPoint3 = new PointF(); Path p = new Path(); p.setFillType(Path.FillType.WINDING); boolean firstMove = true; while (mIndex < mLength) { char command = consumeCommand(); boolean relative = (mCurrentToken == TOKEN_RELATIVE_COMMAND); switch (command) { case 'M': case 'm': { // move command boolean firstPoint = true; while (advanceToNextToken() == TOKEN_VALUE) { consumeAndTransformPoint(tempPoint1, relative && mCurrentPoint.x != Float.NaN); if (firstPoint) { p.moveTo(tempPoint1.x, tempPoint1.y); firstPoint = false; if (firstMove) { mCurrentPoint.set(tempPoint1); firstMove = false; } } else { p.lineTo(tempPoint1.x, tempPoint1.y); } } mCurrentPoint.set(tempPoint1); break; } case 'C': case 'c': { // curve command if (mCurrentPoint.x == Float.NaN) { throw new ParseException("Relative commands require current point", mIndex); } while (advanceToNextToken() == TOKEN_VALUE) { consumeAndTransformPoint(tempPoint1, relative); consumeAndTransformPoint(tempPoint2, relative); consumeAndTransformPoint(tempPoint3, relative); p.cubicTo(tempPoint1.x, tempPoint1.y, tempPoint2.x, tempPoint2.y, tempPoint3.x, tempPoint3.y); } mCurrentPoint.set(tempPoint3); break; } case 'L': case 'l': { // line command if (mCurrentPoint.x == Float.NaN) { throw new ParseException("Relative commands require current point", mIndex); } while (advanceToNextToken() == TOKEN_VALUE) { consumeAndTransformPoint(tempPoint1, relative); p.lineTo(tempPoint1.x, tempPoint1.y); } mCurrentPoint.set(tempPoint1); break; } case 'H': case 'h': { // horizontal line command if (mCurrentPoint.x == Float.NaN) { throw new ParseException("Relative commands require current point", mIndex); } while (advanceToNextToken() == TOKEN_VALUE) { float x = transformX(consumeValue()); if (relative) { x += mCurrentPoint.x; } p.lineTo(x, mCurrentPoint.y); } mCurrentPoint.set(tempPoint1); break; } case 'V': case 'v': { // vertical line command if (mCurrentPoint.x == Float.NaN) { throw new ParseException("Relative commands require current point", mIndex); } while (advanceToNextToken() == TOKEN_VALUE) { float y = transformY(consumeValue()); if (relative) { y += mCurrentPoint.y; } p.lineTo(mCurrentPoint.x, y); } mCurrentPoint.set(tempPoint1); break; } case 'Q': case 'q': { // curve command if (mCurrentPoint.x == Float.NaN) { throw new ParseException("Relative commands require current point", mIndex); } while (advanceToNextToken() == TOKEN_VALUE) { consumeAndTransformPoint(tempPoint1, relative); consumeAndTransformPoint(tempPoint2, relative); p.quadTo(tempPoint1.x, tempPoint1.y, tempPoint2.x, tempPoint2.y); } mCurrentPoint.set(tempPoint2); break; } case 'Z': case 'z': { // close command p.close(); break; } } } return p; } private int advanceToNextToken() { while (mIndex < mLength) { char c = mPathString.charAt(mIndex); if ('a' <= c && c <= 'z') { return (mCurrentToken = TOKEN_RELATIVE_COMMAND); } else if ('A' <= c && c <= 'Z') { return (mCurrentToken = TOKEN_ABSOLUTE_COMMAND); } else if (('0' <= c && c <= '9') || c == '.' || c == '-') { return (mCurrentToken = TOKEN_VALUE); } // skip unrecognized character ++mIndex; } return (mCurrentToken = TOKEN_EOF); } private char consumeCommand() throws ParseException { advanceToNextToken(); if (mCurrentToken != TOKEN_RELATIVE_COMMAND && mCurrentToken != TOKEN_ABSOLUTE_COMMAND) { throw new ParseException("Expected command", mIndex); } return mPathString.charAt(mIndex++); } private void consumeAndTransformPoint(PointF out, boolean relative) throws ParseException { out.x = transformX(consumeValue()); out.y = transformY(consumeValue()); if (relative) { out.x += mCurrentPoint.x; out.y += mCurrentPoint.y; } } private float consumeValue() throws ParseException { advanceToNextToken(); if (mCurrentToken != TOKEN_VALUE) { throw new ParseException("Expected value", mIndex); } boolean start = true; boolean seenDot = false; int index = mIndex; while (index < mLength) { char c = mPathString.charAt(index); if (!('0' <= c && c <= '9') && (c != '.' || seenDot) && (c != '-' || !start)) { // end of value break; } if (c == '.') { seenDot = true; } start = false; ++index; } if (index == mIndex) { throw new ParseException("Expected value", mIndex); } String str = mPathString.substring(mIndex, index); try { float value = Float.parseFloat(str); mIndex = index; return value; } catch (NumberFormatException e) { throw new ParseException("Invalid float value '" + str + "'.", mIndex); } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/util/TypefaceUtil.java ================================================ package com.yalin.style.util; import android.content.Context; import android.graphics.Typeface; import java.util.HashMap; import java.util.Map; /** * @author jinyalin * @since 2017/4/21. */ public class TypefaceUtil { private static final Map sTypefaceCache = new HashMap<>(); public static Typeface getAndCache(Context context, String assetPath) { synchronized (sTypefaceCache) { if (!sTypefaceCache.containsKey(assetPath)) { Typeface tf = Typeface.createFromAsset( context.getApplicationContext().getAssets(), assetPath); sTypefaceCache.put(assetPath, tf); } return sTypefaceCache.get(assetPath); } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/AdvanceSettingView.kt ================================================ package com.yalin.style.view import com.yalin.style.model.AdvanceWallpaperItem /** * @author jinyalin * @since 2017/7/28. */ interface AdvanceSettingView : LoadingDataView { fun renderWallpapers(wallpapers: List) fun showEmpty() fun complete() fun wallpaperSelected(wallpaperId: String) fun showDownloadHintDialog(item: AdvanceWallpaperItem) fun showDownloadingDialog(item: AdvanceWallpaperItem) fun updateDownloadingProgress(downloaded: Long) fun downloadComplete(item: AdvanceWallpaperItem) fun showDownloadError(item: AdvanceWallpaperItem, e: Exception) fun showAd(item: AdvanceWallpaperItem) fun adViewed(item: AdvanceWallpaperItem) } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/GallerySettingView.kt ================================================ package com.yalin.style.view import com.yalin.style.model.GalleryWallpaperItem /** * @author jinyalin * @since 2017/5/25. */ interface GallerySettingView : LoadingDataView { fun renderGalleryWallpapers(wallpaperItems: List) fun renderUpdateInterval(intervalMin: Int) } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/LoadingDataView.kt ================================================ package com.yalin.style.view import android.content.Context /** * @author jinyalin * * * @since 2017/4/20. */ interface LoadingDataView { /** * Show a view with a progress bar indicating a loading process. */ fun showLoading() /** * Hide a loading view. */ fun hideLoading() /** * Show a retry view in case of an error when retrieving data. */ fun showRetry() /** * Hide a retry view shown if there was an error when retrieving data. */ fun hideRetry() /** * Show an error message * @param message A string representing an error. */ fun showError(message: String) /** * Get a [android.content.Context]. */ fun context(): Context } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/SourceChooseView.kt ================================================ package com.yalin.style.view import com.yalin.style.model.SourceItem /** * @author jinyalin * @since 2017/5/23. */ interface SourceChooseView : LoadingDataView { fun renderSources(sources: List) fun sourceSelected(sources: List, selectedItem: SourceItem) } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/WallpaperDetailView.kt ================================================ package com.yalin.style.view import android.content.Intent import com.yalin.style.model.WallpaperItem /** * @author jinyalin * * * @since 2017/4/20. */ interface WallpaperDetailView : LoadingDataView { fun renderWallpaper(wallpaperItem: WallpaperItem) fun showNextButton(show: Boolean) fun shareWallpaper(shareIntent: Intent) fun validLikeAction(valid: Boolean) fun updateLikeState(item: WallpaperItem, liked: Boolean) } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/activity/AboutActivity.kt ================================================ package com.yalin.style.view.activity import android.content.Intent import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.text.Html import android.text.method.LinkMovementMethod import android.view.View import android.view.ViewPropertyAnimator import com.yalin.style.BuildConfig import com.yalin.style.R import com.yalin.style.analytics.Analytics import com.yalin.style.analytics.Event import com.yalin.style.view.fragment.AnimatedStyleLogoFragment import com.yalin.style.view.fragment.StyleRenderFragment import kotlinx.android.synthetic.main.activity_about.* import kotlinx.android.synthetic.main.layout_include_about_content.* /** * @author jinyalin * * * @since 2017/5/3. */ class AboutActivity : AppCompatActivity() { private var mAnimator: ViewPropertyAnimator? = null public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_about) maybeLogEvent(intent) window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE appBar.setNavigationOnClickListener { onNavigateUp() } if (savedInstanceState == null) { fragmentManager.beginTransaction() .add(R.id.demoViewContainer, StyleRenderFragment.createInstance(true, true)) .commit() } // Build the about body view and append the link to see OSS licenses appVersion.text = Html.fromHtml( getString(R.string.about_version_template, BuildConfig.VERSION_NAME)) aboutBody.text = Html.fromHtml(getString(R.string.about_body)) aboutBody.movementMethod = LinkMovementMethod() } private fun maybeLogEvent(intent: Intent) { val uri = intent.data uri?.let { Analytics.logEvent(this, Event.SHORTCUTS_ABOUT) } } override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) demoViewContainer.alpha = 0f mAnimator = demoViewContainer.animate() .alpha(1f) .setStartDelay(250) .setDuration(1000) .withEndAction { val logoFragment = fragmentManager.findFragmentById(R.id.animatedLogoFragment) as AnimatedStyleLogoFragment logoFragment.start() } } override fun onDestroy() { super.onDestroy() if (mAnimator != null) { mAnimator!!.cancel() } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/activity/AdvanceSettingActivity.kt ================================================ package com.yalin.style.view.activity import android.content.Context import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.support.v4.content.ContextCompat import android.support.v4.view.ViewCompat import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.GridLayoutManager import android.support.v7.widget.RecyclerView import android.text.Html import android.text.TextUtils import android.view.* import android.widget.ImageView import android.widget.TextView import com.afollestad.materialdialogs.MaterialDialog import com.bumptech.glide.Glide import com.google.android.gms.ads.AdListener import com.yalin.style.R import com.yalin.style.StyleApplication import com.yalin.style.analytics.Analytics import com.yalin.style.analytics.Event import com.yalin.style.data.log.LogUtil import com.yalin.style.data.utils.WallpaperFileHelper import com.yalin.style.exception.ErrorMessageFactory import com.yalin.style.model.AdvanceWallpaperItem import com.yalin.style.presenter.AdvanceSettingPresenter import com.yalin.style.util.ImageLoader import com.yalin.style.util.maybeAttachAd import com.yalin.style.util.maybeAttachInterstitialAd import com.yalin.style.util.maybeShowInterstitialAd import com.yalin.style.view.AdvanceSettingView import com.yalin.style.view.component.DownloadingDialog import kotlinx.android.synthetic.main.activity_advance_setting.* import org.jetbrains.anko.toast import java.util.ArrayList import javax.inject.Inject /** * @author jinyalin * @since 2017/7/28. */ class AdvanceSettingActivity : BaseActivity(), AdvanceSettingView { companion object { val TAG = "AdvanceSettingActivity" val LOAD_STATE = "load_state" val LOAD_STATE_NORMAL = 0 val LOAD_STATE_LOADING = 1 val LOAD_STATE_RETRY = 2 } @Inject lateinit internal var presenter: AdvanceSettingPresenter val wallpapers = ArrayList() var imageLoader: ImageLoader? = null private var loadState = LOAD_STATE_NORMAL private var placeHolderDrawable: ColorDrawable? = null private var mItemSize = 10 private var downloadDialog: DownloadingDialog? = null private val insertAdListener = object : AdListener() { var currentAdItem: AdvanceWallpaperItem? = null override fun onAdLeftApplication() { super.onAdLeftApplication() } override fun onAdFailedToLoad(p0: Int) { super.onAdFailedToLoad(p0) adLoaded = true maybeShowWallpaper() } override fun onAdClosed() { super.onAdClosed() if (currentAdItem != null) { presenter.adViewed(currentAdItem!!) currentAdItem = null } } override fun onAdOpened() { super.onAdOpened() } override fun onAdLoaded() { super.onAdLoaded() adLoaded = true maybeShowWallpaper() } } private var adLoaded = false private var wallpaperLoaded = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) StyleApplication.instance.applicationComponent.inject(this) setContentView(R.layout.activity_advance_setting) setSupportActionBar(appBar) placeHolderDrawable = ColorDrawable(ContextCompat.getColor(this, R.color.gallery_chosen_photo_placeholder)) initViews() imageLoader = ImageLoader(this) presenter.setView(this) if (savedInstanceState != null) { loadState = savedInstanceState.getInt(LOAD_STATE) presenter.onRestoreInstanceState(savedInstanceState) } handleState() maybeAttachAd(this) maybeAttachInterstitialAd(this, insertAdListener) } private fun handleState() { if (loadState == LOAD_STATE_NORMAL) { presenter.initialize() } else if (loadState == LOAD_STATE_LOADING) { presenter.loadAdvanceWallpaper() } else if (loadState == LOAD_STATE_RETRY) { showRetry() } } private fun initViews() { val itemAnimator = DefaultItemAnimator() itemAnimator.supportsChangeAnimations = false wallpaperList.itemAnimator = itemAnimator val gridLayoutManager = GridLayoutManager(this, 1) wallpaperList.layoutManager = gridLayoutManager btnLoadAdvanceWallpaper.setOnClickListener { presenter.loadAdvanceWallpaper() Analytics.logEvent(this@AdvanceSettingActivity, Event.LOAD_ADVANCES) } btnRetry.setOnClickListener { presenter.loadAdvanceWallpaper() Analytics.logEvent(this@AdvanceSettingActivity, Event.LOAD_ADVANCES) } wallpaperList.viewTreeObserver .addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { val width = wallpaperList.width - wallpaperList.paddingStart - wallpaperList.paddingEnd if (width <= 0) { return } // Compute number of columns val maxItemWidth = resources.getDimensionPixelSize( R.dimen.advance_grid_max_item_size) var numColumns = 1 while (true) { if (width / numColumns > maxItemWidth) { ++numColumns } else { break } } val spacing = resources.getDimensionPixelSize( R.dimen.gallery_chosen_photo_grid_spacing) mItemSize = (width - spacing * (numColumns - 1)) / numColumns // Complete setup gridLayoutManager.spanCount = numColumns advanceWallpaperAdapter.setHasStableIds(true) wallpaperList.adapter = advanceWallpaperAdapter wallpaperList.viewTreeObserver.removeOnGlobalLayoutListener(this) } }) ViewCompat.setOnApplyWindowInsetsListener(wallpaperList) { v, insets -> val gridSpacing = resources .getDimensionPixelSize(R.dimen.gallery_chosen_photo_grid_spacing) ViewCompat.onApplyWindowInsets(v, insets.replaceSystemWindowInsets( insets.systemWindowInsetLeft + gridSpacing, gridSpacing, insets.systemWindowInsetRight + gridSpacing, insets.systemWindowInsetBottom + insets.systemWindowInsetTop + gridSpacing)) insets } downloadDialog = DownloadingDialog(this) } override fun onSaveInstanceState(outState: Bundle) { outState.putInt(LOAD_STATE, loadState) presenter.onSaveInstanceState(outState) super.onSaveInstanceState(outState) } override fun onResume() { super.onResume() presenter.resume() } override fun onPause() { super.onPause() presenter.pause() } override fun onDestroy() { super.onDestroy() presenter.destroy() } override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) menuInflater.inflate(R.menu.advance_activity, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.action_advance_hint) { val dialogBuilder = MaterialDialog.Builder(this) .iconRes(R.drawable.advance_wallpaper_msg) .title(R.string.hint) .content(Html.fromHtml(getString(R.string.advance_hint))) .positiveText(R.string.confirm) dialogBuilder.build().show() return true } return super.onOptionsItemSelected(item) } override fun renderWallpapers(wallpapers: List) { loadState = LOAD_STATE_NORMAL this.wallpapers.clear() this.wallpapers.addAll(wallpapers) wallpaperLoaded = true maybeShowWallpaper() } private fun maybeShowWallpaper() { if (wallpaperList.visibility == View.VISIBLE) { return } if (adLoaded && wallpaperLoaded) { wallpaperList.visibility = View.VISIBLE empty.visibility = View.GONE loading.visibility = View.GONE retry.visibility = View.GONE advanceWallpaperAdapter.notifyDataSetChanged() } } override fun showLoading() { loadState = LOAD_STATE_LOADING wallpaperList.visibility = View.GONE empty.visibility = View.GONE loading.visibility = View.VISIBLE retry.visibility = View.GONE } override fun hideLoading() { } override fun showRetry() { loadState = LOAD_STATE_RETRY wallpaperList.visibility = View.GONE empty.visibility = View.GONE loading.visibility = View.GONE retry.visibility = View.VISIBLE } override fun hideRetry() { } override fun showError(message: String) { toast(message) } override fun showEmpty() { wallpaperList.visibility = View.GONE empty.visibility = View.VISIBLE loading.visibility = View.GONE retry.visibility = View.GONE } override fun context(): Context { return applicationContext } override fun complete() { finish() } override fun wallpaperSelected(wallpaperId: String) { wallpapers.forEach { it -> it.isSelected = TextUtils.equals(it.wallpaperId, wallpaperId) } advanceWallpaperAdapter.notifyDataSetChanged() } override fun showDownloadHintDialog(item: AdvanceWallpaperItem) { val needAd = item.needAd val downloadCallback = MaterialDialog.SingleButtonCallback { _, _ -> presenter.requestDownload(item) Analytics.logEvent(this@AdvanceSettingActivity, Event.DOWNLOAD_COMPONENT, item.name) } val adCallback = MaterialDialog.SingleButtonCallback { dialog, which -> downloadCallback.onClick(dialog, which) showAd(item) } val dialogBuilder = MaterialDialog.Builder(this) .iconRes(R.drawable.advance_wallpaper_msg) .title(R.string.hint) .content(if (needAd) Html.fromHtml(getString(R.string.advance_ad_download_hint)) else Html.fromHtml(getString(R.string.advance_download_hint))) .positiveText(if (needAd) R.string.advance_ad_download_msg else R.string.advance_download_msg) .onPositive(if (needAd) adCallback else downloadCallback) dialogBuilder.build().show() } override fun showDownloadingDialog(item: AdvanceWallpaperItem) { LogUtil.D(TAG, "showDownloadingDialog ${item.name}") downloadDialog!!.show() } override fun updateDownloadingProgress(downloaded: Long) { LogUtil.D(TAG, "updateDownloadingProgress $downloaded") downloadDialog!!.updateProgress(downloaded) } override fun downloadComplete(item: AdvanceWallpaperItem) { val position = wallpapers.indices.firstOrNull { TextUtils.equals(wallpapers[it].wallpaperId, item.wallpaperId) } ?: -1 if (position >= 0) { advanceWallpaperAdapter.notifyItemChanged(position) } downloadDialog!!.dismiss() } override fun showDownloadError(item: AdvanceWallpaperItem, e: Exception) { downloadDialog!!.dismiss() showError(ErrorMessageFactory.create(this, e)) } override fun showAd(item: AdvanceWallpaperItem) { if (maybeShowInterstitialAd()) { insertAdListener.currentAdItem = item } else { adViewed(item) } } override fun adViewed(item: AdvanceWallpaperItem) { val position = wallpapers.indices.firstOrNull { TextUtils.equals(wallpapers[it].wallpaperId, item.wallpaperId) } ?: -1 if (position >= 0) { wallpapers[position].needAd = false } } class AdvanceViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var checkedOverlayView: View = itemView.findViewById(R.id.checked_overlay) var downloadOverlayView: View = itemView.findViewById(R.id.download_overlay) var thumbnail: ImageView = itemView.findViewById(R.id.thumbnail) as ImageView var tvName: TextView = itemView.findViewById(R.id.tvName) as TextView } private val advanceWallpaperAdapter = object : RecyclerView.Adapter() { override fun onBindViewHolder(holder: AdvanceViewHolder, position: Int) { val item = wallpapers[position] holder.thumbnail.layoutParams.width = mItemSize holder.thumbnail.layoutParams.height = mItemSize Glide.with(this@AdvanceSettingActivity) .load(item.iconUrl) .override(mItemSize, mItemSize) .placeholder(placeHolderDrawable) .into(holder.thumbnail) if (item.isSelected) { holder.checkedOverlayView.visibility = View.VISIBLE } else { holder.checkedOverlayView.visibility = View.GONE } val downloadingItem = presenter.getDownloadingItem() if (WallpaperFileHelper.isNeedDownloadAdvanceComponent(item.lazyDownload, item.storePath) || (downloadingItem != null && TextUtils.equals(downloadingItem.wallpaperId, item.wallpaperId))) { holder.downloadOverlayView.visibility = View.VISIBLE } else { holder.downloadOverlayView.visibility = View.GONE } holder.tvName.text = item.name } override fun getItemCount(): Int { return wallpapers.size } override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): AdvanceViewHolder { val view = LayoutInflater.from(this@AdvanceSettingActivity) .inflate(R.layout.advance_chosen_wallpaper_item, parent, false) val vh = AdvanceViewHolder(view) view.setOnClickListener { val item = wallpapers[vh.adapterPosition] presenter.selectAdvanceWallpaper(item) } return vh } override fun getItemId(position: Int): Long { return wallpapers[position].id } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/activity/BaseActivity.kt ================================================ package com.yalin.style.view.activity import android.support.v7.app.AppCompatActivity import com.yalin.style.StyleApplication import com.yalin.style.injection.component.ApplicationComponent /** * @author jinyalin * * * @since 2017/4/20. */ abstract class BaseActivity : AppCompatActivity() { protected val applicationComponent: ApplicationComponent get() = (application as StyleApplication).applicationComponent } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/activity/GallerySettingActivity.kt ================================================ package com.yalin.style.view.activity import android.Manifest import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.annotation.TargetApi import android.app.Activity import android.content.* import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.graphics.drawable.ColorDrawable import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler import android.provider.Settings import android.support.design.widget.Snackbar import android.support.v4.app.ActivityCompat import android.support.v4.content.ContextCompat import android.support.v4.view.ViewCompat import android.support.v7.app.AlertDialog import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.GridLayoutManager import android.support.v7.widget.RecyclerView import android.support.v7.widget.Toolbar import android.text.TextUtils import android.util.SparseIntArray import android.view.* import android.widget.ImageView import com.bumptech.glide.Glide import com.yalin.style.R import com.yalin.style.StyleApplication import com.yalin.style.analytics.Analytics import com.yalin.style.analytics.Event import com.yalin.style.data.utils.getDisplayNameForTreeUri import com.yalin.style.data.utils.getImagesFromTreeUri import com.yalin.style.model.GalleryWallpaperItem import com.yalin.style.presenter.GallerySettingPresenter import com.yalin.style.util.MultiSelectionController import com.yalin.style.util.maybeAttachAd import com.yalin.style.view.GallerySettingView import kotlinx.android.synthetic.main.activity_gallery_setting.* import org.jetbrains.anko.toast import java.util.* import javax.inject.Inject /** * @author jinyalin * @since 2017/5/24. */ class GallerySettingActivity : BaseActivity(), GallerySettingView { companion object { private val REQUEST_CHOOSE_PHOTOS = 1 private val REQUEST_CHOOSE_FOLDER = 2 private val REQUEST_STORAGE_PERMISSION = 3 private val SHARED_PREF_NAME = "GallerySettingsActivity" private val SHOW_INTERNAL_STORAGE_MESSAGE = "show_internal_storage_message" private val ITEM_TYPE_URI = 0 private val ITEM_TYPE_TREE = 1 private val STATE_SELECTION = "selection" } @Inject lateinit internal var presenter: GallerySettingPresenter private var mPlaceholderDrawable: ColorDrawable? = null private var mPlaceholderSmallDrawable: ColorDrawable? = null private var mItemSize = 10 private val mMultiSelectionController = MultiSelectionController(STATE_SELECTION) private var mUpdatePosition = -1 private val mWallpapers = ArrayList() private var mLastTouchPosition: Int = 0 private var mLastTouchX: Int = 0 private var mLastTouchY: Int = 0 private val mGetContentActivities = ArrayList() private val sRotateMenuIdsByMin = SparseIntArray() private val sRotateMinsByMenuId = SparseIntArray() init { sRotateMenuIdsByMin.put(0, R.id.action_rotate_interval_none) sRotateMenuIdsByMin.put(60, R.id.action_rotate_interval_1h) sRotateMenuIdsByMin.put(60 * 3, R.id.action_rotate_interval_3h) sRotateMenuIdsByMin.put(60 * 6, R.id.action_rotate_interval_6h) sRotateMenuIdsByMin.put(60 * 24, R.id.action_rotate_interval_24h) sRotateMenuIdsByMin.put(60 * 72, R.id.action_rotate_interval_72h) for (i in 0..(sRotateMenuIdsByMin.size() - 1)) { sRotateMinsByMenuId.put(sRotateMenuIdsByMin.valueAt(i), sRotateMenuIdsByMin.keyAt(i)) } } private var mOptionsMenu: Menu? = null private var mUpdateInterval: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) StyleApplication.instance.applicationComponent.inject(this) setContentView(R.layout.activity_gallery_setting) setSupportActionBar(appBar) mPlaceholderDrawable = ColorDrawable(ContextCompat.getColor(this, R.color.gallery_chosen_photo_placeholder)) mPlaceholderSmallDrawable = ColorDrawable(ContextCompat.getColor(this, R.color.gallery_chosen_photo_placeholder)) initViews() presenter.setView(this) presenter.initialize() maybeAttachAd(this) } override fun onResume() { super.onResume() presenter.resume() onDataSetChanged() } override fun onPause() { super.onPause() presenter.pause() } override fun onDestroy() { super.onDestroy() presenter.destroy() } private fun initViews() { val itemAnimator = DefaultItemAnimator() itemAnimator.supportsChangeAnimations = false photoGrid.itemAnimator = itemAnimator setupMultiSelect() val gridLayoutManager = GridLayoutManager(this, 1) photoGrid.layoutManager = gridLayoutManager val vto = photoGrid.viewTreeObserver vto.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { val width = photoGrid.width - photoGrid.paddingStart - photoGrid.paddingEnd if (width <= 0) { return } // Compute number of columns val maxItemWidth = resources.getDimensionPixelSize( R.dimen.gallery_chosen_photo_grid_max_item_size) var numColumns = 1 while (true) { if (width / numColumns > maxItemWidth) { ++numColumns } else { break } } val spacing = resources.getDimensionPixelSize( R.dimen.gallery_chosen_photo_grid_spacing) mItemSize = (width - spacing * (numColumns - 1)) / numColumns // Complete setup gridLayoutManager.spanCount = numColumns mChosenPhotosAdapter.setHasStableIds(true) photoGrid.adapter = mChosenPhotosAdapter photoGrid.viewTreeObserver.removeOnGlobalLayoutListener(this) tryUpdateSelection(false) } }) ViewCompat.setOnApplyWindowInsetsListener(photoGrid) { v, insets -> val gridSpacing = resources .getDimensionPixelSize(R.dimen.gallery_chosen_photo_grid_spacing) ViewCompat.onApplyWindowInsets(v, insets.replaceSystemWindowInsets( insets.systemWindowInsetLeft + gridSpacing, gridSpacing, insets.systemWindowInsetRight + gridSpacing, insets.systemWindowInsetBottom + insets.systemWindowInsetTop + gridSpacing + resources.getDimensionPixelSize(R.dimen.gallery_fab_space))) insets } btnGalleryEnableRandom.setOnClickListener { requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_STORAGE_PERMISSION) } btnGalleryEditPermissionSettings.setOnClickListener { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", packageName, null)) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(intent) } addFab.setOnClickListener { Analytics.logEvent(this, Event.ADD_PHOTO_CLICK) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // On Lollipop and higher, we show the add toolbar to allow users to add either // individual photos or a whole directory showAddToolbar() } else { requestPhotos() } } addPhotos.setOnClickListener { requestPhotos() } addFolder.setOnClickListener { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) try { startActivityForResult(intent, REQUEST_CHOOSE_FOLDER) val preferences = getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) if (preferences.getBoolean(SHOW_INTERNAL_STORAGE_MESSAGE, true)) { toast(R.string.gallery_internal_storage_message) } } catch (e: ActivityNotFoundException) { Snackbar.make(photoGrid, R.string.gallery_add_folder_error, Snackbar.LENGTH_LONG).show() hideAddToolbar(true) } } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode != REQUEST_STORAGE_PERMISSION) { return } onDataSetChanged() } override fun renderGalleryWallpapers(wallpaperItems: List) { mWallpapers.clear() mWallpapers.addAll(wallpaperItems) mChosenPhotosAdapter.notifyDataSetChanged() onDataSetChanged() } override fun renderUpdateInterval(intervalMin: Int) { mUpdateInterval = intervalMin val menuId = sRotateMenuIdsByMin[intervalMin] if (menuId != 0 && mOptionsMenu != null) { val item = mOptionsMenu!!.findItem(menuId) item?.isChecked = true } } override fun showLoading() { } override fun hideLoading() { } override fun showRetry() { } override fun hideRetry() { } override fun showError(message: String) { toast(message) } override fun context(): Context { return this.applicationContext } override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) menuInflater.inflate(R.menu.gallery_activity, menu) mOptionsMenu = menu val menuId = sRotateMenuIdsByMin[mUpdateInterval] if (menuId != 0 && mOptionsMenu != null) { val item = mOptionsMenu!!.findItem(menuId) item?.isChecked = true } return true } override fun onPrepareOptionsMenu(menu: Menu): Boolean { super.onPrepareOptionsMenu(menu) // Make sure the 'Import photos' MenuItem is set up properly based on the number of // activities that handle ACTION_GET_CONTENT // 0 = hide the MenuItem // 1 = show 'Import photos from APP_NAME' to go to the one app that exists // 2 = show 'Import photos...' to have the user pick which app to import photos from val intent = Intent(Intent.ACTION_GET_CONTENT) intent.type = "image/*" intent.addCategory(Intent.CATEGORY_OPENABLE) val getContentActivities = packageManager.queryIntentActivities(intent, 0) mGetContentActivities.clear() for (info in getContentActivities) { // Filter out the default system UI if (TextUtils.equals(info.activityInfo.packageName, "com.android.documentsui")) { continue } // Filter out non-exported activities if (!info.activityInfo.exported) { continue } // Filter out activities we don't have permission to start if (!TextUtils.isEmpty(info.activityInfo.permission) && packageManager.checkPermission(info.activityInfo.permission, packageName) != PackageManager.PERMISSION_GRANTED) { continue } mGetContentActivities.add(info.activityInfo) } // Hide the 'Import photos' action if there are no activities found val importPhotosMenuItem = menu.findItem(R.id.action_import_photos) importPhotosMenuItem.isVisible = !mGetContentActivities.isEmpty() // If there's only one app that supports ACTION_GET_CONTENT, tell the user what that app is if (mGetContentActivities.size == 1) { importPhotosMenuItem.title = getString(R.string.gallery_action_import_photos_from, mGetContentActivities[0].loadLabel(packageManager)) } else { importPhotosMenuItem.setTitle(R.string.gallery_action_import_photos) } return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { val itemId = item.itemId val rotateMin = sRotateMinsByMenuId.get(itemId, -1) if (rotateMin != -1) { Analytics.logEvent(this, Event.SETUP_UPDATE_INTERVAL, rotateMin.toString()) presenter.setUpdateInterval(rotateMin) item.isChecked = true return true } if (itemId == R.id.action_import_photos) { Analytics.logEvent(this, Event.IMPORT_FROM_GALLERY) if (mGetContentActivities.size == 1) { // Just start the one ACTION_GET_CONTENT app requestGetContent(mGetContentActivities[0]) } else { // Let the user pick which app they want to import photos from val packageManager = packageManager val items = arrayOfNulls(mGetContentActivities.size) for (h in mGetContentActivities.indices) { items[h] = mGetContentActivities[h].loadLabel(packageManager) } AlertDialog.Builder(this) .setTitle(R.string.gallery_import_dialog_title) .setItems(items, { _, which -> requestGetContent(mGetContentActivities[which]) }) .show() } return true } else if (itemId == R.id.action_clear_photos) { Analytics.logEvent(this, Event.CLEAR_WALLPAPER) presenter.removeGalleryWallpaper(mWallpapers) return true } return super.onOptionsItemSelected(item) } private fun requestGetContent(info: ActivityInfo) { val intent = Intent(Intent.ACTION_GET_CONTENT) intent.type = "image/*" intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setClassName(info.packageName, info.name) intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) startActivityForResult(intent, REQUEST_CHOOSE_PHOTOS) } private fun requestPhotos() { // Use ACTION_OPEN_DOCUMENT by default for adding photos. // This allows us to use persistent URI permissions to access the underlying photos // meaning we don't need to use additional storage space and will pull in edits automatically // in addition to syncing deletions. // (There's a separate 'Import photos' option which uses ACTION_GET_CONTENT to support legacy apps) val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.type = "image/*" intent.addCategory(Intent.CATEGORY_OPENABLE) intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) try { startActivityForResult(intent, REQUEST_CHOOSE_PHOTOS) } catch (e: ActivityNotFoundException) { Snackbar.make(photoGrid, R.string.gallery_add_photos_error, Snackbar.LENGTH_LONG).show() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { hideAddToolbar(true) } } } override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { super.onActivityResult(requestCode, resultCode, result) if (requestCode != REQUEST_CHOOSE_PHOTOS && requestCode != REQUEST_CHOOSE_FOLDER) { return } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (!addToolbar.isAttachedToWindow) { // Can't animate detached Views addToolbar.visibility = View.INVISIBLE addFab.visibility = View.VISIBLE } else { hideAddToolbar(true) } } if (resultCode != Activity.RESULT_OK) { return } if (result == null) { return } if (requestCode == REQUEST_CHOOSE_FOLDER) { val preferences = getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) preferences.edit().putBoolean(SHOW_INTERNAL_STORAGE_MESSAGE, false).apply() } // Add chosen items val uris = HashSet() if (result.data != null) { uris.add(result.data) } // When selecting multiple images, "Photos" returns the first URI in getData and all URIs // in getClipData. val clipData = result.clipData if (clipData != null) { val count = clipData.itemCount for (i in 0..count - 1) { val uri = clipData.getItemAt(i).uri if (uri != null) { uris.add(uri) } } } if (uris.isEmpty()) { // Nothing to do, so we can avoid posting the runnable at all return } presenter.addGalleryWallpaper(uris) } private fun onDataSetChanged() { if (mWallpapers.size > 0 && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { photoGrid.visibility = View.VISIBLE addFab.visibility = View.VISIBLE empty.visibility = View.GONE } else { // No chosen images, show the empty View photoGrid.visibility = View.GONE addFab.visibility = View.GONE empty.visibility = View.VISIBLE if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { // Permission is granted, we can show the random camera photos image emptyAnimator.displayedChild = 0 emptyDescription.setText(R.string.gallery_empty) addFab.visibility = View.VISIBLE } else { if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { // We should show rationale on why they should enable the storage permission and // random camera photos emptyAnimator.displayedChild = 1 emptyDescription.setText(R.string.gallery_permission_rationale) } else { // The user has permanently denied the storage permission. Give them a link to app settings emptyAnimator.displayedChild = 2 emptyDescription.setText(R.string.gallery_denied_explanation) } } } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private fun showAddToolbar() { // Divide by two since we're doing two animations but we want the total time to the short animation time val duration = resources.getInteger(android.R.integer.config_shortAnimTime) / 2 // Hide the add button addFab.animate() .scaleX(0f) .scaleY(0f) .translationY(resources.getDimension(R.dimen.gallery_fab_margin)) .setDuration(duration.toLong()) .withEndAction { addFab.visibility = View.INVISIBLE // Then show the toolbar addToolbar.visibility = View.VISIBLE ViewAnimationUtils.createCircularReveal( addToolbar, addToolbar.width / 2, addToolbar.height / 2, 0f, (addToolbar.width / 2).toFloat()) .setDuration(duration.toLong()) .start() } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private fun hideAddToolbar(showAddButton: Boolean) { // Divide by two since we're doing two animations but we want the total time to the short animation time val duration = resources.getInteger(android.R.integer.config_shortAnimTime) / 2 // Hide the toolbar val hideAnimator = ViewAnimationUtils.createCircularReveal( addToolbar, addToolbar.width / 2, addToolbar.height / 2, (addToolbar.width / 2).toFloat(), 0f).setDuration((if (showAddButton) duration else duration * 2).toLong()) hideAnimator.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { addToolbar.visibility = View.INVISIBLE if (showAddButton) { addFab.visibility = View.VISIBLE addFab.animate() .scaleX(1f) .scaleY(1f) .translationY(0f).duration = duration.toLong() } else { // Just reset the translationY addFab.translationY = 0f } } }) hideAnimator.start() } override fun onBackPressed() { if (mMultiSelectionController.getSelectedCount() > 0) { mMultiSelectionController.reset(true) } else if (addToolbar.visibility == View.VISIBLE) { hideAddToolbar(true) } else { super.onBackPressed() } } private fun tryUpdateSelection(allowAnimate: Boolean) { if (mUpdatePosition >= 0) { mChosenPhotosAdapter.notifyItemChanged(mUpdatePosition) mUpdatePosition = -1 } else { mChosenPhotosAdapter.notifyDataSetChanged() } val selectedCount = mMultiSelectionController.getSelectedCount() val toolbarVisible = selectedCount > 0 var showForceNow = selectedCount == 1 if (showForceNow) { // Double check to make sure we can force a URI for the selected URI val selectedItem = mMultiSelectionController.getSelection().iterator().next() // Only show the force now icon if it isn't a tree URI or there is at least one image in the tree showForceNow = !selectedItem.isTreeUri || !getImagesFromTreeUri(this, Uri.parse(selectedItem.uri), 1).isEmpty() } selectionToolbar.menu.findItem(R.id.action_force_now).isVisible = showForceNow val tag = selectionToolbarContainer.getTag(0xDEADBEEF.toInt()) val previouslyVisible = if (tag == null) false else tag as Boolean if (previouslyVisible != toolbarVisible) { selectionToolbarContainer.setTag(0xDEADBEEF.toInt(), toolbarVisible) val duration = if (allowAnimate) resources.getInteger(android.R.integer.config_shortAnimTime) else 0 if (toolbarVisible) { selectionToolbarContainer.visibility = View.VISIBLE selectionToolbarContainer.translationY = (-selectionToolbarContainer.height).toFloat() selectionToolbarContainer.animate() .translationY(0f) .setDuration(duration.toLong()) .withEndAction(null) if (addToolbar.visibility == View.VISIBLE) { hideAddToolbar(false) } else { addFab.animate() .scaleX(0f) .scaleY(0f) .setDuration(duration.toLong()) .withEndAction({ addFab.visibility = View.INVISIBLE }) } } else { selectionToolbarContainer.animate() .translationY((-selectionToolbarContainer.height).toFloat()) .setDuration(duration.toLong()) .withEndAction { selectionToolbarContainer.visibility = View.INVISIBLE } addFab.visibility = View.VISIBLE addFab.animate() .scaleY(1f) .scaleX(1f) .setDuration(duration.toLong()) .withEndAction(null) } } if (toolbarVisible) { var title = Integer.toString(selectedCount) if (selectedCount == 1) { // If they've selected a tree URI, show the DISPLAY_NAME instead of just '1' val selectedItem = mMultiSelectionController.getSelection().iterator().next() if (selectedItem.isTreeUri) { val displayName = getDisplayNameForTreeUri(this, Uri.parse(selectedItem.uri)) if (!TextUtils.isEmpty(displayName)) { title = displayName } } } selectionToolbar.title = title } } private fun setupMultiSelect() { // Set up toolbar selectionToolbar.setNavigationOnClickListener { mMultiSelectionController.reset(true) } selectionToolbar.inflateMenu(R.menu.gallery_selection) selectionToolbar.setOnMenuItemClickListener(Toolbar.OnMenuItemClickListener { item -> val itemId = item.itemId if (itemId == R.id.action_force_now) { val selection = mMultiSelectionController.getSelection() if (selection.isNotEmpty()) { val selectedItems = selection.iterator().next() presenter.forceNow(selectedItems.uri) toast(R.string.gallery_temporary_force_image) } mMultiSelectionController.reset(true) return@OnMenuItemClickListener true } else if (itemId == R.id.action_remove) { val removeItems = ArrayList( mMultiSelectionController.getSelection()) presenter.removeGalleryWallpaper(removeItems) mMultiSelectionController.reset(true) return@OnMenuItemClickListener true } false }) // Set up controller mMultiSelectionController.setCallbacks(object : MultiSelectionController.Callbacks { override fun onSelectionChanged(restored: Boolean, fromUser: Boolean) { tryUpdateSelection(!restored) } }) } open class CheckableViewHolder(root: View) : RecyclerView.ViewHolder(root) { var mRootView: View = root var mCheckedOverlayView: View = root.findViewById(R.id.checked_overlay) } internal class PhotoViewHolder(root: View) : CheckableViewHolder(root) { val mThumbView: ImageView = root.findViewById(R.id.thumbnail) as ImageView } internal class TreeViewHolder(root: View) : CheckableViewHolder(root) { val mThumbViews: MutableList = ArrayList() init { mThumbViews.add(root.findViewById(R.id.thumbnail1) as ImageView) mThumbViews.add(root.findViewById(R.id.thumbnail2) as ImageView) mThumbViews.add(root.findViewById(R.id.thumbnail3) as ImageView) mThumbViews.add(root.findViewById(R.id.thumbnail4) as ImageView) } } private val mChosenPhotosAdapter = object : RecyclerView.Adapter() { override fun getItemViewType(position: Int): Int { val wallpaperItem = mWallpapers[position] return if (wallpaperItem.isTreeUri) ITEM_TYPE_TREE else ITEM_TYPE_URI } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheckableViewHolder { val isTreeUri = viewType != 0 val v: View val vh: CheckableViewHolder if (isTreeUri) { v = LayoutInflater.from(this@GallerySettingActivity) .inflate(R.layout.gallery_chosen_photo_tree_item, parent, false) vh = TreeViewHolder(v) } else { v = LayoutInflater.from(this@GallerySettingActivity) .inflate(R.layout.gallery_chosen_photo_item, parent, false) vh = PhotoViewHolder(v) } v.layoutParams.height = mItemSize v.setOnTouchListener { _, motionEvent -> if (motionEvent.actionMasked != MotionEvent.ACTION_CANCEL) { mLastTouchPosition = vh.adapterPosition mLastTouchX = motionEvent.x.toInt() mLastTouchY = motionEvent.y.toInt() } false } v.setOnClickListener { mUpdatePosition = vh.adapterPosition if (mUpdatePosition != RecyclerView.NO_POSITION) { mMultiSelectionController.toggle(mWallpapers[mUpdatePosition], true) } } return vh } override fun onBindViewHolder(vh: CheckableViewHolder, position: Int) { val wallpaperItem = mWallpapers[position] val isTreeUri = getItemViewType(position) != 0 if (isTreeUri) { val treeVh = vh as TreeViewHolder val maxImages = treeVh.mThumbViews.size val imageUri = Uri.parse(wallpaperItem.uri) val images = getImagesFromTreeUri(this@GallerySettingActivity, imageUri, maxImages) val numImages = images.size for (h in 0..numImages - 1) { Glide.with(this@GallerySettingActivity) .load(images[h]) .override(mItemSize / 2, mItemSize / 2) .placeholder(mPlaceholderSmallDrawable) .into(treeVh.mThumbViews[h]) } for (h in numImages..maxImages - 1) { treeVh.mThumbViews[h].setImageDrawable(mPlaceholderSmallDrawable) } } else { val photoVh = vh as PhotoViewHolder Glide.with(this@GallerySettingActivity) .load(Uri.parse(wallpaperItem.uri)) .override(mItemSize, mItemSize) .placeholder(mPlaceholderDrawable) .into(photoVh.mThumbView) } val checked = mMultiSelectionController.isSelected(wallpaperItem) vh.mRootView.setTag(R.id.gallery_viewtag_position, position) if (mLastTouchPosition == vh.adapterPosition && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Handler().post { if (!vh.mCheckedOverlayView.isAttachedToWindow) { // Can't animate detached Views vh.mCheckedOverlayView.visibility = if (checked) View.VISIBLE else View.GONE return@post } if (checked) { vh.mCheckedOverlayView.visibility = View.VISIBLE } // find the smallest radius that'll cover the item val coverRadius = maxDistanceToCorner( mLastTouchX, mLastTouchY, 0, 0, vh.mRootView.width, vh.mRootView.height) val revealAnim = ViewAnimationUtils.createCircularReveal( vh.mCheckedOverlayView, mLastTouchX, mLastTouchY, if (checked) 0f else coverRadius, if (checked) coverRadius else 0f) .setDuration(150) if (!checked) { revealAnim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { vh.mCheckedOverlayView.visibility = View.GONE } }) } revealAnim.start() } } else { vh.mCheckedOverlayView.visibility = if (checked) View.VISIBLE else View.GONE } } private fun maxDistanceToCorner(x: Int, y: Int, left: Int, top: Int, right: Int, bottom: Int): Float { var maxDistance = 0f maxDistance = Math.max(maxDistance, Math.hypot((x - left).toDouble(), (y - top).toDouble()).toFloat()) maxDistance = Math.max(maxDistance, Math.hypot((x - right).toDouble(), (y - top).toDouble()).toFloat()) maxDistance = Math.max(maxDistance, Math.hypot((x - left).toDouble(), (y - bottom).toDouble()).toFloat()) maxDistance = Math.max(maxDistance, Math.hypot((x - right).toDouble(), (y - bottom).toDouble()).toFloat()) return maxDistance } override fun getItemCount(): Int { return mWallpapers.size } override fun getItemId(position: Int): Long { return mWallpapers[position].id } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/activity/SettingsActivity.kt ================================================ package com.yalin.style.view.activity import android.animation.ObjectAnimator import android.app.Fragment import android.app.FragmentTransaction import android.content.Intent import android.os.Bundle import android.support.v7.widget.Toolbar import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.AdapterView import android.widget.BaseAdapter import android.widget.TextView import com.yalin.style.R import com.yalin.style.analytics.Analytics import com.yalin.style.analytics.Event import com.yalin.style.injection.HasComponent import com.yalin.style.injection.component.DaggerSourceComponent import com.yalin.style.injection.component.SourceComponent import com.yalin.style.view.fragment.SettingsAdvanceFragment import com.yalin.style.view.fragment.SettingsChooseSourceFragment import kotlinx.android.synthetic.main.activity_settings.* /** * @author jinyalin * * * @since 2017/5/2. */ class SettingsActivity : BaseActivity(), HasComponent, SettingsChooseSourceFragment.Callbacks { companion object { val START_SECTION_SOURCE = 0 val START_SECTION_ADVANCED = 1 val SECTION_LABELS = intArrayOf(R.string.section_choose_source, R.string.section_advanced) private val SECTION_FRAGMENTS = arrayOf>(SettingsChooseSourceFragment::class.java, SettingsAdvanceFragment::class.java) } private val sourceComponent: SourceComponent by lazy { initializeInjector() } private var mBackgroundAnimator: ObjectAnimator? = null private var mStartSection = START_SECTION_SOURCE override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE setContentView(R.layout.activity_settings) getSectionFromIntent(intent) setupAppBar() drawInsetsFrameLayout.setOnInsetsCallback { insets -> val lp = mainContainer.layoutParams as ViewGroup.MarginLayoutParams lp.leftMargin = insets.left lp.topMargin = insets.top lp.rightMargin = insets.right lp.bottomMargin = insets.bottom mainContainer.layoutParams = lp } if (mBackgroundAnimator != null) { mBackgroundAnimator!!.cancel() } mBackgroundAnimator = ObjectAnimator.ofFloat(this, "backgroundOpacity", 0f, 1f) mBackgroundAnimator!!.duration = 1000 mBackgroundAnimator!!.start() } private fun getSectionFromIntent(intent: Intent) { val uri = intent.data uri?.let { val path = uri.pathSegments[0] when (path) { "advance" -> { mStartSection = START_SECTION_ADVANCED Analytics.logEvent(this, Event.SHORTCUTS_ADVANCE_SETTINGS) } else -> { mStartSection = START_SECTION_SOURCE Analytics.logEvent(this, Event.SHORTCUTS_SETTINGS) } } } } private fun initializeInjector() = DaggerSourceComponent.builder() .applicationComponent(applicationComponent) .build() private fun setupAppBar() { appBar.setNavigationOnClickListener { onNavigateUp() } val inflater = LayoutInflater.from(this) sectionSpinner.adapter = object : BaseAdapter() { override fun getCount(): Int { return SECTION_LABELS.size } override fun getItem(position: Int): Any { return SECTION_LABELS[position] } override fun getItemId(position: Int): Long { return (position + 1).toLong() } override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { var view = convertView if (view == null) { view = inflater.inflate(R.layout.settings_ab_spinner_list_item, parent, false) } (view!!.findViewById(android.R.id.text1) as TextView).text = getString(SECTION_LABELS[position]) return view } override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { var view = convertView if (view == null) { view = inflater.inflate(R.layout.settings_ab_spinner_list_item_dropdown, parent, false) } (view!!.findViewById(android.R.id.text1) as TextView).text = getString(SECTION_LABELS[position]) return view as View } } sectionSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(spinner: AdapterView<*>?, view: View?, position: Int, id: Long) { val fragmentClass = SECTION_FRAGMENTS[position] val currentFragment = fragmentManager.findFragmentById( R.id.contentContainer) if (currentFragment != null && fragmentClass == currentFragment.javaClass) { return } inflateMenuFromFragment(0) try { val newFragment = fragmentClass.newInstance() fragmentManager.beginTransaction() .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .setTransitionStyle(R.style.Style_SimpleFadeFragmentAnimation) .replace(R.id.contentContainer, newFragment as Fragment) .commitAllowingStateLoss() } catch (e: Exception) { throw RuntimeException(e) } } override fun onNothingSelected(spinner: AdapterView<*>) {} } sectionSpinner.setSelection(mStartSection) inflateMenuFromFragment(0) appBar.setOnMenuItemClickListener(Toolbar.OnMenuItemClickListener { item -> when (item.itemId) { R.id.action_reset -> { val currentFragment = fragmentManager.findFragmentById( R.id.contentContainer) if (currentFragment != null && currentFragment is SettingsActivityMenuListener) { currentFragment .onSettingsActivityMenuItemClick(item) } return@OnMenuItemClickListener true } R.id.action_about -> { Analytics.logEvent(this, Event.ABOUT_OPEN) startActivity(Intent(this, AboutActivity::class.java)) return@OnMenuItemClickListener true } } val currentFragment = supportFragmentManager.findFragmentById( R.id.contentContainer) if (currentFragment != null && currentFragment is SettingsActivityMenuListener) { currentFragment.onSettingsActivityMenuItemClick(item) } false }) } fun inflateMenuFromFragment(menuResId: Int) { appBar.menu.clear() if (menuResId != 0) { appBar.inflateMenu(menuResId) } appBar.inflateMenu(R.menu.menu_settings) } override val component: SourceComponent get() = sourceComponent override fun onRequestCloseActivity() { finish() } interface SettingsActivityMenuListener { fun onSettingsActivityMenuItemClick(item: MenuItem) } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/activity/StyleActivity.kt ================================================ package com.yalin.style.view.activity import android.Manifest import android.app.Fragment import android.app.WallpaperManager import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle import android.os.Handler import android.preference.PreferenceManager import android.support.v4.app.ActivityCompat import android.support.v4.content.ContextCompat import android.view.View import com.yalin.style.R import com.yalin.style.StyleApplication import com.yalin.style.StyleWallpaperService import com.yalin.style.analytics.Analytics import com.yalin.style.analytics.Event import com.yalin.style.data.BuildConfig import com.yalin.style.data.log.LogUtil import com.yalin.style.data.repository.AdvanceWallpaperDataRepository import com.yalin.style.event.MainContainerInsetsChangedEvent import com.yalin.style.event.SeenTutorialEvent import com.yalin.style.event.WallpaperActivateEvent import com.yalin.style.event.WallpaperDetailOpenedEvent import com.yalin.style.injection.HasComponent import com.yalin.style.injection.component.DaggerWallpaperComponent import com.yalin.style.injection.component.WallpaperComponent import com.yalin.style.view.component.PanScaleProxyView import com.yalin.style.view.fragment.AnimatedStyleLogoFragment import com.yalin.style.view.fragment.StyleRenderFragment import com.yalin.style.view.fragment.TutorialFragment import com.yalin.style.view.fragment.WallpaperDetailFragment import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.layout_include_wallpaper_detail.* import kotlinx.android.synthetic.main.layout_include_wallpaper_tutorial.* import kotlinx.android.synthetic.main.layout_include_active.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.jetbrains.anko.toast import javax.inject.Inject /** * @author jinyalin * @since 2017/5/9. */ class StyleActivity : BaseActivity(), HasComponent, PanScaleProxyView.OnOtherGestureListener { companion object { private val TAG = "StyleActivity" // ui mode private val MODE_UNKNOWN = -1 private val MODE_ACTIVATE = 0 private val MODE_DETAIL = 1 private val MODE_TUTORIAL = 2 private val PREF_SEEN_TUTORIAL = "seen_tutorial" private val REQUEST_PERMISSION_CODE = 10000 private val mHandler = Handler() } private val wallpaperComponent: WallpaperComponent by lazy { initializeInjector() } private var mUiMode = MODE_UNKNOWN private var mWindowHasFocus = false private var mPaused = false private var mStyleActive = false private var mSeenTutorial = false private var wallpaperDetailFragment: WallpaperDetailFragment? = null private var startLogAnim: Runnable? = null @Inject lateinit var advanceWallpaperRepository: AdvanceWallpaperDataRepository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) StyleApplication.instance.applicationComponent.inject(this) advanceWallpaperRepository.maybeRollback() Analytics.setUserProperty(this, "device_type", "Android") Analytics.onStartSession(this) setupActiveView() setupDetailView() setupTutorialView() mainContainer.setOnInsetsCallback { insets -> EventBus.getDefault().postSticky(MainContainerInsetsChangedEvent(insets)) } showHideChrome(true) EventBus.getDefault().register(this) val sp = PreferenceManager.getDefaultSharedPreferences(this) mSeenTutorial = sp.getBoolean(PREF_SEEN_TUTORIAL, false) } override fun onStart() { super.onStart() if (BuildConfig.ENABLE_EXTERNAL_LOG && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_PERMISSION_CODE) } } override fun onPostResume() { super.onPostResume() mPaused = false // update intro mode UI to latest wallpaper active state val e = EventBus.getDefault() .getStickyEvent(WallpaperActivateEvent::class.java) if (e != null) { onEventMainThread(e) } else { onEventMainThread(WallpaperActivateEvent(false)) } updateUi() val decorView = window.decorView decorView.alpha = 0f decorView.animate().cancel() decorView.animate() .setStartDelay(500) .alpha(1f).duration = 300 maybeUpdateWallpaperDetailOpenedClosed() } override fun onPause() { super.onPause() mPaused = true maybeUpdateWallpaperDetailOpenedClosed() } override fun onDestroy() { super.onDestroy() EventBus.getDefault().unregister(this) Analytics.onEndSession(this) if (startLogAnim != null) mHandler.removeCallbacks(startLogAnim) } private fun showHideChrome(show: Boolean) { var flags = if (show) 0 else View.SYSTEM_UI_FLAG_LOW_PROFILE flags = flags or (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE) if (!show) { flags = flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE) } mainContainer.systemUiVisibility = flags } private fun setupActiveView() { activateStyleButton.setOnClickListener { Analytics.logEvent(this, Event.ACTIVATE) setWallpaper() } } private fun setupDetailView() { } private fun setupTutorialView() { } private fun getContainerFromMode(uiMode: Int): View { when (uiMode) { MODE_DETAIL -> return detailContainer MODE_TUTORIAL -> return tutorialContainer else -> return activeContainer } } private fun updateUi() { // default activate mode var newMode = MODE_ACTIVATE if (mStyleActive) { newMode = MODE_TUTORIAL if (mSeenTutorial) { newMode = MODE_DETAIL } } if (mUiMode == newMode) { return } LogUtil.D(TAG, "update UI") val oldModeView = getContainerFromMode(mUiMode) val newModeView = getContainerFromMode(newMode) oldModeView.animate() .alpha(0f) .setDuration(1000) .withEndAction { oldModeView.visibility = View.GONE } if (newModeView.alpha == 1f) { newModeView.alpha = 0f } newModeView.visibility = View.VISIBLE newModeView.animate() .alpha(1f) .setDuration(1000) .withEndAction(null) if (newMode == MODE_ACTIVATE) { val logoFragment = fragmentManager.findFragmentById(R.id.animatedLogoFragment) as AnimatedStyleLogoFragment logoFragment.reset() logoFragment.setOnFillStartedCallback { activateStyleButton.animate().alpha(1f).duration = 500 } startLogAnim = Runnable { logoFragment.start() } mHandler.postDelayed(startLogAnim, 1000) } if (mUiMode == MODE_ACTIVATE || newMode == MODE_ACTIVATE) { val demoFragment = fragmentManager.findFragmentById(R.id.demoContainerLayout) if (newMode == MODE_ACTIVATE && demoFragment == null) { fragmentManager.beginTransaction() .add(R.id.demoContainerLayout, StyleRenderFragment.createInstance(true, true)) .commit() } else if (mUiMode == MODE_ACTIVATE && demoFragment != null) { fragmentManager.beginTransaction() .remove(demoFragment) .commit() } } if (newMode == MODE_DETAIL) { val detailFragment = fragmentManager.findFragmentById(R.id.detailContainer) if (detailFragment == null) { wallpaperDetailFragment = WallpaperDetailFragment.createInstance() fragmentManager.beginTransaction() .add(R.id.detailContainer, wallpaperDetailFragment) .commit() } else { wallpaperDetailFragment = detailFragment as WallpaperDetailFragment? } mainContainer.setOnSystemUiVisibilityChangeListener(wallpaperDetailFragment) } if (mUiMode == MODE_TUTORIAL || newMode == MODE_TUTORIAL) { var tutorialFragment: Fragment? = fragmentManager.findFragmentById(R.id.mainContainer) if (newMode == MODE_TUTORIAL && tutorialFragment == null) { Analytics.logEvent(this, Event.TUTORIAL_BEGIN) tutorialFragment = TutorialFragment.newInstance() fragmentManager.beginTransaction() .add(R.id.mainContainer, tutorialFragment) .commit() } else if (mUiMode == MODE_TUTORIAL && tutorialFragment != null) { Analytics.logEvent(this, Event.TUTORIAL_COMPLETE) fragmentManager.beginTransaction() .remove(tutorialFragment) .commit() } } mUiMode = newMode maybeUpdateWallpaperDetailOpenedClosed() } private fun maybeUpdateWallpaperDetailOpenedClosed() { var currentlyOpened = false val wdoe = EventBus.getDefault() .getStickyEvent(WallpaperDetailOpenedEvent::class.java) if (wdoe != null) { currentlyOpened = wdoe.isWallpaperDetailOpened } var shouldBeOpened = false if (mUiMode == MODE_DETAIL) { val overflowMenuVisible = wallpaperDetailFragment != null && wallpaperDetailFragment!!.isOverflowMenuVisible if ((mWindowHasFocus || overflowMenuVisible) && !mPaused) { shouldBeOpened = true } } if (currentlyOpened != shouldBeOpened) { EventBus.getDefault().postSticky(WallpaperDetailOpenedEvent(shouldBeOpened)) } } @Subscribe fun onEventMainThread(e: WallpaperActivateEvent) { if (mPaused) { return } mStyleActive = e.isWallpaperActivate updateUi() } @Subscribe fun onEventMainThread(e: SeenTutorialEvent) { mSeenTutorial = true PreferenceManager.getDefaultSharedPreferences(this).edit() .putBoolean(PREF_SEEN_TUTORIAL, true) .apply() updateUi() } override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) mWindowHasFocus = hasFocus maybeUpdateWallpaperDetailOpenedClosed() } private fun initializeInjector() = DaggerWallpaperComponent.builder() .applicationComponent(applicationComponent) .build() private fun setWallpaper() { try { startActivity(Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER) .putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, ComponentName(this, StyleWallpaperService::class.java)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } catch (e: ActivityNotFoundException) { try { startActivity(Intent(WallpaperManager.ACTION_LIVE_WALLPAPER_CHOOSER) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } catch (e2: ActivityNotFoundException) { toast(R.string.exception_message_device_unsupported) Analytics.logEvent(this, Event.DEVICE_UNSUPPORTED) } } } override val component: WallpaperComponent get() = wallpaperComponent override fun onSingleTapUp() { if (mUiMode == MODE_DETAIL) { showHideChrome(mainContainer.systemUiVisibility and View.SYSTEM_UI_FLAG_LOW_PROFILE != 0) } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/component/CircleProgressView.java ================================================ package com.yalin.style.view.component; import android.animation.Animator; import android.animation.ObjectAnimator; 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.Paint.Cap; import android.graphics.RectF; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.util.Property; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import com.yalin.style.R; /** * @author jinyalin * @since 2017/7/31. */ public class CircleProgressView extends View { private static final Interpolator ANGLE_INTERPOLATOR = new LinearInterpolator(); private static final Interpolator SWEEP_INTERPOLATOR = new AccelerateDecelerateInterpolator(); private static final int ANGLE_ANIMATOR_DURATION = 2000; private static final int SWEEP_ANIMATOR_DURATION = 900; private static final int MIN_SWEEP_ANGLE = 30; private static final int DEFAULT_BORDER_WIDTH = 3; private final RectF fBounds = new RectF(); private ObjectAnimator mObjectAnimatorSweep; private ObjectAnimator mObjectAnimatorAngle; private boolean mModeAppearing = true; private Paint mPaint; private float mCurrentGlobalAngleOffset; private float mCurrentGlobalAngle; private float mCurrentSweepAngle; private float mBorderWidth; private boolean mRunning; private int[] mColors; private int mCurrentColorIndex; private int mNextColorIndex; public CircleProgressView(Context context) { this(context, null); } public CircleProgressView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CircleProgressView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); float density = context.getResources().getDisplayMetrics().density; TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircularProgress, defStyleAttr, 0); mBorderWidth = a.getDimension(R.styleable.CircularProgress_progress_borderWidth, DEFAULT_BORDER_WIDTH * density); a.recycle(); mColors = new int[4]; mColors[0] = ContextCompat.getColor(context, R.color.red); mColors[1] = ContextCompat.getColor(context, R.color.yellow); mColors[2] = ContextCompat.getColor(context, R.color.green); mColors[3] = ContextCompat.getColor(context, R.color.blue); mCurrentColorIndex = 0; mNextColorIndex = 1; mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeCap(Cap.ROUND); mPaint.setStrokeWidth(mBorderWidth); mPaint.setColor(mColors[mCurrentColorIndex]); setupAnimations(); } private void start() { if (isRunning()) { return; } mRunning = true; mObjectAnimatorAngle.start(); mObjectAnimatorSweep.start(); invalidate(); } private void stop() { if (!isRunning()) { return; } mRunning = false; mObjectAnimatorAngle.cancel(); mObjectAnimatorSweep.cancel(); invalidate(); } private boolean isRunning() { return mRunning; } @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (visibility == VISIBLE) { start(); } else { stop(); } } @Override protected void onAttachedToWindow() { start(); super.onAttachedToWindow(); } @Override protected void onDetachedFromWindow() { stop(); super.onDetachedFromWindow(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); fBounds.left = mBorderWidth / 2f + .5f; fBounds.right = w - mBorderWidth / 2f - .5f; fBounds.top = mBorderWidth / 2f + .5f; fBounds.bottom = h - mBorderWidth / 2f - .5f; } @Override public void draw(Canvas canvas) { super.draw(canvas); float startAngle = mCurrentGlobalAngle - mCurrentGlobalAngleOffset; float sweepAngle = mCurrentSweepAngle; if (mModeAppearing) { mPaint.setColor(gradient(mColors[mCurrentColorIndex], mColors[mNextColorIndex], mCurrentSweepAngle / (360 - MIN_SWEEP_ANGLE * 2))); sweepAngle += MIN_SWEEP_ANGLE; } else { startAngle = startAngle + sweepAngle; sweepAngle = 360 - sweepAngle - MIN_SWEEP_ANGLE; } canvas.drawArc(fBounds, startAngle, sweepAngle, false, mPaint); } private static int gradient(int color1, int color2, float p) { int r1 = (color1 & 0xff0000) >> 16; int g1 = (color1 & 0xff00) >> 8; int b1 = color1 & 0xff; int r2 = (color2 & 0xff0000) >> 16; int g2 = (color2 & 0xff00) >> 8; int b2 = color2 & 0xff; int newr = (int) (r2 * p + r1 * (1 - p)); int newg = (int) (g2 * p + g1 * (1 - p)); int newb = (int) (b2 * p + b1 * (1 - p)); return Color.argb(255, newr, newg, newb); } private void toggleAppearingMode() { mModeAppearing = !mModeAppearing; if (mModeAppearing) { mCurrentColorIndex = ++mCurrentColorIndex % 4; mNextColorIndex = ++mNextColorIndex % 4; mCurrentGlobalAngleOffset = (mCurrentGlobalAngleOffset + MIN_SWEEP_ANGLE * 2) % 360; } } // //////////////////////////////////////////////////////////////////////////// // ////////////// Animation private Property mAngleProperty = new Property(Float.class, "angle") { @Override public Float get(CircleProgressView object) { return object.getCurrentGlobalAngle(); } @Override public void set(CircleProgressView object, Float value) { object.setCurrentGlobalAngle(value); } }; private Property mSweepProperty = new Property(Float.class, "arc") { @Override public Float get(CircleProgressView object) { return object.getCurrentSweepAngle(); } @Override public void set(CircleProgressView object, Float value) { object.setCurrentSweepAngle(value); } }; private void setupAnimations() { mObjectAnimatorAngle = ObjectAnimator.ofFloat(this, mAngleProperty, 360f); mObjectAnimatorAngle.setInterpolator(ANGLE_INTERPOLATOR); mObjectAnimatorAngle.setDuration(ANGLE_ANIMATOR_DURATION); mObjectAnimatorAngle.setRepeatMode(ValueAnimator.RESTART); mObjectAnimatorAngle.setRepeatCount(ValueAnimator.INFINITE); mObjectAnimatorSweep = ObjectAnimator.ofFloat(this, mSweepProperty, 360f - MIN_SWEEP_ANGLE * 2); mObjectAnimatorSweep.setInterpolator(SWEEP_INTERPOLATOR); mObjectAnimatorSweep.setDuration(SWEEP_ANIMATOR_DURATION); mObjectAnimatorSweep.setRepeatMode(ValueAnimator.RESTART); mObjectAnimatorSweep.setRepeatCount(ValueAnimator.INFINITE); mObjectAnimatorSweep.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { toggleAppearingMode(); } }); } public void setCurrentGlobalAngle(float currentGlobalAngle) { mCurrentGlobalAngle = currentGlobalAngle; invalidate(); } public float getCurrentGlobalAngle() { return mCurrentGlobalAngle; } public void setCurrentSweepAngle(float currentSweepAngle) { mCurrentSweepAngle = currentSweepAngle; invalidate(); } public float getCurrentSweepAngle() { return mCurrentSweepAngle; } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/component/DownloadingDialog.kt ================================================ package com.yalin.style.view.component import android.content.Context import android.widget.TextView import com.afollestad.materialdialogs.MaterialDialog import com.yalin.style.R import com.yalin.style.model.AdvanceWallpaperItem /** * @author jinyalin * * * @since 2017/8/11. */ class DownloadingDialog constructor(context: Context) { private val dialog = MaterialDialog.Builder(context) .iconRes(R.drawable.advance_downloading) .title(R.string.downloading) .cancelable(false) .customView(R.layout.dialog_downloading, false).build() private val progressView = dialog.findViewById(R.id.downloadProgress) as TextView fun show() { dialog.show() } fun dismiss() { dialog.dismiss() } fun updateProgress(progress: Long) { progressView.text = formatSize(progress) } fun showError(item: AdvanceWallpaperItem, e: Exception) { } companion object { val C = 1024 } private fun formatSize(progress: Long): String { if (progress < C) { return "$progress B" } if (progress < C * C) { return "${progress / C} KB" } return "%.2f MB".format(progress / (C * C).toFloat()) } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/component/DrawInsetsFrameLayout.java ================================================ package com.yalin.style.view.component; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.widget.FrameLayout; import com.yalin.style.R; /** * @author jinyalin * @since 2017/4/21. */ public class DrawInsetsFrameLayout extends FrameLayout { private Drawable mInsetBackground; private Drawable mTopInsetBackground; private Drawable mBottomInsetBackground; private Drawable mSideInsetBackground; private Rect mInsets; private Rect mTempRect = new Rect(); private OnInsetsCallback mOnInsetsCallback; public DrawInsetsFrameLayout(Context context) { super(context); init(context, null, 0); } public DrawInsetsFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0); } public DrawInsetsFrameLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs, defStyle); } private void init(Context context, AttributeSet attrs, int defStyle) { final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DrawInsetsFrameLayout, defStyle, 0); assert a != null; mInsetBackground = a.getDrawable(R.styleable.DrawInsetsFrameLayout_insetBackground); mTopInsetBackground = a.getDrawable(R.styleable.DrawInsetsFrameLayout_topInsetBackground); mBottomInsetBackground = a.getDrawable(R.styleable.DrawInsetsFrameLayout_bottomInsetBackground); mSideInsetBackground = a.getDrawable(R.styleable.DrawInsetsFrameLayout_sideInsetBackground); a.recycle(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mInsetBackground != null) { mInsetBackground.setCallback(this); } if (mTopInsetBackground != null) { mTopInsetBackground.setCallback(this); } if (mBottomInsetBackground != null) { mBottomInsetBackground.setCallback(this); } if (mSideInsetBackground != null) { mSideInsetBackground.setCallback(this); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mInsetBackground != null) { mInsetBackground.setCallback(null); } if (mTopInsetBackground != null) { mTopInsetBackground.setCallback(null); } if (mBottomInsetBackground != null) { mBottomInsetBackground.setCallback(null); } if (mSideInsetBackground != null) { mSideInsetBackground.setCallback(null); } } public void setOnInsetsCallback(OnInsetsCallback onInsetsCallback) { mOnInsetsCallback = onInsetsCallback; } @Override protected boolean fitSystemWindows(Rect insets) { mInsets = new Rect(insets); setWillNotDraw(false); postInvalidateOnAnimation(); if (mOnInsetsCallback != null) { mOnInsetsCallback.onInsetsChanged(insets); } return true; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int width = getWidth(); int height = getHeight(); if (mInsets != null) { // Top mTempRect.set(0, 0, width, mInsets.top); if (mInsetBackground != null) { mInsetBackground.setBounds(mTempRect); mInsetBackground.draw(canvas); } if (mTopInsetBackground != null) { mTopInsetBackground.setBounds(mTempRect); mTopInsetBackground.draw(canvas); } // Bottom mTempRect.set(0, height - mInsets.bottom, width, height); if (mInsetBackground != null) { mInsetBackground.setBounds(mTempRect); mInsetBackground.draw(canvas); } if (mTopInsetBackground != null) { mBottomInsetBackground.setBounds(mTempRect); mBottomInsetBackground.draw(canvas); } // Left mTempRect.set(0, mInsets.top, mInsets.left, height - mInsets.bottom); if (mInsetBackground != null) { mInsetBackground.setBounds(mTempRect); mInsetBackground.draw(canvas); } if (mSideInsetBackground != null) { mSideInsetBackground.setBounds(mTempRect); mSideInsetBackground.draw(canvas); } // Right mTempRect.set(width - mInsets.right, mInsets.top, width, height - mInsets.bottom); if (mInsetBackground != null) { mInsetBackground.setBounds(mTempRect); mInsetBackground.draw(canvas); } if (mSideInsetBackground != null) { mSideInsetBackground.setBounds(mTempRect); mSideInsetBackground.draw(canvas); } } } public interface OnInsetsCallback { void onInsetsChanged(Rect insets); } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/component/GalleryEmptyStateGraphicView.kt ================================================ package com.yalin.style.view.component import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.graphics.RectF import android.os.SystemClock import android.support.v4.content.ContextCompat import android.util.AttributeSet import android.util.TypedValue import android.view.View import com.yalin.style.R import java.util.* /** * @author jinyalin * @since 2017/5/24. */ class GalleryEmptyStateGraphicView(context: Context, attrs: AttributeSet) : View(context, attrs) { private val BITMAP = intArrayOf( 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) private val COLS = 8 private val ROWS = BITMAP.size / COLS private val CELL_SPACING_DIP = 2 private val CELL_ROUNDING_DIP = 1 private val CELL_SIZE_DIP = 8 private val ON_TIME_MILLIS = 400 private val FADE_TIME_MILLIS = 100 private val OFF_TIME_MILLIS = 50 private val mOffPaint = Paint() private val mOnPaint = Paint() private var mWidth: Int = 0 private var mHeight: Int = 0 private var mOnTime: Long = 0 private var mOnX: Int = 0 private var mOnY: Int = 0 private val mRandom = Random() private val mTempRectF = RectF() private var mCellSpacing: Int private var mCellRounding: Int private var mCellSize: Int init { val res = resources mOffPaint.isAntiAlias = true mOffPaint.color = ContextCompat.getColor(context, R.color.gallery_empty_state_dark) mOnPaint.isAntiAlias = true mOnPaint.color = ContextCompat.getColor(context, R.color.gallery_empty_state_light) mCellSpacing = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CELL_SPACING_DIP.toFloat(), res.displayMetrics).toInt() mCellSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CELL_SIZE_DIP.toFloat(), res.displayMetrics).toInt() mCellRounding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CELL_ROUNDING_DIP.toFloat(), res.displayMetrics).toInt() } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) mWidth = w mHeight = h } override fun onVisibilityChanged(changedView: View, visibility: Int) { super.onVisibilityChanged(changedView, visibility) if (isShown) { postInvalidateOnAnimation() } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) setMeasuredDimension( View.resolveSize(COLS * mCellSize + (COLS - 1) * mCellSpacing, widthMeasureSpec), View.resolveSize(ROWS * mCellSize + (ROWS - 1) * mCellSpacing, heightMeasureSpec)) } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (!isShown || mWidth == 0 || mHeight == 0) { return } // tick timer val nowElapsed = SystemClock.elapsedRealtime() if (nowElapsed > mOnTime + ON_TIME_MILLIS.toLong() + (FADE_TIME_MILLIS * 2).toLong() + OFF_TIME_MILLIS.toLong()) { mOnTime = nowElapsed while (true) { val x = mRandom.nextInt(COLS) val y = mRandom.nextInt(ROWS) if ((x != mOnX || y != mOnY) && BITMAP[y * COLS + x] == 1) { mOnX = x mOnY = y break } } } val t = (nowElapsed - mOnTime).toInt() for (y in 0..ROWS - 1) { for (x in 0..COLS - 1) { if (BITMAP[y * COLS + x] != 1) { continue } mTempRectF.set( (x * (mCellSize + mCellSpacing)).toFloat(), (y * (mCellSize + mCellSpacing)).toFloat(), (x * (mCellSize + mCellSpacing) + mCellSize).toFloat(), (y * (mCellSize + mCellSpacing) + mCellSize).toFloat()) canvas.drawRoundRect(mTempRectF, mCellRounding.toFloat(), mCellRounding.toFloat(), mOffPaint) if (nowElapsed <= mOnTime + ON_TIME_MILLIS.toLong() + (FADE_TIME_MILLIS * 2).toLong() && mOnX == x && mOnY == y) { // draw items if (t < FADE_TIME_MILLIS) { mOnPaint.alpha = t * 255 / FADE_TIME_MILLIS } else if (t < FADE_TIME_MILLIS + ON_TIME_MILLIS) { mOnPaint.alpha = 255 } else { mOnPaint.alpha = 255 - (t - ON_TIME_MILLIS - FADE_TIME_MILLIS) * 255 / FADE_TIME_MILLIS } canvas.drawRoundRect(mTempRectF, mCellRounding.toFloat(), mCellRounding.toFloat(), mOnPaint) } } } postInvalidateOnAnimation() } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/component/ObservableHorizontalScrollView.kt ================================================ package com.yalin.style.view.component import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import android.widget.HorizontalScrollView /** * @author jinyalin * @since 2017/5/22. */ class ObservableHorizontalScrollView(context: Context, attrs: AttributeSet) : HorizontalScrollView(context, attrs) { private var mCallbacks: Callbacks? = null override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) { super.onScrollChanged(l, t, oldl, oldt) mCallbacks?.onScrollChanged(l) } override fun onTouchEvent(ev: MotionEvent): Boolean { when (ev.actionMasked) { MotionEvent.ACTION_DOWN -> mCallbacks?.onDownMotionEvent() } return super.onTouchEvent(ev) } public override fun computeHorizontalScrollRange(): Int { return super.computeHorizontalScrollRange() } fun setCallbacks(listener: Callbacks) { mCallbacks = listener } interface Callbacks { fun onScrollChanged(scrollX: Int) fun onDownMotionEvent() } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/component/PanScaleProxyView.java ================================================ package com.yalin.style.view.component; import android.content.Context; import android.graphics.Point; import android.graphics.PointF; import android.graphics.RectF; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; import android.support.v4.os.ParcelableCompat; import android.support.v4.os.ParcelableCompatCreatorCallbacks; import android.support.v4.view.GestureDetectorCompat; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.widget.OverScroller; import com.yalin.style.util.MathUtil; /** * @author jinyalin * @since 2017/4/21. */ public class PanScaleProxyView extends View { /** * The current viewport. This rectangle represents the currently visible chart domain * and range. The currently visible chart X values are from this rectangle's left to its right. * The currently visible chart Y values are from this rectangle's top to its bottom. */ private RectF mCurrentViewport = new RectF(0, 0, 1, 1); private Point mSurfaceSizeBuffer = new Point(); private int mWidth = 1; private int mHeight = 1; private float mRelativeAspectRatio = 1f; private boolean mPanScaleEnabled = true; private float mMinViewportWidthOrHeight = 0.01f; // State objects and values related to gesture tracking. private ScaleGestureDetector mScaleGestureDetector; private GestureDetectorCompat mGestureDetector; private OverScroller mScroller; private Zoomer mZoomer; private PointF mZoomFocalPoint = new PointF(); private RectF mScrollerStartViewport = new RectF(); // Used only for zooms and flings. private boolean mDragZoomed = false; private boolean mMotionEventDown; private Handler mHandler = new Handler(); private OnOtherGestureListener mOnOtherGestureListener; private OnViewportChangedListener mOnViewportChangedListener; public PanScaleProxyView(Context context) { this(context, null, 0); } public PanScaleProxyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PanScaleProxyView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setWillNotDraw(true); // Sets up interactions mScaleGestureDetector = new ScaleGestureDetector(context, mScaleGestureListener); mScaleGestureDetector.setQuickScaleEnabled(true); mGestureDetector = new GestureDetectorCompat(context, mGestureListener); mScroller = new OverScroller(context); mZoomer = new Zoomer(context); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = Math.max(1, w); mHeight = Math.max(1, h); } //////////////////////////////////////////////////////////////////////////////////////////////// // // Methods and objects related to gesture handling // //////////////////////////////////////////////////////////////////////////////////////////////// /** * Finds the chart point (i.e. within the chart's domain and range) represented by the * given pixel coordinates. The "dest" argument is set to the point and * this function returns true. */ private void hitTest(float x, float y, PointF dest) { dest.set(mCurrentViewport.left + mCurrentViewport.width() * x / mWidth, mCurrentViewport.top + mCurrentViewport.height() * y / mHeight); } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mMotionEventDown = true; } boolean retVal = mScaleGestureDetector.onTouchEvent(event); retVal = mGestureDetector.onTouchEvent(event) || retVal; if (mMotionEventDown && event.getActionMasked() == MotionEvent.ACTION_UP) { mMotionEventDown = false; } return retVal || super.onTouchEvent(event); } /** * The scale listener, used for handling multi-finger scale gestures. */ private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() { /** * This is the active focal point in terms of the viewport. Could be a local * variable but kept here to minimize per-frame allocations. */ private PointF viewportFocus = new PointF(); @Override public boolean onScaleBegin(ScaleGestureDetector detector) { if (!mPanScaleEnabled) { return false; } mDragZoomed = true; return true; } @Override public boolean onScale(ScaleGestureDetector scaleGestureDetector) { if (!mPanScaleEnabled) { return false; } float newWidth = 1 / scaleGestureDetector.getScaleFactor() * mCurrentViewport.width(); float newHeight = 1 / scaleGestureDetector.getScaleFactor() * mCurrentViewport.height(); float focusX = scaleGestureDetector.getFocusX(); float focusY = scaleGestureDetector.getFocusY(); hitTest(focusX, focusY, viewportFocus); mCurrentViewport.set( viewportFocus.x - newWidth * focusX / mWidth, viewportFocus.y - newHeight * focusY / mHeight, 0, 0); mCurrentViewport.right = mCurrentViewport.left + newWidth; mCurrentViewport.bottom = mCurrentViewport.top + newHeight; constrainViewport(); triggerViewportChangedListener(); return true; } }; /** * Ensures that current viewport is inside the viewport extremes and original * aspect ratio is kept. */ private void constrainViewport() { if (mRelativeAspectRatio > 1) { if (mCurrentViewport.top < 0) { mCurrentViewport.offset(0, -mCurrentViewport.top); } if (mCurrentViewport.bottom > 1) { float requestedHeight = mCurrentViewport.height(); mCurrentViewport.bottom = 1; mCurrentViewport.top = Math.max(0, mCurrentViewport.bottom - requestedHeight); } if (mCurrentViewport.height() < mMinViewportWidthOrHeight) { mCurrentViewport.bottom = (mCurrentViewport.bottom + mCurrentViewport.top) / 2 + mMinViewportWidthOrHeight / 2; mCurrentViewport.top = mCurrentViewport.bottom - mMinViewportWidthOrHeight; } float halfWidth = mCurrentViewport.height() / mRelativeAspectRatio / 2; float centerX = MathUtil.constrain(halfWidth, 1 - halfWidth, (mCurrentViewport.right + mCurrentViewport.left) / 2); mCurrentViewport.left = centerX - halfWidth; mCurrentViewport.right = centerX + halfWidth; } else { if (mCurrentViewport.left < 0) { mCurrentViewport.offset(-mCurrentViewport.left, 0); } if (mCurrentViewport.right > 1) { float requestedWidth = mCurrentViewport.width(); mCurrentViewport.right = 1; mCurrentViewport.left = Math.max(0, mCurrentViewport.right - requestedWidth); } if (mCurrentViewport.width() < mMinViewportWidthOrHeight) { mCurrentViewport.right = (mCurrentViewport.right + mCurrentViewport.left) / 2 + mMinViewportWidthOrHeight / 2; mCurrentViewport.left = mCurrentViewport.right - mMinViewportWidthOrHeight; } float halfHeight = mCurrentViewport.width() * mRelativeAspectRatio / 2; float centerY = MathUtil.constrain(halfHeight, 1 - halfHeight, (mCurrentViewport.bottom + mCurrentViewport.top) / 2); mCurrentViewport.top = centerY - halfHeight; mCurrentViewport.bottom = centerY + halfHeight; } } /** * The gesture listener, used for handling simple gestures such as double touches, scrolls, * and flings. */ private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { if (!mPanScaleEnabled) { return false; } mDragZoomed = false; mScrollerStartViewport.set(mCurrentViewport); mScroller.forceFinished(true); return true; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { if (mOnOtherGestureListener != null) { mOnOtherGestureListener.onSingleTapUp(); } return true; } @Override public boolean onDoubleTapEvent(MotionEvent e) { if (!mPanScaleEnabled || mDragZoomed || e.getActionMasked() != MotionEvent.ACTION_UP) { return false; } mZoomer.forceFinished(true); hitTest(e.getX(), e.getY(), mZoomFocalPoint); float startZoom; if (mRelativeAspectRatio > 1) { startZoom = 1 / mCurrentViewport.height(); } else { startZoom = 1 / mCurrentViewport.width(); } boolean zoomIn = (startZoom < 1.5f); mZoomer.startZoom(startZoom, zoomIn ? 2f : 1f); triggerViewportChangedListener(); postAnimateTick(); // Workaround for 11952668; blow away the entire scale gesture detector after // a double tap mScaleGestureDetector = new ScaleGestureDetector(getContext(), mScaleGestureListener); mScaleGestureDetector.setQuickScaleEnabled(true); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (!mPanScaleEnabled) { return false; } // Scrolling uses math based on the viewport (as opposed to math using pixels). /* * Pixel offset is the offset in screen pixels, while viewport offset is the * offset within the current viewport. For additional information on surface sizes * and pixel offsets, see the docs for {@link computeScrollSurfaceSize()}. For * additional information about the viewport, see the comments for * {@link mCurrentViewport}. */ float viewportOffsetX = distanceX * mCurrentViewport.width() / mWidth; float viewportOffsetY = distanceY * mCurrentViewport.height() / mHeight; computeScrollSurfaceSize(mSurfaceSizeBuffer); setViewportTopLeft( mCurrentViewport.left + viewportOffsetX, mCurrentViewport.top + viewportOffsetY); return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (!mPanScaleEnabled) { return false; } fling((int) -velocityX, (int) -velocityY); return true; } }; private void fling(int velocityX, int velocityY) { // Flings use math in pixels (as opposed to math based on the viewport). computeScrollSurfaceSize(mSurfaceSizeBuffer); mScrollerStartViewport.set(mCurrentViewport); int startX = (int) (mSurfaceSizeBuffer.x * mScrollerStartViewport.left); int startY = (int) (mSurfaceSizeBuffer.y * mScrollerStartViewport.top); mScroller.forceFinished(true); mScroller.fling( startX, startY, velocityX, velocityY, 0, mSurfaceSizeBuffer.x - mWidth, 0, mSurfaceSizeBuffer.y - mHeight, mWidth / 2, mHeight / 2); postAnimateTick(); triggerViewportChangedListener(); } private void postAnimateTick() { mHandler.removeCallbacks(mAnimateTickRunnable); mHandler.post(mAnimateTickRunnable); } private Runnable mAnimateTickRunnable = new Runnable() { @Override public void run() { boolean needsInvalidate = false; if (mScroller.computeScrollOffset()) { // The scroller isn't finished, meaning a fling or programmatic pan operation is // currently active. computeScrollSurfaceSize(mSurfaceSizeBuffer); int currX = mScroller.getCurrX(); int currY = mScroller.getCurrY(); float currXRange = currX * 1f / mSurfaceSizeBuffer.x; float currYRange = currY * 1f / mSurfaceSizeBuffer.y; setViewportTopLeft(currXRange, currYRange); needsInvalidate = true; } if (mZoomer.computeZoom()) { // Performs the zoom since a zoom is in progress. float newWidth, newHeight; if (mRelativeAspectRatio > 1) { newHeight = 1 / mZoomer.getCurrZoom(); newWidth = newHeight / mRelativeAspectRatio; } else { newWidth = 1 / mZoomer.getCurrZoom(); newHeight = newWidth * mRelativeAspectRatio; } // focalPointOnScreen... 0 = left/top edge of screen, 1 = right/bottom edge of sreen float focalPointOnScreenX = (mZoomFocalPoint.x - mScrollerStartViewport.left) / mScrollerStartViewport.width(); float focalPointOnScreenY = (mZoomFocalPoint.y - mScrollerStartViewport.top) / mScrollerStartViewport.height(); mCurrentViewport.set( mZoomFocalPoint.x - newWidth * focalPointOnScreenX, mZoomFocalPoint.y - newHeight * focalPointOnScreenY, mZoomFocalPoint.x + newWidth * (1 - focalPointOnScreenX), mZoomFocalPoint.y + newHeight * (1 - focalPointOnScreenY)); constrainViewport(); needsInvalidate = true; } if (needsInvalidate) { triggerViewportChangedListener(); postAnimateTick(); } } }; /** * Computes the current scrollable surface size, in pixels. For example, if the entire chart * area is visible, this is simply the current view width and height. If the chart * is zoomed in 200% in both directions, the returned size will be twice as large horizontally * and vertically. */ private void computeScrollSurfaceSize(Point out) { out.set( (int) (mWidth / mCurrentViewport.width()), (int) (mHeight / mCurrentViewport.height())); } /** * Sets the current viewport (defined by {@link #mCurrentViewport}) to the given * X and Y positions. Note that the Y value represents the topmost pixel position, and thus * the bottom of the {@link #mCurrentViewport} rectangle. For more details on why top and * bottom are flipped, see {@link #mCurrentViewport}. */ private void setViewportTopLeft(float x, float y) { /** * Constrains within the scroll range. The scroll range is simply the viewport extremes * (AXIS_X_MAX, etc.) minus the viewport size. For example, if the extrema were 0 and 10, * and the viewport size was 2, the scroll range would be 0 to 8. */ float curWidth = mCurrentViewport.width(); float curHeight = mCurrentViewport.height(); x = Math.max(0, Math.min(x, 1 - curWidth)); y = Math.max(0, Math.min(y, 1 - curHeight)); mCurrentViewport.set(x, y, x + curWidth, y + curHeight); triggerViewportChangedListener(); } //////////////////////////////////////////////////////////////////////////////////////////////// // // Methods for programmatically changing the viewport // //////////////////////////////////////////////////////////////////////////////////////////////// /** * Returns the current viewport (visible extremes for the chart domain and range.) */ public RectF getCurrentViewport() { return new RectF(mCurrentViewport); } private void triggerViewportChangedListener() { if (mOnViewportChangedListener != null) { mOnViewportChangedListener.onViewportChanged(); } } //////////////////////////////////////////////////////////////////////////////////////////////// // // Methods and classes related to view state persistence. // //////////////////////////////////////////////////////////////////////////////////////////////// @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.viewport = mCurrentViewport; return ss; } @Override public void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); mCurrentViewport = ss.viewport; } public void setRelativeAspectRatio(float relativeAspectRatio) { mRelativeAspectRatio = relativeAspectRatio; constrainViewport(); triggerViewportChangedListener(); } public void setViewport(RectF viewport) { mCurrentViewport.set(viewport); triggerViewportChangedListener(); } public void setMaxZoom(int maxZoom) { mMinViewportWidthOrHeight = 1f / maxZoom; } public void setOnViewportChangedListener(OnViewportChangedListener onViewportChangedListener) { mOnViewportChangedListener = onViewportChangedListener; } public void setOnOtherGestureListener(OnOtherGestureListener onOtherGestureListener) { mOnOtherGestureListener = onOtherGestureListener; } public void enablePanScale(boolean panScaleEnabled) { mPanScaleEnabled = panScaleEnabled; } /** * Persistent state that is saved by PanScaleProxyView. */ public static class SavedState extends BaseSavedState { private RectF viewport; public SavedState(Parcelable superState) { super(superState); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeFloat(viewport.left); out.writeFloat(viewport.top); out.writeFloat(viewport.right); out.writeFloat(viewport.bottom); } @Override public String toString() { return "PanScaleProxyView.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " viewport=" + viewport.toString() + "}"; } public static final Parcelable.Creator CREATOR = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks() { @Override public SavedState createFromParcel(Parcel in, ClassLoader loader) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }); SavedState(Parcel in) { super(in); viewport = new RectF(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat()); } } public interface OnViewportChangedListener { void onViewportChanged(); } public interface OnOtherGestureListener { void onSingleTapUp(); } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/component/Scrollbar.kt ================================================ package com.yalin.style.view.component import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.graphics.Path import android.graphics.RectF import android.util.AttributeSet import android.view.View import com.yalin.style.R import com.yalin.style.util.MathUtil /** * @author jinyalin * @since 2017/5/22. */ class Scrollbar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : View(context, attrs, defStyle) { companion object { private val DEFAULT_BACKGROUND_COLOR = 0x80000000.toInt() private val DEFAULT_INDICATOR_COLOR = 0xff000000.toInt() } private var mHidden = true private val mAnimationDuration: Int = resources.getInteger(android.R.integer.config_shortAnimTime) private var mIndicatorWidth: Float = 0.toFloat() private val mBackgroundPaint: Paint private val mIndicatorPaint: Paint private val mTempPath = Path() private val mTempRectF = RectF() private var mWidth: Int = 0 private var mHeight: Int = 0 private var mScrollRange: Int = 0 private var mViewportWidth: Int = 0 private var mPosition: Float = 0.toFloat() init { val a = context.obtainStyledAttributes(attrs, R.styleable.Scrollbar) val mBackgroundColor = a.getColor(R.styleable.Scrollbar_backgroundColor, DEFAULT_BACKGROUND_COLOR) val mIndicatorColor = a.getColor(R.styleable.Scrollbar_indicatorColor, DEFAULT_INDICATOR_COLOR) a.recycle() mBackgroundPaint = Paint() mBackgroundPaint.color = mBackgroundColor mBackgroundPaint.isAntiAlias = true mIndicatorPaint = Paint() mIndicatorPaint.color = mIndicatorColor mIndicatorPaint.isAntiAlias = true alpha = 0f } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (mScrollRange <= mViewportWidth) { return } mTempRectF.top = 0f mTempRectF.bottom = mHeight.toFloat() mTempRectF.left = 0f mTempRectF.right = mWidth.toFloat() drawPill(canvas, mTempRectF, mBackgroundPaint) mTempRectF.top = 0f mTempRectF.bottom = mHeight.toFloat() mTempRectF.left = mPosition * 1f / (mScrollRange - mViewportWidth) * mWidth.toFloat() * (1 - mIndicatorWidth) mTempRectF.right = mTempRectF.left + mIndicatorWidth * mWidth drawPill(canvas, mTempRectF, mIndicatorPaint) } private fun drawPill(canvas: Canvas, rectF: RectF, paint: Paint) { val radius = rectF.height() / 2 var temp: Float mTempPath.reset() mTempPath.moveTo(rectF.left + radius, rectF.top) mTempPath.lineTo(rectF.right - radius, rectF.top) temp = rectF.left rectF.left = rectF.right - 2 * radius mTempPath.arcTo(rectF, 270f, 180f) rectF.left = temp mTempPath.lineTo(rectF.left + radius, rectF.bottom) temp = rectF.right rectF.right = rectF.left + rectF.height() mTempPath.arcTo(rectF, 90f, 180f) rectF.right = temp mTempPath.close() canvas.drawPath(mTempPath, paint) } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { setMeasuredDimension( View.resolveSize(0, widthMeasureSpec), View.resolveSize(0, heightMeasureSpec)) } fun setScrollPosition(position: Int) { mPosition = MathUtil.constrain(0f, mScrollRange.toFloat(), position.toFloat()) postInvalidateOnAnimation() } fun setScrollRangeAndViewportWidth(scrollRange: Int, viewportWidth: Int) { mScrollRange = scrollRange mViewportWidth = viewportWidth mIndicatorWidth = 0.1f if (mScrollRange > 0) { mIndicatorWidth = MathUtil.constrain(mIndicatorWidth, 1f, mViewportWidth * 1f / mScrollRange) } postInvalidateOnAnimation() } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) mHeight = h mWidth = w } fun show() { if (!mHidden) { return } mHidden = false animate().cancel() animate().alpha(1f).duration = mAnimationDuration.toLong() } fun hide() { if (mHidden) { return } mHidden = true animate().cancel() animate().alpha(0f).duration = mAnimationDuration.toLong() } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/component/ShadowDipsTextView.kt ================================================ /* * Copyright 2014 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.yalin.style.view.component import android.content.Context import android.content.res.TypedArray import android.support.v7.widget.AppCompatTextView import android.util.AttributeSet import com.yalin.style.R class ShadowDipsTextView : AppCompatTextView { constructor(context: Context) : super(context) { init(context, null, 0) } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(context, attrs, 0) } constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) { init(context, attrs, defStyle) } private fun init(context: Context, attrs: AttributeSet?, defStyle: Int) { val a = context.obtainStyledAttributes(attrs, R.styleable.ShadowDipsTextView, defStyle, 0) val shadowDx = a.getDimensionPixelSize(R.styleable.ShadowDipsTextView_shadowDx, 0) val shadowDy = a.getDimensionPixelSize(R.styleable.ShadowDipsTextView_shadowDy, 0) val shadowRadius = a.getDimensionPixelSize(R.styleable.ShadowDipsTextView_shadowRadius, 0) val shadowColor = a.getColor(R.styleable.ShadowDipsTextView_shadowColor, 0) if (shadowColor != 0) { setShadowLayer(shadowRadius.toFloat(), shadowDx.toFloat(), shadowDy.toFloat(), shadowColor) } a.recycle() } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/component/TintableImageButton.kt ================================================ package com.yalin.style.view.component import android.content.Context import android.content.res.ColorStateList import android.util.AttributeSet import com.yalin.style.R /** * @author jinyalin * * * @since 2017/4/30. */ class TintableImageButton : android.support.v7.widget.AppCompatImageButton { private var tint: ColorStateList? = null constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(context, attrs, 0) } constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) { init(context, attrs, defStyle) } private fun init(context: Context, attrs: AttributeSet, defStyle: Int) { val a = context.obtainStyledAttributes(attrs, R.styleable.TintableImageButton, defStyle, 0) tint = a.getColorStateList(R.styleable.TintableImageButton_tint) a.recycle() } override fun drawableStateChanged() { super.drawableStateChanged() if (tint != null && tint!!.isStateful) { updateTintColor() } } fun setColorFilter(tint: ColorStateList) { this.tint = tint super.setColorFilter(tint.getColorForState(drawableState, 0)) } private fun updateTintColor() { val color = tint!!.getColorForState(drawableState, 0) setColorFilter(color) } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/component/Zoomer.kt ================================================ package com.yalin.style.view.component import android.content.Context import android.os.SystemClock import android.view.animation.DecelerateInterpolator import android.view.animation.Interpolator import com.yalin.style.util.MathUtil /** * @author jinyalin * * * @since 2017/4/21. * * A simple class that animates double-touch zoom gestures. Functionally similar to a [ ]. */ internal class Zoomer(context: Context) { /** * The interpolator, used for making zooms animate 'naturally.' */ private val mInterpolator: Interpolator /** * The total animation duration for a zoom. */ private val mAnimationDurationMillis: Int /** * Whether or not the current zoom has finished. */ private var mFinished = true /** * The starting zoom value. */ private var mStartZoom = 1f /** * The current zoom value; computed by [.computeZoom]. */ /** * Returns the current zoom level. * @see android.widget.Scroller.getCurrX */ var currZoom: Float = 0.toFloat() private set /** * The time the zoom started, computed using [android.os.SystemClock.elapsedRealtime]. */ private var mStartRTC: Long = 0 /** * The destination zoom factor. */ private var mEndZoom: Float = 0.toFloat() init { mInterpolator = DecelerateInterpolator() mAnimationDurationMillis = context.resources.getInteger( android.R.integer.config_shortAnimTime) } /** * Forces the zoom finished state to the given value. Unlike [.abortAnimation], the * current zoom value isn't set to the ending value. * @see android.widget.Scroller.forceFinished */ fun forceFinished(finished: Boolean) { mFinished = finished } /** * Aborts the animation, setting the current zoom value to the ending value. * @see android.widget.Scroller.abortAnimation */ fun abortAnimation() { mFinished = true currZoom = mEndZoom } /** * Starts a zoom from startZoom to endZoom. That is, to zoom from 100% to 125%, endZoom should * by 0.25f. * @see android.widget.Scroller.startScroll */ fun startZoom(startZoom: Float, endZoom: Float) { mStartRTC = SystemClock.elapsedRealtime() mEndZoom = endZoom mFinished = false mStartZoom = startZoom currZoom = startZoom } /** * Computes the current zoom level, returning true if the zoom is still active and false if the * zoom has finished. * @see android.widget.Scroller.computeScrollOffset */ fun computeZoom(): Boolean { if (mFinished) { return false } val tRTC = SystemClock.elapsedRealtime() - mStartRTC if (tRTC >= mAnimationDurationMillis) { mFinished = true currZoom = mEndZoom return false } val t = tRTC * 1f / mAnimationDurationMillis currZoom = MathUtil.interpolate( mStartZoom, mEndZoom, mInterpolator.getInterpolation(t)) return true } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/fragment/AnimatedStyleLogoFragment.kt ================================================ package com.yalin.style.view.fragment import android.app.Fragment import android.os.Bundle import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.ViewPropertyAnimator import android.view.animation.OvershootInterpolator import com.yalin.style.R import kotlinx.android.synthetic.main.animated_logo_fragment.* /** * YaLin 2017/1/4. */ class AnimatedStyleLogoFragment : Fragment() { private var mOnFillStartedCallback: (() -> Unit)? = null private var mInitialLogoOffset: Float = 0.toFloat() private var mAnimator: ViewPropertyAnimator? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mInitialLogoOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16f, resources.displayMetrics) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = inflater.inflate(R.layout.animated_logo_fragment, container, false) override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) reset() } fun start() { mAnimator = logoView.animate().translationY(0f) .withEndAction { with(logoSubtitle) { visibility = View.VISIBLE translationY = (-height).toFloat() val interpolator = OvershootInterpolator() animate() .translationY(0f) .setInterpolator(interpolator).setDuration(500) .start() } mOnFillStartedCallback?.invoke() }.setDuration(500) mAnimator!!.start() } override fun onDetach() { super.onDetach() if (mAnimator != null) { mAnimator!!.cancel() } } fun setOnFillStartedCallback(fillStartedCallback: () -> Unit) { mOnFillStartedCallback = fillStartedCallback } fun reset() { logoView.translationY = mInitialLogoOffset logoSubtitle.visibility = View.INVISIBLE } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/fragment/BaseFragment.kt ================================================ package com.yalin.style.view.fragment import android.app.Fragment import android.widget.Toast import com.yalin.style.injection.HasComponent /** * @author jinyalin * * * @since 2017/4/20. */ abstract class BaseFragment : Fragment() { /** * Shows a [android.widget.Toast] message. * @param message An string representing a message to be shown. */ protected fun showToastMessage(message: String) { Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() } /** * Gets a component for dependency injection by its type. */ protected fun getComponent(componentType: Class): C { @Suppress("UNCHECKED_CAST") return componentType.cast((activity as HasComponent).component) } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/fragment/SettingsAdvanceFragment.kt ================================================ package com.yalin.style.view.fragment import android.app.Activity import android.os.Bundle import android.os.Handler import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.SeekBar import com.yalin.style.R import com.yalin.style.render.StyleBlurRenderer import com.yalin.style.settings.Prefs import com.yalin.style.view.activity.SettingsActivity import kotlinx.android.synthetic.main.layout_include_settings_content.* /** * @author jinyalin * * * @since 2017/5/2. */ class SettingsAdvanceFragment : BaseFragment(), SettingsActivity.SettingsActivityMenuListener { companion object { private val mHandler = Handler() fun newInstance(): SettingsAdvanceFragment { return SettingsAdvanceFragment() } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = inflater.inflate(R.layout.layout_style_settings, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupViews() } override fun onAttach(activity: Activity) { (activity as SettingsActivity).inflateMenuFromFragment(R.menu.menu_settings_advanced) super.onAttach(activity) } override fun onDestroy() { super.onDestroy() mHandler.removeCallbacksAndMessages(null) } private fun setupViews() { blurAmount.apply { progress = Prefs.getSharedPreferences(activity) .getInt(Prefs.PREF_BLUR_AMOUNT, StyleBlurRenderer.DEFAULT_BLUR) setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { if (fromUser) { mHandler.removeCallbacks(mUpdateBlurRunnable) mHandler.postDelayed(mUpdateBlurRunnable, 750) } } override fun onStartTrackingTouch(seekBar: SeekBar) {} override fun onStopTrackingTouch(seekBar: SeekBar) {} }) } dimAmount.apply { progress = Prefs.getSharedPreferences(activity) .getInt(Prefs.PREF_DIM_AMOUNT, StyleBlurRenderer.DEFAULT_MAX_DIM) setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { if (fromUser) { mHandler.removeCallbacks(mUpdateDimRunnable) mHandler.postDelayed(mUpdateDimRunnable, 750) } } override fun onStartTrackingTouch(seekBar: SeekBar) {} override fun onStopTrackingTouch(seekBar: SeekBar) {} }) } greyAmount.apply { progress = Prefs.getSharedPreferences(activity) .getInt(Prefs.PREF_GREY_AMOUNT, StyleBlurRenderer.DEFAULT_GREY) setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { if (fromUser) { mHandler.removeCallbacks(mUpdateGreyRunnable) mHandler.postDelayed(mUpdateGreyRunnable, 750) } } override fun onStartTrackingTouch(seekBar: SeekBar) {} override fun onStopTrackingTouch(seekBar: SeekBar) { } }) } blurOnLockscreen.apply { setOnCheckedChangeListener { _, checked -> Prefs.getSharedPreferences(activity).edit() .putBoolean(Prefs.PREF_DISABLE_BLUR_WHEN_LOCKED, !checked) .apply() } isChecked = !Prefs.getSharedPreferences(activity) .getBoolean(Prefs.PREF_DISABLE_BLUR_WHEN_LOCKED, false) } } private val mUpdateBlurRunnable = Runnable { Prefs.getSharedPreferences(activity).edit() .putInt(Prefs.PREF_BLUR_AMOUNT, blurAmount.progress) .apply() } private val mUpdateDimRunnable = Runnable { Prefs.getSharedPreferences(activity).edit() .putInt(Prefs.PREF_DIM_AMOUNT, dimAmount.progress) .apply() } private val mUpdateGreyRunnable = Runnable { Prefs.getSharedPreferences(activity).edit() .putInt(Prefs.PREF_GREY_AMOUNT, greyAmount.progress) .apply() } override fun onSettingsActivityMenuItemClick(item: MenuItem) { if (item.itemId == R.id.action_reset) { Prefs.getSharedPreferences(activity).edit() .putInt(Prefs.PREF_BLUR_AMOUNT, StyleBlurRenderer.DEFAULT_BLUR) .putInt(Prefs.PREF_DIM_AMOUNT, StyleBlurRenderer.DEFAULT_MAX_DIM) .putInt(Prefs.PREF_GREY_AMOUNT, StyleBlurRenderer.DEFAULT_GREY) .apply() blurAmount.progress = StyleBlurRenderer.DEFAULT_BLUR dimAmount.progress = StyleBlurRenderer.DEFAULT_MAX_DIM greyAmount.progress = StyleBlurRenderer.DEFAULT_GREY } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/fragment/SettingsChooseSourceFragment.kt ================================================ package com.yalin.style.view.fragment import android.animation.ObjectAnimator import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.graphics.* import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.os.Bundle import android.os.Handler import android.support.v4.content.ContextCompat import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import com.yalin.style.R import com.yalin.style.analytics.Analytics import com.yalin.style.analytics.Event import com.yalin.style.data.log.LogUtil import com.yalin.style.domain.repository.SourcesRepository import com.yalin.style.injection.component.SourceComponent import com.yalin.style.model.SourceItem import com.yalin.style.presenter.SettingsChooseSourcePresenter import com.yalin.style.view.SourceChooseView import com.yalin.style.view.activity.AdvanceSettingActivity import com.yalin.style.view.activity.GallerySettingActivity import com.yalin.style.view.component.ObservableHorizontalScrollView import kotlinx.android.synthetic.main.layout_settings_choose_source.* import javax.inject.Inject import kotlin.collections.HashMap /** * @author jinyalin * @since 2017/5/22. */ class SettingsChooseSourceFragment : BaseFragment(), SourceChooseView { companion object { private val TAG = "SettingsChooseSourceFragment" private val SCROLLBAR_HIDE_DELAY_MILLIS = 1000 private val ALPHA_UNSELECTED = 0.4f } private val mHandler = Handler() private var mCurrentScroller: ObjectAnimator? = null private val mHideScrollbarRunnable = { if (sourceScrollbar != null) { sourceScrollbar.hide() } } private var mAnimationDuration: Int = 0 private var mItemWidth: Int = 0 private var mItemImageSize: Int = 0 private var mItemEstimatedHeight: Int = 0 private val mTempRectF = RectF() private val mImageFillPaint = Paint() private val mAlphaPaint = Paint() private var mSelectedSourceImage: Drawable? = null @Inject lateinit internal var settingsPresenter: SettingsChooseSourcePresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime) mItemWidth = resources.getDimensionPixelSize( R.dimen.settings_choose_source_item_width) mItemEstimatedHeight = resources.getDimensionPixelSize( R.dimen.settings_choose_source_item_estimated_height) mItemImageSize = resources.getDimensionPixelSize( R.dimen.settings_choose_source_item_image_size) getComponent(SourceComponent::class.java).inject(this) prepareGenerateSourceImages() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = inflater.inflate(R.layout.layout_settings_choose_source, container, false)!! override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) settingsPresenter.setView(this) sourceScroller.setCallbacks(object : ObservableHorizontalScrollView.Callbacks { override fun onScrollChanged(scrollX: Int) { showScrollbar() } override fun onDownMotionEvent() { mCurrentScroller?.cancel() } }) settingsPresenter.restoreInstanceState(savedInstanceState) settingsPresenter.initialize() } override fun onResume() { super.onResume() settingsPresenter.resume() } override fun onPause() { super.onPause() settingsPresenter.pause() } override fun onDestroy() { super.onDestroy() settingsPresenter.destroy() } override fun onSaveInstanceState(outState: Bundle) { settingsPresenter.saveInstanceState(outState) super.onSaveInstanceState(outState) } override fun renderSources(sources: List) { redrawSources(sources) } override fun sourceSelected(sources: List, selectedItem: SourceItem) { updateSelectedItem(sources, selectedItem, true) } override fun showLoading() { } override fun hideLoading() { } override fun showRetry() { } override fun hideRetry() { } override fun showError(message: String) { } override fun context(): Context { return activity } val sourcesMap = HashMap() private fun redrawSources(sources: List) { if (!isAdded) { return } sourcesMap.clear() sourceContainer.removeAllViews() for (source in sources) { val rootView = LayoutInflater.from(activity).inflate( R.layout.settings_choose_source_item, sourceContainer, false) sourcesMap.put(source.id, rootView) with(rootView!!) { alpha = ALPHA_UNSELECTED val selectSourceButton = findViewById(R.id.source_image) selectSourceButton.setOnClickListener { if (source.selected) { (activity as Callbacks).onRequestCloseActivity() } else { Analytics.logEvent(context, Event.SELECT_WALLPAPER_SOURCE, source.title!!) settingsPresenter.selectSource(source.id) } } val icon = generateSourceImage(source.iconId) icon.setColorFilter(source.color, PorterDuff.Mode.SRC_ATOP) selectSourceButton.background = icon adjustSourceColor(source) val titleView = findViewById(R.id.source_title) as TextView titleView.text = source.title titleView.setTextColor(source.color) (findViewById(R.id.source_status) as TextView).text = source.description val settingsButton = findViewById(R.id.source_settings_button) settingsButton.setOnClickListener { Analytics.logEvent(context, Event.CUSTOM_WALLPAPER_SETTINGS) launchSourceSettings(source) } animateSettingsButton(settingsButton, false, false) sourceContainer.addView(rootView) } } } private fun updateSelectedItem(sources: List, selectedItem: SourceItem, allowAnimate: Boolean) { var selectedIndex = -1 var index = -1 for (source in sources) { index++ val selected = source.id == selectedItem.id selectedIndex = if (selected) index else selectedIndex with(sourcesMap[source.id]!!) { val sourceImageButton = findViewById(R.id.source_image) val drawable = if (selected) mSelectedSourceImage else generateSourceImage(source.iconId) drawable!!.setColorFilter(source.color, PorterDuff.Mode.SRC_ATOP) sourceImageButton.background = drawable val alpha = if (source.selected) 1f else ALPHA_UNSELECTED animate().alpha(alpha).duration = mAnimationDuration.toLong() val settingsButton = findViewById(R.id.source_settings_button) animateSettingsButton(settingsButton, source.selected && source.hasSetting, allowAnimate) } } if (selectedIndex >= 0 && allowAnimate) { mCurrentScroller?.cancel() // For some reason smoothScrollTo isn't very smooth.. mCurrentScroller = ObjectAnimator.ofInt(sourceScroller, "scrollX", mItemWidth * selectedIndex) mCurrentScroller!!.duration = mAnimationDuration.toLong() mCurrentScroller!!.start() } } private fun animateSettingsButton(settingsButton: View, show: Boolean, allowAnimate: Boolean) { if (show && settingsButton.visibility == View.VISIBLE || !show && settingsButton.visibility == View.INVISIBLE) { return } settingsButton.visibility = View.VISIBLE settingsButton.animate() .translationY((if (show) 0 else -resources.getDimensionPixelSize( R.dimen.settings_choose_source_settings_button_animate_distance)) .toFloat()) .alpha(if (show) 1f else 0f) .rotation((if (show) 0 else -90).toFloat()) .setDuration((if (allowAnimate) 300 else 0).toLong()) .setStartDelay((if (show && allowAnimate) 200 else 0).toLong()) .withLayer() .withEndAction { if (!show) { settingsButton.visibility = View.INVISIBLE } } } private fun showScrollbar() { mHandler.removeCallbacks(mHideScrollbarRunnable) sourceScrollbar?.setScrollRangeAndViewportWidth( sourceScroller.computeHorizontalScrollRange(), sourceScroller.width) sourceScrollbar?.setScrollPosition(sourceScroller.scrollX) sourceScrollbar?.show() mHandler.postDelayed(mHideScrollbarRunnable, SCROLLBAR_HIDE_DELAY_MILLIS.toLong()) } private fun generateSourceImage(iconId: Int): BitmapDrawable { val image = ContextCompat.getDrawable(activity, iconId) val bitmap = Bitmap.createBitmap(mItemImageSize, mItemImageSize, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) mTempRectF.set(0f, 0f, mItemImageSize.toFloat(), mItemImageSize.toFloat()) canvas.drawOval(mTempRectF, mImageFillPaint) if (image != null) { canvas.saveLayer(0f, 0f, mItemImageSize.toFloat(), mItemImageSize.toFloat(), mAlphaPaint, Canvas.ALL_SAVE_FLAG) image.setBounds(0, 0, mItemImageSize, mItemImageSize) image.draw(canvas) canvas.restore() } return BitmapDrawable(resources, bitmap) } private fun prepareGenerateSourceImages() { mImageFillPaint.color = Color.WHITE mImageFillPaint.isAntiAlias = true mAlphaPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) mSelectedSourceImage = generateSourceImage(R.drawable.ic_source_selected) } private fun adjustSourceColor(source: SourceItem) = with(source) { try { val hsv = FloatArray(3) Color.colorToHSV(color, hsv) var adjust = false if (hsv[2] < 0.8f) { hsv[2] = 0.8f adjust = true } if (hsv[1] > 0.4f) { hsv[1] = 0.4f adjust = true } if (adjust) { color = Color.HSVToColor(hsv) } if (Color.alpha(color) != 255) { color = Color.argb(255, Color.red(color), Color.green(color), Color.blue(color)) } } catch (ignored: IllegalArgumentException) { } } private fun launchSourceSettings(source: SourceItem) { try { if (source.id == SourcesRepository.SOURCE_ID_CUSTOM) { val settingsIntent = Intent(activity, GallerySettingActivity::class.java) startActivity(settingsIntent) } else if (source.id == SourcesRepository.SOURCE_ID_ADVANCE) { val settingsIntent = Intent(activity, AdvanceSettingActivity::class.java) startActivity(settingsIntent) } } catch (e: ActivityNotFoundException) { LogUtil.E(TAG, "Can't launch source settings.", e) } catch (e: SecurityException) { LogUtil.E(TAG, "Can't launch source settings.", e) } } interface Callbacks { fun onRequestCloseActivity() } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/fragment/StyleRenderFragment.java ================================================ package com.yalin.style.view.fragment; import android.app.ActivityManager; import android.app.Fragment; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.os.Bundle; import android.support.annotation.Nullable; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import com.bumptech.glide.request.animation.GlideAnimation; import com.bumptech.glide.request.target.BaseTarget; import com.bumptech.glide.request.target.SizeReadyCallback; import com.bumptech.glide.request.target.Target; import com.yalin.style.StyleApplication; import com.yalin.style.render.DemoRenderController; import com.yalin.style.render.GLTextureView; import com.yalin.style.render.ImageBlurrer; import com.yalin.style.render.RenderController; import com.yalin.style.render.StyleBlurRenderer; import com.yalin.style.util.ImageLoader; import javax.inject.Inject; /** * @author jinyalin * @since 2017/4/19. */ public class StyleRenderFragment extends Fragment implements RenderController.Callbacks, StyleBlurRenderer.Callbacks { private static final String ARG_DEMO_MODE = "demo_mode"; private static final String ARG_DEMO_FOCUS = "demo_focus"; private boolean mDemoMode = false; private boolean mDemoFocus = false; private StyleView mView; private ImageView mSimpleDemoModeImageView; private ImageLoader mImageLoader; public static StyleRenderFragment createInstance(boolean demoMode, boolean demoFocus) { StyleRenderFragment fragment = new StyleRenderFragment(); Bundle args = new Bundle(); args.putBoolean(ARG_DEMO_MODE, demoMode); args.putBoolean(ARG_DEMO_FOCUS, demoFocus); fragment.setArguments(args); return fragment; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { Bundle args = getArguments(); mDemoMode = args.getBoolean(ARG_DEMO_MODE); mDemoFocus = args.getBoolean(ARG_DEMO_FOCUS); } else { mDemoMode = savedInstanceState.getBoolean(ARG_DEMO_MODE); mDemoFocus = savedInstanceState.getBoolean(ARG_DEMO_FOCUS); } } @Override public void onSaveInstanceState(Bundle outState) { outState.putBoolean(ARG_DEMO_MODE, mDemoMode); outState.putBoolean(ARG_DEMO_FOCUS, mDemoFocus); super.onSaveInstanceState(outState); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { ActivityManager activityManager = (ActivityManager) getActivity().getSystemService(Context.ACTIVITY_SERVICE); if (mDemoMode && activityManager.isLowRamDevice()) { DisplayMetrics dm = getResources().getDisplayMetrics(); int targetWidth = dm.widthPixels; int targetHeight = dm.heightPixels; mSimpleDemoModeImageView = new ImageView(getActivity()); mSimpleDemoModeImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); mImageLoader = new ImageLoader(getActivity()); mImageLoader.beginImageLoad("file:///android_asset/painterly-architectonic.jpg", null, true) .override(targetWidth, targetHeight) .into(mSimpleDemoModeLoadTarget); return mSimpleDemoModeImageView; } else { mView = new StyleView(getActivity()); mView.setPreserveEGLContextOnPause(true); return mView; } } @Override public void onHiddenChanged(boolean hidden) { super.onHiddenChanged(hidden); if (mView != null) { mView.mRenderController.setVisible(!hidden); } } @Override public void onDestroyView() { super.onDestroyView(); mView = null; } @Override public void onResume() { super.onResume(); if (mView != null) { mView.onResume(); } } @Override public void onPause() { super.onPause(); if (mView != null) { mView.onPause(); } } @Override public void queueEventOnGlThread(Runnable runnable) { if (mView != null) { mView.queueEvent(runnable); } } @Override public void requestRender() { if (mView != null) { mView.requestRender(); } } private Target mSimpleDemoModeLoadTarget = new BaseTarget() { @Override public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { if (!mDemoFocus) { ImageBlurrer imageBlurrer = new ImageBlurrer(getActivity(), resource); Bitmap blurred = imageBlurrer.blurBitmap(ImageBlurrer.MAX_SUPPORTED_BLUR_PIXELS, 0); imageBlurrer.destroy(); Canvas c = new Canvas(blurred); c.drawColor(Color.argb(255 - StyleBlurRenderer.DEFAULT_MAX_DIM, 0, 0, 0)); resource = blurred; } mSimpleDemoModeImageView.setImageBitmap(resource); } @Override public void getSize(SizeReadyCallback cb) { } }; public class StyleView extends GLTextureView { @Inject DemoRenderController mRenderController; private StyleBlurRenderer mRenderer; public StyleView(Context context) { super(context); mRenderer = new StyleBlurRenderer(context, StyleRenderFragment.this); setEGLContextClientVersion(2); setEGLConfigChooser(8, 8, 8, 8, 0, 0); setRenderer(mRenderer); setRenderMode(RENDERMODE_WHEN_DIRTY); mRenderer.setDemoMode(mDemoMode); StyleApplication.Companion.getInstance().getApplicationComponent().inject(this); mRenderController.setComponent(mRenderer, StyleRenderFragment.this); mRenderController.setVisible(true); mRenderController.start(mDemoFocus); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mRenderer.hintViewportSize(w, h); mRenderController.reloadCurrentWallpaper(); } @Override protected void onDetachedFromWindow() { mRenderController.destroy(); queueEventOnGlThread(new Runnable() { @Override public void run() { mRenderer.destroy(); } }); super.onDetachedFromWindow(); } } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/fragment/TutorialFragment.kt ================================================ package com.yalin.style.view.fragment import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.graphics.drawable.AnimatedVectorDrawable import android.os.Build import android.os.Bundle import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.OvershootInterpolator import android.widget.ImageView import com.yalin.style.R import com.yalin.style.event.MainContainerInsetsChangedEvent import com.yalin.style.event.SeenTutorialEvent import kotlinx.android.synthetic.main.layout_include_tutorial_content.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe /** * @author jinyalin * * * @since 2017/5/2. */ class TutorialFragment : BaseFragment() { companion object { fun newInstance(): TutorialFragment { return TutorialFragment() } } private var animatorSet: AnimatorSet? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) EventBus.getDefault().register(this) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.layout_include_tutorial_content, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) tutorialIconAffordance.setOnClickListener { EventBus.getDefault().post(SeenTutorialEvent()) } if (savedInstanceState == null) { val animateDistance = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100f, resources.displayMetrics) tutorialMainText.alpha = 0f tutorialMainText.translationY = -animateDistance / 5 tutorialSubText.alpha = 0f tutorialSubText.translationY = -animateDistance / 5 tutorialIconAffordance.alpha = 0f tutorialIconAffordance.translationY = animateDistance tutorialIconText.alpha = 0f tutorialIconText.translationY = animateDistance AnimatorSet().apply { startDelay = 500 duration = 250 playTogether( ObjectAnimator.ofFloat(tutorialMainText, View.ALPHA, 1f), ObjectAnimator.ofFloat(tutorialSubText, View.ALPHA, 1f)) start() } animatorSet = AnimatorSet().apply { startDelay = 2000 // Bug in older versions where set.setInterpolator didn't work val interpolator = OvershootInterpolator() val a1 = ObjectAnimator.ofFloat(tutorialIconAffordance, View.TRANSLATION_Y, 0f) val a2 = ObjectAnimator.ofFloat(tutorialIconText, View.TRANSLATION_Y, 0f) val a3 = ObjectAnimator.ofFloat(tutorialMainText, View.TRANSLATION_Y, 0f) val a4 = ObjectAnimator.ofFloat(tutorialSubText, View.TRANSLATION_Y, 0f) a1.interpolator = interpolator a2.interpolator = interpolator a3.interpolator = interpolator a4.interpolator = interpolator duration = 500 playTogether( ObjectAnimator.ofFloat(tutorialIconAffordance, View.ALPHA, 1f), ObjectAnimator.ofFloat(tutorialIconText, View.ALPHA, 1f), a1, a2, a3, a4) if (Build.VERSION.SDK_INT >= 21) { addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { playImageAvd(tutorialIconEmanate) } }) } start() } } else { playImageAvd(tutorialIconEmanate) } val mcisce = EventBus.getDefault().getStickyEvent( MainContainerInsetsChangedEvent::class.java) if (mcisce != null) { onEventMainThread(mcisce) } } private fun playImageAvd(emanateView: ImageView) { if (Build.VERSION.SDK_INT >= 21) { val avd = resources.getDrawable( R.drawable.avd_tutorial_icon_emanate, activity.theme) as AnimatedVectorDrawable emanateView.setImageDrawable(avd) avd.start() } } override fun onDetach() { super.onDetach() animatorSet?.cancel() } override fun onDestroy() { super.onDestroy() EventBus.getDefault().unregister(this) } @Subscribe fun onEventMainThread(spe: MainContainerInsetsChangedEvent) { val insets = spe.insets tutorialContainer.setPadding( insets.left, insets.top, insets.right, insets.bottom) } } ================================================ FILE: presentation/src/main/java/com/yalin/style/view/fragment/WallpaperDetailFragment.kt ================================================ package com.yalin.style.view.fragment import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle import android.util.TypedValue import android.view.Gravity import android.view.LayoutInflater import android.view.Menu import android.view.View import android.view.ViewGroup import android.widget.PopupMenu import com.yalin.style.WallpaperDetailViewport import com.yalin.style.R import com.yalin.style.analytics.Analytics import com.yalin.style.analytics.Event import com.yalin.style.data.log.LogUtil import com.yalin.style.event.MainContainerInsetsChangedEvent import com.yalin.style.event.StyleWallpaperSizeChangedEvent import com.yalin.style.event.SwitchingPhotosStateChangedEvent import com.yalin.style.event.SystemWallpaperSizeChangedEvent import com.yalin.style.injection.component.WallpaperComponent import com.yalin.style.model.WallpaperItem import com.yalin.style.presenter.WallpaperDetailPresenter import com.yalin.style.util.ScrimUtil import com.yalin.style.util.TypefaceUtil import com.yalin.style.view.WallpaperDetailView import com.yalin.style.view.activity.SettingsActivity import com.yalin.style.view.component.PanScaleProxyView import kotlinx.android.synthetic.main.layout_wallpaper_detail.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.jetbrains.anko.toast import javax.inject.Inject /** * @author jinyalin * * * @since 2017/4/20. */ class WallpaperDetailFragment : BaseFragment(), WallpaperDetailView, View.OnSystemUiVisibilityChangeListener { companion object { val TAG = "WallpaperDetailFragment" fun createInstance(): WallpaperDetailFragment { return WallpaperDetailFragment() } } @Inject lateinit internal var presenter: WallpaperDetailPresenter private val overflowMenu: PopupMenu by lazy { PopupMenu(activity, btnOverflow) } private var currentViewportId = 0 private var systemWallpaperAspectRatio: Float = 0.toFloat() private var styleWallpaperAspectRatio: Float = 0.toFloat() private var deferResetViewport: Boolean = false private var mGuardViewportChangeListener = false var isOverflowMenuVisible = false private set override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) getComponent(WallpaperComponent::class.java).inject(this) EventBus.getDefault().register(this) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.layout_wallpaper_detail, container, false) } override fun onDestroyView() { super.onDestroyView() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupDetailViews() presenter.setView(this) val syswsce = EventBus.getDefault().getStickyEvent( SystemWallpaperSizeChangedEvent::class.java) if (syswsce != null) { onEventMainThread(syswsce) } val swsce = EventBus.getDefault().getStickyEvent( StyleWallpaperSizeChangedEvent::class.java) if (swsce != null) { onEventMainThread(swsce) } val wdvp = EventBus.getDefault().getStickyEvent( WallpaperDetailViewport::class.java) if (wdvp != null) { onEventMainThread(wdvp) } val mcisce = EventBus.getDefault().getStickyEvent( MainContainerInsetsChangedEvent::class.java) if (mcisce != null) { onEventMainThread(mcisce) } val spsce = EventBus.getDefault().getStickyEvent( SwitchingPhotosStateChangedEvent::class.java) if (spsce != null) { onEventMainThread(spsce) } if (savedInstanceState == null) { loadWallpaper() } else { presenter.restoreInstanceState(savedInstanceState) } } override fun onResume() { super.onResume() presenter.resume() } override fun onPause() { super.onPause() presenter.pause() } override fun onStop() { super.onStop() overflowMenu.dismiss() } override fun onDestroy() { super.onDestroy() presenter.destroy() EventBus.getDefault().unregister(this) } override fun onSaveInstanceState(outState: Bundle) { presenter.saveInstanceState(outState) super.onSaveInstanceState(outState) } private fun setupDetailViews() { metadata.setOnClickListener { val uri = "http://www.kinglloy.com" val viewIntent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) viewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) Analytics.logEvent(context(), Event.VIEW_WALLPAPER_DETAIL) try { startActivity(viewIntent) } catch (e: RuntimeException) { toast(R.string.error_view_details) LogUtil.E(TAG, "Error viewing wallpaper details.", e) Analytics.logEvent(context(), Event.VIEW_WALLPAPER_DETAIL_FAILED) } } chromeContainer.background = ScrimUtil.makeCubicGradientScrimDrawable( 0xaa000000.toInt(), 8, Gravity.BOTTOM) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { statusBarScrimView.visibility = View.GONE } else { statusBarScrimView.background = ScrimUtil.makeCubicGradientScrimDrawable( 0x44000000, 8, Gravity.TOP) } panScaleProxyView.setMaxZoom(5) panScaleProxyView.setOnViewportChangedListener( PanScaleProxyView.OnViewportChangedListener { if (mGuardViewportChangeListener || panScaleProxyView == null) { return@OnViewportChangedListener } WallpaperDetailViewport.instance.setViewport( currentViewportId, panScaleProxyView.currentViewport, true) }) if (activity is PanScaleProxyView.OnOtherGestureListener) { panScaleProxyView.setOnOtherGestureListener( activity as PanScaleProxyView.OnOtherGestureListener) } btnNext.setOnClickListener { Analytics.logEvent(activity, Event.SWITCH) presenter.getNextWallpaper() } setupOverflowButton() } private fun setupOverflowButton() { btnOverflow.setOnTouchListener(overflowMenu.dragToOpenListener) btnOverflow.setOnClickListener { isOverflowMenuVisible = true overflowMenu.show() } overflowMenu.setOnDismissListener { isOverflowMenuVisible = false } overflowMenu.setOnMenuItemClickListener(PopupMenu.OnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.action_like -> { presenter.likeWallpaper() return@OnMenuItemClickListener true } R.id.action_share -> { Analytics.logEvent(activity, Event.SHARE) presenter.shareWallpaper() return@OnMenuItemClickListener true } R.id.action_settings -> { Analytics.logEvent(activity, Event.SETTINGS_OPEN) startActivity(Intent(activity, SettingsActivity::class.java)) return@OnMenuItemClickListener true } } false }) overflowMenu.menu.clear() overflowMenu.inflate(R.menu.style_overflow) overflowMenu.menu.add(0, R.id.action_share, 0, R.string.action_share) } private fun loadWallpaper() { presenter.initialize() } @Subscribe fun onEventMainThread(syswsce: SystemWallpaperSizeChangedEvent) { if (syswsce.height > 0) { systemWallpaperAspectRatio = syswsce.width * 1f / syswsce.height } else { systemWallpaperAspectRatio = panScaleProxyView.width * 1f / panScaleProxyView.height } resetProxyViewport() } @Subscribe fun onEventMainThread(swsce: StyleWallpaperSizeChangedEvent) { styleWallpaperAspectRatio = swsce.width * 1f / swsce.height resetProxyViewport() } @Subscribe fun onEventMainThread(e: WallpaperDetailViewport) { if (!e.isFromUser && panScaleProxyView != null) { mGuardViewportChangeListener = true panScaleProxyView.setViewport(e.getViewport(currentViewportId)) mGuardViewportChangeListener = false } } @Subscribe fun onEventMainThread(spe: SwitchingPhotosStateChangedEvent) { currentViewportId = spe.currentId if (panScaleProxyView != null) { panScaleProxyView.enablePanScale(!spe.isSwitchingPhotos) } // Process deferred wallpaper size change when done switching if (!spe.isSwitchingPhotos && deferResetViewport) { resetProxyViewport() } } @Subscribe fun onEventMainThread(spe: MainContainerInsetsChangedEvent) { val insets = spe.insets chromeContainer.setPadding( insets.left, insets.top, insets.right, insets.bottom) } private fun resetProxyViewport() { if (systemWallpaperAspectRatio == 0f || styleWallpaperAspectRatio == 0f) { return } deferResetViewport = false val spe = EventBus.getDefault() .getStickyEvent(SwitchingPhotosStateChangedEvent::class.java) if (spe != null && spe.isSwitchingPhotos) { deferResetViewport = true return } panScaleProxyView.setRelativeAspectRatio( styleWallpaperAspectRatio / systemWallpaperAspectRatio) } override fun renderWallpaper(wallpaperItem: WallpaperItem) { val titleFont = "AlegreyaSans-Black.ttf" val bylineFont = "AlegreyaSans-Medium.ttf" tvTitle.typeface = TypefaceUtil.getAndCache(context(), titleFont) tvTitle.text = wallpaperItem.title tvAttribution.text = wallpaperItem.attribution tvByline.typeface = TypefaceUtil.getAndCache(context(), bylineFont) tvByline.text = wallpaperItem.byline Analytics.logEvent(activity, Event.PRESENT_WALLPAPER, wallpaperItem.title) } override fun showNextButton(show: Boolean) { btnNext.visibility = if (show) View.VISIBLE else View.GONE } override fun shareWallpaper(shareIntent: Intent) { shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(shareIntent) } override fun validLikeAction(valid: Boolean) { if (valid) { val keepItem = overflowMenu.menu.findItem(R.id.action_like) if (keepItem == null) { overflowMenu.menu.add(0, R.id.action_like, Menu.FIRST, R.string.action_like) } } else { overflowMenu.menu.removeItem(R.id.action_like) btnNext.isActivated = false } } override fun updateLikeState(item: WallpaperItem, liked: Boolean) { overflowMenu.menu .findItem(R.id.action_like) .setTitle(if (liked) R.string.action_unlike else R.string.action_like) btnNext.isActivated = liked } override fun showLoading() { } override fun hideLoading() { } override fun showRetry() { } override fun hideRetry() { } override fun showError(message: String) { showToastMessage(message) } override fun context(): Context { return activity } override fun onSystemUiVisibilityChange(visibility: Int) { val visible = visibility and View.SYSTEM_UI_FLAG_LOW_PROFILE == 0 val metadataSlideDistance = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics) chromeContainer.visibility = View.VISIBLE chromeContainer.animate() .alpha(if (visible) 1f else 0f) .translationY(if (visible) 0f else metadataSlideDistance) .setDuration(200) .withEndAction { if (!visible) { chromeContainer.visibility = View.GONE } } statusBarScrimView.visibility = View.VISIBLE statusBarScrimView.animate() .alpha(if (visible) 1f else 0f) .setDuration(200) .withEndAction { if (!visible) { statusBarScrimView.visibility = View.GONE } } } } ================================================ FILE: presentation/src/main/res/anim/image_fade_in.xml ================================================ ================================================ FILE: presentation/src/main/res/anim-v21/tutorial_icon_emanate_interpolator.xml ================================================ ================================================ FILE: presentation/src/main/res/animator/fade_in.xml ================================================ ================================================ FILE: presentation/src/main/res/animator/fade_out.xml ================================================ ================================================ FILE: presentation/src/main/res/animator/tutorial_icon_emanate_wave1.xml ================================================ ================================================ FILE: presentation/src/main/res/animator/tutorial_icon_emanate_wave1_path.xml ================================================ ================================================ FILE: presentation/src/main/res/animator/tutorial_icon_emanate_wave2.xml ================================================ ================================================ FILE: presentation/src/main/res/animator/tutorial_icon_emanate_wave2_path.xml ================================================ ================================================ FILE: presentation/src/main/res/animator/tutorial_icon_overlay_state_list.xml ================================================ ================================================ FILE: presentation/src/main/res/color/selector_skip_tint.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/gallery_ic_add.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/gallery_ic_add_folder.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/gallery_ic_add_photo.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/gallery_ic_folder.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/grey_selectable_item_background_circle.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/intro_background_protection.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/popup_background.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/scrubber_control_selector.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/scrubber_progress_blur_amount.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/scrubber_progress_dim_amount.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/scrubber_progress_grey_amount.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/scrubber_progress_horizontal.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/settings_source_item_image_overlay.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/tutorial_icon_on_overlay.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable/white_circle_button.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable-v21/avd_tutorial_icon_emanate.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable-v21/grey_selectable_item_background_circle.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable-v21/settings_source_item_image_overlay.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable-v21/tutorial_icon_emanate.xml ================================================ ================================================ FILE: presentation/src/main/res/drawable-v21/tutorial_icon_on_overlay.xml ================================================ ================================================ FILE: presentation/src/main/res/layout/activity_about.xml ================================================ ================================================ FILE: presentation/src/main/res/layout/activity_advance_setting.xml ================================================