From 60d4aa0b205bd74593645fa9c45b4f67b836fb45 Mon Sep 17 00:00:00 2001 From: Zhanty Date: Tue, 2 Jul 2019 15:48:57 +0800 Subject: [PATCH] Initial commit --- lib_base/.gitignore | 22 + lib_base/README.md | 20 + lib_base/build.gradle | 130 ++ lib_base/proguard-rules.pro | 17 + lib_base/src/main/AndroidManifest.xml | 7 + .../com/android/base/adapter/DataManager.java | 81 + .../com/android/base/adapter/ItemHelper.java | 72 + .../base/adapter/list/BaseListAdapter.java | 178 ++ .../adapter/list/ListDataManagerImpl.java | 183 ++ .../base/adapter/list/SmartViewHolder.java | 21 + .../android/base/adapter/list/ViewHolder.java | 35 + .../adapter/pager/SimpleViewPagerAdapter.kt | 33 + .../pager/ViewPageFragmentAdapter.java | 47 + .../base/adapter/pager/ViewPageInfo.java | 17 + .../pager/ViewPageStateFragmentAdapter.java | 48 + .../base/adapter/pager/ViewPagerAdapter.java | 69 + .../adapter/pager/recycler/RecycleBin.java | 152 ++ .../pager/recycler/RecyclingPagerAdapter.java | 114 + .../recycler/AdapterDataObserverProxy.java | 49 + .../adapter/recycler/DiffRecyclerAdapter.java | 255 +++ .../base/adapter/recycler/HeaderSize.java | 5 + .../base/adapter/recycler/ItemViewBinder.kt | 35 + .../adapter/recycler/MultiTypeAdapter.java | 166 ++ .../adapter/recycler/RecyclerAdapter.java | 164 ++ .../recycler/RecyclerDataManagerImpl.java | 248 +++ .../adapter/recycler/SimpleRecyclerAdapter.kt | 59 + .../adapter/recycler/SmartViewHolder.java | 22 + .../base/adapter/recycler/ViewHolder.java | 28 + .../android/base/app/ApplicationDelegate.java | 116 + .../com/android/base/app/BaseAppContext.java | 46 + .../java/com/android/base/app/BaseKit.java | 208 ++ .../com/android/base/app/CrashHandler.java | 113 + .../android/base/app/aac/AutoClearedValue.kt | 79 + .../base/app/aac/AutoDisposeLiveData.kt | 225 ++ .../android/base/app/aac/ResourceLiveData.kt | 227 ++ .../android/base/app/aac/SingleLiveData.kt | 36 + .../android/base/app/aac/SingleLiveEvent.java | 61 + .../base/app/activity/ActivityDelegate.java | 68 + .../app/activity/ActivityDelegateOwner.java | 19 + .../base/app/activity/ActivityDelegates.java | 137 ++ .../base/app/activity/ActivityStatus.kt | 11 + .../base/app/activity/BackHandlerHelper.java | 71 + .../base/app/activity/BaseActivity.java | 222 ++ .../app/activity/OnBackPressListener.java | 13 + .../base/app/dagger/ActivityScope.java | 14 + .../android/base/app/dagger/ContextType.java | 22 + .../base/app/dagger/FragmentScope.java | 14 + .../base/app/dagger/HasViewInjector.java | 18 + .../android/base/app/dagger/Injectable.java | 31 + .../base/app/dagger/ViewComponent.java | 14 + .../base/app/dagger/ViewComponentBuilder.java | 15 + .../base/app/dagger/ViewInjection.java | 60 + .../com/android/base/app/dagger/ViewKey.java | 18 + .../base/app/dagger/ViewModelFactory.java | 45 + .../android/base/app/dagger/ViewModelKey.java | 18 + .../base/app/dagger/ViewModelModule.java | 22 + .../android/base/app/dagger/ViewScope.java | 19 + .../base/app/fragment/BaseDialogFragment.java | 278 +++ .../base/app/fragment/BaseFragment.java | 289 +++ .../base/app/fragment/BaseListFragment.java | 150 ++ .../base/app/fragment/BaseListV2Fragment.kt | 102 + .../app/fragment/BaseStateDialogFragment.java | 145 ++ .../base/app/fragment/BaseStateFragment.java | 152 ++ .../android/base/app/fragment/DelegateEx.kt | 26 + .../base/app/fragment/FragmentConfig.java | 24 + .../base/app/fragment/FragmentDelegate.java | 73 + .../app/fragment/FragmentDelegateOwner.java | 16 + .../base/app/fragment/FragmentDelegates.java | 166 ++ .../base/app/fragment/FragmentInfo.java | 132 ++ .../android/base/app/fragment/Fragments.kt | 463 ++++ .../base/app/fragment/LazyDelegate.java | 117 + .../base/app/fragment/LoadingViewFactory.java | 11 + .../RefreshLoadMoreStateLayoutImpl.kt | 68 + .../fragment/RefreshableStateLayoutImpl.java | 189 ++ .../android/base/app/fragment/TabManager.java | 207 ++ .../android/base/app/mvp/AbstractPresenter.kt | 44 + .../com/android/base/app/mvp/IBaseView.kt | 3 + .../com/android/base/app/mvp/IPresenter.kt | 13 + .../com/android/base/app/mvp/Lifecycle.kt | 31 + .../android/base/app/mvp/PresenterBinder.kt | 66 + .../com/android/base/app/mvp/RxPresenter.kt | 65 + .../android/base/app/mvvm/ArchViewModel.kt | 60 + .../com/android/base/app/mvvm/ViewModelEx.kt | 32 + .../android/base/app/ui/AutoPageNumber.java | 39 + .../com/android/base/app/ui/CommonId.java | 22 + .../com/android/base/app/ui/LoadingView.java | 28 + .../base/app/ui/OnRetryActionListener.java | 9 + .../com/android/base/app/ui/PageNumber.java | 99 + .../base/app/ui/RefreshListLayout.java | 31 + .../base/app/ui/RefreshLoadMoreView.java | 40 + .../base/app/ui/RefreshLoadViewFactory.java | 31 + .../base/app/ui/RefreshStateLayout.java | 11 + .../com/android/base/app/ui/RefreshView.java | 32 + .../base/app/ui/RefreshViewFactory.java | 35 + .../com/android/base/app/ui/StateLayout.java | 27 + .../base/app/ui/StateLayoutConfig.java | 63 + .../android/base/app/ui/SwipeRefreshView.java | 51 + .../main/java/com/android/base/app/ui/UIEx.kt | 153 ++ .../com/android/base/concurrent/GoUtil.java | 106 + .../android/base/concurrent/JobExecutor.java | 37 + .../base/concurrent/OverloadPolicy.java | 31 + .../base/concurrent/PriorityRunnable.java | 37 + .../base/concurrent/SchedulePolicy.java | 25 + .../base/concurrent/SmartExecutor.java | 416 ++++ .../java/com/android/base/data/Resource.java | 119 + .../java/com/android/base/data/Resource.kt | 45 + .../java/com/android/base/data/Status.java | 15 + .../base/imageloader/DisplayConfig.java | 137 ++ .../base/imageloader/GlideImageLoader.java | 389 ++++ .../android/base/imageloader/ImageLoader.java | 113 + .../base/imageloader/ImageLoaderFactory.java | 21 + .../base/imageloader/LoadListener.java | 14 + .../base/imageloader/LoadListenerAdapter.java | 21 + .../base/imageloader/ProgressGlideModule.java | 38 + .../base/imageloader/ProgressInfo.java | 102 + .../base/imageloader/ProgressListener.java | 13 + .../base/imageloader/ProgressManager.java | 201 ++ .../imageloader/ProgressResponseBody.java | 97 + .../com/android/base/imageloader/Source.java | 43 + .../base/interfaces/OnItemClickListener.java | 24 + .../interfaces/OnItemLongClickListener.java | 25 + .../ActivityLifecycleCallbacksAdapter.java | 38 + .../adapter/DrawerListenerAdapter.java | 25 + .../adapter/OnPageChangeListenerAdapter.java | 24 + .../OnSeekBarChangeListenerAdapter.java | 24 + .../adapter/OnTabSelectedListenerAdapter.java | 19 + .../adapter/TextWatcherAdapter.java | 25 + .../adapter/TransitionListenerAdapter.java | 31 + .../com/android/base/kotlin/AttrStyleEx.kt | 38 + .../java/com/android/base/kotlin/ContextEx.kt | 15 + .../java/com/android/base/kotlin/DialogEx.kt | 13 + .../com/android/base/kotlin/FragmentEx.kt | 18 + .../com/android/base/kotlin/KtViewHolder.kt | 15 + .../java/com/android/base/kotlin/LangEx.kt | 58 + .../com/android/base/kotlin/LiveDataEx.kt | 23 + .../java/com/android/base/kotlin/MenuEx.kt | 16 + .../com/android/base/kotlin/RecyclerViewEx.kt | 29 + .../com/android/base/kotlin/ResourceEx.kt | 21 + .../java/com/android/base/kotlin/SizeEx.kt | 27 + .../com/android/base/kotlin/TextViewEx.kt | 121 + .../java/com/android/base/kotlin/UtilsEx.kt | 30 + .../java/com/android/base/kotlin/ViewEx.kt | 148 ++ .../permission/AndPermissionFragment.java | 78 + .../permission/AndPermissionRequester.java | 205 ++ .../com/android/base/permission/AndUtils.java | 21 + .../permission/AutoPermissionFragment.java | 95 + .../permission/AutoPermissionRequester.java | 145 ++ .../DefaultPermissionUIProvider.java | 99 + .../base/permission/EasyPermissions.java | 219 ++ .../permission/IPermissionUIProvider.java | 37 + .../OnAllPermissionGrantedListener.java | 10 + .../OnPermissionDeniedListener.java | 12 + .../android/base/permission/Permission.java | 209 ++ .../base/permission/PermissionCallback.java | 27 + .../base/permission/PermissionCode.java | 8 + .../base/permission/PermissionRequester.java | 73 + .../permission/PermissionRequesterImpl.java | 80 + .../PermissionUIProviderFactory.java | 19 + .../base/receiver/NetStateReceiver.java | 130 ++ .../android/base/receiver/NetworkState.java | 27 + .../java/com/android/base/rx/AutoDispose.kt | 103 + .../android/base/rx/FlowableRetryDelay.java | 48 + .../base/rx/LifecycleScopeProviderEx.kt | 80 + .../android/base/rx/ObservableRetryDelay.java | 47 + .../com/android/base/rx/RetryChecker.java | 7 + .../main/java/com/android/base/rx/RxBus.java | 83 + .../src/main/java/com/android/base/rx/RxEx.kt | 116 + .../main/java/com/android/base/rx/RxKit.java | 119 + .../com/android/base/rx/SchedulerProvider.kt | 46 + .../android/base/rx/ThreadTransformer.java | 85 + .../com/android/base/utils/BaseUtils.java | 45 + .../base/utils/android/ActFragWrapper.java | 68 + .../base/utils/android/ClipboardUtils.java | 88 + .../base/utils/android/DebugUtils.java | 32 + .../base/utils/android/DevicesUtils.java | 127 ++ .../base/utils/android/DirectoryUtils.java | 69 + .../base/utils/android/ResourceUtils.java | 82 + .../base/utils/android/SoftKeyboardUtils.java | 85 + .../android/base/utils/android/SpCache.java | 209 ++ .../android/base/utils/android/TintUtils.java | 74 + .../base/utils/android/UnitConverter.java | 72 + .../base/utils/android/VideoUtils.java | 24 + .../android/base/utils/android/ViewUtils.java | 207 ++ .../base/utils/android/WebViewUtils.java | 123 ++ .../base/utils/android/WindowUtils.java | 157 ++ .../android/base/utils/android/XAppUtils.java | 129 ++ .../base/utils/android/XIntentUtils.java | 150 ++ .../utils/android/anim/ActivityAnimUtils.java | 72 + .../utils/android/compat/AndroidVersion.java | 37 + .../utils/android/compat/SystemBarCompat.java | 363 +++ .../orientation/AbsOrientationProvider.java | 108 + ...elerometerMagneticOrientationProvider.java | 88 + .../android/orientation/Orientation.java | 58 + .../orientation/OrientationProvider.java | 16 + .../RotationVectorOrientationProvider.java | 48 + .../filter/MeanFilterSmoothing.java | 143 ++ .../base/utils/android/text/HtmlBuilder.java | 582 +++++ .../android/text/RoundedBackgroundSpan.java | 42 + .../android/base/utils/common/Checker.java | 59 + .../android/base/utils/common/CloseUtils.java | 62 + .../base/utils/common/CollectionUtils.java | 31 + .../android/base/utils/common/FileUtils.java | 90 + .../base/utils/common/StringChecker.java | 126 ++ .../utils/security/AESCipherStrategy.java | 42 + .../base/utils/security/CipherStrategy.java | 43 + .../utils/security/DESCipherStrategy.java | 47 + .../utils/security/RSACipherStrategy.java | 70 + .../base/utils/security/SimpleRsa.java | 87 + .../base/utils/security/util/AESUtils.java | 177 ++ .../base/utils/security/util/DESUtils.java | 75 + .../base/utils/security/util/MD5Util.java | 113 + .../base/utils/security/util/RSAUtils.java | 288 +++ .../android/base/widget/MultiStateView.java | 381 ++++ .../widget/ScrollChildSwipeRefreshLayout.java | 87 + .../com/android/base/widget/SimpleLayout.java | 89 + .../base/widget/SimpleMultiStateView.java | 145 ++ .../base/widget/StateActionProcessor.java | 197 ++ .../android/base/widget/StateProcessor.java | 24 + .../widget/pulltozoom/NestedScrollView.java | 1937 +++++++++++++++++ .../pulltozoom/PullToZoomScrollView.java | 250 +++ .../base/widget/ratio/RatioHelper.java | 54 + .../base/widget/ratio/RatioImageView.java | 36 + .../base/widget/ratio/RatioLayout.java | 57 + .../base/widget/ratio/RatioTextView.java | 36 + .../ratio/TopDrawableCenterTextView.java | 58 + .../recyclerview/DividerItemDecoration.java | 174 ++ .../recyclerview/HidingScrollListener.java | 45 + .../widget/recyclerview/MarginDecoration.java | 39 + .../OnRecyclerViewScrollBottomListener.java | 99 + .../widget/viewpager/BannerPagerAdapter.java | 91 + .../widget/viewpager/IPagerNumberView.java | 16 + .../widget/viewpager/NoScrollViewPager.java | 37 + .../widget/viewpager/OnPageClickListener.java | 7 + .../viewpager/OptimizeBannerPagerAdapter.java | 96 + .../base/widget/viewpager/PageNumberView.java | 111 + .../base/widget/viewpager/PhotoViewPager.java | 42 + .../viewpager/ScalableBannerPagerFactory.java | 32 + .../base/widget/viewpager/ZViewPager.java | 205 ++ .../res/drawable-xxhdpi/base_img_error.png | Bin 0 -> 3872 bytes .../drawable-xxhdpi/base_img_no_network.png | Bin 0 -> 17882 bytes .../main/res/layout/base_fragment_list.xml | 11 + .../res/layout/base_fragment_refresh_list.xml | 21 + .../src/main/res/layout/base_layout_empty.xml | 43 + .../src/main/res/layout/base_layout_error.xml | 46 + .../main/res/layout/base_layout_loading.xml | 14 + .../res/layout/base_layout_no_network.xml | 46 + .../res/layout/base_layout_server_error.xml | 46 + .../main/res/layout/base_layout_toolbar.xml | 13 + .../res/layout/base_widget_banner_view.xml | 12 + .../src/main/res/values-en/base_strings.xml | 32 + lib_base/src/main/res/values/base_attrs.xml | 85 + .../main/res/values/base_color_palette.xml | 18 + lib_base/src/main/res/values/base_ids.xml | 18 + lib_base/src/main/res/values/base_strings.xml | 31 + lib_base/src/main/res/values/base_styles.xml | 34 + .../java/com/android/base/rx/RxBusTest.java | 127 ++ .../com/android/base/rx/TestApplication.java | 28 + .../com/android/base/rx/TestBaseActivity.java | 29 + .../com/android/base/rx/TestListFragment.java | 26 + .../android/base/rx/TestStateFragment.java | 44 + lib_cache/.gitignore | 22 + lib_cache/README.md | 8 + lib_cache/build.gradle | 35 + lib_cache/proguard-rules.pro | 21 + lib_cache/src/main/AndroidManifest.xml | 2 + .../com/android/sdk/cache/CacheEntity.java | 15 + .../com/android/sdk/cache/CommonImpl.java | 79 + .../com/android/sdk/cache/DiskLruCache.java | 980 +++++++++ .../android/sdk/cache/DiskLruCacheHelper.java | 426 ++++ .../sdk/cache/DiskLruStorageFactoryImpl.java | 42 + .../android/sdk/cache/DiskLruStorageImpl.java | 163 ++ .../java/com/android/sdk/cache/JsonUtils.java | 51 + .../sdk/cache/MMKVStorageFactoryImpl.java | 30 + .../android/sdk/cache/MMKVStorageImpl.java | 172 ++ .../java/com/android/sdk/cache/Storage.java | 59 + .../java/com/android/sdk/cache/StorageEx.kt | 16 + .../com/android/sdk/cache/StorageFactory.java | 43 + .../android/sdk/cache/StrictLineReader.java | 197 ++ .../java/com/android/sdk/cache/TypeFlag.java | 42 + .../main/java/com/android/sdk/cache/Util.java | 81 + .../java/com/android/sdk/cache/Utils.java | 102 + lib_cache/src/main/res/values/strings.xml | 3 + lib_media_selector/.gitignore | 22 + lib_media_selector/README.md | 27 + lib_media_selector/build.gradle | 57 + lib_media_selector/proguard-rules.pro | 17 + .../src/main/AndroidManifest.xml | 28 + .../sdk/mediaselector/BoxingGlideLoader.java | 81 + .../sdk/mediaselector/BoxingUcrop.java | 74 + .../sdk/mediaselector/CropOptions.java | 82 + .../sdk/mediaselector/MediaSelector.java | 159 ++ .../mediaselector/SystemMediaSelector.java | 294 +++ .../com/android/sdk/mediaselector/Utils.java | 368 ++++ .../res/drawable-xxhdpi/ic_boxing_camera.png | Bin 0 -> 786 bytes lib_network/.gitignore | 22 + lib_network/README.md | 1 + lib_network/build.gradle | 46 + lib_network/proguard-rules.pro | 21 + lib_network/src/main/AndroidManifest.xml | 1 + .../java/com/android/sdk/net/NetContext.java | 119 + .../java/com/android/sdk/net/NetProvider.java | 88 + .../android/sdk/net/core/DataExtractor.java | 7 + .../sdk/net/core/ExceptionFactory.java | 13 + .../sdk/net/core/HttpResultTransformer.java | 106 + .../java/com/android/sdk/net/core/Result.java | 31 + .../net/errorhandler/ErrorMessageFactory.java | 76 + .../sdk/net/exception/ApiErrorException.java | 27 + .../net/exception/NetworkErrorException.java | 10 + .../net/exception/ServerErrorException.java | 36 + .../net/gson/AutoGenTypeAdapterFactory.java | 24 + .../com/android/sdk/net/gson/AutoGson.java | 29 + .../ErrorJsonLenientConverterFactory.java | 56 + .../com/android/sdk/net/gson/GsonUtils.java | 44 + .../sdk/net/gson/JsonDeserializers.java | 93 + .../com/android/sdk/net/https/HttpsUtils.java | 167 ++ .../com/android/sdk/net/kit/Consumer.java | 14 + .../com/android/sdk/net/kit/Extensions.kt | 10 + .../android/sdk/net/kit/ResultHandlers.java | 120 + .../com/android/sdk/net/kit/RxResultKit.java | 123 ++ .../com/android/sdk/net/kit/RxResultKit.kt | 58 + .../com/android/sdk/net/kit/Selector.java | 13 + .../android/sdk/net/progress/Dispatcher.java | 18 + .../sdk/net/progress/ProgressListener.java | 10 + .../sdk/net/progress/ProgressRequestBody.java | 92 + .../net/progress/ProgressResponseBody.java | 79 + .../progress/RequestProgressInterceptor.java | 59 + .../progress/ResponseProgressInterceptor.java | 58 + .../sdk/net/progress/UrlProgressListener.java | 8 + .../android/sdk/net/provider/ApiHandler.java | 16 + .../sdk/net/provider/ErrorDataAdapter.java | 20 + .../sdk/net/provider/ErrorMessage.java | 42 + .../android/sdk/net/provider/HttpConfig.java | 25 + .../sdk/net/provider/NetworkChecker.java | 15 + .../sdk/net/provider/PostTransformer.java | 17 + .../sdk/net/service/ServiceFactory.java | 70 + .../sdk/net/service/ServiceHelper.java | 33 + lib_push/.gitignore | 22 + lib_push/README.md | 3 + lib_push/build.gradle | 26 + lib_push/src/main/AndroidManifest.xml | 47 + .../com/android/sdk/push/MessageHandler.java | 27 + .../main/java/com/android/sdk/push/Push.java | 39 + .../com/android/sdk/push/PushCallBack.java | 7 + .../com/android/sdk/push/PushContext.java | 53 + .../com/android/sdk/push/PushMessage.java | 76 + .../main/java/com/android/sdk/push/Utils.java | 25 + .../push/exception/PushUnConfigException.java | 16 + .../com/android/sdk/push/jpush/JPush.java | 124 ++ .../android/sdk/push/jpush/JPushReceiver.java | 128 ++ .../android/sdk/push/jpush/JPushUtils.java | 195 ++ .../jpush/TagAliasJPushMessageReceiver.java | 37 + .../push/jpush/TagAliasOperatorHelper.java | 412 ++++ .../com/android/sdk/push/mipush/MiPush.java | 7 + lib_qrcode/.gitignore | 22 + lib_qrcode/README.md | 3 + lib_qrcode/build.gradle | 40 + lib_qrcode/libs/fotoapparat.aar | Bin 0 -> 314592 bytes lib_qrcode/proguard-rules.pro | 17 + lib_qrcode/src/main/AndroidManifest.xml | 7 + .../com/android/sdk/qrcode/CameraUtils.java | 131 ++ .../java/com/android/sdk/qrcode/Debug.java | 25 + .../android/sdk/qrcode/ProcessDataTask.java | 66 + .../java/com/android/sdk/qrcode/QRCodeView.kt | 281 +++ .../com/android/sdk/qrcode/ScanBoxView.java | 568 +++++ .../java/com/android/sdk/qrcode/Utils.java | 84 + .../sdk/qrcode/zxing/QRCodeDecoder.java | 104 + .../sdk/qrcode/zxing/QRCodeEncoder.java | 122 ++ .../android/sdk/qrcode/zxing/ZXingView.java | 61 + .../qrcode_default_grid_scan_line.png | Bin 0 -> 6033 bytes .../qrcode_default_scan_line.png | Bin 0 -> 1981 bytes .../src/main/res/values/qrcode_attrs.xml | 69 + lib_social/.gitignore | 22 + lib_social/README.md | 58 + lib_social/build.gradle | 57 + lib_social/libs/alipaySdk.aar | Bin 0 -> 264696 bytes lib_social/proguard-rules.pro | 21 + lib_social/src/main/AndroidManifest.xml | 21 + .../sdk/social/ali/AliPayExecutor.java | 132 ++ .../android/sdk/social/ali/AliPayResult.java | 85 + .../sdk/social/ali/PayResultCallback.java | 14 + .../com/android/sdk/social/common/Status.java | 97 + .../com/android/sdk/social/common/Utils.java | 17 + .../sdk/social/wechat/AbsWXEntryActivity.java | 58 + .../social/wechat/AbsWXPayEntryActivity.java | 41 + .../android/sdk/social/wechat/AuthResult.java | 32 + .../android/sdk/social/wechat/PayInfo.java | 86 + .../sdk/social/wechat/SingleLiveData.java | 42 + .../sdk/social/wechat/WXApiFactory.java | 45 + .../android/sdk/social/wechat/WXToken.java | 40 + .../com/android/sdk/social/wechat/WXUser.java | 92 + .../social/wechat/WeChatLoginException.java | 56 + .../sdk/social/wechat/WeChatLoginImpl.java | 24 + .../sdk/social/wechat/WeChatManager.java | 291 +++ .../sdk/social/wechat/WeChatPayException.java | 14 + 394 files changed, 32468 insertions(+) create mode 100644 lib_base/.gitignore create mode 100644 lib_base/README.md create mode 100644 lib_base/build.gradle create mode 100644 lib_base/proguard-rules.pro create mode 100644 lib_base/src/main/AndroidManifest.xml create mode 100644 lib_base/src/main/java/com/android/base/adapter/DataManager.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/ItemHelper.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/list/BaseListAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/list/ListDataManagerImpl.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/list/SmartViewHolder.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/list/ViewHolder.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/pager/SimpleViewPagerAdapter.kt create mode 100644 lib_base/src/main/java/com/android/base/adapter/pager/ViewPageFragmentAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/pager/ViewPageInfo.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/pager/ViewPageStateFragmentAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/pager/ViewPagerAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecycleBin.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecyclingPagerAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/recycler/AdapterDataObserverProxy.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/recycler/DiffRecyclerAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/recycler/HeaderSize.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/recycler/ItemViewBinder.kt create mode 100644 lib_base/src/main/java/com/android/base/adapter/recycler/MultiTypeAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerDataManagerImpl.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/recycler/SimpleRecyclerAdapter.kt create mode 100644 lib_base/src/main/java/com/android/base/adapter/recycler/SmartViewHolder.java create mode 100644 lib_base/src/main/java/com/android/base/adapter/recycler/ViewHolder.java create mode 100644 lib_base/src/main/java/com/android/base/app/ApplicationDelegate.java create mode 100644 lib_base/src/main/java/com/android/base/app/BaseAppContext.java create mode 100644 lib_base/src/main/java/com/android/base/app/BaseKit.java create mode 100644 lib_base/src/main/java/com/android/base/app/CrashHandler.java create mode 100644 lib_base/src/main/java/com/android/base/app/aac/AutoClearedValue.kt create mode 100644 lib_base/src/main/java/com/android/base/app/aac/AutoDisposeLiveData.kt create mode 100644 lib_base/src/main/java/com/android/base/app/aac/ResourceLiveData.kt create mode 100644 lib_base/src/main/java/com/android/base/app/aac/SingleLiveData.kt create mode 100644 lib_base/src/main/java/com/android/base/app/aac/SingleLiveEvent.java create mode 100644 lib_base/src/main/java/com/android/base/app/activity/ActivityDelegate.java create mode 100644 lib_base/src/main/java/com/android/base/app/activity/ActivityDelegateOwner.java create mode 100644 lib_base/src/main/java/com/android/base/app/activity/ActivityDelegates.java create mode 100644 lib_base/src/main/java/com/android/base/app/activity/ActivityStatus.kt create mode 100644 lib_base/src/main/java/com/android/base/app/activity/BackHandlerHelper.java create mode 100644 lib_base/src/main/java/com/android/base/app/activity/BaseActivity.java create mode 100644 lib_base/src/main/java/com/android/base/app/activity/OnBackPressListener.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/ActivityScope.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/ContextType.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/FragmentScope.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/HasViewInjector.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/Injectable.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/ViewComponent.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/ViewComponentBuilder.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/ViewInjection.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/ViewKey.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/ViewModelFactory.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/ViewModelKey.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/ViewModelModule.java create mode 100644 lib_base/src/main/java/com/android/base/app/dagger/ViewScope.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/BaseDialogFragment.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/BaseFragment.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/BaseListFragment.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/BaseListV2Fragment.kt create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/BaseStateDialogFragment.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/BaseStateFragment.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/DelegateEx.kt create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/FragmentConfig.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegate.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegateOwner.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegates.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/FragmentInfo.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/Fragments.kt create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/LazyDelegate.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/LoadingViewFactory.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/RefreshLoadMoreStateLayoutImpl.kt create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/RefreshableStateLayoutImpl.java create mode 100644 lib_base/src/main/java/com/android/base/app/fragment/TabManager.java create mode 100644 lib_base/src/main/java/com/android/base/app/mvp/AbstractPresenter.kt create mode 100644 lib_base/src/main/java/com/android/base/app/mvp/IBaseView.kt create mode 100644 lib_base/src/main/java/com/android/base/app/mvp/IPresenter.kt create mode 100644 lib_base/src/main/java/com/android/base/app/mvp/Lifecycle.kt create mode 100644 lib_base/src/main/java/com/android/base/app/mvp/PresenterBinder.kt create mode 100644 lib_base/src/main/java/com/android/base/app/mvp/RxPresenter.kt create mode 100644 lib_base/src/main/java/com/android/base/app/mvvm/ArchViewModel.kt create mode 100644 lib_base/src/main/java/com/android/base/app/mvvm/ViewModelEx.kt create mode 100644 lib_base/src/main/java/com/android/base/app/ui/AutoPageNumber.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/CommonId.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/LoadingView.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/OnRetryActionListener.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/PageNumber.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/RefreshListLayout.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/RefreshLoadMoreView.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/RefreshLoadViewFactory.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/RefreshStateLayout.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/RefreshView.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/RefreshViewFactory.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/StateLayout.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/StateLayoutConfig.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/SwipeRefreshView.java create mode 100644 lib_base/src/main/java/com/android/base/app/ui/UIEx.kt create mode 100644 lib_base/src/main/java/com/android/base/concurrent/GoUtil.java create mode 100644 lib_base/src/main/java/com/android/base/concurrent/JobExecutor.java create mode 100644 lib_base/src/main/java/com/android/base/concurrent/OverloadPolicy.java create mode 100644 lib_base/src/main/java/com/android/base/concurrent/PriorityRunnable.java create mode 100644 lib_base/src/main/java/com/android/base/concurrent/SchedulePolicy.java create mode 100644 lib_base/src/main/java/com/android/base/concurrent/SmartExecutor.java create mode 100644 lib_base/src/main/java/com/android/base/data/Resource.java create mode 100644 lib_base/src/main/java/com/android/base/data/Resource.kt create mode 100644 lib_base/src/main/java/com/android/base/data/Status.java create mode 100644 lib_base/src/main/java/com/android/base/imageloader/DisplayConfig.java create mode 100644 lib_base/src/main/java/com/android/base/imageloader/GlideImageLoader.java create mode 100644 lib_base/src/main/java/com/android/base/imageloader/ImageLoader.java create mode 100644 lib_base/src/main/java/com/android/base/imageloader/ImageLoaderFactory.java create mode 100644 lib_base/src/main/java/com/android/base/imageloader/LoadListener.java create mode 100644 lib_base/src/main/java/com/android/base/imageloader/LoadListenerAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/imageloader/ProgressGlideModule.java create mode 100644 lib_base/src/main/java/com/android/base/imageloader/ProgressInfo.java create mode 100644 lib_base/src/main/java/com/android/base/imageloader/ProgressListener.java create mode 100644 lib_base/src/main/java/com/android/base/imageloader/ProgressManager.java create mode 100644 lib_base/src/main/java/com/android/base/imageloader/ProgressResponseBody.java create mode 100644 lib_base/src/main/java/com/android/base/imageloader/Source.java create mode 100644 lib_base/src/main/java/com/android/base/interfaces/OnItemClickListener.java create mode 100644 lib_base/src/main/java/com/android/base/interfaces/OnItemLongClickListener.java create mode 100644 lib_base/src/main/java/com/android/base/interfaces/adapter/ActivityLifecycleCallbacksAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/interfaces/adapter/DrawerListenerAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/interfaces/adapter/OnPageChangeListenerAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/interfaces/adapter/OnSeekBarChangeListenerAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/interfaces/adapter/OnTabSelectedListenerAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/interfaces/adapter/TextWatcherAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/interfaces/adapter/TransitionListenerAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/kotlin/AttrStyleEx.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/ContextEx.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/DialogEx.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/FragmentEx.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/KtViewHolder.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/LangEx.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/LiveDataEx.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/MenuEx.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/RecyclerViewEx.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/ResourceEx.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/SizeEx.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/TextViewEx.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/UtilsEx.kt create mode 100644 lib_base/src/main/java/com/android/base/kotlin/ViewEx.kt create mode 100644 lib_base/src/main/java/com/android/base/permission/AndPermissionFragment.java create mode 100644 lib_base/src/main/java/com/android/base/permission/AndPermissionRequester.java create mode 100644 lib_base/src/main/java/com/android/base/permission/AndUtils.java create mode 100644 lib_base/src/main/java/com/android/base/permission/AutoPermissionFragment.java create mode 100644 lib_base/src/main/java/com/android/base/permission/AutoPermissionRequester.java create mode 100644 lib_base/src/main/java/com/android/base/permission/DefaultPermissionUIProvider.java create mode 100644 lib_base/src/main/java/com/android/base/permission/EasyPermissions.java create mode 100644 lib_base/src/main/java/com/android/base/permission/IPermissionUIProvider.java create mode 100644 lib_base/src/main/java/com/android/base/permission/OnAllPermissionGrantedListener.java create mode 100644 lib_base/src/main/java/com/android/base/permission/OnPermissionDeniedListener.java create mode 100644 lib_base/src/main/java/com/android/base/permission/Permission.java create mode 100644 lib_base/src/main/java/com/android/base/permission/PermissionCallback.java create mode 100644 lib_base/src/main/java/com/android/base/permission/PermissionCode.java create mode 100644 lib_base/src/main/java/com/android/base/permission/PermissionRequester.java create mode 100644 lib_base/src/main/java/com/android/base/permission/PermissionRequesterImpl.java create mode 100644 lib_base/src/main/java/com/android/base/permission/PermissionUIProviderFactory.java create mode 100644 lib_base/src/main/java/com/android/base/receiver/NetStateReceiver.java create mode 100644 lib_base/src/main/java/com/android/base/receiver/NetworkState.java create mode 100644 lib_base/src/main/java/com/android/base/rx/AutoDispose.kt create mode 100644 lib_base/src/main/java/com/android/base/rx/FlowableRetryDelay.java create mode 100644 lib_base/src/main/java/com/android/base/rx/LifecycleScopeProviderEx.kt create mode 100644 lib_base/src/main/java/com/android/base/rx/ObservableRetryDelay.java create mode 100644 lib_base/src/main/java/com/android/base/rx/RetryChecker.java create mode 100644 lib_base/src/main/java/com/android/base/rx/RxBus.java create mode 100644 lib_base/src/main/java/com/android/base/rx/RxEx.kt create mode 100644 lib_base/src/main/java/com/android/base/rx/RxKit.java create mode 100644 lib_base/src/main/java/com/android/base/rx/SchedulerProvider.kt create mode 100644 lib_base/src/main/java/com/android/base/rx/ThreadTransformer.java create mode 100644 lib_base/src/main/java/com/android/base/utils/BaseUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/ActFragWrapper.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/ClipboardUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/DebugUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/DevicesUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/DirectoryUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/ResourceUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/SoftKeyboardUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/SpCache.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/TintUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/UnitConverter.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/VideoUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/ViewUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/WebViewUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/WindowUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/XAppUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/XIntentUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/anim/ActivityAnimUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/compat/AndroidVersion.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/compat/SystemBarCompat.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/orientation/AbsOrientationProvider.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/orientation/AccelerometerMagneticOrientationProvider.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/orientation/Orientation.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/orientation/OrientationProvider.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/orientation/RotationVectorOrientationProvider.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/orientation/filter/MeanFilterSmoothing.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/text/HtmlBuilder.java create mode 100644 lib_base/src/main/java/com/android/base/utils/android/text/RoundedBackgroundSpan.java create mode 100644 lib_base/src/main/java/com/android/base/utils/common/Checker.java create mode 100644 lib_base/src/main/java/com/android/base/utils/common/CloseUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/common/CollectionUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/common/FileUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/common/StringChecker.java create mode 100644 lib_base/src/main/java/com/android/base/utils/security/AESCipherStrategy.java create mode 100644 lib_base/src/main/java/com/android/base/utils/security/CipherStrategy.java create mode 100644 lib_base/src/main/java/com/android/base/utils/security/DESCipherStrategy.java create mode 100644 lib_base/src/main/java/com/android/base/utils/security/RSACipherStrategy.java create mode 100644 lib_base/src/main/java/com/android/base/utils/security/SimpleRsa.java create mode 100644 lib_base/src/main/java/com/android/base/utils/security/util/AESUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/security/util/DESUtils.java create mode 100644 lib_base/src/main/java/com/android/base/utils/security/util/MD5Util.java create mode 100644 lib_base/src/main/java/com/android/base/utils/security/util/RSAUtils.java create mode 100644 lib_base/src/main/java/com/android/base/widget/MultiStateView.java create mode 100644 lib_base/src/main/java/com/android/base/widget/ScrollChildSwipeRefreshLayout.java create mode 100644 lib_base/src/main/java/com/android/base/widget/SimpleLayout.java create mode 100644 lib_base/src/main/java/com/android/base/widget/SimpleMultiStateView.java create mode 100644 lib_base/src/main/java/com/android/base/widget/StateActionProcessor.java create mode 100644 lib_base/src/main/java/com/android/base/widget/StateProcessor.java create mode 100644 lib_base/src/main/java/com/android/base/widget/pulltozoom/NestedScrollView.java create mode 100644 lib_base/src/main/java/com/android/base/widget/pulltozoom/PullToZoomScrollView.java create mode 100644 lib_base/src/main/java/com/android/base/widget/ratio/RatioHelper.java create mode 100644 lib_base/src/main/java/com/android/base/widget/ratio/RatioImageView.java create mode 100644 lib_base/src/main/java/com/android/base/widget/ratio/RatioLayout.java create mode 100644 lib_base/src/main/java/com/android/base/widget/ratio/RatioTextView.java create mode 100644 lib_base/src/main/java/com/android/base/widget/ratio/TopDrawableCenterTextView.java create mode 100644 lib_base/src/main/java/com/android/base/widget/recyclerview/DividerItemDecoration.java create mode 100644 lib_base/src/main/java/com/android/base/widget/recyclerview/HidingScrollListener.java create mode 100644 lib_base/src/main/java/com/android/base/widget/recyclerview/MarginDecoration.java create mode 100644 lib_base/src/main/java/com/android/base/widget/recyclerview/OnRecyclerViewScrollBottomListener.java create mode 100644 lib_base/src/main/java/com/android/base/widget/viewpager/BannerPagerAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/widget/viewpager/IPagerNumberView.java create mode 100644 lib_base/src/main/java/com/android/base/widget/viewpager/NoScrollViewPager.java create mode 100644 lib_base/src/main/java/com/android/base/widget/viewpager/OnPageClickListener.java create mode 100644 lib_base/src/main/java/com/android/base/widget/viewpager/OptimizeBannerPagerAdapter.java create mode 100644 lib_base/src/main/java/com/android/base/widget/viewpager/PageNumberView.java create mode 100644 lib_base/src/main/java/com/android/base/widget/viewpager/PhotoViewPager.java create mode 100644 lib_base/src/main/java/com/android/base/widget/viewpager/ScalableBannerPagerFactory.java create mode 100644 lib_base/src/main/java/com/android/base/widget/viewpager/ZViewPager.java create mode 100644 lib_base/src/main/res/drawable-xxhdpi/base_img_error.png create mode 100644 lib_base/src/main/res/drawable-xxhdpi/base_img_no_network.png create mode 100644 lib_base/src/main/res/layout/base_fragment_list.xml create mode 100644 lib_base/src/main/res/layout/base_fragment_refresh_list.xml create mode 100644 lib_base/src/main/res/layout/base_layout_empty.xml create mode 100644 lib_base/src/main/res/layout/base_layout_error.xml create mode 100644 lib_base/src/main/res/layout/base_layout_loading.xml create mode 100644 lib_base/src/main/res/layout/base_layout_no_network.xml create mode 100644 lib_base/src/main/res/layout/base_layout_server_error.xml create mode 100644 lib_base/src/main/res/layout/base_layout_toolbar.xml create mode 100644 lib_base/src/main/res/layout/base_widget_banner_view.xml create mode 100644 lib_base/src/main/res/values-en/base_strings.xml create mode 100644 lib_base/src/main/res/values/base_attrs.xml create mode 100644 lib_base/src/main/res/values/base_color_palette.xml create mode 100644 lib_base/src/main/res/values/base_ids.xml create mode 100644 lib_base/src/main/res/values/base_strings.xml create mode 100644 lib_base/src/main/res/values/base_styles.xml create mode 100644 lib_base/src/test/java/com/android/base/rx/RxBusTest.java create mode 100644 lib_base/src/test/java/com/android/base/rx/TestApplication.java create mode 100644 lib_base/src/test/java/com/android/base/rx/TestBaseActivity.java create mode 100644 lib_base/src/test/java/com/android/base/rx/TestListFragment.java create mode 100644 lib_base/src/test/java/com/android/base/rx/TestStateFragment.java create mode 100644 lib_cache/.gitignore create mode 100644 lib_cache/README.md create mode 100644 lib_cache/build.gradle create mode 100644 lib_cache/proguard-rules.pro create mode 100644 lib_cache/src/main/AndroidManifest.xml create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/CacheEntity.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/CommonImpl.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/DiskLruCache.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/DiskLruCacheHelper.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageFactoryImpl.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageImpl.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/JsonUtils.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageFactoryImpl.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageImpl.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/Storage.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/StorageEx.kt create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/StorageFactory.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/StrictLineReader.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/TypeFlag.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/Util.java create mode 100644 lib_cache/src/main/java/com/android/sdk/cache/Utils.java create mode 100644 lib_cache/src/main/res/values/strings.xml create mode 100644 lib_media_selector/.gitignore create mode 100644 lib_media_selector/README.md create mode 100644 lib_media_selector/build.gradle create mode 100644 lib_media_selector/proguard-rules.pro create mode 100644 lib_media_selector/src/main/AndroidManifest.xml create mode 100644 lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingGlideLoader.java create mode 100644 lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingUcrop.java create mode 100644 lib_media_selector/src/main/java/com/android/sdk/mediaselector/CropOptions.java create mode 100644 lib_media_selector/src/main/java/com/android/sdk/mediaselector/MediaSelector.java create mode 100644 lib_media_selector/src/main/java/com/android/sdk/mediaselector/SystemMediaSelector.java create mode 100644 lib_media_selector/src/main/java/com/android/sdk/mediaselector/Utils.java create mode 100644 lib_media_selector/src/main/res/drawable-xxhdpi/ic_boxing_camera.png create mode 100644 lib_network/.gitignore create mode 100644 lib_network/README.md create mode 100644 lib_network/build.gradle create mode 100644 lib_network/proguard-rules.pro create mode 100644 lib_network/src/main/AndroidManifest.xml create mode 100644 lib_network/src/main/java/com/android/sdk/net/NetContext.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/NetProvider.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/core/DataExtractor.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/core/ExceptionFactory.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/core/HttpResultTransformer.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/core/Result.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/errorhandler/ErrorMessageFactory.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/exception/ApiErrorException.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/exception/NetworkErrorException.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/exception/ServerErrorException.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/gson/AutoGenTypeAdapterFactory.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/gson/AutoGson.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/gson/ErrorJsonLenientConverterFactory.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/gson/GsonUtils.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/gson/JsonDeserializers.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/https/HttpsUtils.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/kit/Consumer.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/kit/Extensions.kt create mode 100644 lib_network/src/main/java/com/android/sdk/net/kit/ResultHandlers.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.kt create mode 100644 lib_network/src/main/java/com/android/sdk/net/kit/Selector.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/progress/Dispatcher.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/progress/ProgressListener.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/progress/ProgressRequestBody.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/progress/ProgressResponseBody.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/progress/RequestProgressInterceptor.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/progress/ResponseProgressInterceptor.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/progress/UrlProgressListener.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/provider/ApiHandler.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/provider/ErrorDataAdapter.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/provider/ErrorMessage.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/provider/HttpConfig.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/provider/NetworkChecker.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/provider/PostTransformer.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/service/ServiceFactory.java create mode 100644 lib_network/src/main/java/com/android/sdk/net/service/ServiceHelper.java create mode 100644 lib_push/.gitignore create mode 100644 lib_push/README.md create mode 100644 lib_push/build.gradle create mode 100644 lib_push/src/main/AndroidManifest.xml create mode 100644 lib_push/src/main/java/com/android/sdk/push/MessageHandler.java create mode 100644 lib_push/src/main/java/com/android/sdk/push/Push.java create mode 100644 lib_push/src/main/java/com/android/sdk/push/PushCallBack.java create mode 100644 lib_push/src/main/java/com/android/sdk/push/PushContext.java create mode 100644 lib_push/src/main/java/com/android/sdk/push/PushMessage.java create mode 100644 lib_push/src/main/java/com/android/sdk/push/Utils.java create mode 100644 lib_push/src/main/java/com/android/sdk/push/exception/PushUnConfigException.java create mode 100644 lib_push/src/main/java/com/android/sdk/push/jpush/JPush.java create mode 100644 lib_push/src/main/java/com/android/sdk/push/jpush/JPushReceiver.java create mode 100644 lib_push/src/main/java/com/android/sdk/push/jpush/JPushUtils.java create mode 100644 lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasJPushMessageReceiver.java create mode 100644 lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasOperatorHelper.java create mode 100644 lib_push/src/main/java/com/android/sdk/push/mipush/MiPush.java create mode 100644 lib_qrcode/.gitignore create mode 100644 lib_qrcode/README.md create mode 100644 lib_qrcode/build.gradle create mode 100644 lib_qrcode/libs/fotoapparat.aar create mode 100644 lib_qrcode/proguard-rules.pro create mode 100644 lib_qrcode/src/main/AndroidManifest.xml create mode 100644 lib_qrcode/src/main/java/com/android/sdk/qrcode/CameraUtils.java create mode 100644 lib_qrcode/src/main/java/com/android/sdk/qrcode/Debug.java create mode 100644 lib_qrcode/src/main/java/com/android/sdk/qrcode/ProcessDataTask.java create mode 100644 lib_qrcode/src/main/java/com/android/sdk/qrcode/QRCodeView.kt create mode 100644 lib_qrcode/src/main/java/com/android/sdk/qrcode/ScanBoxView.java create mode 100644 lib_qrcode/src/main/java/com/android/sdk/qrcode/Utils.java create mode 100644 lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeDecoder.java create mode 100644 lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeEncoder.java create mode 100644 lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/ZXingView.java create mode 100644 lib_qrcode/src/main/res/mipmap-xxhdpi/qrcode_default_grid_scan_line.png create mode 100644 lib_qrcode/src/main/res/mipmap-xxhdpi/qrcode_default_scan_line.png create mode 100644 lib_qrcode/src/main/res/values/qrcode_attrs.xml create mode 100644 lib_social/.gitignore create mode 100644 lib_social/README.md create mode 100644 lib_social/build.gradle create mode 100644 lib_social/libs/alipaySdk.aar create mode 100644 lib_social/proguard-rules.pro create mode 100644 lib_social/src/main/AndroidManifest.xml create mode 100644 lib_social/src/main/java/com/android/sdk/social/ali/AliPayExecutor.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/ali/AliPayResult.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/ali/PayResultCallback.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/common/Status.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/common/Utils.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXEntryActivity.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXPayEntryActivity.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/wechat/AuthResult.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/wechat/PayInfo.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/wechat/SingleLiveData.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/wechat/WXApiFactory.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/wechat/WXToken.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/wechat/WXUser.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginException.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginImpl.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/wechat/WeChatManager.java create mode 100644 lib_social/src/main/java/com/android/sdk/social/wechat/WeChatPayException.java diff --git a/lib_base/.gitignore b/lib_base/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_base/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_base/README.md b/lib_base/README.md new file mode 100644 index 0000000..527c57e --- /dev/null +++ b/lib_base/README.md @@ -0,0 +1,20 @@ +# Android Development Base Library + +## 1 Third party Libraries + +- [AndroidX](https://developer.android.com/jetpack/androidx) +- [RxJava(2)](https://github.com/ReactiveX/RxJava) +- [RxAndroid(2)](https://github.com/ReactiveX/RxAndroid) +- [AutoDispose](https://github.com/uber/AutoDispose) +- [Dagger2](https://github.com/google/dagger) +- [Glide](https://github.com/bumptech/glide) +- [OkHttp](https://github.com/square/okhttp) +- [Timber](https://github.com/JakeWharton/timber) +- [WrapperAdapter](https://github.com/Ztiany/WrapperAdapter) +- [MultiTypeAdapter](https://github.com/drakeet/MultiType) +- [AndroidUtilCode](https://github.com/Blankj/AndroidUtilCode) + +## 2 Environment + +- AndroidStudio 3+ +- Java8 with desugar \ No newline at end of file diff --git a/lib_base/build.gradle b/lib_base/build.gradle new file mode 100644 index 0000000..f3326c2 --- /dev/null +++ b/lib_base/build.gradle @@ -0,0 +1,130 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + +androidExtensions { + experimental = true +} + +android { + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + + javaCompileOptions { + annotationProcessorOptions { + includeCompileClasspath false + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + lintOptions { + abortOnError false + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs += "src/github/java" + res.srcDirs += "src/github/res" + } + } + + project.afterEvaluate { + Task assembleReleaseTask = project.tasks.findByPath("assembleRelease") + if (assembleReleaseTask != null) { + assembleReleaseTask.doLast { + File infile = project.file("build/outputs/aar/") + File outfile = project.file("./release") + project.copy { + from infile + into outfile + } + } + } + } + +} + + +dependencies { + api fileTree(dir: 'libs', include: ['*.jar']) + + //测试 + testImplementation testLibraries.junit + + //AndroidSupport + api androidLibraries.androidCompatV4 + api androidLibraries.androidCompatV7 + api androidLibraries.androidRecyclerView + api androidLibraries.androidDesign + api androidLibraries.androidPrecent + api androidLibraries.constraintLayout + + //AAC + api androidLibraries.lifecycle + api androidLibraries.lifecycleJava8 + api androidLibraries.lifecycleExtensions + api androidLibraries.liveDataReactiveStreams + + //Kotlin + api kotlinLibraries.kotlinStdlib + api kotlinLibraries.kotlinReflect + api kotlinLibraries.kotlinCoroutines + api kotlinLibraries.kotlinAndroidCoroutines + + //RxJava + api thirdLibraries.rxJava + api thirdLibraries.rxAndroid + api thirdLibraries.rxBinding + api thirdLibraries.autoDispose + api thirdLibraries.autoDisposeLifecycleArchcomponents + + /*Dagger2*/ + api thirdLibraries.dagger2 + api thirdLibraries.jsr305 + api thirdLibraries.dagger2Android + api thirdLibraries.dagger2AndroidSupport + kapt thirdLibraries.dagger2Apt + kapt thirdLibraries.dagger2AndroidApt + + //LoadMore + api uiLibraries.wrapperAdapter + + //Adapter + api uiLibraries.multiType + api uiLibraries.multiTypeKotlin + + //Log + api thirdLibraries.timber + + //ImageLoader + api thirdLibraries.glide + api thirdLibraries.glideOkHttp + api thirdLibraries.okHttp + + //Utils + api thirdLibraries.utilcode + api thirdLibraries.jOOR + api thirdLibraries.andPermission + api thirdLibraries.supportOptional + +} diff --git a/lib_base/proguard-rules.pro b/lib_base/proguard-rules.pro new file mode 100644 index 0000000..070a2ee --- /dev/null +++ b/lib_base/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in E:\DeveloperTools\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 *; +#} diff --git a/lib_base/src/main/AndroidManifest.xml b/lib_base/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7cde085 --- /dev/null +++ b/lib_base/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lib_base/src/main/java/com/android/base/adapter/DataManager.java b/lib_base/src/main/java/com/android/base/adapter/DataManager.java new file mode 100644 index 0000000..f92f4ad --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/DataManager.java @@ -0,0 +1,81 @@ +package com.android.base.adapter; + +import java.util.List; + +/** + *
+ *     注意数据源引用的替换,只有setDataSource方法会把elements替换掉之前的数据源引用,其他方法都是基于现有数据集合做删除与添加操作。
+ * 
+ * + * @author Ztiany + * Date : 2016-09-12 11:33 + * Email: 1169654504@qq.com + */ +public interface DataManager { + + //Add op + void add(T element); + + void addAt(int location, T element); + + void addItems(List elements); + + /** + * 添加元素前会使用equals方法进行比较,荣旧的数据集合中删除相同的数据在添加 + * + * @param elements 元素 + */ + void addItemsChecked(List elements); + + void addItemsAt(int location, List elements); + + //update op + void replace(T oldElement, T newElement); + + void replaceAt(int index, T element); + + /** + * 清除之前集合中的数据,然后把elements添加到之前的集合中,不会使用elements作为数据源 + * + * @param elements 元素 + */ + void replaceAll(List elements); + + /** + * 此方法会使用elements替换掉之前的数据源,而不对之前的数据源做任何操作 + * + * @param newDataSource 新的数据集 + * @param notifyDataSetChanged 是否调用adapter的notifyDataSetChanged方法 + */ + void setDataSource(List newDataSource, boolean notifyDataSetChanged); + + //remove opt + void remove(T element); + + void removeAt(int index); + + void removeItems(List elements); + + //get + T getItem(int position); + + List getItems(); + + int getDataSize(); + + //contains + boolean contains(T element); + + boolean isEmpty(); + + //clear opt + void clear(); + + //Utils + + /** + * @param t element + * @return -1 if not contains this element + */ + int getItemPosition(T t); +} diff --git a/lib_base/src/main/java/com/android/base/adapter/ItemHelper.java b/lib_base/src/main/java/com/android/base/adapter/ItemHelper.java new file mode 100644 index 0000000..57f9d6c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/ItemHelper.java @@ -0,0 +1,72 @@ +package com.android.base.adapter; + +import android.support.annotation.IdRes; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.util.SparseArray; +import android.view.View; +import android.widget.TextView; + + +/** + * @author Ztiany + * Email :1169654504@qq.com + * Date :015-12-29 20:47 + */ +public class ItemHelper { + + private View mItemView; + private SparseArray views; + + public ItemHelper(View itemView) { + mItemView = itemView; + views = new SparseArray<>(); + } + + @SuppressWarnings("unchecked") + public T getView(int viewId) { + View view = views.get(viewId); + if (view == null) { + view = mItemView.findViewById(viewId); + views.put(viewId, view); + } + return (T) view; + } + + public ItemHelper setText(CharSequence str, @IdRes int viewId) { + ((TextView) getView(viewId)).setText(str == null ? "" : str); + return this; + } + + public ItemHelper setText(@StringRes int strId, @IdRes int viewId) { + ((TextView) getView(viewId)).setText(strId); + return this; + } + + public ItemHelper setTag(@NonNull Object object, @IdRes int tagId, @IdRes int viewID) { + View view = getView(viewID); + view.setTag(tagId, object); + return this; + } + + public ItemHelper setTag(@NonNull Object object, @IdRes int viewID) { + View view = getView(viewID); + view.setTag(object); + return this; + } + + public T getTag(@IdRes int tagId, @IdRes int viewID) { + View view = getView(viewID); + @SuppressWarnings("unchecked") + T tag = (T) view.getTag(tagId); + return tag; + } + + public T getTag(@IdRes int viewID) { + View view = getView(viewID); + @SuppressWarnings("unchecked") + T tag = (T) view.getTag(); + return tag; + } + +} diff --git a/lib_base/src/main/java/com/android/base/adapter/list/BaseListAdapter.java b/lib_base/src/main/java/com/android/base/adapter/list/BaseListAdapter.java new file mode 100644 index 0000000..ba792db --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/list/BaseListAdapter.java @@ -0,0 +1,178 @@ +package com.android.base.adapter.list; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +import com.android.base.R; +import com.android.base.adapter.DataManager; + +import java.util.List; + +/** + * absListView通用的Adapter,注意:只有setDataSource才能替换原有数据源的引用。 + * + * @param 数据模型 + * @author Ztiany + */ +@SuppressWarnings("unused") +public abstract class BaseListAdapter extends BaseAdapter implements DataManager { + + protected final Context mContext; + private final static int ITEM_ID = R.id.base_item_tag_view_id; + private DataManager mDataManager; + private final LayoutInflater mLayoutInflater; + + public BaseListAdapter(@NonNull Context context) { + this(context, null); + } + + @SuppressWarnings("all") + public BaseListAdapter(Context context, List data) { + this.mContext = context; + mLayoutInflater = LayoutInflater.from(context); + mDataManager = new ListDataManagerImpl<>(data, this); + } + + @Override + public int getCount() { + return getDataSize(); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + @SuppressWarnings("unchecked") + public View getView(int position, View convertView, ViewGroup parent) { + VH viewHolder; + int type = getItemViewType(position); + if (convertView == null) { + viewHolder = onCreateViewHolder(mLayoutInflater, parent, type); + viewHolder.mItemView.setTag(ITEM_ID, viewHolder); + } else { + viewHolder = (VH) convertView.getTag(ITEM_ID); + } + viewHolder.setPosition(position); + viewHolder.setType(type); + T item = getItem(position); + onBindData(viewHolder, item); + return viewHolder.mItemView; + } + + @SuppressWarnings("all") + protected abstract void onBindData(@NonNull VH viewHolder, T item); + + @NonNull + protected abstract VH onCreateViewHolder(@NonNull LayoutInflater layoutInflater, @NonNull ViewGroup parent, int type); + + @Override + public int getItemViewType(int position) { + return super.getItemViewType(position); + } + + @Override + public int getViewTypeCount() { + return super.getViewTypeCount(); + } + + + /////////////////////////////////////////////////////////////////////////// + // DataManager + /////////////////////////////////////////////////////////////////////////// + + @Override + public void add(T elem) { + mDataManager.add(elem); + } + + @Override + public void addAt(int location, T elem) { + mDataManager.addAt(location, elem); + } + + @Override + public void addItems(List elements) { + mDataManager.addItems(elements); + } + + @Override + public void addItemsChecked(List elements) { + mDataManager.addItemsChecked(elements); + } + + @Override + public void addItemsAt(int location, List elements) { + mDataManager.addItemsAt(location, elements); + } + + @Override + public void replace(T oldElem, T newElem) { + mDataManager.replace(oldElem, newElem); + } + + @Override + public void replaceAt(int index, T elem) { + mDataManager.replaceAt(index, elem); + } + + @Override + public void replaceAll(List elements) { + mDataManager.replaceAll(elements); + } + + @Override + public void remove(T elem) { + mDataManager.remove(elem); + } + + @Override + public void removeItems(List elements) { + mDataManager.removeItems(elements); + } + + @Override + public void removeAt(int index) { + mDataManager.removeAt(index); + } + + @Override + public T getItem(int position) { + return mDataManager.getItem(position); + } + + @Override + public final int getDataSize() { + return mDataManager.getDataSize(); + } + + @Override + public boolean contains(T elem) { + return mDataManager.contains(elem); + } + + @Override + public void setDataSource(List elements, boolean notifyDataSetChanged) { + mDataManager.setDataSource(elements, notifyDataSetChanged); + } + + @Override + public int getItemPosition(T t) { + return mDataManager.getItemPosition(t); + } + + @Override + public void clear() { + mDataManager.clear(); + } + + @Override + public List getItems() { + return mDataManager.getItems(); + } +} diff --git a/lib_base/src/main/java/com/android/base/adapter/list/ListDataManagerImpl.java b/lib_base/src/main/java/com/android/base/adapter/list/ListDataManagerImpl.java new file mode 100644 index 0000000..c6efa6a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/list/ListDataManagerImpl.java @@ -0,0 +1,183 @@ +package com.android.base.adapter.list; + +import android.widget.BaseAdapter; + +import com.android.base.adapter.DataManager; +import com.android.base.utils.common.Checker; + +import java.util.ArrayList; +import java.util.List; + + +final class ListDataManagerImpl implements DataManager { + + private List mData; + private BaseAdapter mBaseAdapter; + + ListDataManagerImpl(List tList, BaseAdapter adapter) { + mData = tList; + mBaseAdapter = adapter; + } + + private void checkData() { + if (mData == null) { + mData = new ArrayList<>(); + } + } + + @Override + public void add(T elem) { + checkData(); + mData.add(elem); + notifyDataSetChanged(); + } + + @Override + public void addAt(int location, T elem) { + checkData(); + mData.add(location, elem); + notifyDataSetChanged(); + } + + @Override + public void addItems(List elements) { + checkData(); + mData.addAll(elements); + notifyDataSetChanged(); + } + + @Override + public void addItemsChecked(List elements) { + if (Checker.isEmpty(elements)) { + return; + } + if (mData == null) { + addItems(elements); + return; + } + for (T element : elements) { + if (element == null) { + continue; + } + mData.remove(element); + } + mData.addAll(elements); + notifyDataSetChanged(); + } + + @Override + public void addItemsAt(int location, List elements) { + checkData(); + mData.addAll(location, elements); + notifyDataSetChanged(); + } + + @Override + public void replace(T oldElem, T newElem) { + if (mData != null && mData.contains(oldElem)) { + replaceAt(mData.indexOf(oldElem), newElem); + } + } + + @Override + public void replaceAt(int index, T elem) { + if (mData != null && mData.size() > index) { + mData.set(index, elem); + notifyDataSetChanged(); + } + } + + @Override + public void replaceAll(List elements) { + if (mData == null) { + mData = new ArrayList<>(); + } + mData.clear(); + if (elements != null) { + mData.addAll(elements); + } + notifyDataSetChanged(); + } + + @Override + public void setDataSource(List newDataSource, boolean notifyDataSetChanged) { + mData = newDataSource; + if (notifyDataSetChanged) { + notifyDataSetChanged(); + } + } + + @Override + public void remove(T elem) { + if (mData != null && mData.contains(elem)) { + mData.remove(elem); + notifyDataSetChanged(); + } + } + + @Override + public void removeItems(List elements) { + if (mData != null && mData.containsAll(elements)) { + mData.removeAll(elements); + notifyDataSetChanged(); + } + } + + @Override + public void removeAt(int index) { + if (mData != null && mData.size() > index) { + mData.remove(index); + notifyDataSetChanged(); + } + } + + @Override + public T getItem(int position) { + if (mData != null && mData.size() > position) { + return mData.get(position); + } + return null; + } + + @Override + public final int getDataSize() { + return mData == null ? 0 : mData.size(); + } + + @Override + public boolean contains(T elem) { + return mData != null && mData.contains(elem); + } + + @Override + public boolean isEmpty() { + return mData == null || mData.size() == 0; + } + + @Override + public void clear() { + if (mData != null && !mData.isEmpty()) { + mData.clear(); + notifyDataSetChanged(); + } + } + + @Override + public int getItemPosition(T t) { + List items = getItems(); + if (items == null) { + return -1; + } + return items.indexOf(t); + } + + @Override + public List getItems() { + return mData; + } + + private void notifyDataSetChanged() { + mBaseAdapter.notifyDataSetChanged(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/adapter/list/SmartViewHolder.java b/lib_base/src/main/java/com/android/base/adapter/list/SmartViewHolder.java new file mode 100644 index 0000000..71c0123 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/list/SmartViewHolder.java @@ -0,0 +1,21 @@ +package com.android.base.adapter.list; + +import android.view.View; + +import com.android.base.adapter.ItemHelper; + +@SuppressWarnings("unused") +public class SmartViewHolder extends ViewHolder { + + protected final ItemHelper mHelper; + + public SmartViewHolder(View itemView) { + super(itemView); + mHelper = new ItemHelper(itemView); + } + + public ItemHelper helper() { + return mHelper; + } + +} diff --git a/lib_base/src/main/java/com/android/base/adapter/list/ViewHolder.java b/lib_base/src/main/java/com/android/base/adapter/list/ViewHolder.java new file mode 100644 index 0000000..ba9fc47 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/list/ViewHolder.java @@ -0,0 +1,35 @@ +package com.android.base.adapter.list; + +import android.view.View; + +public class ViewHolder { + + @SuppressWarnings("all") + protected final View mItemView; + private int mPosition; + private int mType; + + public ViewHolder(View itemView) { + mItemView = itemView; + } + + public int getPosition() { + return mPosition; + } + + void setPosition(int position) { + mPosition = position; + } + + public int getType() { + return mType; + } + + void setType(int type) { + mType = type; + } + + public View getItemView() { + return mItemView; + } +} diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/SimpleViewPagerAdapter.kt b/lib_base/src/main/java/com/android/base/adapter/pager/SimpleViewPagerAdapter.kt new file mode 100644 index 0000000..6794c17 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/SimpleViewPagerAdapter.kt @@ -0,0 +1,33 @@ +package com.android.base.adapter.pager + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import kotlinx.android.extensions.LayoutContainer + +/** + * 如果使用可缩放的 View 作为 pager,可能不适合使用此Adapter + * + * @param 数据 + * @param View Holder类型 + */ +abstract class SimpleViewPagerAdapter(data: List) : ViewPagerAdapter(data) { + + override fun createViewHolder(container: ViewGroup): KtViewHolder { + val layout = provideLayout(container) + val itemView = if (layout is Int) { + LayoutInflater.from(container.context).inflate(layout, container, false) + } else { + layout as View + } + return KtViewHolder(itemView) + } + + /**provide a layout id or a View*/ + abstract fun provideLayout(parent: ViewGroup): Any + +} + +class KtViewHolder(itemView: View) : ViewPagerAdapter.ViewHolder(itemView), LayoutContainer { + override val containerView: View = itemView +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageFragmentAdapter.java b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageFragmentAdapter.java new file mode 100644 index 0000000..5204619 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageFragmentAdapter.java @@ -0,0 +1,47 @@ +package com.android.base.adapter.pager; + +import android.content.Context; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("unused") +public class ViewPageFragmentAdapter extends FragmentPagerAdapter { + + private final List mTabs; + private Context mContext; + + public ViewPageFragmentAdapter(FragmentManager fragmentManager, Context context) { + super(fragmentManager); + mContext = context; + mTabs = new ArrayList<>(); + } + + public void setDataSource(List viewPageInfoList) { + mTabs.clear(); + mTabs.addAll(viewPageInfoList); + } + + @Override + public int getCount() { + return mTabs.size(); + } + + @Override + public Fragment getItem(int position) { + ViewPageInfo viewPageInfo = mTabs.get(position); + return Fragment.instantiate(mContext, viewPageInfo.clazz.getName(), viewPageInfo.args); + } + + @Override + public CharSequence getPageTitle(int position) { + return mTabs.get(position).title; + } + + public List getTabs() { + return mTabs; + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageInfo.java b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageInfo.java new file mode 100644 index 0000000..9cac811 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageInfo.java @@ -0,0 +1,17 @@ +package com.android.base.adapter.pager; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +@SuppressWarnings("all") +public class ViewPageInfo { + + public final Class clazz; + public final Bundle args; + public final String title; + + public ViewPageInfo(String title, Class clazz, Bundle args) { + this.title = title; + this.clazz = clazz; + this.args = args; + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageStateFragmentAdapter.java b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageStateFragmentAdapter.java new file mode 100644 index 0000000..a6a26cb --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPageStateFragmentAdapter.java @@ -0,0 +1,48 @@ +package com.android.base.adapter.pager; + +import android.content.Context; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; + +import java.util.ArrayList; +import java.util.List; + +public class ViewPageStateFragmentAdapter extends FragmentStatePagerAdapter { + + private final List mTabs; + private Context mContext; + + public ViewPageStateFragmentAdapter(FragmentManager fragmentManager, Context context) { + super(fragmentManager); + mContext = context; + mTabs = new ArrayList<>(); + } + + public void setDataSource(List viewPageInfoList) { + mTabs.clear(); + mTabs.addAll(viewPageInfoList); + } + + @Override + public int getCount() { + return mTabs.size(); + } + + @Override + public Fragment getItem(int position) { + ViewPageInfo viewPageInfo = mTabs.get(position); + return Fragment.instantiate(mContext, viewPageInfo.clazz.getName(), viewPageInfo.args); + } + + @Override + public CharSequence getPageTitle(int position) { + return mTabs.get(position).title; + } + + protected List getTabs() { + return mTabs; + } + + +} diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/ViewPagerAdapter.java b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPagerAdapter.java new file mode 100644 index 0000000..0719894 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/ViewPagerAdapter.java @@ -0,0 +1,69 @@ +package com.android.base.adapter.pager; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; +import android.view.ViewGroup; + +import com.android.base.R; +import com.android.base.adapter.pager.recycler.RecyclingPagerAdapter; +import com.android.base.utils.common.Checker; + +import java.util.List; + +/** + * 如果使用可缩放的 View 作为 pager,可能不适合使用此Adapter + * + * @param 数据 + * @param View Holder类型 + */ +public abstract class ViewPagerAdapter extends RecyclingPagerAdapter { + + private List mData; + + public ViewPagerAdapter(@Nullable List data) { + mData = data; + } + + @Override + @SuppressWarnings("unchecked") + public View getView(int position, View convertView, ViewGroup container) { + VH viewHolder; + if (convertView == null) { + viewHolder = createViewHolder(container); + } else { + viewHolder = (VH) convertView.getTag(R.id.base_item_tag_view_id); + } + T item = getItem(position); + onBindData(viewHolder, item); + return viewHolder.itemView; + } + + protected abstract VH createViewHolder(@NonNull ViewGroup container); + + protected abstract void onBindData(@NonNull VH viewHolder, @NonNull T item); + + @Override + public int getCount() { + return Checker.isEmpty(mData) ? 0 : mData.size(); + } + + public T getItem(int position) { + if (position < 0 || position >= mData.size()) { + return null; + } + return mData.get(position); + } + + public static class ViewHolder { + + public View itemView; + + public ViewHolder(@NonNull View itemView) { + this.itemView = itemView; + itemView.setTag(R.id.base_item_tag_view_id, this); + } + + } + +} diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecycleBin.java b/lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecycleBin.java new file mode 100644 index 0000000..2b2d01e --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecycleBin.java @@ -0,0 +1,152 @@ +package com.android.base.adapter.pager.recycler; + +import android.os.Build; +import android.util.SparseArray; +import android.view.View; + +/** + * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of + * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the + * start of a layout. By construction, they are displaying current information. At the end of + * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that + * could potentially be used by the adapter to avoid allocating views unnecessarily. + *

+ * This class was taken from Android's implementation of {@link android.widget.AbsListView} which + * is copyrighted 2006 The Android Open Source Project. + */ +public class RecycleBin { + /** + * Views that were on screen at the start of layout. This array is populated at the start of + * layout, and at the end of layout all view in activeViews are moved to scrapViews. + * Views in activeViews represent a contiguous range of Views, with position of the first + * view store in mFirstActivePosition. + */ + private View[] activeViews = new View[0]; + private int[] activeViewTypes = new int[0]; + + /** Unsorted views that can be used by the adapter as a convert view. */ + private SparseArray[] scrapViews; + + private int viewTypeCount; + + private SparseArray currentScrapViews; + + public void setViewTypeCount(int viewTypeCount) { + if (viewTypeCount < 1) { + throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); + } + //noinspection unchecked + SparseArray[] scrapViews = new SparseArray[viewTypeCount]; + for (int i = 0; i < viewTypeCount; i++) { + scrapViews[i] = new SparseArray(); + } + this.viewTypeCount = viewTypeCount; + currentScrapViews = scrapViews[0]; + this.scrapViews = scrapViews; + } + + protected boolean shouldRecycleViewType(int viewType) { + return viewType >= 0; + } + + /** @return A view from the ScrapViews collection. These are unordered. */ + View getScrapView(int position, int viewType) { + if (viewTypeCount == 1) { + return retrieveFromScrap(currentScrapViews, position); + } else if (viewType >= 0 && viewType < scrapViews.length) { + return retrieveFromScrap(scrapViews[viewType], position); + } + return null; + } + + /** + * Put a view into the ScrapViews list. These views are unordered. + * + * @param scrap The view to add + */ + void addScrapView(View scrap, int position, int viewType) { + if (viewTypeCount == 1) { + currentScrapViews.put(position, scrap); + } else { + scrapViews[viewType].put(position, scrap); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + scrap.setAccessibilityDelegate(null); + } + } + + /** Move all views remaining in activeViews to scrapViews. */ + void scrapActiveViews() { + final View[] activeViews = this.activeViews; + final int[] activeViewTypes = this.activeViewTypes; + final boolean multipleScraps = viewTypeCount > 1; + + SparseArray scrapViews = currentScrapViews; + final int count = activeViews.length; + for (int i = count - 1; i >= 0; i--) { + final View victim = activeViews[i]; + if (victim != null) { + int whichScrap = activeViewTypes[i]; + + activeViews[i] = null; + activeViewTypes[i] = -1; + + if (!shouldRecycleViewType(whichScrap)) { + continue; + } + + if (multipleScraps) { + scrapViews = this.scrapViews[whichScrap]; + } + scrapViews.put(i, victim); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + victim.setAccessibilityDelegate(null); + } + } + } + + pruneScrapViews(); + } + + /** + * Makes sure that the size of scrapViews does not exceed the size of activeViews. + * (This can happen if an adapter does not recycle its views). + */ + private void pruneScrapViews() { + final int maxViews = activeViews.length; + final int viewTypeCount = this.viewTypeCount; + final SparseArray[] scrapViews = this.scrapViews; + for (int i = 0; i < viewTypeCount; ++i) { + final SparseArray scrapPile = scrapViews[i]; + int size = scrapPile.size(); + final int extras = size - maxViews; + size--; + for (int j = 0; j < extras; j++) { + scrapPile.remove(scrapPile.keyAt(size--)); + } + } + } + + static View retrieveFromScrap(SparseArray scrapViews, int position) { + int size = scrapViews.size(); + if (size > 0) { + // See if we still have a view for this position. + for (int i = 0; i < size; i++) { + int fromPosition = scrapViews.keyAt(i); + View view = scrapViews.get(fromPosition); + if (fromPosition == position) { + scrapViews.remove(fromPosition); + return view; + } + } + int index = size - 1; + View r = scrapViews.valueAt(index); + scrapViews.remove(scrapViews.keyAt(index)); + return r; + } else { + return null; + } + } +} diff --git a/lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecyclingPagerAdapter.java b/lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecyclingPagerAdapter.java new file mode 100644 index 0000000..56f2383 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/pager/recycler/RecyclingPagerAdapter.java @@ -0,0 +1,114 @@ +package com.android.base.adapter.pager.recycler; + +import android.support.v4.view.PagerAdapter; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; + +/** + * A {@link PagerAdapter} which behaves like an {@link android.widget.Adapter} with view types and view recycling. + * + * @see JakeWharton/salvage + */ +public abstract class RecyclingPagerAdapter extends PagerAdapter { + + static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE; + + private final RecycleBin recycleBin; + + public RecyclingPagerAdapter() { + this(new RecycleBin()); + } + + RecyclingPagerAdapter(RecycleBin recycleBin) { + this.recycleBin = recycleBin; + recycleBin.setViewTypeCount(getViewTypeCount()); + } + + @Override + public void notifyDataSetChanged() { + recycleBin.scrapActiveViews(); + super.notifyDataSetChanged(); + } + + @Override + public final Object instantiateItem(ViewGroup container, int position) { + int viewType = getItemViewType(position); + View view = null; + if (viewType != IGNORE_ITEM_VIEW_TYPE) { + view = recycleBin.getScrapView(position, viewType); + } + view = getView(position, view, container); + container.addView(view); + return view; + } + + @Override + public final void destroyItem(ViewGroup container, int position, Object object) { + View view = (View) object; + container.removeView(view); + int viewType = getItemViewType(position); + if (viewType != IGNORE_ITEM_VIEW_TYPE) { + recycleBin.addScrapView(view, position, viewType); + } + } + + @Override + public final boolean isViewFromObject(View view, Object object) { + return view == object; + } + + /** + *

+ * Returns the number of types of Views that will be created by + * {@link #getView}. Each type represents a set of views that can be + * converted in {@link #getView}. If the adapter always returns the same + * type of View for all items, this method should return 1. + *

+ *

+ * This method will only be called when when the adapter is set on the + * the {@link AdapterView}. + *

+ * + * @return The number of types of Views that will be created by this adapter + */ + public int getViewTypeCount() { + return 1; + } + + /** + * Get the type of View that will be created by {@link #getView} for the specified item. + * + * @param position The position of the item within the adapter's data set whose view type we + * want. + * @return An integer representing the type of View. Two views should share the same type if one + * can be converted to the other in {@link #getView}. Note: Integers must be in the + * range 0 to {@link #getViewTypeCount} - 1. {@link #IGNORE_ITEM_VIEW_TYPE} can + * also be returned. + * @see #IGNORE_ITEM_VIEW_TYPE + */ + @SuppressWarnings("UnusedParameters") // Argument potentially used by subclasses. + public int getItemViewType(int position) { + return 0; + } + + /** + * Get a View that displays the data at the specified position in the data set. You can either + * create a View manually or inflate it from an XML layout file. When the View is inflated, the + * parent View (GridView, ListView...) will apply default layout parameters unless you use + * {@link android.view.LayoutInflater#inflate(int, ViewGroup, boolean)} + * to specify a root view and to prevent attachment to the root. + * + * @param position The position of the item within the adapter's data set of the item whose view + * we want. + * @param convertView The old view to reuse, if possible. Note: You should check that this view + * is non-null and of an appropriate type before using. If it is not possible to convert + * this view to display the correct data, this method can create a new view. + * Heterogeneous lists can specify their number of view types, so that this View is + * always of the right type (see {@link #getViewTypeCount()} and + * {@link #getItemViewType(int)}). + * @param parent The parent that this view will eventually be attached to + * @return A View corresponding to the data at the specified position. + */ + public abstract View getView(int position, View convertView, ViewGroup container); +} diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/AdapterDataObserverProxy.java b/lib_base/src/main/java/com/android/base/adapter/recycler/AdapterDataObserverProxy.java new file mode 100644 index 0000000..bbdbf71 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/AdapterDataObserverProxy.java @@ -0,0 +1,49 @@ +package com.android.base.adapter.recycler; + +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; + +/** + * @see PagingWithHeader + */ +class AdapterDataObserverProxy extends RecyclerView.AdapterDataObserver { + + private RecyclerView.AdapterDataObserver adapterDataObserver; + private int headerCount; + + AdapterDataObserverProxy(RecyclerView.AdapterDataObserver adapterDataObserver, int headerCount) { + this.adapterDataObserver = adapterDataObserver; + this.headerCount = headerCount; + } + + @Override + public void onChanged() { + adapterDataObserver.onChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { + adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount, payload); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + adapterDataObserver.onItemRangeInserted(positionStart + headerCount, itemCount); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + adapterDataObserver.onItemRangeRemoved(positionStart + headerCount, itemCount); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + super.onItemRangeMoved(fromPosition + headerCount, toPosition + headerCount, itemCount); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/DiffRecyclerAdapter.java b/lib_base/src/main/java/com/android/base/adapter/recycler/DiffRecyclerAdapter.java new file mode 100644 index 0000000..6054520 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/DiffRecyclerAdapter.java @@ -0,0 +1,255 @@ +package com.android.base.adapter.recycler; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.recyclerview.extensions.AsyncDifferConfig; +import android.support.v7.recyclerview.extensions.AsyncListDiffer; +import android.support.v7.util.AdapterListUpdateCallback; +import android.support.v7.util.DiffUtil; +import android.support.v7.widget.RecyclerView; +import android.view.ViewGroup; + +import com.android.base.adapter.DataManager; +import com.android.base.utils.common.Checker; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * RecyclerView 的适配器,注意: 只有{@link #setDataSource(List, boolean)}才能替换原有数据源的引用。 + * + * @param 当前列表使用的数据类型 + * @author Ztiany + * date : 2015-05-11 22:38 + * email: 1169654504@qq.com + */ +@SuppressWarnings("unused") +public abstract class DiffRecyclerAdapter extends RecyclerView.Adapter implements DataManager { + + @NonNull + protected Context mContext; + + private AsyncListDiffer mAsyncListDiffer; + + private final int mHeaderCount; + + @SuppressWarnings("WeakerAccess") + public DiffRecyclerAdapter(@NonNull Context context, @NonNull DiffUtil.ItemCallback itemCallback, @Nullable Executor executor, int headerCount) { + mContext = context; + mHeaderCount = headerCount; + + AsyncDifferConfig.Builder tBuilder = new AsyncDifferConfig.Builder<>(itemCallback); + if (executor != null) { + tBuilder.setBackgroundThreadExecutor(executor); + } + AsyncDifferConfig differConfig = tBuilder.build(); + + mAsyncListDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), differConfig); + } + + public DiffRecyclerAdapter(@NonNull Context context, @NonNull DiffUtil.ItemCallback itemCallback, int headerCount) { + this(context, itemCallback, null, headerCount); + } + + public DiffRecyclerAdapter(@NonNull Context context, @NonNull DiffUtil.ItemCallback itemCallback) { + this(context, itemCallback, null, 0); + } + + @Override + public void registerAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) { + if (mHeaderCount != 0) { + super.registerAdapterDataObserver(new AdapterDataObserverProxy(observer, mHeaderCount)); + } else { + super.registerAdapterDataObserver(observer); + } + } + + @Override + public int getItemCount() { + return getDataSize(); + } + + @NonNull + @Override + public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType); + + @Override + public void onBindViewHolder(@NonNull VH holder, int position, @NonNull List payloads) { + super.onBindViewHolder(holder, position, payloads); + } + + @Override + public abstract void onBindViewHolder(@NonNull VH viewHolder, int position); + + public void notifyEntryChanged(T t) { + int itemPosition = getItemPosition(t); + if (itemPosition != -1) { + notifyItemChanged(itemPosition); + } + } + + @Override + public void add(T element) { + List newList = newList(); + newList.add(element); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void addAt(int location, T element) { + if (location > getDataSize()) { + location = getDataSize(); + } + List newList = newList(); + newList.add(location, element); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void addItems(List elements) { + if (Checker.isEmpty(elements)) { + return; + } + List newList = newList(); + newList.addAll(elements); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void addItemsChecked(List elements) { + if (Checker.isEmpty(elements)) { + return; + } + + List newList = newList(); + + for (T element : elements) { + if (element == null) { + continue; + } + newList.remove(element); + } + + newList.addAll(elements); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void addItemsAt(int location, List elements) { + if (Checker.isEmpty(elements)) { + return; + } + List newList = newList(); + + if (location > newList.size()) { + location = newList.size(); + } + + newList.addAll(location, elements); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void replace(T oldElement, T newElement) { + if (!contains(oldElement)) { + return; + } + List newList = newList(); + newList.set(newList.indexOf(oldElement), newElement); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void replaceAt(int index, T element) { + if (getDataSize() > index) { + List newList = newList(); + newList.set(index, element); + mAsyncListDiffer.submitList(newList); + } + } + + @Override + public void replaceAll(List elements) { + List newList = new ArrayList<>(elements); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void remove(T element) { + if (contains(element)) { + List newList = newList(); + newList.remove(element); + mAsyncListDiffer.submitList(newList); + } + } + + @Override + public void removeItems(List elements) { + if (Checker.isEmpty(elements) || isEmpty() || !getItems().containsAll(elements)) { + return; + } + List newList = newList(); + newList.removeAll(elements); + mAsyncListDiffer.submitList(newList); + } + + @Override + public void removeAt(int index) { + if (getDataSize() > index) { + List newList = newList(); + newList.remove(index); + mAsyncListDiffer.submitList(newList); + } + } + + @Override + public T getItem(int position) { + return getDataSize() > position ? getItems().get(position) : null; + } + + @Override + public final int getDataSize() { + return getItems() == null ? 0 : getItems().size(); + } + + @Override + public boolean contains(T element) { + return !isEmpty() && getItems().contains(element); + } + + @Override + public void clear() { + mAsyncListDiffer.submitList(null); + } + + @Override + public void setDataSource(List elements, boolean notifyDataSetChanged) { + mAsyncListDiffer.submitList(elements); + } + + @Override + public List getItems() { + return mAsyncListDiffer.getCurrentList(); + } + + @Override + public boolean isEmpty() { + return getDataSize() == 0; + } + + @Override + public int getItemPosition(T t) { + return isEmpty() ? -1 : getItems().indexOf(t); + } + + private List newList() { + if (getItems() == null) { + return new ArrayList<>(); + } else { + return new ArrayList<>(getItems()); + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/HeaderSize.java b/lib_base/src/main/java/com/android/base/adapter/recycler/HeaderSize.java new file mode 100644 index 0000000..da9af1c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/HeaderSize.java @@ -0,0 +1,5 @@ +package com.android.base.adapter.recycler; + +public interface HeaderSize { + int getHeaderSize(); +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/ItemViewBinder.kt b/lib_base/src/main/java/com/android/base/adapter/recycler/ItemViewBinder.kt new file mode 100644 index 0000000..3b85ca3 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/ItemViewBinder.kt @@ -0,0 +1,35 @@ +package com.android.base.adapter.recycler + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.android.base.kotlin.KtViewHolder + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-09-13 15:33 + */ +abstract class ItemViewBinder : me.drakeet.multitype.ItemViewBinder() { + + protected val dataManager: MultiTypeAdapter + get() = adapter as MultiTypeAdapter + +} + +abstract class SimpleItemViewBinder : ItemViewBinder() { + + override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): KtViewHolder { + val layout = provideLayout(inflater, parent) + val itemView = if (layout is Int) { + inflater.inflate(layout, parent, false) + } else + layout as View + return KtViewHolder(itemView) + } + + /**provide a layout id or a View*/ + abstract fun provideLayout(inflater: LayoutInflater, parent: ViewGroup): Any + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/MultiTypeAdapter.java b/lib_base/src/main/java/com/android/base/adapter/recycler/MultiTypeAdapter.java new file mode 100644 index 0000000..d29f4d7 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/MultiTypeAdapter.java @@ -0,0 +1,166 @@ +package com.android.base.adapter.recycler; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.android.base.adapter.DataManager; + +import java.util.ArrayList; +import java.util.List; + +import me.drakeet.multitype.TypePool; + +/** + * @see drakeet/MultiTypeAdapter + */ +public class MultiTypeAdapter extends me.drakeet.multitype.MultiTypeAdapter implements DataManager { + + protected final Context mContext; + + private RecyclerDataManagerImpl mRecyclerDataManager; + + public MultiTypeAdapter(Context context) { + super(); + mContext = context; + ArrayList objects = new ArrayList<>(); + mRecyclerDataManager = new RecyclerDataManagerImpl<>(objects, this); + super.setItems(objects); + } + + public MultiTypeAdapter(Context context, @NonNull List items) { + super(); + mContext = context; + ArrayList objects = new ArrayList<>(items); + mRecyclerDataManager = new RecyclerDataManagerImpl<>(objects, this); + super.setItems(objects); + } + + public MultiTypeAdapter(Context context, @NonNull List items, int initialCapacity) { + super(items, initialCapacity); + mContext = context; + ArrayList objects = new ArrayList<>(items); + mRecyclerDataManager = new RecyclerDataManagerImpl<>(objects, this); + super.setItems(objects); + } + + public MultiTypeAdapter(Context context, @NonNull List items, @NonNull TypePool pool) { + super(items, pool); + mContext = context; + ArrayList objects = new ArrayList<>(items); + mRecyclerDataManager = new RecyclerDataManagerImpl<>(objects, this); + super.setItems(objects); + } + + @SuppressWarnings("unused") + public void notifyEntryChanged(Object entry) { + int itemPosition = getItemPosition(entry); + if (itemPosition != -1) { + notifyItemChanged(itemPosition); + } + } + + @Override + public void add(Object elem) { + mRecyclerDataManager.add(elem); + } + + @Override + public void addAt(int location, Object elem) { + mRecyclerDataManager.addAt(location, elem); + } + + @Override + public void addItems(List elements) { + mRecyclerDataManager.addItems(elements); + } + + @Override + public void addItemsChecked(List elements) { + mRecyclerDataManager.addItemsChecked(elements); + } + + @Override + public void addItemsAt(int location, List elements) { + mRecyclerDataManager.addItemsAt(location, elements); + } + + @Override + public void replace(Object oldElem, Object newElem) { + mRecyclerDataManager.replace(oldElem, newElem); + } + + @Override + public void replaceAt(int index, Object elem) { + mRecyclerDataManager.replaceAt(index, elem); + } + + @Override + public void replaceAll(List elements) { + mRecyclerDataManager.replaceAll(elements); + } + + @Override + public void setDataSource(List newDataSource, boolean notifyDataSetChanged) { + super.setItems(newDataSource); + mRecyclerDataManager.setDataSource(newDataSource, notifyDataSetChanged); + } + + @Override + public void remove(Object elem) { + mRecyclerDataManager.remove(elem); + } + + @Override + public void removeAt(int index) { + mRecyclerDataManager.removeAt(index); + } + + @Override + public void removeItems(List elements) { + mRecyclerDataManager.removeItems(elements); + } + + @Override + public Object getItem(int position) { + return mRecyclerDataManager.getItem(position); + } + + @Override + public int getDataSize() { + return mRecyclerDataManager.getDataSize(); + } + + @Override + public boolean contains(Object elem) { + return mRecyclerDataManager.contains(elem); + } + + @Override + public boolean isEmpty() { + return mRecyclerDataManager.isEmpty(); + } + + @Override + public void clear() { + mRecyclerDataManager.clear(); + } + + @NonNull + @Override + public List getItems() { + return mRecyclerDataManager.getItems(); + } + + @Override + public int getItemPosition(Object o) { + return mRecyclerDataManager.getItemPosition(o); + } + + @Override + public void setItems(@NonNull List items) { + ArrayList objects = new ArrayList<>(items); + super.setItems(objects); + mRecyclerDataManager.setDataSource(objects, true); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerAdapter.java b/lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerAdapter.java new file mode 100644 index 0000000..0bf632f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerAdapter.java @@ -0,0 +1,164 @@ +package com.android.base.adapter.recycler; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.ViewGroup; + +import com.android.base.adapter.DataManager; + +import java.util.List; + +/** + * RecyclerView 的适配器 + * 注意: 只有setDataSource才能替换原有数据源的引用。 + * + * @param 当前列表使用的数据类型 + * @author Ztiany + * date : 2015-05-11 22:38 + * email: 1169654504@qq.com + */ +public abstract class RecyclerAdapter extends RecyclerView.Adapter implements DataManager { + + private RecyclerDataManagerImpl mDataManager; + + @NonNull + protected Context mContext; + + public RecyclerAdapter(@NonNull Context context, List data) { + mDataManager = new RecyclerDataManagerImpl<>(data, this); + this.mContext = context; + } + + public RecyclerAdapter(@NonNull Context context) { + this(context, null); + } + + @Override + public int getItemCount() { + return getDataSize(); + } + + @NonNull + @Override + public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType); + + @Override + public void onBindViewHolder(@NonNull VH holder, int position, @NonNull List payloads) { + super.onBindViewHolder(holder, position, payloads); + } + + @Override + public abstract void onBindViewHolder(@NonNull VH viewHolder, int position); + + public void notifyEntryChanged(T t) { + int itemPosition = getItemPosition(t); + if (itemPosition != -1) { + notifyItemChanged(itemPosition); + } + } + + /////////////////////////////////////////////////////////////////////////// + // DataManager + /////////////////////////////////////////////////////////////////////////// + + @Override + public void add(T elem) { + mDataManager.add(elem); + } + + @Override + public void addAt(int location, T elem) { + mDataManager.addAt(location, elem); + } + + @Override + public void addItems(List elements) { + mDataManager.addItems(elements); + } + + @Override + public void addItemsChecked(List elements) { + mDataManager.addItemsChecked(elements); + } + + @Override + public void addItemsAt(int location, List elements) { + mDataManager.addItemsAt(location, elements); + } + + @Override + public void replace(T oldElem, T newElem) { + mDataManager.replace(oldElem, newElem); + } + + @Override + public void replaceAt(int index, T elem) { + mDataManager.replaceAt(index, elem); + } + + @Override + public void replaceAll(List elements) { + mDataManager.replaceAll(elements); + } + + @Override + public void remove(T elem) { + mDataManager.remove(elem); + } + + @Override + public void removeItems(List elements) { + mDataManager.removeItems(elements); + } + + @Override + public void removeAt(int index) { + mDataManager.removeAt(index); + } + + @Override + public T getItem(int position) { + return mDataManager.getItem(position); + } + + @Override + public final int getDataSize() { + return mDataManager.getDataSize(); + } + + @Override + public boolean contains(T elem) { + return mDataManager.contains(elem); + } + + @Override + public void clear() { + mDataManager.clear(); + } + + @Override + public void setDataSource(List elements, boolean notifyDataSetChanged) { + mDataManager.setDataSource(elements, notifyDataSetChanged); + } + + @Override + public List getItems() { + return mDataManager.getItems(); + } + + protected final void setHeaderSize(HeaderSize headerSize) { + mDataManager.setHeaderSize(headerSize); + } + + @Override + public boolean isEmpty() { + return mDataManager.isEmpty(); + } + + @Override + public int getItemPosition(T t) { + return mDataManager.getItemPosition(t); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerDataManagerImpl.java b/lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerDataManagerImpl.java new file mode 100644 index 0000000..1524222 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerDataManagerImpl.java @@ -0,0 +1,248 @@ +package com.android.base.adapter.recycler; + +import android.support.v7.widget.RecyclerView; + +import com.android.base.adapter.DataManager; +import com.android.base.utils.common.Checker; + +import java.util.ArrayList; +import java.util.List; + + +final class RecyclerDataManagerImpl implements DataManager { + + private List mData; + private RecyclerView.Adapter mAdapter; + private HeaderSize mHeaderSize; + + RecyclerDataManagerImpl(List tList, RecyclerView.Adapter adapter) { + mData = tList; + mAdapter = adapter; + } + + @Override + public boolean isEmpty() { + return getDataSize() == 0; + } + + @Override + public void add(T element) { + addAt(getDataSize(), element); + } + + @Override + public void addAt(int location, T element) { + if (mData != null) { + if (mData.isEmpty()) { + mData.add(element); + notifyItemInserted(getHeaderSize()); + } else { + int size = mData.size(); + int lastIndex = (location >= size ? size : location) + getHeaderSize(); + mData.add(location, element); + notifyItemInserted(lastIndex); + } + } else { + mData = new ArrayList<>(); + mData.add(element); + notifyDataSetChanged(); + } + } + + @Override + public void addItems(List elements) { + addItemsAt(getDataSize(), elements); + } + + @Override + public void addItemsChecked(List elements) { + if (Checker.isEmpty(elements)) { + return; + } + if (mData == null) { + addItems(elements); + return; + } + boolean hasRemovedElements = false; + for (T element : elements) { + if (element == null) { + continue; + } + if (mData.contains(element)) { + mData.remove(element); + if (!hasRemovedElements) { + hasRemovedElements = true; + } + } + } + if (hasRemovedElements) { + mData.addAll(elements); + notifyDataSetChanged(); + } else { + addItems(elements); + } + } + + @Override + public void addItemsAt(int location, List elements) { + if (Checker.isEmpty(elements)) { + return; + } + if (this.mData != null) { + + if (mData.isEmpty()) { + mData.addAll(elements); + notifyItemRangeInserted(getHeaderSize(), elements.size()); + } else { + int size = mData.size(); + int lastIndex = (location >= size ? size : location) + getHeaderSize(); + this.mData.addAll(location, elements); + notifyItemRangeInserted(lastIndex, elements.size()); + } + + } else { + this.mData = new ArrayList<>(elements.size()); + mData.addAll(elements); + notifyDataSetChanged(); + } + } + + @Override + public void replace(T oldElem, T newElem) { + if (mData != null && mData.contains(newElem)) { + replaceAt(mData.indexOf(oldElem), newElem); + } + } + + @Override + public void replaceAt(int index, T element) { + if (getDataSize() > index) { + mData.set(index, element); + notifyItemChanged(index + getHeaderSize()); + } + } + + @Override + public void replaceAll(List elements) { + if (mData == null) { + mData = new ArrayList<>(); + } + mData.clear(); + if (elements != null) { + mData.addAll(elements); + } + notifyDataSetChanged(); + } + + @Override + public void setDataSource(List newDataSource, boolean notifyDataSetChanged) { + mData = newDataSource; + if (notifyDataSetChanged) { + notifyDataSetChanged(); + } + } + + @Override + public void remove(T element) { + if (mData == null || mData.isEmpty()) { + return; + } + if (mData.contains(element)) { + int index = mData.indexOf(element) + getHeaderSize(); + mData.remove(element); + notifyItemRemoved(index); + } + } + + @Override + public void removeAt(int index) { + if (getDataSize() > index) { + mData.remove(index); + notifyItemRemoved(index + getHeaderSize()); + } + } + + @Override + public void removeItems(List elements) { + if (!Checker.isEmpty(elements) && mData != null && mData.containsAll(elements)) { + mData.removeAll(elements); + notifyDataSetChanged(); + } + } + + @Override + public T getItem(int position) { + position = position - getHeaderSize(); + if (position >= 0 && getDataSize() > position) { + return mData.get(position); + } + return null; + } + + @Override + public final int getDataSize() { + return mData == null ? 0 : mData.size(); + } + + @Override + public boolean contains(T element) { + return mData != null && mData.contains(element); + } + + @Override + public void clear() { + if (mData != null) { + mData.clear(); + notifyDataSetChanged(); + } + } + + @Override + public int getItemPosition(T t) { + List items = getItems(); + if (items == null) { + return -1; + } + return items.indexOf(t); + } + + @Override + public List getItems() { + return mData; + } + + /////////////////////////////////////////////////////////////////////////// + // Size + /////////////////////////////////////////////////////////////////////////// + private int getHeaderSize() { + return mHeaderSize == null ? 0 : mHeaderSize.getHeaderSize(); + } + + /////////////////////////////////////////////////////////////////////////// + // Adapter Call + /////////////////////////////////////////////////////////////////////////// + private void notifyItemChanged(int position) { + mAdapter.notifyItemChanged(position); + } + + private void notifyDataSetChanged() { + mAdapter.notifyDataSetChanged(); + } + + private void notifyItemInserted(int position) { + mAdapter.notifyItemInserted(position); + } + + private void notifyItemRangeInserted(int position, int size) { + mAdapter.notifyItemRangeInserted(position, size); + } + + private void notifyItemRemoved(int index) { + mAdapter.notifyItemRemoved(index); + } + + void setHeaderSize(HeaderSize headerSize) { + mHeaderSize = headerSize; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/SimpleRecyclerAdapter.kt b/lib_base/src/main/java/com/android/base/adapter/recycler/SimpleRecyclerAdapter.kt new file mode 100644 index 0000000..ed14837 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/SimpleRecyclerAdapter.kt @@ -0,0 +1,59 @@ +package com.android.base.adapter.recycler + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import kotlin.reflect.KClass +import kotlin.reflect.full.primaryConstructor + +/** + * A simple way to build a simple list. If you want to build a multi type list, perhaps you need to use [MultiTypeAdapter]. + * + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-01-15 11:41 + */ +abstract class SimpleRecyclerAdapter(context: Context, data: List? = null) : RecyclerAdapter(context, data) { + + private var mLayoutInflater: LayoutInflater = LayoutInflater.from(mContext) + + final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { + val layout = provideLayout(parent, viewType) + val itemView = if (layout is Int) { + mLayoutInflater.inflate(layout, parent, false) + } else + layout as View + return provideViewHolder(itemView) + } + + @Suppress("UNCHECKED_CAST") + open fun provideViewHolder(itemView: View): VH { + return (this::class.supertypes[0].arguments[1].type?.classifier as? KClass)?.primaryConstructor?.call(itemView) + ?: throw IllegalArgumentException("need primaryConstructor, and arguments is (View)") + } + + /**provide a layout id or a View*/ + abstract fun provideLayout(parent: ViewGroup, viewType: Int): Any + + override fun getItemViewType(position: Int): Int { + return TYPE_ITEM + } + + override fun onBindViewHolder(viewHolder: VH, position: Int) { + if (viewHolder.itemViewType == TYPE_ITEM) { + bind(viewHolder, getItem(position)) + } else { + bindOtherTypes(viewHolder, position) + } + } + + protected abstract fun bind(viewHolder: VH, item: T) + + protected open fun bindOtherTypes(viewHolder: ViewHolder, position: Int) {} + + companion object { + protected const val TYPE_ITEM = 0 + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/SmartViewHolder.java b/lib_base/src/main/java/com/android/base/adapter/recycler/SmartViewHolder.java new file mode 100644 index 0000000..b22d6d5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/SmartViewHolder.java @@ -0,0 +1,22 @@ +package com.android.base.adapter.recycler; + +import android.view.View; + +import com.android.base.adapter.ItemHelper; + + +public class SmartViewHolder extends ViewHolder { + + @SuppressWarnings("all") + protected final ItemHelper mHelper; + + public SmartViewHolder(View itemView) { + super(itemView); + mHelper = new ItemHelper(itemView); + } + + public ItemHelper helper() { + return mHelper; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/adapter/recycler/ViewHolder.java b/lib_base/src/main/java/com/android/base/adapter/recycler/ViewHolder.java new file mode 100644 index 0000000..67f135f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/adapter/recycler/ViewHolder.java @@ -0,0 +1,28 @@ +package com.android.base.adapter.recycler; + +import android.content.Context; +import android.support.annotation.IdRes; +import android.support.v7.widget.RecyclerView; +import android.view.View; + + +public class ViewHolder extends RecyclerView.ViewHolder { + + public ViewHolder(View itemView) { + super(itemView); + } + + protected Context getContext() { + return itemView.getContext(); + } + + public V findView(@IdRes int viewId) { + return itemView.findViewById(viewId); + } + + public ViewHolder setItemClickListener(View.OnClickListener onClickListener) { + itemView.setOnClickListener(onClickListener); + return this; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/ApplicationDelegate.java b/lib_base/src/main/java/com/android/base/app/ApplicationDelegate.java new file mode 100644 index 0000000..67d09a9 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ApplicationDelegate.java @@ -0,0 +1,116 @@ +package com.android.base.app; + +import android.app.Application; +import android.content.Context; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.net.ConnectivityManager; + +import com.android.base.receiver.NetStateReceiver; +import com.android.base.utils.BaseUtils; +import com.blankj.utilcode.util.AppUtils; +import com.blankj.utilcode.util.Utils; + +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.Flowable; +import io.reactivex.processors.BehaviorProcessor; +import timber.log.Timber; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-10-12 18:19 + */ +@SuppressWarnings("WeakerAccess,unused") +public final class ApplicationDelegate { + + private Application mApplication; + + private CrashHandler mCrashHandler; + private BehaviorProcessor mAppStatus; + + private AtomicBoolean mOnCreateCalled = new AtomicBoolean(false); + private AtomicBoolean mOnAttachBaseCalled = new AtomicBoolean(false); + + ApplicationDelegate() { + } + + public void attachBaseContext(@SuppressWarnings("unused") Context base) { + if (!mOnAttachBaseCalled.compareAndSet(false, true)) { + throw new IllegalStateException("Can only be called once"); + } + //no op + } + + public void onCreate(Application application) { + if (!mOnCreateCalled.compareAndSet(false, true)) { + throw new IllegalStateException("Can only be called once"); + } + mApplication = application; + //工具类初始化 + BaseUtils.init(application); + //异常日志记录 + mCrashHandler = CrashHandler.register(application); + //网络状态 + listenNetworkState(); + //App前台后台 + listenActivityLifecycleCallbacks(); + } + + public void onTerminate() { + //no op + } + + public void onConfigurationChanged(Configuration newConfig) { + //no op + } + + public void onTrimMemory(int level) { + //no op + } + + public void onLowMemory() { + //no op + } + + private void listenNetworkState() { + NetStateReceiver netStateReceiver = new NetStateReceiver(); + mApplication.registerReceiver(netStateReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + } + + private void listenActivityLifecycleCallbacks() { + mAppStatus = BehaviorProcessor.create(); + AppUtils.registerAppStatusChangedListener(this, new Utils.OnAppStatusChangedListener() { + @Override + public void onForeground() { + Timber.d("app进入前台"); + mAppStatus.onNext(true); + } + + @Override + public void onBackground() { + Timber.d("app进入后台"); + mAppStatus.onNext(false); + } + }); + } + + /** + * 获取可观察的 app 生命周期 + */ + Flowable appAppState() { + return mAppStatus; + } + + void setCrashProcessor(BaseKit.CrashProcessor crashProcessor) { + if (mCrashHandler != null) { + mCrashHandler.setCrashProcessor(crashProcessor); + } + } + + Application getApplication() { + return mApplication; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/BaseAppContext.java b/lib_base/src/main/java/com/android/base/app/BaseAppContext.java new file mode 100644 index 0000000..72a3d3c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/BaseAppContext.java @@ -0,0 +1,46 @@ +package com.android.base.app; + +import android.app.Application; +import android.content.Context; +import android.content.res.Configuration; + + +public class BaseAppContext extends Application { + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + BaseKit.get().getApplicationDelegate().attachBaseContext(base); + } + + @Override + public void onCreate() { + super.onCreate(); + BaseKit.get().getApplicationDelegate().onCreate(this); + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + BaseKit.get().getApplicationDelegate().onLowMemory(); + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + BaseKit.get().getApplicationDelegate().onTrimMemory(level); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + BaseKit.get().getApplicationDelegate().onConfigurationChanged(newConfig); + } + + @Override + public void onTerminate() { + super.onTerminate(); + BaseKit.get().getApplicationDelegate().onTerminate(); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/BaseKit.java b/lib_base/src/main/java/com/android/base/app/BaseKit.java new file mode 100644 index 0000000..39219c9 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/BaseKit.java @@ -0,0 +1,208 @@ +package com.android.base.app; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; + +import com.android.base.app.dagger.Injectable; +import com.android.base.app.fragment.FragmentConfig; +import com.android.base.app.fragment.LoadingViewFactory; +import com.android.base.app.ui.PageNumber; +import com.android.base.app.ui.RefreshLoadViewFactory; +import com.android.base.app.ui.RefreshViewFactory; +import com.android.base.interfaces.adapter.ActivityLifecycleCallbacksAdapter; +import com.android.base.receiver.NetworkState; +import com.blankj.utilcode.util.ActivityUtils; +import com.blankj.utilcode.util.AppUtils; + +import dagger.android.AndroidInjection; +import dagger.android.support.AndroidSupportInjection; +import io.reactivex.Flowable; + +/** + * 基础库工具 + * + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-04-16 17:12 + */ +@UiThread +public final class BaseKit { + + private static final BaseKit ONLY_BASE = new BaseKit(); + + private BaseKit() { + mApplicationDelegate = new ApplicationDelegate(); + } + + public static BaseKit get() { + return ONLY_BASE; + } + + /** + * LoadingView + */ + private LoadingViewFactory mLoadingViewFactory; + + /** + * Application lifecycle delegate + */ + private ApplicationDelegate mApplicationDelegate; + + /** + * 错误类型检查 + */ + private ErrorClassifier mErrorClassifier; + + /** + * 获取 Application 代理 + */ + @SuppressWarnings("WeakerAccess") + public ApplicationDelegate getApplicationDelegate() { + return mApplicationDelegate; + } + + public BaseKit registerLoadingFactory(LoadingViewFactory loadingViewFactory) { + if (mLoadingViewFactory != null) { + throw new UnsupportedOperationException("LoadingViewFactory had already set"); + } + mLoadingViewFactory = loadingViewFactory; + return this; + } + + public LoadingViewFactory getLoadingViewFactory() { + if (mLoadingViewFactory == null) { + throw new NullPointerException("you have not set the LoadingViewFactory by AndroidBase"); + } + return mLoadingViewFactory; + } + + public Flowable networkState() { + return NetworkState.observableState(); + } + + + public interface CrashProcessor { + void uncaughtException(Thread thread, Throwable ex); + } + + public BaseKit setCrashProcessor(CrashProcessor crashProcessor) { + mApplicationDelegate.setCrashProcessor(crashProcessor); + return this; + } + + public BaseKit setDefaultPageStart(int pageStart) { + PageNumber.setDefaultPageStart(pageStart); + return this; + } + + public BaseKit setDefaultPageSize(int defaultPageSize) { + PageNumber.setDefaultPageSize(defaultPageSize); + return this; + } + + /** + * 给 {@link com.android.base.app.fragment.Fragments } 设置一个默认的容器 id,在使用 其相关方法而没有传入特定的容器 id 时,则使用默认的容器 id。 + * + * @param defaultContainerId 容器id + */ + public BaseKit setDefaultFragmentContainerId(int defaultContainerId) { + FragmentConfig.setDefaultContainerId(defaultContainerId); + return this; + } + + public BaseKit enableAutoInject() { + Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() { + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + if (activity instanceof Injectable) { + if (((Injectable) activity).enableInject()) { + AndroidInjection.inject(activity); + if (activity instanceof FragmentActivity) { + handedFragmentInject((FragmentActivity) activity); + } + } + } + } + + private void handedFragmentInject(FragmentActivity activity) { + activity.getSupportFragmentManager().registerFragmentLifecycleCallbacks(new FragmentManager.FragmentLifecycleCallbacks() { + @Override + public void onFragmentAttached(@NonNull FragmentManager fm, @NonNull Fragment f, @NonNull Context context) { + if (f instanceof Injectable) { + if (((Injectable) f).enableInject()) { + AndroidSupportInjection.inject(f); + } + } + } + }, true); + } + }; + mApplicationDelegate.getApplication().registerActivityLifecycleCallbacks(activityLifecycleCallbacks); + return this; + } + + /** + * 获取可观察的 app 生命周期 + */ + public Flowable appState() { + return mApplicationDelegate.appAppState(); + } + + /** + * 获取当前resume的Activity + * + * @return activity + */ + @Nullable + public Activity getTopActivity() { + return ActivityUtils.getTopActivity(); + } + + /** + * App是否在前台运行 + * + * @return true 表示App在前台运行 + */ + public boolean isForeground() { + return AppUtils.isAppForeground(); + } + + public interface ErrorClassifier { + boolean isNetworkError(Throwable throwable); + + boolean isServerError(Throwable throwable); + } + + @SuppressWarnings("all") + public BaseKit setErrorClassifier(ErrorClassifier errorClassifier) { + if (mErrorClassifier != null) { + throw new UnsupportedOperationException("ErrorClassifier had already set"); + } + mErrorClassifier = errorClassifier; + return this; + } + + public ErrorClassifier errorClassifier() { + return mErrorClassifier; + } + + public BaseKit registerRefreshLoadViewFactory(RefreshLoadViewFactory.Factory factory) { + RefreshLoadViewFactory.registerFactory(factory); + return this; + } + + public BaseKit registerRefreshViewFactory(RefreshViewFactory.Factory factory) { + RefreshViewFactory.registerFactory(factory); + return this; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/CrashHandler.java b/lib_base/src/main/java/com/android/base/app/CrashHandler.java new file mode 100644 index 0000000..1509ee0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/CrashHandler.java @@ -0,0 +1,113 @@ +package com.android.base.app; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.content.Context; +import android.os.Build; +import android.os.Process; + +import com.blankj.utilcode.util.AppUtils; + +import java.io.File; +import java.io.PrintStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +import timber.log.Timber; + +/** + * 全局异常处理 + */ +final class CrashHandler implements Thread.UncaughtExceptionHandler { + + private Context mContext; + private BaseKit.CrashProcessor mCrashProcessor; + + public static CrashHandler register(Application application) { + CrashHandler crashHandler = new CrashHandler(application); + Thread.setDefaultUncaughtExceptionHandler(crashHandler); + return crashHandler; + } + + void setCrashProcessor(BaseKit.CrashProcessor crashProcessor) { + mCrashProcessor = crashProcessor; + } + + private CrashHandler(Context context) { + this.mContext = context; + } + + @Override + public void uncaughtException(Thread thread, Throwable ex) { + + if (mCrashProcessor != null) { + mCrashProcessor.uncaughtException(thread, ex); + } else { + // 收集异常信息,写入到sd卡 + restoreCrash(thread, ex); + //退出 + killProcess(); + } + } + + private void restoreCrash(@SuppressWarnings("unused") Thread thread, Throwable ex) { + ex.printStackTrace(System.err); + // 收集异常信息,写入到sd卡 + File dir = new File(mContext.getExternalFilesDir(null) + File.separator + "crash"); + + if (!dir.exists()) { + boolean mkdirs = dir.mkdirs(); + if (!mkdirs) { + Timber.e("CrashHandler create dir fail"); + return; + } + } + + try { + @SuppressLint("SimpleDateFormat") + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String name = dateFormat.format(new Date(System.currentTimeMillis())) + ".log"; + File fileName = new File(dir, name); + if (!fileName.exists()) { + @SuppressWarnings("unused") + boolean newFile = fileName.createNewFile(); + } + + PrintStream err = new PrintStream(fileName); + + err.println("--------------------------------AppInfo--------------------------------"); + err.println("AndroidVersion: " + AppUtils.getAppVersionName()); + err.println(); + err.println(); + err.println("--------------------------------SystemInfo:--------------------------------"); + err.println("Product: " + android.os.Build.PRODUCT); + err.println("CPU_ABI: " + android.os.Build.CPU_ABI); + err.println("TAGS: " + android.os.Build.TAGS); + err.println("VERSION_CODES.BASE:" + android.os.Build.VERSION_CODES.BASE); + err.println("MODEL: " + android.os.Build.MODEL); + err.println("SDK: " + Build.VERSION.SDK_INT); + err.println("VERSION.RELEASE: " + android.os.Build.VERSION.RELEASE); + err.println("DEVICE: " + android.os.Build.DEVICE); + err.println("DISPLAY: " + android.os.Build.DISPLAY); + err.println("BRAND: " + android.os.Build.BRAND); + err.println("BOARD: " + android.os.Build.BOARD); + err.println("FINGERPRINT: " + android.os.Build.FINGERPRINT); + err.println("ID: " + android.os.Build.ID); + err.println("MANUFACTURER: " + android.os.Build.MANUFACTURER); + err.println("USER: " + android.os.Build.USER); + err.println(); + err.println(); + err.println("--------------------------------CrashContent--------------------------------"); + ex.printStackTrace(err); + err.println(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void killProcess() { + Process.killProcess(Process.myPid()); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/aac/AutoClearedValue.kt b/lib_base/src/main/java/com/android/base/app/aac/AutoClearedValue.kt new file mode 100644 index 0000000..2f29cd1 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/aac/AutoClearedValue.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.base.app.aac + +import android.arch.lifecycle.DefaultLifecycleObserver +import android.arch.lifecycle.Lifecycle +import android.arch.lifecycle.LifecycleOwner +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentActivity +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class AutoClearedValue( + lifecycle: Lifecycle, + private val _event: Lifecycle.Event, + private val onCleared: (() -> Unit)? +) : ReadWriteProperty { + + private var _value: T? = null + + init { + lifecycle.addObserver(object : DefaultLifecycleObserver { + + override fun onPause(owner: LifecycleOwner) { + clearValue(Lifecycle.Event.ON_PAUSE) + } + + override fun onStop(owner: LifecycleOwner) { + clearValue(Lifecycle.Event.ON_STOP) + } + + override fun onDestroy(owner: LifecycleOwner) { + clearValue(Lifecycle.Event.ON_DESTROY) + } + + }) + } + + override fun getValue(thisRef: Fragment, property: KProperty<*>): T { + return _value + ?: throw IllegalStateException("should never call auto-cleared-value get when it might not be available") + } + + override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) { + _value = value + } + + private fun clearValue(event: Lifecycle.Event) { + if (_event == event) { + _value = null + onCleared?.invoke() + } + } + +} + +fun Fragment.autoCleared( + event: Lifecycle.Event = Lifecycle.Event.ON_DESTROY, + onCleared: (() -> Unit)? = null +) = AutoClearedValue(this.lifecycle, event, onCleared) + +fun FragmentActivity.autoCleared( + event: Lifecycle.Event = Lifecycle.Event.ON_DESTROY, + onCleared: (() -> Unit)? = null +) = AutoClearedValue(this.lifecycle, event, onCleared) \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/aac/AutoDisposeLiveData.kt b/lib_base/src/main/java/com/android/base/app/aac/AutoDisposeLiveData.kt new file mode 100644 index 0000000..db50540 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/aac/AutoDisposeLiveData.kt @@ -0,0 +1,225 @@ +package com.android.base.app.aac + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import com.android.base.data.Resource +import com.android.base.rx.subscribeIgnoreError +import com.github.dmstocking.optional.java.util.Optional +import com.uber.autodispose.* + + +fun ObservableSubscribeProxy.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +fun FlowableSubscribeProxy.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +fun SingleSubscribeProxy.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +fun MaybeSubscribeProxy.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +//----------------------------------------------------------------------------------------- + +fun ObservableSubscribeProxy.subscribeWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun ObservableSubscribeProxy.subscribeWithLiveData(liveData: MutableLiveData>, map: (T) -> R) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(map(it))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun ObservableSubscribeProxy>.subscribeOptionalWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it.orElse(null))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun ObservableSubscribeProxy>.subscribeOptionalWithLiveData(liveData: MutableLiveData>, map: (T?) -> R?) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + val value = map(it.orElse(null)) + liveData.postValue(Resource.success(value)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun FlowableSubscribeProxy.subscribeWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun FlowableSubscribeProxy.subscribeWithLiveData(liveData: MutableLiveData>, map: (T) -> R) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(map(it))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun FlowableSubscribeProxy>.subscribeOptionalWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it.orElse(null))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun FlowableSubscribeProxy>.subscribeOptionalWithLiveData(liveData: MutableLiveData>, map: (T?) -> R?) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + val value = map(it.orElse(null)) + liveData.postValue(Resource.success(value)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun CompletableSubscribeProxy.subscribeWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success()) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +//----------------------------------------------------------------------------------------- + +fun ObservableSubscribeProxy.toResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it)) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun ObservableSubscribeProxy>.optionalToResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it.orElse(null))) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun FlowableSubscribeProxy.toResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it)) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun FlowableSubscribeProxy>.optionalToResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it.orElse(null))) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun CompletableSubscribeProxy.toResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success()) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/aac/ResourceLiveData.kt b/lib_base/src/main/java/com/android/base/app/aac/ResourceLiveData.kt new file mode 100644 index 0000000..d2c21fb --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/aac/ResourceLiveData.kt @@ -0,0 +1,227 @@ +package com.android.base.app.aac + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import com.android.base.data.Resource +import com.android.base.rx.subscribeIgnoreError +import com.github.dmstocking.optional.java.util.Optional +import io.reactivex.* + + +//----------------------------------------------------------------------------------------- + +fun Observable.subscribeWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Observable.subscribeWithLiveData(liveData: MutableLiveData>, map: (T) -> R) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(map(it))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Observable>.subscribeOptionalWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it.orElse(null))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Observable>.subscribeOptionalWithLiveData(liveData: MutableLiveData>, map: (T?) -> R?) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + val value = map(it.orElse(null)) + liveData.postValue(Resource.success(value)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Flowable.subscribeWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Flowable.subscribeWithLiveData(liveData: MutableLiveData>, map: (T) -> R) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(map(it))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Flowable>.subscribeOptionalWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success(it.orElse(null))) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Flowable>.subscribeOptionalWithLiveData(liveData: MutableLiveData>, map: (T?) -> R?) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + val value = map(it.orElse(null)) + liveData.postValue(Resource.success(value)) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +fun Completable.subscribeWithLiveData(liveData: MutableLiveData>) { + liveData.postValue(Resource.loading()) + this.subscribe( + { + liveData.postValue(Resource.success()) + }, + { + liveData.postValue(Resource.error(it)) + } + ) +} + +//----------------------------------------------------------------------------------------- + +fun Observable.toResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it)) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun Observable>.optionalToResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it.orElse(null))) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun Flowable.toResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it)) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun Flowable>.optionalToResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success(it.orElse(null))) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +fun Completable.toResourceLiveData(): LiveData> { + val mutableLiveData = MutableLiveData>() + mutableLiveData.value = Resource.loading() + subscribe( + { + mutableLiveData.postValue(Resource.success()) + }, + { + mutableLiveData.postValue(Resource.error(it)) + } + ) + return mutableLiveData +} + +//----------------------------------------------------------------------------------------- + +fun Observable.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +fun Flowable.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +fun Single.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} + +fun Maybe.toLiveData(): LiveData { + val liveData = MutableLiveData() + this.subscribeIgnoreError { + liveData.postValue(it) + } + return liveData +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/aac/SingleLiveData.kt b/lib_base/src/main/java/com/android/base/app/aac/SingleLiveData.kt new file mode 100644 index 0000000..b256889 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/aac/SingleLiveData.kt @@ -0,0 +1,36 @@ +package com.android.base.app.aac + +import android.arch.lifecycle.LifecycleOwner +import android.arch.lifecycle.MediatorLiveData +import android.arch.lifecycle.Observer + +/** https://github.com/Shopify/livedata-ktx */ +class SingleLiveData : MediatorLiveData() { + + private var _version = 0 + private val version: Int get() = _version + + override fun observe(owner: LifecycleOwner, observer: Observer) { + val observerVersion = version + super.observe(owner, Observer { + if (observerVersion < version) { + observer.onChanged(it) + } + }) + } + + override fun observeForever(observer: Observer) { + val observeSinceVersion = version + super.observeForever { + if (version > observeSinceVersion) { + observer.onChanged(it) + } + } + } + + override fun setValue(value: T?) { + _version++ + super.setValue(value) + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/aac/SingleLiveEvent.java b/lib_base/src/main/java/com/android/base/app/aac/SingleLiveEvent.java new file mode 100644 index 0000000..753aca3 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/aac/SingleLiveEvent.java @@ -0,0 +1,61 @@ +package com.android.base.app.aac; + +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.MutableLiveData; +import android.arch.lifecycle.Observer; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import java.util.concurrent.atomic.AtomicBoolean; + + +/** + * A lifecycle-aware observable that sends only new updates after subscription, used for events like + * navigation and Snackbar messages. + *

+ * This avoids a common problem with events: on configuration change (like rotation) an update + * can be emitted if the observer is active. This LiveData only calls the observable if there's an + * explicit call to setValue() or call(). + *

+ * Note that only one observer is going to be notified of changes. + * + * @see SingleLiveEvent + */ +public class SingleLiveEvent extends MutableLiveData { + + private static final String TAG = "SingleLiveEvent"; + + private final AtomicBoolean mPending = new AtomicBoolean(false); + + @MainThread + public void observe(@NonNull LifecycleOwner owner, @NonNull final Observer observer) { + + if (hasActiveObservers()) { + Log.w(TAG, "Multiple observers registered but only one will be notified of changes."); + } + + // Observe the internal MutableLiveData + super.observe(owner, t -> { + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t); + } + }); + } + + @MainThread + public void setValue(@Nullable T t) { + mPending.set(true); + super.setValue(t); + } + + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + public void call() { + setValue(null); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegate.java b/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegate.java new file mode 100644 index 0000000..77d5c20 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegate.java @@ -0,0 +1,68 @@ +package com.android.base.app.activity; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + + +/** + * Activity生命周期代理 + * + * @author Ztiany + * Date : 2016-05-06 15:04 + * Email: 1169654504@qq.com + */ +@SuppressWarnings("unused") +public interface ActivityDelegate { + + default void onAttachedToActivity(@NonNull T activity) { + } + + default void onDetachedFromActivity() { + } + + default void onCreateBeforeSetContentView(@Nullable Bundle savedInstanceState) { + } + + default void onCreateAfterSetContentView(@Nullable Bundle savedInstanceState) { + } + + default void onSaveInstanceState(Bundle savedInstanceState) { + } + + default void onStart() { + } + + default void onResume() { + } + + default void onPause() { + } + + default void onStop() { + } + + default void onDestroy() { + } + + default void onActivityResult(int requestCode, int resultCode, Intent data) { + } + + default void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + } + + default void onRestoreInstanceState(Bundle savedInstanceState) { + } + + default void onRestart() { + } + + default void onPostCreate(@Nullable Bundle savedInstanceState) { + } + + default void onResumeFragments() { + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegateOwner.java b/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegateOwner.java new file mode 100644 index 0000000..245ab97 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegateOwner.java @@ -0,0 +1,19 @@ +package com.android.base.app.activity; + +import android.support.annotation.UiThread; + +import com.github.dmstocking.optional.java.util.function.Predicate; + +@UiThread +@SuppressWarnings("unused") +public interface ActivityDelegateOwner { + + void addDelegate(ActivityDelegate fragmentDelegate); + + boolean removeDelegate(ActivityDelegate fragmentDelegate); + + ActivityDelegate findDelegate(Predicate predicate); + + ActivityStatus getStatus(); + +} diff --git a/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegates.java b/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegates.java new file mode 100644 index 0000000..f27e187 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/ActivityDelegates.java @@ -0,0 +1,137 @@ +package com.android.base.app.activity; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v7.app.AppCompatActivity; + +import com.github.dmstocking.optional.java.util.function.Predicate; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2016-12-20 11:43 + */ +@UiThread +final class ActivityDelegates { + + private final List mDelegates; + private AppCompatActivity mBaseActivity; + + ActivityDelegates(AppCompatActivity baseActivity) { + mDelegates = new ArrayList<>(4); + mBaseActivity = baseActivity; + } + + void callOnCreateBeforeSetContentView(@Nullable Bundle savedInstanceState) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onCreateBeforeSetContentView(savedInstanceState); + } + } + + void callOnCreateAfterSetContentView(@Nullable Bundle savedInstanceState) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onCreateAfterSetContentView(savedInstanceState); + } + } + + void callOnRestoreInstanceState(Bundle savedInstanceState) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onRestoreInstanceState(savedInstanceState); + } + } + + void callOnPostCreate(@Nullable Bundle savedInstanceState) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onPostCreate(savedInstanceState); + } + } + + void callOnSaveInstanceState(Bundle outState) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onSaveInstanceState(outState); + } + } + + void callOnDestroy() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onDestroy(); + } + } + + void callOnStop() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onStop(); + } + } + + void callOnPause() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onPause(); + } + } + + void callOnResume() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onResume(); + } + } + + void callOnStart() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onStart(); + } + } + + void callOnRestart() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onRestart(); + } + } + + void callOnActivityResult(int requestCode, int resultCode, Intent data) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onActivityResult(requestCode, resultCode, data); + } + } + + void callOnRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + void callOnResumeFragments() { + for (ActivityDelegate activityDelegate : mDelegates) { + activityDelegate.onResumeFragments(); + } + } + + @SuppressWarnings("unchecked") + void addActivityDelegate(ActivityDelegate activityDelegate) { + mDelegates.add(activityDelegate); + activityDelegate.onAttachedToActivity(mBaseActivity); + } + + boolean removeActivityDelegate(ActivityDelegate activityDelegate) { + boolean remove = mDelegates.remove(activityDelegate); + if (remove) { + activityDelegate.onDetachedFromActivity(); + } + return remove; + } + + ActivityDelegate findDelegate(Predicate predicate) { + for (ActivityDelegate delegate : mDelegates) { + if (predicate.test(delegate)) { + return delegate; + } + } + return null; + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/activity/ActivityStatus.kt b/lib_base/src/main/java/com/android/base/app/activity/ActivityStatus.kt new file mode 100644 index 0000000..e0fd32a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/ActivityStatus.kt @@ -0,0 +1,11 @@ +package com.android.base.app.activity + +enum class ActivityStatus { + INITIALIZED, + CREATE, + START, + RESUME, + PAUSE, + STOP, + DESTROY +} diff --git a/lib_base/src/main/java/com/android/base/app/activity/BackHandlerHelper.java b/lib_base/src/main/java/com/android/base/app/activity/BackHandlerHelper.java new file mode 100644 index 0000000..a4d98d3 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/BackHandlerHelper.java @@ -0,0 +1,71 @@ +package com.android.base.app.activity; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; + +import java.util.List; + +/** + *

+ *        1.Fragment需要自己处理BackPress事件,如果不处理,就交给子Fragment处理。都不处理则由Activity处理
+ *        2.BackPress的传递由低层往深层传递,同一层级的中外层中的 Fragment优先处理。
+ *        3.在Fragment中嵌套使用Fragment时,请使用getSupportChildFragmentManager
+ * 
+ */ +public class BackHandlerHelper { + + /** + * 将back事件分发给 FragmentManager 中管理的子Fragment,如果该 FragmentManager 中的所有Fragment都 + * 没有处理back事件,则尝试 FragmentManager.popBackStack() + * + * @return 如果处理了back键则返回 true + * @see #handleBackPress(Fragment) + * @see #handleBackPress(FragmentActivity) + */ + public static boolean handleBackPress(FragmentManager fragmentManager) { + List fragments = fragmentManager.getFragments(); + for (int i = fragments.size() - 1; i >= 0; i--) { + Fragment child = fragments.get(i); + if (isFragmentBackHandled(child)) { + return true; + } + } + return false; + } + + /** + * 将back事件分发给Fragment中的子Fragment, + * 该方法调用了 {@link #handleBackPress(FragmentManager)} + * + * @return 如果处理了back键则返回 true + */ + public static boolean handleBackPress(Fragment fragment) { + return handleBackPress(fragment.getChildFragmentManager()); + } + + /** + * 将back事件分发给Activity中的子Fragment, + * 该方法调用了 {@link #handleBackPress(FragmentManager)} + * + * @return 如果处理了back键则返回 true + */ + public static boolean handleBackPress(FragmentActivity fragmentActivity) { + return handleBackPress(fragmentActivity.getSupportFragmentManager()); + } + + /** + * 判断Fragment是否处理了Back键 + * + * @return 如果处理了back键则返回 true + */ + @SuppressWarnings("WeakerAccess") + public static boolean isFragmentBackHandled(Fragment fragment) { + return fragment != null + && fragment.isVisible() + && fragment.getUserVisibleHint() //getUserVisibleHint默认情况下为true,在ViewPager中会被使用到。 + && fragment instanceof OnBackPressListener + && ((OnBackPressListener) fragment).onBackPressed(); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/activity/BaseActivity.java b/lib_base/src/main/java/com/android/base/app/activity/BaseActivity.java new file mode 100644 index 0000000..b7597ac --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/BaseActivity.java @@ -0,0 +1,222 @@ +package com.android.base.app.activity; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v7.app.AppCompatActivity; +import android.view.View; + +import com.android.base.utils.android.compat.AndroidVersion; +import com.github.dmstocking.optional.java.util.function.Predicate; + +import timber.log.Timber; + +/** + *
+ *          1,封装通用流程。
+ *          2,onBackPressed 事件分发,优先交给 Fragment 处理。
+ *          3,提供 RxJava 的生命周期绑定。
+ *  
+ * + * @author Ztiany + * Date : 2016-05-04 15:40 + * Email: 1169654504@qq.com + */ +public abstract class BaseActivity extends AppCompatActivity implements ActivityDelegateOwner { + + private final ActivityDelegates mActivityDelegates = new ActivityDelegates(this); + + private ActivityStatus mActivityStatus = ActivityStatus.INITIALIZED; + + private String tag() { + return this.getClass().getSimpleName(); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + Timber.tag(tag()).d("---->onCreate before call super"); + + initialize(savedInstanceState); + mActivityDelegates.callOnCreateBeforeSetContentView(savedInstanceState); + + super.onCreate(savedInstanceState); + Timber.tag(tag()).d("---->onCreate after call super " + "bundle = " + savedInstanceState); + + Object layout = layout(); + if (layout instanceof View) { + setContentView((View) layout); + } else if (layout instanceof Integer) { + setContentView((Integer) layout); + } else if (layout == null) { + Timber.d("layout() return null layout"); + } else { + throw new IllegalArgumentException("layout() return type no support, layout = " + layout); + } + + mActivityStatus = ActivityStatus.CREATE; + mActivityDelegates.callOnCreateAfterSetContentView(savedInstanceState); + + setupView(savedInstanceState); + } + + @Override + protected void onRestart() { + Timber.tag(tag()).d("---->onRestart before call super"); + super.onRestart(); + Timber.tag(tag()).d("---->onRestart after call super "); + mActivityDelegates.callOnRestart(); + } + + @Override + protected void onStart() { + Timber.tag(tag()).d("---->onStart before call super"); + super.onStart(); + Timber.tag(tag()).d("---->onStart after call super"); + mActivityStatus = ActivityStatus.START; + mActivityDelegates.callOnStart(); + } + + @Override + protected void onResume() { + Timber.tag(tag()).d("---->onResume before call super"); + super.onResume(); + Timber.tag(tag()).d("---->onResume after call super"); + mActivityStatus = ActivityStatus.RESUME; + mActivityDelegates.callOnResume(); + } + + @Override + protected void onPause() { + Timber.tag(tag()).d("---->onPause before call super"); + mActivityStatus = ActivityStatus.PAUSE; + mActivityDelegates.callOnPause(); + super.onPause(); + Timber.tag(tag()).d("---->onPause after call super "); + } + + @Override + protected void onStop() { + Timber.tag(tag()).d("---->onStop before call super"); + mActivityStatus = ActivityStatus.STOP; + mActivityDelegates.callOnStop(); + super.onStop(); + Timber.tag(tag()).d("---->onStop after call super"); + } + + @Override + protected void onDestroy() { + Timber.tag(tag()).d("---->onDestroy before call super"); + mActivityStatus = ActivityStatus.DESTROY; + mActivityDelegates.callOnDestroy(); + super.onDestroy(); + Timber.tag(tag()).d("---->onDestroy after call super"); + } + + @Override + protected void onPostCreate(@Nullable Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + mActivityDelegates.callOnPostCreate(savedInstanceState); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + mActivityDelegates.callOnSaveInstanceState(outState); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mActivityDelegates.callOnRestoreInstanceState(savedInstanceState); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + mActivityDelegates.callOnActivityResult(requestCode, resultCode, data); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + mActivityDelegates.callOnRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + protected void onResumeFragments() { + super.onResumeFragments(); + mActivityDelegates.callOnResumeFragments(); + } + + /////////////////////////////////////////////////////////////////////////// + // interface impl + /////////////////////////////////////////////////////////////////////////// + @UiThread + @Override + public final void addDelegate(@NonNull ActivityDelegate activityDelegate) { + mActivityDelegates.addActivityDelegate(activityDelegate); + } + + @SuppressWarnings("unused") + @UiThread + @Override + public final boolean removeDelegate(@NonNull ActivityDelegate activityDelegate) { + return mActivityDelegates.removeActivityDelegate(activityDelegate); + } + + @Override + public ActivityDelegate findDelegate(Predicate predicate) { + return mActivityDelegates.findDelegate(predicate); + } + + @Override + public ActivityStatus getStatus() { + return mActivityStatus; + } + + /** + * Before call super.onCreate and setContentView + * + * @param savedInstanceState state + */ + protected void initialize(@Nullable Bundle savedInstanceState) { + } + + /** + * provide a layoutId (int) or layout (View) + * + * @return layoutId + */ + @Nullable + protected abstract Object layout(); + + /** + * after setContentView + */ + protected abstract void setupView(@Nullable Bundle savedInstanceState); + + @Override + public void onBackPressed() { + if (BackHandlerHelper.handleBackPress(this)) { + Timber.d("onBackPressed() called but child fragment handle it"); + } else { + superOnBackPressed(); + } + } + + protected void superOnBackPressed() { + super.onBackPressed(); + } + + @Override + public boolean isDestroyed() { + if (AndroidVersion.atLeast(17)) { + return super.isDestroyed(); + } else { + return getStatus() == ActivityStatus.DESTROY; + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/activity/OnBackPressListener.java b/lib_base/src/main/java/com/android/base/app/activity/OnBackPressListener.java new file mode 100644 index 0000000..7c3b426 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/activity/OnBackPressListener.java @@ -0,0 +1,13 @@ +package com.android.base.app.activity; + +/** + * Activity的返回键监听 + */ +public interface OnBackPressListener { + + /** + * @return true 表示Fragment处理back press,false表示由Activity处理 + */ + boolean onBackPressed(); + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ActivityScope.java b/lib_base/src/main/java/com/android/base/app/dagger/ActivityScope.java new file mode 100644 index 0000000..9038e1d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/dagger/ActivityScope.java @@ -0,0 +1,14 @@ +package com.android.base.app.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + + +@Scope +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ActivityScope { +} diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ContextType.java b/lib_base/src/main/java/com/android/base/app/dagger/ContextType.java new file mode 100644 index 0000000..370f713 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/dagger/ContextType.java @@ -0,0 +1,22 @@ +package com.android.base.app.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + + +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface ContextType { + + String ACTIVITY = "Activity"; + String CONTEXT = "Context"; + String APPLICATION = "Application"; + + String value() default APPLICATION; + +} diff --git a/lib_base/src/main/java/com/android/base/app/dagger/FragmentScope.java b/lib_base/src/main/java/com/android/base/app/dagger/FragmentScope.java new file mode 100644 index 0000000..57d2313 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/dagger/FragmentScope.java @@ -0,0 +1,14 @@ +package com.android.base.app.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + + +@Scope +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface FragmentScope { +} diff --git a/lib_base/src/main/java/com/android/base/app/dagger/HasViewInjector.java b/lib_base/src/main/java/com/android/base/app/dagger/HasViewInjector.java new file mode 100644 index 0000000..69919e1 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/dagger/HasViewInjector.java @@ -0,0 +1,18 @@ +package com.android.base.app.dagger; + +import android.view.View; + +import java.util.Map; + +import javax.inject.Provider; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2017-06-21 14:58 + */ +public interface HasViewInjector { + + Map, Provider> viewInjectors(); + +} diff --git a/lib_base/src/main/java/com/android/base/app/dagger/Injectable.java b/lib_base/src/main/java/com/android/base/app/dagger/Injectable.java new file mode 100644 index 0000000..146a7b7 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/dagger/Injectable.java @@ -0,0 +1,31 @@ +package com.android.base.app.dagger; + +/** + * 当 Activity 实现此接口时,如果需要为内部的 Fragment 提供注入容器,需要实现 HasSupportFragmentInjector,具体如下面代码: + * + *
{@code
+ *
+ *       class InjectableActivity implements Injectable, HasSupportFragmentInjector{
+ *              @Inject
+ *              DispatchingAndroidInjector fragmentInjector;
+ *
+ *              public AndroidInjector supportFragmentInjector() {
+ *                         return fragmentInjector;
+ *              }
+ *       }
+ * }
+ *
+ * 标记接口,用于标记此类需要被注入依赖。
+ *
+ * @author Ztiany
+ * Email: ztiany3@gmail.com
+ * Date : 2018-12-11 12:38
+ */
+public interface Injectable {
+
+    default boolean enableInject() {
+        return true;
+    }
+
+}
+
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewComponent.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewComponent.java
new file mode 100644
index 0000000..6ad4058
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewComponent.java
@@ -0,0 +1,14 @@
+package com.android.base.app.dagger;
+
+import android.view.View;
+
+import dagger.MembersInjector;
+
+/**
+ * how to use it? refer https://github.com/Ztiany/Programming-Notes-Code/blob/master/Android/Dagger2AndroidInjection-v2.19/README.md。
+ *
+ * @param 
+ */
+@SuppressWarnings("WeakerAccess")
+public interface ViewComponent extends MembersInjector {
+}
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewComponentBuilder.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewComponentBuilder.java
new file mode 100644
index 0000000..0a0896e
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewComponentBuilder.java
@@ -0,0 +1,15 @@
+package com.android.base.app.dagger;
+
+import android.view.View;
+
+import dagger.BindsInstance;
+
+
+public interface ViewComponentBuilder> {
+
+    @BindsInstance
+    ViewComponentBuilder bindInstance(T t);
+
+    C build();
+
+}
\ No newline at end of file
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewInjection.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewInjection.java
new file mode 100644
index 0000000..d9de77e
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewInjection.java
@@ -0,0 +1,60 @@
+package com.android.base.app.dagger;
+
+import android.app.Application;
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+
+import java.util.Map;
+
+import javax.inject.Provider;
+
+/**
+ * @author Ztiany
+ * Email: ztiany3@gmail.com
+ * Date : 2017-06-21 14:55
+ */
+@SuppressWarnings("unused")
+public class ViewInjection {
+
+    private static final String TAG = ViewInjection.class.getSimpleName();
+
+    @SuppressWarnings("unchecked")
+    public static void inject(View view) {
+
+        if (null == view) {
+            throw new NullPointerException();
+        }
+
+        HasViewInjector hasViewInjector = findHasViewInjectors(view);
+
+        Log.d(TAG, String.format("An injector for %s was found in %s", view.getClass().getCanonicalName(), hasViewInjector.getClass().getCanonicalName()));
+
+        Map, Provider> viewInjectors = hasViewInjector.viewInjectors();
+        Provider provider = viewInjectors.get(view.getClass());
+
+        if (provider != null) {
+            ViewComponentBuilder viewComponentBuilder = provider.get();
+            viewComponentBuilder
+                    .bindInstance(view)
+                    .build()
+                    .injectMembers(view);
+        } else {
+            throw new NullPointerException("ViewInjection  fail ");
+        }
+
+    }
+
+    private static HasViewInjector findHasViewInjectors(View view) {
+        Context context = view.getContext();
+        if (context instanceof HasViewInjector) {
+            return (HasViewInjector) context;
+        }
+        Application application = (Application) context.getApplicationContext();
+        if (application instanceof HasViewInjector) {
+            return (HasViewInjector) application;
+        }
+        throw new IllegalArgumentException(String.format("No injector was found for %s", view.getClass().getCanonicalName()));
+    }
+
+}
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewKey.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewKey.java
new file mode 100644
index 0000000..e29e78e
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewKey.java
@@ -0,0 +1,18 @@
+package com.android.base.app.dagger;
+
+import android.view.View;
+
+import java.lang.annotation.Target;
+
+import dagger.MapKey;
+import dagger.internal.Beta;
+
+import static java.lang.annotation.ElementType.METHOD;
+
+@Beta
+@MapKey
+@Target(METHOD)
+@SuppressWarnings("unused")
+public @interface ViewKey {
+    Class value();
+}
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewModelFactory.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewModelFactory.java
new file mode 100644
index 0000000..87823c0
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewModelFactory.java
@@ -0,0 +1,45 @@
+package com.android.base.app.dagger;
+
+import android.arch.lifecycle.ViewModel;
+import android.arch.lifecycle.ViewModelProvider;
+import android.support.annotation.NonNull;
+
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+/**
+ * @author Ztiany
+ * Email: ztiany3@gmail.com
+ * Date : 2018-11-05 14:31
+ */
+public class ViewModelFactory implements ViewModelProvider.Factory {
+
+    private final Map, Provider> mCreatorMap;
+
+    @Inject
+    ViewModelFactory(Map, Provider> classProviderMap) {
+        mCreatorMap = classProviderMap;
+    }
+
+    @NonNull
+    @Override
+    @SuppressWarnings("unchecked")
+    public  T create(@NonNull Class modelClass) {
+        Provider viewModelProvider = mCreatorMap.get(modelClass);
+        if (viewModelProvider == null) {
+            for (Map.Entry, Provider> entry : mCreatorMap.entrySet()) {
+                if (modelClass.isAssignableFrom(entry.getKey())) {
+                    viewModelProvider = entry.getValue();
+                    break;
+                }
+            }
+        }
+        if (viewModelProvider != null) {
+            return (T) viewModelProvider.get();
+        }
+        throw new NullPointerException("can not find provider for " + modelClass);
+    }
+
+}
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewModelKey.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewModelKey.java
new file mode 100644
index 0000000..fef7306
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewModelKey.java
@@ -0,0 +1,18 @@
+package com.android.base.app.dagger;
+
+import android.arch.lifecycle.ViewModel;
+
+import dagger.MapKey;
+import kotlin.annotation.AnnotationTarget;
+import kotlin.annotation.MustBeDocumented;
+import kotlin.annotation.Retention;
+import kotlin.annotation.Target;
+
+@MustBeDocumented
+@Target(allowedTargets = AnnotationTarget.FUNCTION)
+@Retention()
+@MapKey
+public @interface ViewModelKey {
+
+    Class value();
+}
\ No newline at end of file
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewModelModule.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewModelModule.java
new file mode 100644
index 0000000..e4ea530
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewModelModule.java
@@ -0,0 +1,22 @@
+package com.android.base.app.dagger;
+
+import android.arch.lifecycle.ViewModelProvider;
+
+import dagger.Binds;
+import dagger.Module;
+
+/**
+ * 使用 ViewModelModule 情况下,所有的 ViewModule 都由 Activity 界别容器提供,因此 Fragment 级容器无法为其 ViewModule 提供依赖。这是仅有的局限性。
+ *
+ * @author Ztiany
+ * Email: ztiany3@gmail.com
+ * Date : 2018-11-05 16:20
+ */
+@Module
+public abstract class ViewModelModule {
+
+    @Binds
+    @ActivityScope
+    abstract ViewModelProvider.Factory provideViewModelFactory(ViewModelFactory viewModelFactory);
+
+}
diff --git a/lib_base/src/main/java/com/android/base/app/dagger/ViewScope.java b/lib_base/src/main/java/com/android/base/app/dagger/ViewScope.java
new file mode 100644
index 0000000..922cb40
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/dagger/ViewScope.java
@@ -0,0 +1,19 @@
+package com.android.base.app.dagger;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Scope;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * @author Ztiany
+ *         Email: ztiany3@gmail.com
+ *         Date : 2017-05-23 09:59
+ */
+@Scope
+@Documented
+@Retention(RUNTIME)
+public @interface ViewScope {
+}
diff --git a/lib_base/src/main/java/com/android/base/app/fragment/BaseDialogFragment.java b/lib_base/src/main/java/com/android/base/app/fragment/BaseDialogFragment.java
new file mode 100644
index 0000000..b4bc83e
--- /dev/null
+++ b/lib_base/src/main/java/com/android/base/app/fragment/BaseDialogFragment.java
@@ -0,0 +1,278 @@
+package com.android.base.app.fragment;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.annotation.UiThread;
+import android.support.v7.app.AppCompatDialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.base.app.BaseKit;
+import com.android.base.app.activity.BackHandlerHelper;
+import com.android.base.app.activity.OnBackPressListener;
+import com.android.base.app.ui.LoadingView;
+import com.github.dmstocking.optional.java.util.function.Predicate;
+
+import timber.log.Timber;
+
+/**
+ * 提供:
+ * 
+ *     1. RxJava 生命周期绑定。
+ *     2. 返回键监听。
+ *     3. 显示 LoadingDialog 和 Message。
+ *     4. 可以添加生命周期代理。
+ * 
+ * + * @author Ztiany + * date : 2016-03-19 23:09 + * email: 1169654504@qq.com + */ +public class BaseDialogFragment extends AppCompatDialogFragment implements LoadingView, OnBackPressListener, FragmentDelegateOwner { + + private LoadingView mLoadingViewImpl; + + private View mLayoutView; + + /* just for cache*/ + private View mCachedView; + + private final FragmentDelegates mFragmentDelegates = new FragmentDelegates(this); + + private String tag() { + return this.getClass().getSimpleName(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + Timber.tag(tag()).d("onAttach() called with: context = [" + context + "]"); + mFragmentDelegates.onAttach(context); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Timber.tag(tag()).d("-->onCreate savedInstanceState = " + savedInstanceState); + mFragmentDelegates.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + if (mCachedView == null) { + Object layout = provideLayout(); + if (layout == null) { + return null; + } + if (layout instanceof Integer) { + return mCachedView = inflater.inflate((Integer) layout, container, false); + } + if (layout instanceof View) { + return mCachedView = (View) layout; + } + throw new IllegalArgumentException("Here you should provide a layout id or a View"); + } + return mCachedView; + } + + /** + * 使用此方法提供的布局,将只会被缓存起来,即此方法将只会被调用一次。 + * + * @return provide a layout id or a View + */ + @Nullable + @SuppressWarnings("unused") + protected Object provideLayout() { + return null; + } + + @Override + public final void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Timber.tag(tag()).d("-->onViewCreated savedInstanceState = " + savedInstanceState); + if (mLayoutView != view) { + mLayoutView = view; + internalOnViewPrepared(view, savedInstanceState); + onViewPrepared(view, savedInstanceState); + } + mFragmentDelegates.onViewCreated(view, savedInstanceState); + } + + void internalOnViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + } + + /** + * View is prepared, If {@link android.support.v4.app.Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)} return same layout, it will be called once + * + * @param view view of fragment + */ + protected void onViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + Timber.tag(tag()).d("-->onActivityCreated savedInstanceState = " + savedInstanceState); + mFragmentDelegates.onActivityCreated(savedInstanceState); + } + + @Override + public void onStart() { + super.onStart(); + Timber.tag(tag()).d("-->onStart"); + mFragmentDelegates.onStart(); + } + + @Override + public void onResume() { + super.onResume(); + Timber.tag(tag()).d("-->onResume"); + mFragmentDelegates.onResume(); + } + + @Override + public void onPause() { + Timber.tag(tag()).d("-->onPause"); + mFragmentDelegates.onPause(); + super.onPause(); + } + + @Override + public void onStop() { + Timber.tag(tag()).d("-->onStop"); + mFragmentDelegates.onStop(); + super.onStop(); + } + + @Override + public void onDestroyView() { + Timber.tag(tag()).d("-->onDestroyView"); + mFragmentDelegates.onDestroyView(); + super.onDestroyView(); + } + + @Override + public void onDestroy() { + Timber.tag(tag()).d("-->onDestroy"); + mFragmentDelegates.onDestroy(); + super.onDestroy(); + dismissLoadingDialog(); + } + + @Override + public void onDetach() { + Timber.tag(tag()).d("-->onDetach"); + mFragmentDelegates.onDetach(); + super.onDetach(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + mFragmentDelegates.onSaveInstanceState(outState); + super.onSaveInstanceState(outState); + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + Timber.tag(tag()).d("-->setUserVisibleHint ==" + isVisibleToUser); + mFragmentDelegates.setUserVisibleHint(isVisibleToUser); + } + + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + Timber.tag(tag()).d("-->onHiddenChanged = " + hidden); + mFragmentDelegates.onHiddenChanged(hidden); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + mFragmentDelegates.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + mFragmentDelegates.onActivityResult(requestCode, resultCode, data); + } + + @Override + @UiThread + public final void addDelegate(FragmentDelegate fragmentDelegate) { + mFragmentDelegates.addDelegate(fragmentDelegate); + } + + @Override + @UiThread + public final boolean removeDelegate(FragmentDelegate fragmentDelegate) { + return mFragmentDelegates.removeDelegate(fragmentDelegate); + } + + @Override + public FragmentDelegate findDelegate(Predicate predicate) { + return mFragmentDelegates.findDelegate(predicate); + } + + @Override + public boolean onBackPressed() { + return handleBackPress() || BackHandlerHelper.handleBackPress(this); + } + + /** + * Fragment需要自己处理BackPress事件,如果不处理,就交给子Fragment处理。都不处理则由Activity处理 + */ + protected boolean handleBackPress() { + return false; + } + + private LoadingView getLoadingViewImpl() { + if (mLoadingViewImpl == null) { + mLoadingViewImpl = BaseKit.get().getLoadingViewFactory().createLoadingDelegate(getContext()); + } + return mLoadingViewImpl; + } + + @Override + public void showLoadingDialog() { + getLoadingViewImpl().showLoadingDialog(true); + } + + @Override + public void showLoadingDialog(boolean cancelable) { + getLoadingViewImpl().showLoadingDialog(cancelable); + } + + @Override + public void showLoadingDialog(CharSequence message, boolean cancelable) { + getLoadingViewImpl().showLoadingDialog(message, cancelable); + } + + @Override + public void showLoadingDialog(@StringRes int messageId, boolean cancelable) { + getLoadingViewImpl().showLoadingDialog(messageId, cancelable); + } + + @Override + public void dismissLoadingDialog() { + getLoadingViewImpl().dismissLoadingDialog(); + } + + @Override + public void showMessage(CharSequence message) { + getLoadingViewImpl().showMessage(message); + } + + @Override + public void showMessage(@StringRes int messageId) { + getLoadingViewImpl().showMessage(messageId); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/BaseFragment.java b/lib_base/src/main/java/com/android/base/app/fragment/BaseFragment.java new file mode 100644 index 0000000..d54170b --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/BaseFragment.java @@ -0,0 +1,289 @@ +package com.android.base.app.fragment; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.annotation.UiThread; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import com.android.base.app.BaseKit; +import com.android.base.app.activity.BackHandlerHelper; +import com.android.base.app.activity.OnBackPressListener; +import com.android.base.app.ui.LoadingView; +import com.github.dmstocking.optional.java.util.function.Predicate; + +import timber.log.Timber; + +/** + * 提供: + *
+ *     1. RxJava 生命周期绑定。
+ *     2. 返回键监听。
+ *     3. 显示 LoadingDialog 和 Message。
+ *     4. 可以添加生命周期代理。
+ * 
+ * + * @author Ztiany + * date : 2016-03-19 23:09 + * email: 1169654504@qq.com + */ +public class BaseFragment extends Fragment implements LoadingView, OnBackPressListener, FragmentDelegateOwner { + + private LoadingView mLoadingViewImpl; + + private View mLayoutView; + + /* just for cache*/ + private View mCachedView; + + private final FragmentDelegates mFragmentDelegates = new FragmentDelegates(this); + + private String tag() { + return this.getClass().getSimpleName(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + Timber.tag(tag()).d("onAttach() called with: context = [" + context + "]"); + mFragmentDelegates.onAttach(context); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Timber.tag(tag()).d("-->onCreate savedInstanceState = " + savedInstanceState); + mFragmentDelegates.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + if (mCachedView == null) { + Object layout = provideLayout(); + if (layout == null) { + return null; + } + if (layout instanceof Integer) { + return mCachedView = inflater.inflate((Integer) layout, container, false); + } + if (layout instanceof View) { + return mCachedView = (View) layout; + } + throw new IllegalArgumentException("Here you should provide a layout id or a View"); + } + + Timber.tag(tag()).d("mCachedView.parent: " + mCachedView.getParent()); + + if (mCachedView.getParent() != null) { + ViewParent parent = mCachedView.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(mCachedView); + } + } + + return mCachedView; + } + + /** + * 使用此方法提供的布局,将只会被缓存起来,即此方法将只会被调用一次。 + * + * @return provide a layout id or a View + */ + @Nullable + @SuppressWarnings("unused") + protected Object provideLayout() { + return null; + } + + @Override + public final void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Timber.tag(tag()).d("-->onViewCreated savedInstanceState = " + savedInstanceState); + if (mLayoutView != view) { + mLayoutView = view; + internalOnViewPrepared(view, savedInstanceState); + onViewPrepared(view, savedInstanceState); + } + mFragmentDelegates.onViewCreated(view, savedInstanceState); + } + + void internalOnViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + } + + /** + * View is prepared, If {@link android.support.v4.app.Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)} return same layout, it will be called once + * + * @param view view of fragment + */ + protected void onViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + Timber.tag(tag()).d("-->onActivityCreated savedInstanceState = " + savedInstanceState); + mFragmentDelegates.onActivityCreated(savedInstanceState); + } + + @Override + public void onStart() { + super.onStart(); + Timber.tag(tag()).d("-->onStart"); + mFragmentDelegates.onStart(); + } + + @Override + public void onResume() { + super.onResume(); + Timber.tag(tag()).d("-->onResume"); + mFragmentDelegates.onResume(); + } + + @Override + public void onPause() { + Timber.tag(tag()).d("-->onPause"); + mFragmentDelegates.onPause(); + super.onPause(); + } + + @Override + public void onStop() { + Timber.tag(tag()).d("-->onStop"); + mFragmentDelegates.onStop(); + super.onStop(); + } + + @Override + public void onDestroyView() { + Timber.tag(tag()).d("-->onDestroyView"); + mFragmentDelegates.onDestroyView(); + super.onDestroyView(); + } + + @Override + public void onDestroy() { + Timber.tag(tag()).d("-->onDestroy"); + mFragmentDelegates.onDestroy(); + super.onDestroy(); + dismissLoadingDialog(); + } + + @Override + public void onDetach() { + Timber.tag(tag()).d("-->onDetach"); + mFragmentDelegates.onDetach(); + super.onDetach(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + mFragmentDelegates.onSaveInstanceState(outState); + super.onSaveInstanceState(outState); + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + Timber.tag(tag()).d("-->setUserVisibleHint ==" + isVisibleToUser); + mFragmentDelegates.setUserVisibleHint(isVisibleToUser); + } + + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + Timber.tag(tag()).d("-->onHiddenChanged = " + hidden); + mFragmentDelegates.onHiddenChanged(hidden); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + mFragmentDelegates.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + mFragmentDelegates.onActivityResult(requestCode, resultCode, data); + } + + @Override + @UiThread + public final void addDelegate(FragmentDelegate fragmentDelegate) { + mFragmentDelegates.addDelegate(fragmentDelegate); + } + + @Override + @UiThread + public final boolean removeDelegate(FragmentDelegate fragmentDelegate) { + return mFragmentDelegates.removeDelegate(fragmentDelegate); + } + + @Override + public FragmentDelegate findDelegate(Predicate predicate) { + return mFragmentDelegates.findDelegate(predicate); + } + + @Override + public boolean onBackPressed() { + return handleBackPress() || BackHandlerHelper.handleBackPress(this); + } + + /** + * Fragment需要自己处理BackPress事件,如果不处理,就交给子Fragment处理。都不处理则由Activity处理 + */ + protected boolean handleBackPress() { + return false; + } + + private LoadingView getLoadingViewImpl() { + if (mLoadingViewImpl == null) { + mLoadingViewImpl = BaseKit.get().getLoadingViewFactory().createLoadingDelegate(getContext()); + } + return mLoadingViewImpl; + } + + @Override + public void showLoadingDialog() { + getLoadingViewImpl().showLoadingDialog(true); + } + + @Override + public void showLoadingDialog(boolean cancelable) { + getLoadingViewImpl().showLoadingDialog(cancelable); + } + + @Override + public void showLoadingDialog(CharSequence message, boolean cancelable) { + getLoadingViewImpl().showLoadingDialog(message, cancelable); + } + + @Override + public void showLoadingDialog(@StringRes int messageId, boolean cancelable) { + getLoadingViewImpl().showLoadingDialog(messageId, cancelable); + } + + @Override + public void dismissLoadingDialog() { + getLoadingViewImpl().dismissLoadingDialog(); + } + + @Override + public void showMessage(CharSequence message) { + getLoadingViewImpl().showMessage(message); + } + + @Override + public void showMessage(@StringRes int messageId) { + getLoadingViewImpl().showMessage(messageId); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/BaseListFragment.java b/lib_base/src/main/java/com/android/base/app/fragment/BaseListFragment.java new file mode 100644 index 0000000..1658b14 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/BaseListFragment.java @@ -0,0 +1,150 @@ +package com.android.base.app.fragment; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; + +import com.android.base.adapter.DataManager; +import com.android.base.app.ui.AutoPageNumber; +import com.android.base.app.ui.PageNumber; +import com.android.base.app.ui.RefreshListLayout; +import com.ztiany.loadmore.adapter.ILoadMore; +import com.ztiany.loadmore.adapter.OnLoadMoreListener; +import com.ztiany.loadmore.adapter.WrapperAdapter; + +import java.util.List; + +/** + * 通用的RecyclerView列表界面:支持下拉刷新和加载更多。 + * + * @param 当前列表使用的数据类型 + * @author Ztiany + * date : 2016-03-19 23:09 + * email: 1169654504@qq.com + */ +public abstract class BaseListFragment extends BaseStateFragment implements RefreshListLayout { + + /** + * 加载更多 + */ + private ILoadMore mLoadMore; + + /** + * 列表数据管理 + */ + private DataManager mDataManager; + + /** + * 分页页码 + */ + private PageNumber mPageNumber; + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (mDataManager == null) { + throw new NullPointerException("you need set DataManager"); + } + } + + protected final void setDataManager(@NonNull DataManager dataManager) { + mDataManager = dataManager; + } + + /** + * Default PageNumber is {@link AutoPageNumber} + * + * @param recyclerAdapter adapter + * @return recycler adapter wrapper + */ + protected final RecyclerView.Adapter setupLoadMore(@NonNull RecyclerView.Adapter recyclerAdapter) { + if (mDataManager == null) { + throw new IllegalStateException("you should setup a DataManager before call this method"); + } + return setupLoadMore(recyclerAdapter, new AutoPageNumber(this, mDataManager)); + } + + protected final RecyclerView.Adapter setupLoadMore(@NonNull RecyclerView.Adapter recyclerAdapter, @NonNull PageNumber pageNumber) { + mPageNumber = pageNumber; + + WrapperAdapter wrap = WrapperAdapter.wrap(recyclerAdapter); + mLoadMore = wrap; + mLoadMore.setOnLoadMoreListener(new OnLoadMoreListener() { + @Override + public void onLoadMore() { + BaseListFragment.this.onLoadMore(); + } + + @Override + public boolean canLoadMore() { + return !isRefreshing(); + } + }); + return wrap; + } + + @Override + protected void onRefresh() { + onStartLoad(); + } + + protected void onLoadMore() { + onStartLoad(); + } + + @Override + final boolean canRefresh() { + return !isLoadingMore(); + } + + /** + * call by {@link #onRefresh()} or {@link #onLoadMore()}, you can get current loading type from {@link #isRefreshing()} or {@link #isLoadingMore()}. + */ + protected void onStartLoad() { + } + + @Override + public void replaceData(List data) { + mDataManager.replaceAll(data); + } + + @Override + public void addData(List data) { + mDataManager.addItems(data); + } + + protected final ILoadMore getLoadMoreController() { + return mLoadMore; + } + + @Override + public PageNumber getPager() { + return mPageNumber; + } + + @Override + public boolean isEmpty() { + return mDataManager.isEmpty(); + } + + @Override + public boolean isLoadingMore() { + return mLoadMore != null && mLoadMore.isLoadingMore(); + } + + @Override + public void loadMoreCompleted(boolean hasMore) { + if (mLoadMore != null) { + mLoadMore.loadCompleted(hasMore); + } + } + + @Override + public void loadMoreFailed() { + if (mLoadMore != null) { + mLoadMore.loadFail(); + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/BaseListV2Fragment.kt b/lib_base/src/main/java/com/android/base/app/fragment/BaseListV2Fragment.kt new file mode 100644 index 0000000..aecb072 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/BaseListV2Fragment.kt @@ -0,0 +1,102 @@ +package com.android.base.app.fragment + +import android.os.Bundle +import android.view.View +import com.android.base.adapter.DataManager +import com.android.base.app.ui.AutoPageNumber +import com.android.base.app.ui.RefreshListLayout +import com.android.base.app.ui.StateLayoutConfig + +/** + * 区别于 [BaseListFragment] 只能支持 RecyclerView。[BaseListFragment] 采用包装 [android.support.v7.widget.RecyclerView.Adapter] 的方式, + * 在底部添加 load more view 的 item,来实现加载更多。BaseListV2Fragment 没有采用此种方式,所以你使用的刷新视图应该是支持这下来刷新和加载更多功能的。 + * + * 在调用BaseListV2Fragment [onActivityCreated] 之前,你应该设置好 [dataManager]。 + * + *@author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-03-26 15:06 + */ +abstract class BaseListV2Fragment : BaseFragment(), RefreshListLayout { + + companion object { + protected const val CONTENT = StateLayoutConfig.CONTENT + protected const val LOADING = StateLayoutConfig.LOADING + protected const val ERROR = StateLayoutConfig.ERROR + protected const val EMPTY = StateLayoutConfig.EMPTY + protected const val NET_ERROR = StateLayoutConfig.NET_ERROR + protected const val SERVER_ERROR = StateLayoutConfig.SERVER_ERROR + } + + private lateinit var stateLayout: RefreshLoadMoreStateLayoutImpl + protected open lateinit var dataManager: DataManager + + internal override fun internalOnViewPrepared(view: View, savedInstanceState: Bundle?) { + stateLayout = RefreshLoadMoreStateLayoutImpl.init(view) + stateLayout.refreshView.setRefreshHandler { + onRefresh() + } + stateLayout.refreshView.setLoadMoreHandler { + onLoadMore() + } + stateLayout.setStateRetryListener(this::onRetry) + } + + protected open fun onRetry(state: Int) { + if (!isRefreshing) { + autoRefresh() + } + } + + protected open fun onRefresh() = onStartLoad() + + protected open fun onLoadMore() = onStartLoad() + + /** + * call by [onRefresh] or [onLoadMore], you can get current loading type from [isRefreshing] or [isLoadingMore]. + */ + protected open fun onStartLoad() {} + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + if (::dataManager.isInitialized) { + throw NullPointerException("you need set DataManager") + } + } + + override fun onDestroyView() { + super.onDestroyView() + refreshCompleted() + } + + override fun replaceData(data: MutableList?) { + dataManager.replaceAll(data) + } + + override fun addData(data: MutableList?) { + dataManager.addItems(data) + } + + fun setRefreshEnable(enable: Boolean) = stateLayout.refreshView.setRefreshEnable(enable) + + fun setLoadMoreEnable(enable: Boolean) = stateLayout.refreshView.setLoadMoreEnable(enable) + + override fun getPager() = AutoPageNumber(this, dataManager) + override fun isEmpty() = dataManager.isEmpty + override fun loadMoreCompleted(hasMore: Boolean) = stateLayout.refreshView.loadMoreCompleted(hasMore) + override fun loadMoreFailed() = stateLayout.refreshView.loadMoreFailed() + override fun isRefreshing() = stateLayout.refreshView.isRefreshing + override fun showContentLayout() = stateLayout.showContentLayout() + override fun showLoadingLayout() = stateLayout.showLoadingLayout() + override fun refreshCompleted() = stateLayout.refreshView.refreshCompleted() + override fun showEmptyLayout() = stateLayout.showEmptyLayout() + override fun showErrorLayout() = stateLayout.showErrorLayout() + override fun showRequesting() = stateLayout.showRequesting() + override fun showBlank() = stateLayout.showBlank() + override fun getStateLayoutConfig() = stateLayout.stateLayoutConfig + override fun autoRefresh() = stateLayout.refreshView.autoRefresh() + override fun showNetErrorLayout() = stateLayout.showNetErrorLayout() + override fun showServerErrorLayout() = stateLayout.showServerErrorLayout() + override fun isLoadingMore() = stateLayout.refreshView.isLoadingMore + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/BaseStateDialogFragment.java b/lib_base/src/main/java/com/android/base/app/fragment/BaseStateDialogFragment.java new file mode 100644 index 0000000..136719d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/BaseStateDialogFragment.java @@ -0,0 +1,145 @@ +package com.android.base.app.fragment; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; + +import com.android.base.app.ui.RefreshStateLayout; +import com.android.base.app.ui.RefreshView; +import com.android.base.app.ui.StateLayoutConfig; + +/** + * @author Ztiany + * date : 2016-03-19 23:09 + * email: 1169654504@qq.com + * @see BaseStateFragment + */ +@SuppressWarnings("unused") +public abstract class BaseStateDialogFragment extends BaseDialogFragment implements RefreshStateLayout { + + private RefreshableStateLayoutImpl mStateLayout; + + protected static final int CONTENT = StateLayoutConfig.CONTENT; + protected static final int LOADING = StateLayoutConfig.LOADING; + protected static final int ERROR = StateLayoutConfig.ERROR; + protected static final int EMPTY = StateLayoutConfig.EMPTY; + protected static final int NET_ERROR = StateLayoutConfig.NET_ERROR; + protected static final int SERVER_ERROR = StateLayoutConfig.SERVER_ERROR; + + @Override + void internalOnViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + mStateLayout = RefreshableStateLayoutImpl.init(view); + mStateLayout.setRefreshHandler(new RefreshView.RefreshHandler() { + @Override + public void onRefresh() { + BaseStateDialogFragment.this.onRefresh(); + } + + @Override + public boolean canRefresh() { + return BaseStateDialogFragment.this.canRefresh(); + } + }); + + mStateLayout.setStateRetryListenerUnchecked(this::onRetry); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + refreshCompleted(); + } + + boolean canRefresh() { + return true; + } + + final RefreshView getRefreshView() { + return mStateLayout.getRefreshView(); + } + + protected void onRetry(int state) { + if (getRefreshView() != null) { + if (!getRefreshView().isRefreshing()) { + autoRefresh(); + } + } else { + onRefresh(); + } + } + + public final void setRefreshEnable(boolean enable) { + if (getRefreshView() != null) { + getRefreshView().setRefreshEnable(enable); + } + } + + protected void onRefresh() { + } + + @Override + public final StateLayoutConfig getStateLayoutConfig() { + return mStateLayout.getStateLayoutConfig(); + } + + private RefreshStateLayout getStateLayout() { + return mStateLayout; + } + + @Override + public final boolean isRefreshing() { + return mStateLayout.isRefreshing(); + } + + @Override + public void refreshCompleted() { + getStateLayout().refreshCompleted(); + } + + @Override + public void autoRefresh() { + getStateLayout().autoRefresh(); + } + + @Override + public void showContentLayout() { + getStateLayout().showContentLayout(); + } + + @Override + public void showLoadingLayout() { + getStateLayout().showLoadingLayout(); + } + + @Override + public void showEmptyLayout() { + getStateLayout().showEmptyLayout(); + } + + @Override + public void showErrorLayout() { + getStateLayout().showErrorLayout(); + } + + @Override + public void showRequesting() { + getStateLayout().showRequesting(); + } + + @Override + public void showBlank() { + getStateLayout().showBlank(); + } + + @Override + public void showNetErrorLayout() { + getStateLayout().showNetErrorLayout(); + } + + @Override + public void showServerErrorLayout() { + getStateLayout().showServerErrorLayout(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/BaseStateFragment.java b/lib_base/src/main/java/com/android/base/app/fragment/BaseStateFragment.java new file mode 100644 index 0000000..e9b2885 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/BaseStateFragment.java @@ -0,0 +1,152 @@ +package com.android.base.app.fragment; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; + +import com.android.base.app.ui.RefreshStateLayout; +import com.android.base.app.ui.RefreshView; +import com.android.base.app.ui.StateLayoutConfig; + +/** + *
+ *          1: 支持显示{CONTENT、LOADING、ERROR、EMPTY}四种布局、支持下拉刷新
+ *          2: 使用的布局中必须有一个id = R.id.base_status_layout的Layout,切改Layout实现了StateLayout
+ *          3: RefreshView(下拉刷新)的id必须设置为 :R.id.refresh_layout,没有添加则表示不需要下拉刷新功能
+ *          4: 默认所有重试和下拉刷新都会调用{@link #onRefresh()},子类可以修改该行为
+ * 
+ * + * @author Ztiany + * date : 2016-03-19 23:09 + * email: 1169654504@qq.com + */ +@SuppressWarnings("unused") +public abstract class BaseStateFragment extends BaseFragment implements RefreshStateLayout { + + private RefreshableStateLayoutImpl mStateLayout; + + protected static final int CONTENT = StateLayoutConfig.CONTENT; + protected static final int LOADING = StateLayoutConfig.LOADING; + protected static final int ERROR = StateLayoutConfig.ERROR; + protected static final int EMPTY = StateLayoutConfig.EMPTY; + protected static final int NET_ERROR = StateLayoutConfig.NET_ERROR; + protected static final int SERVER_ERROR = StateLayoutConfig.SERVER_ERROR; + + @Override + void internalOnViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + mStateLayout = RefreshableStateLayoutImpl.init(view); + mStateLayout.setRefreshHandler(new RefreshView.RefreshHandler() { + @Override + public void onRefresh() { + BaseStateFragment.this.onRefresh(); + } + + @Override + public boolean canRefresh() { + return BaseStateFragment.this.canRefresh(); + } + }); + + mStateLayout.setStateRetryListenerUnchecked(this::onRetry); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + refreshCompleted(); + } + + boolean canRefresh() { + return true; + } + + final RefreshView getRefreshView() { + return mStateLayout.getRefreshView(); + } + + protected void onRetry(int state) { + if (getRefreshView() != null) { + if (!getRefreshView().isRefreshing()) { + autoRefresh(); + } + } else { + onRefresh(); + } + } + + protected void onRefresh() { + } + + public final void setRefreshEnable(boolean enable) { + if (getRefreshView() != null) { + getRefreshView().setRefreshEnable(enable); + } + } + + @Override + @NonNull + public final StateLayoutConfig getStateLayoutConfig() { + return mStateLayout.getStateLayoutConfig(); + } + + private RefreshStateLayout getStateLayout() { + return mStateLayout; + } + + @Override + public final boolean isRefreshing() { + return mStateLayout.isRefreshing(); + } + + @Override + public void refreshCompleted() { + getStateLayout().refreshCompleted(); + } + + @Override + public void autoRefresh() { + getStateLayout().autoRefresh(); + } + + @Override + public void showContentLayout() { + getStateLayout().showContentLayout(); + } + + @Override + public void showLoadingLayout() { + getStateLayout().showLoadingLayout(); + } + + @Override + public void showEmptyLayout() { + getStateLayout().showEmptyLayout(); + } + + @Override + public void showErrorLayout() { + getStateLayout().showErrorLayout(); + } + + @Override + public void showRequesting() { + getStateLayout().showRequesting(); + } + + @Override + public void showBlank() { + getStateLayout().showBlank(); + } + + @Override + public void showNetErrorLayout() { + getStateLayout().showNetErrorLayout(); + } + + @Override + public void showServerErrorLayout() { + getStateLayout().showServerErrorLayout(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/DelegateEx.kt b/lib_base/src/main/java/com/android/base/app/fragment/DelegateEx.kt new file mode 100644 index 0000000..043e42d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/DelegateEx.kt @@ -0,0 +1,26 @@ +package com.android.base.app.fragment + +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * 懒加载代理 + * + *@author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-03-08 12:50 + */ +class LazyLoad(private val onPrepared: (() -> Unit)) : ReadOnlyProperty { + + private lateinit var lazyDelegate: LazyDelegate + + override fun getValue(thisRef: BaseFragment, property: KProperty<*>): LazyDelegate { + if (!::lazyDelegate.isInitialized) { + lazyDelegate = LazyDelegate.attach(thisRef) { + onPrepared.invoke() + } + } + return lazyDelegate + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/FragmentConfig.java b/lib_base/src/main/java/com/android/base/app/fragment/FragmentConfig.java new file mode 100644 index 0000000..5ccac3c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/FragmentConfig.java @@ -0,0 +1,24 @@ +package com.android.base.app.fragment; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-03-05 15:25 + */ +public class FragmentConfig { + + private static final int INVALIDATE_ID = -1; + private static int sDefaultContainerId = INVALIDATE_ID; + + public static void setDefaultContainerId(int defaultContainerId) { + sDefaultContainerId = defaultContainerId; + } + + public static int defaultContainerId() { + if (sDefaultContainerId == INVALIDATE_ID) { + throw new IllegalStateException("sDefaultContainerId has not set"); + } + return sDefaultContainerId; + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegate.java b/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegate.java new file mode 100644 index 0000000..e4d00ae --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegate.java @@ -0,0 +1,73 @@ +package com.android.base.app.fragment; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.View; + +public interface FragmentDelegate { + + /** + * 当该Delegate被添加到Fragment中 + */ + default void onAttachToFragment(T fragment) { + } + + /** + * 调用此方法时,清除Fragment的引用 + */ + default void onDetachFromFragment() { + } + + default void onAttach(Context context) { + } + + default void onCreate(@Nullable Bundle savedInstanceState) { + } + + default void onActivityCreated(@Nullable Bundle savedInstanceState) { + } + + default void onSaveInstanceState(Bundle savedInstanceState) { + } + + default void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + } + + default void onStart() { + } + + default void onResume() { + } + + default void onPause() { + } + + default void onStop() { + } + + default void onDestroy() { + } + + default void onDestroyView() { + } + + default void onDetach() { + } + + default void setUserVisibleHint(boolean isVisibleToUser) { + } + + default void onHiddenChanged(boolean hidden) { + } + + default void onActivityResult(int requestCode, int resultCode, Intent data) { + } + + default void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegateOwner.java b/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegateOwner.java new file mode 100644 index 0000000..0aca2a8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegateOwner.java @@ -0,0 +1,16 @@ +package com.android.base.app.fragment; + +import android.support.annotation.UiThread; + +import com.github.dmstocking.optional.java.util.function.Predicate; + +@UiThread +public interface FragmentDelegateOwner { + + void addDelegate(FragmentDelegate fragmentDelegate); + + boolean removeDelegate(FragmentDelegate fragmentDelegate); + + FragmentDelegate findDelegate(Predicate predicate); + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegates.java b/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegates.java new file mode 100644 index 0000000..9de2fd1 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/FragmentDelegates.java @@ -0,0 +1,166 @@ +package com.android.base.app.fragment; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v4.app.Fragment; +import android.view.View; + +import com.github.dmstocking.optional.java.util.function.Predicate; + +import java.util.ArrayList; +import java.util.List; + + +@UiThread +final class FragmentDelegates implements FragmentDelegate, FragmentDelegateOwner { + + private final Fragment mDelegateOwner; + private List mDelegates = new ArrayList<>(4); + + FragmentDelegates(T delegateOwner) { + mDelegateOwner = delegateOwner; + } + + @Override + public void onAttach(Context context) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onAttach(context); + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onCreate(savedInstanceState); + } + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onViewCreated(view, savedInstanceState); + } + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onActivityCreated(savedInstanceState); + } + } + + @Override + public void onStart() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onStart(); + } + } + + @Override + public void onResume() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onResume(); + } + } + + @Override + public void onPause() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onPause(); + } + } + + @Override + public void onStop() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onStop(); + } + } + + @Override + public void onDestroy() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onDestroy(); + } + } + + @Override + public void onDestroyView() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onDestroyView(); + } + } + + @Override + public void onDetach() { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onDetach(); + } + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.setUserVisibleHint(isVisibleToUser); + } + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onSaveInstanceState(savedInstanceState); + } + } + + @Override + public void onHiddenChanged(boolean hidden) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onHiddenChanged(hidden); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + for (FragmentDelegate fragmentDelegate : mDelegates) { + fragmentDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + @Override + @SuppressWarnings("unchecked") + public void addDelegate(FragmentDelegate fragmentDelegate) { + mDelegates.add(fragmentDelegate); + fragmentDelegate.onAttachToFragment(mDelegateOwner); + } + + @Override + public boolean removeDelegate(FragmentDelegate fragmentDelegate) { + boolean remove = mDelegates.remove(fragmentDelegate); + if (remove) { + fragmentDelegate.onDetachFromFragment(); + } + return remove; + } + + @Override + public FragmentDelegate findDelegate(Predicate predicate) { + for (FragmentDelegate delegate : mDelegates) { + if (predicate.test(delegate)) { + return delegate; + } + } + return null; + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/FragmentInfo.java b/lib_base/src/main/java/com/android/base/app/fragment/FragmentInfo.java new file mode 100644 index 0000000..935f581 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/FragmentInfo.java @@ -0,0 +1,132 @@ +package com.android.base.app.fragment; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; + +import java.lang.ref.WeakReference; + +@SuppressWarnings("WeakerAccess,unused") +public class FragmentInfo { + + private final int mPageId; + private final String mTag; + private final Class mClazz; + private final int mTitleId; + private final Bundle mArguments; + private final boolean mIsToStack; + private final String mStackName; + private WeakReference mFragment; + + private FragmentInfo(int pageId, String tag, Class clazz, int titleId, Bundle arguments, boolean toStack, String stackName) { + mPageId = pageId; + mTag = tag; + mClazz = clazz; + mTitleId = titleId; + mArguments = arguments; + mIsToStack = toStack; + mStackName = stackName; + } + + public Fragment getInstance() { + return mFragment == null ? null : mFragment.get(); + } + + public void setInstance(Fragment fragment) { + mFragment = new WeakReference<>(fragment); + } + + public Fragment newFragment(Context context) { + return Fragment.instantiate(context, mClazz.getName(), mArguments); + } + + public boolean isToStack() { + return mIsToStack; + } + + public String getStackName() { + return mStackName; + } + + public Bundle getArguments() { + return mArguments; + } + + public int getTitleId() { + return mTitleId; + } + + public int getPageId() { + return mPageId; + } + + public String getTag() { + return mTag; + } + + public Class getClazz() { + return mClazz; + } + + public static PageBuilder builder() { + return new PageBuilder(); + } + + + public static class PageBuilder { + + private int mPagerId; + private String mTag; + private Class mClazz; + private int mTitleId; + private Bundle mArguments; + private boolean mIsToStack; + private String mStackName; + + public FragmentInfo build() { + return new FragmentInfo(mPagerId, mTag, mClazz, mTitleId, mArguments, mIsToStack, mStackName); + } + + public PageBuilder pagerId(int pagerId) { + mPagerId = pagerId; + return this; + } + + public PageBuilder tag(String tag) { + this.mTag = tag; + return this; + } + + public PageBuilder clazz(Class clazz) { + mClazz = clazz; + return this; + } + + public PageBuilder titleId(int titleId) { + mTitleId = titleId; + return this; + } + + public PageBuilder arguments(Bundle arguments) { + mArguments = arguments; + return this; + } + + public PageBuilder toStack(boolean toStack) { + this.mIsToStack = toStack; + return this; + } + + /** + * 如果需要加入到Stack,建议加上StackName。 + * + * @param stackName StackName + * @return PageBuilder + */ + public PageBuilder stackName(String stackName) { + mStackName = stackName; + return this; + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/Fragments.kt b/lib_base/src/main/java/com/android/base/app/fragment/Fragments.kt new file mode 100644 index 0000000..d0c8b8f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/Fragments.kt @@ -0,0 +1,463 @@ +@file:JvmName("Fragments") + +package com.android.base.app.fragment + + +import android.support.annotation.NonNull +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentActivity +import android.support.v4.app.FragmentManager +import android.support.v4.app.FragmentTransaction +import android.view.View +import com.android.base.app.activity.ActivityDelegate +import com.android.base.app.activity.ActivityDelegateOwner +import com.android.base.app.activity.ActivityStatus +import com.android.base.kotlin.javaClassName +import kotlin.reflect.KClass + +@JvmOverloads +fun Fragment.exitFragment(immediate: Boolean = false) { + activity.exitFragment(immediate) +} + +@JvmOverloads +fun FragmentActivity?.exitFragment(immediate: Boolean = false) { + if (this == null) { + return + } + val supportFragmentManager = this.supportFragmentManager + val backStackEntryCount = supportFragmentManager.backStackEntryCount + if (backStackEntryCount > 0) { + if (immediate) { + supportFragmentManager.popBackStackImmediate() + } else { + supportFragmentManager.popBackStack() + } + } else { + this.supportFinishAfterTransition() + } +} + +/** + * @param clazz the interface container must implemented + * @param Type + * @return the interface context must implemented + */ +fun Fragment.requireContainerImplement(clazz: Class): T? { + if (clazz.isInstance(parentFragment)) { + return clazz.cast(parentFragment) + } + return if (clazz.isInstance(activity)) { + clazz.cast(activity) + } else { + throw RuntimeException("use this Fragment:$this, Activity or Fragment must impl interface :$clazz") + } +} + +/** + * @param clazz the interface context must implemented + * @param Type + * @return the interface context must implemented + */ +fun Fragment.requireContextImplement(clazz: Class): T? { + return if (!clazz.isInstance(activity)) { + throw RuntimeException("use this Fragment:$this, Activity must impl interface :$clazz") + } else { + clazz.cast(activity) + } +} + +/** + * @param clazz the interface parent must implemented + * @param Type + * @return the interface context must implemented + */ +fun Fragment.requireParentImplement(clazz: Class): T? { + return if (!clazz.isInstance(parentFragment)) { + throw RuntimeException("use this Fragment:$this, ParentFragment must impl interface :$clazz") + } else { + clazz.cast(parentFragment) + } +} + +/** 使用 [clazz] 的全限定类名作为 tag 查找 Fragment */ +fun FragmentManager.findFragmentByTag(clazz: KClass): T? { + @Suppress("UNCHECKED_CAST") + return findFragmentByTag(clazz.java.name) as? T +} + +fun FragmentManager.popBackTo(flag: String, immediate: Boolean = false) { + if (immediate) { + popBackStackImmediate(flag, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } else { + popBackStack(flag, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } +} + +fun FragmentManager.clearBackStack(immediate: Boolean = false) { + if (immediate) { + this.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } else { + this.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } +} + +private fun FragmentManager.isFragmentInStack(clazz: Class): Boolean { + val backStackEntryCount = backStackEntryCount + if (backStackEntryCount == 0) { + return false + } + for (i in 0 until backStackEntryCount) { + if (clazz.name == getBackStackEntryAt(i).name) { + return true + } + } + return false +} + +inline fun FragmentManager.inTransaction(func: EnhanceFragmentTransaction.() -> Unit) { + val fragmentTransaction = beginTransaction() + EnhanceFragmentTransaction(this, fragmentTransaction).func() + fragmentTransaction.commit() +} + +inline fun FragmentActivity.inFragmentTransaction(func: EnhanceFragmentTransaction.() -> Unit) { + val transaction = supportFragmentManager.beginTransaction() + EnhanceFragmentTransaction(supportFragmentManager, transaction).func() + transaction.commit() +} + +fun T.inSafelyFragmentTransaction( + func: EnhanceFragmentTransaction.() -> Unit +): Boolean where T : FragmentActivity, T : ActivityDelegateOwner { + + var delegate = findDelegate { + it is SafelyFragmentTransactionActivityDelegate + }as? SafelyFragmentTransactionActivityDelegate + + if (delegate == null) { + delegate = SafelyFragmentTransactionActivityDelegate() + addDelegate(delegate) + } + + val transaction = supportFragmentManager.beginTransaction() + + EnhanceFragmentTransaction(supportFragmentManager, transaction).func() + + return delegate.safeCommit(this, transaction) +} + +inline fun Fragment.inChildFragmentTransaction(func: EnhanceFragmentTransaction.() -> Unit) { + val transaction = childFragmentManager.beginTransaction() + EnhanceFragmentTransaction(childFragmentManager, transaction).func() + transaction.commit() +} + +fun T.inSafelyChildFragmentTransaction( + func: EnhanceFragmentTransaction.() -> Unit +): Boolean where T : Fragment, T : FragmentDelegateOwner { + + var delegate: SafelyFragmentTransactionFragmentDelegate? = findDelegate { + it is SafelyFragmentTransactionFragmentDelegate + } as? SafelyFragmentTransactionFragmentDelegate + + if (delegate == null) { + delegate = SafelyFragmentTransactionFragmentDelegate() + addDelegate(delegate) + } + + val transaction = childFragmentManager.beginTransaction() + + EnhanceFragmentTransaction(childFragmentManager, transaction).func() + + return delegate.safeCommit(this, transaction) +} + +private class SafelyFragmentTransactionActivityDelegate : ActivityDelegate { + + private val mPendingTransactions = mutableListOf() + + fun safeCommit(@NonNull activityDelegateOwner: ActivityDelegateOwner, @NonNull transaction: FragmentTransaction): Boolean { + val status = activityDelegateOwner.status + val isCommitterResumed = (status == ActivityStatus.CREATE || status == ActivityStatus.START || status == ActivityStatus.RESUME) + + return if (isCommitterResumed) { + transaction.commit() + false + } else { + mPendingTransactions.add(transaction) + true + } + } + + override fun onResumeFragments() { + if (mPendingTransactions.isNotEmpty()) { + mPendingTransactions.forEach { it.commit() } + mPendingTransactions.clear() + } + } + +} + +private class SafelyFragmentTransactionFragmentDelegate : FragmentDelegate { + + private val mPendingTransactions = mutableListOf() + + fun safeCommit(@NonNull fragment: Fragment, @NonNull transaction: FragmentTransaction): Boolean { + return if (fragment.isResumed) { + transaction.commit() + false + } else { + mPendingTransactions.add(transaction) + true + } + } + + override fun onResume() { + if (!mPendingTransactions.isEmpty()) { + mPendingTransactions.forEach { it.commit() } + mPendingTransactions.clear() + } + } + +} + +class EnhanceFragmentTransaction constructor( + private val fragmentManager: FragmentManager, + private val fragmentTransaction: FragmentTransaction +) : FragmentTransaction() { + + //------------------------------------------------------------------------------------------------ + // extra functions + //------------------------------------------------------------------------------------------------ + + /** + * 把 [fragment] 添加到回退栈中,并 hide 其他 fragment, + * 如果 [containerId]==0,则使用 [com.android.base.app.BaseKit.setDefaultFragmentContainerId] 中配置的 id, + * 如果 [tag] ==null 则使用 fragment 对应 class 的全限定类名。 + */ + fun addWithStack(containerId: Int = 0, fragment: Fragment, tag: String? = null, transition: Boolean = true): EnhanceFragmentTransaction { + //hide top + hideTopFragment() + //set add to stack + val nonnullTag = (tag ?: fragment.javaClassName()) + addToBackStack(nonnullTag) + //add + fragmentTransaction.add(containerId.confirmId(), fragment, nonnullTag) + if (transition) { + //set a transition + setTransitionOpen() + } + return this + } + + /** + * replace 方式把 [fragment] 添加到回退栈中, + * 如果 [containerId]==0,则使用 [com.android.base.app.BaseKit.setDefaultFragmentContainerId] 中配置的 id, + * 如果 [tag] ==null 则使用 fragment 对应 class 的全限定类名。 + */ + fun replaceWithStack(containerId: Int = 0, fragment: Fragment, tag: String? = null, transition: Boolean = true): EnhanceFragmentTransaction { + //set add to stack + val nonnullTag = (tag ?: fragment.javaClassName()) + addToBackStack(nonnullTag) + //add + fragmentTransaction.replace(containerId.confirmId(), fragment, nonnullTag) + //set a transition + if (transition) { + setTransitionOpen() + } + return this + } + + private fun Int.confirmId(): Int { + return if (this == 0) { + FragmentConfig.defaultContainerId() + } else { + this + } + } + + /** + * 添加 [fragment], + * 默认使用 [com.android.base.app.BaseKit.setDefaultFragmentContainerId] 中配置的 id, + * 如果 [tag] 为null,则使用 [fragment] 的全限定类名* + */ + fun addWithDefaultContainer(fragment: Fragment, tag: String? = null): FragmentTransaction { + val nonnullTag = (tag ?: fragment.javaClassName()) + return fragmentTransaction.add(FragmentConfig.defaultContainerId(), fragment, nonnullTag) + } + + /** + * 替换为 [fragment], + * id 使用 [com.android.base.app.BaseKit.setDefaultFragmentContainerId] 中配置的 id, + * 如果 [tag] 为null,则使用 [fragment] 的全限定类名 + */ + fun replaceWithDefaultContainer(fragment: Fragment, tag: String? = null, transition: Boolean = true): FragmentTransaction { + val nonnullTag = (tag ?: fragment.javaClassName()) + if (transition) { + setTransitionOpen() + } + return fragmentTransaction.replace(FragmentConfig.defaultContainerId(), fragment, nonnullTag) + } + + /**隐藏所有的 fragment */ + private fun hideOtherFragments() { + for (fragment in fragmentManager.fragments) { + if (fragment != null && fragment.isVisible) { + fragmentTransaction.hide(fragment) + } + } + } + + /**隐藏第一个可见的 fragment */ + private fun hideTopFragment() { + fragmentManager.fragments.lastOrNull { it.isVisible }?.let { + fragmentTransaction.hide(it) + } + } + + fun setTransitionOpen(): FragmentTransaction { + return fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + } + + fun setTransitionClose(): FragmentTransaction { + return fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE) + } + + fun setTransitionFade(): FragmentTransaction { + return fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + } + + //------------------------------------------------------------------------------------------------ + // original functions + //------------------------------------------------------------------------------------------------ + override fun setBreadCrumbShortTitle(res: Int): FragmentTransaction { + return fragmentTransaction.setBreadCrumbShortTitle(res) + } + + override fun setBreadCrumbShortTitle(text: CharSequence?): FragmentTransaction { + return fragmentTransaction.setBreadCrumbShortTitle(text) + } + + override fun setPrimaryNavigationFragment(fragment: Fragment?): FragmentTransaction { + return fragmentTransaction.setPrimaryNavigationFragment(fragment) + } + + override fun runOnCommit(runnable: Runnable): FragmentTransaction { + return fragmentTransaction.runOnCommit(runnable) + } + + override fun add(fragment: Fragment, tag: String?): FragmentTransaction { + return fragmentTransaction.add(fragment, tag) + } + + override fun add(containerViewId: Int, fragment: Fragment): FragmentTransaction { + return fragmentTransaction.add(containerViewId, fragment) + } + + override fun add(containerViewId: Int, fragment: Fragment, tag: String?): FragmentTransaction { + return fragmentTransaction.add(containerViewId, fragment, tag) + } + + override fun hide(fragment: Fragment): FragmentTransaction { + return fragmentTransaction.hide(fragment) + } + + override fun replace(containerViewId: Int, fragment: Fragment): FragmentTransaction { + return fragmentTransaction.replace(containerViewId, fragment) + } + + override fun replace(containerViewId: Int, fragment: Fragment, tag: String?): FragmentTransaction { + return fragmentTransaction.replace(containerViewId, fragment, tag) + } + + override fun detach(fragment: Fragment): FragmentTransaction { + return fragmentTransaction.detach(fragment) + } + + @Deprecated("") + override fun setAllowOptimization(allowOptimization: Boolean): FragmentTransaction { + return fragmentTransaction.setAllowOptimization(allowOptimization) + } + + override fun setCustomAnimations(enter: Int, exit: Int): FragmentTransaction { + return fragmentTransaction.setCustomAnimations(enter, exit) + } + + override fun setCustomAnimations(enter: Int, exit: Int, popEnter: Int, popExit: Int): FragmentTransaction { + return fragmentTransaction.setCustomAnimations(enter, exit, popEnter, popExit) + } + + override fun addToBackStack(name: String?): FragmentTransaction { + return fragmentTransaction.addToBackStack(name) + } + + override fun disallowAddToBackStack(): FragmentTransaction { + return fragmentTransaction.disallowAddToBackStack() + } + + override fun setTransitionStyle(styleRes: Int): FragmentTransaction { + return fragmentTransaction.setTransitionStyle(styleRes) + } + + override fun setTransition(transit: Int): FragmentTransaction { + return fragmentTransaction.setTransition(transit) + } + + override fun attach(fragment: Fragment): FragmentTransaction { + return fragmentTransaction.attach(fragment) + } + + override fun show(fragment: Fragment): FragmentTransaction { + return fragmentTransaction.show(fragment) + } + + override fun isEmpty(): Boolean { + return fragmentTransaction.isEmpty + } + + override fun remove(fragment: Fragment): FragmentTransaction { + return fragmentTransaction.remove(fragment) + } + + override fun isAddToBackStackAllowed(): Boolean { + return fragmentTransaction.isAddToBackStackAllowed + } + + override fun addSharedElement(sharedElement: View, name: String): FragmentTransaction { + return fragmentTransaction.addSharedElement(sharedElement, name) + } + + override fun setBreadCrumbTitle(res: Int): FragmentTransaction { + return fragmentTransaction.setBreadCrumbTitle(res) + } + + override fun setBreadCrumbTitle(text: CharSequence?): FragmentTransaction { + return fragmentTransaction.setBreadCrumbTitle(text) + } + + override fun setReorderingAllowed(reorderingAllowed: Boolean): FragmentTransaction { + return fragmentTransaction.setReorderingAllowed(reorderingAllowed) + } + + @Deprecated("commit will be called automatically") + override fun commit(): Int { + throw UnsupportedOperationException("commit will be called automatically") + } + + @Deprecated("commitAllowingStateLoss will be called automatically") + override fun commitAllowingStateLoss(): Int { + throw UnsupportedOperationException("commitAllowingStateLoss will be called automatically") + } + + @Deprecated("commitNow will be called automatically", ReplaceWith("throw UnsupportedOperationException(\"commitNow will be called automatically\")")) + override fun commitNow() { + throw UnsupportedOperationException("commitNow will be called automatically") + } + + @Deprecated("commitNowAllowingStateLoss will be called automatically", ReplaceWith("throw UnsupportedOperationException(\"commitNowAllowingStateLoss will be called automatically\")")) + override fun commitNowAllowingStateLoss() { + throw UnsupportedOperationException("commitNowAllowingStateLoss will be called automatically") + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/LazyDelegate.java b/lib_base/src/main/java/com/android/base/app/fragment/LazyDelegate.java new file mode 100644 index 0000000..2b6af63 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/LazyDelegate.java @@ -0,0 +1,117 @@ +package com.android.base.app.fragment; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.View; + +/** + *
+ *    在ViewPager中实现懒加载的Fragment
+ *          changed--1: Android Support 24 把setUserVisibleHint方法放到了Attach之前调用了,所以请在在构造代码块中设置LazyDelegate
+ * 
+ * + * @author Ztiany + * Date : Date : 2016-05-06 15:02 + * Email: 1169654504@qq.com + */ +public class LazyDelegate implements FragmentDelegate { + + /** + * View是否准备好,如果不需要绑定view数据,只是加载网络数据,那么该字段可以去掉 + */ + private boolean mIsViewPrepared; + /** + * 滑动过来后,View是否可见 + */ + private boolean mIsViewVisible; + + private onPreparedListener mOnPreparedListener; + + private LazyDelegate() { + } + + public static LazyDelegate attach(FragmentDelegateOwner delegateFragment, final onPreparedListener onPreparedListener) { + LazyDelegate delegate = new LazyDelegate(); + delegate.mOnPreparedListener = onPreparedListener; + delegateFragment.addDelegate(delegate); + return delegate; + } + + /** + * 在这里实现Fragment数据的缓加载. + * + * @param isVisibleToUser true表用户可见,false表不可见 + */ + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + if (isVisibleToUser) { + mIsViewVisible = true; + onVisible(); + } else { + mIsViewVisible = false; + onInvisible(); + } + } + + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + mIsViewPrepared = true; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + lazyLoad(); + } + + /** + * 滑动过来后,界面可见时执行 + */ + @SuppressWarnings("all") + protected void onVisible() { + lazyLoad(); + } + + /** + * 滑动过来后,界面不可见时执行 + */ + @SuppressWarnings("all") + protected void onInvisible() { + } + + private void lazyLoad() { + if (mIsViewPrepared && mIsViewVisible) { + notifyLazyLoad(); + } + } + + /** + * 懒加载数据,并在此绑定View数据 + */ + private void notifyLazyLoad() { + if (mOnPreparedListener != null) { + mOnPreparedListener.onPrepared(); + } + } + + public interface onPreparedListener { + void onPrepared(); + } + + public static abstract class SimpleLazyLoadListener implements onPreparedListener { + + private boolean mIsCalled; + + @Override + public final void onPrepared() { + if (!mIsCalled) { + onFirstLoad(); + mIsCalled = true; + } + } + + protected abstract void onFirstLoad(); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/LoadingViewFactory.java b/lib_base/src/main/java/com/android/base/app/fragment/LoadingViewFactory.java new file mode 100644 index 0000000..77295b4 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/LoadingViewFactory.java @@ -0,0 +1,11 @@ +package com.android.base.app.fragment; + +import android.content.Context; + +import com.android.base.app.ui.LoadingView; + +public interface LoadingViewFactory { + + LoadingView createLoadingDelegate(Context context); + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/RefreshLoadMoreStateLayoutImpl.kt b/lib_base/src/main/java/com/android/base/app/fragment/RefreshLoadMoreStateLayoutImpl.kt new file mode 100644 index 0000000..0ba52b2 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/RefreshLoadMoreStateLayoutImpl.kt @@ -0,0 +1,68 @@ +package com.android.base.app.fragment + +import android.graphics.drawable.Drawable +import android.view.View +import com.android.base.app.ui.CommonId +import com.android.base.app.ui.* + +internal class RefreshLoadMoreStateLayoutImpl private constructor(private val mLayout: View) : StateLayout, StateLayoutConfig { + + companion object { + fun init(view: View): RefreshLoadMoreStateLayoutImpl { + return RefreshLoadMoreStateLayoutImpl(view) + } + } + + private var mMultiStateView: StateLayout = mLayout.findViewById(CommonId.STATE_ID) as StateLayout + private var mRefreshView: RefreshLoadMoreView + + val refreshView: RefreshLoadMoreView + get() = mRefreshView + + init { + val refreshLayout = mLayout.findViewById(CommonId.REFRESH_ID) + mRefreshView = RefreshLoadViewFactory.createRefreshView(refreshLayout) + } + + override fun showLoadingLayout() = mMultiStateView.showLoadingLayout() + override fun showContentLayout() = mMultiStateView.showContentLayout() + override fun showEmptyLayout() = mMultiStateView.showEmptyLayout() + override fun showErrorLayout() = mMultiStateView.showErrorLayout() + override fun showRequesting() = mMultiStateView.showRequesting() + override fun showBlank() = mMultiStateView.showBlank() + override fun showNetErrorLayout() = mMultiStateView.showNetErrorLayout() + override fun showServerErrorLayout() = mMultiStateView.showServerErrorLayout() + + override fun getStateLayoutConfig(): StateLayoutConfig = mMultiStateView.stateLayoutConfig + + override fun setStateMessage(state: Int, message: CharSequence?): StateLayoutConfig { + mMultiStateView.stateLayoutConfig.setStateMessage(state, message) + return mMultiStateView.stateLayoutConfig + } + + override fun setStateIcon(state: Int, drawable: Drawable?): StateLayoutConfig { + mMultiStateView.stateLayoutConfig.setStateIcon(state, drawable) + return mMultiStateView.stateLayoutConfig + } + + override fun setStateIcon(state: Int, drawableId: Int): StateLayoutConfig { + mMultiStateView.stateLayoutConfig.setStateIcon(state, drawableId) + return mMultiStateView.stateLayoutConfig + } + + override fun setStateAction(state: Int, actionText: CharSequence?): StateLayoutConfig { + mMultiStateView.stateLayoutConfig.setStateAction(state, actionText) + return mMultiStateView.stateLayoutConfig + } + + override fun setStateRetryListener(retryActionListener: OnRetryActionListener?): StateLayoutConfig { + mMultiStateView.stateLayoutConfig.setStateRetryListener(retryActionListener) + return mMultiStateView.stateLayoutConfig + } + + override fun disableOperationWhenRequesting(disable: Boolean): StateLayoutConfig { + mMultiStateView.stateLayoutConfig.disableOperationWhenRequesting(disable) + return mMultiStateView.stateLayoutConfig + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/fragment/RefreshableStateLayoutImpl.java b/lib_base/src/main/java/com/android/base/app/fragment/RefreshableStateLayoutImpl.java new file mode 100644 index 0000000..638f62d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/RefreshableStateLayoutImpl.java @@ -0,0 +1,189 @@ +package com.android.base.app.fragment; + +import android.graphics.drawable.Drawable; +import android.support.annotation.DrawableRes; +import android.view.View; + +import com.android.base.app.ui.OnRetryActionListener; +import com.android.base.app.ui.RefreshStateLayout; +import com.android.base.app.ui.RefreshView; +import com.android.base.app.ui.RefreshViewFactory; +import com.android.base.app.ui.StateLayout; +import com.android.base.app.ui.StateLayoutConfig; + +import static com.android.base.app.ui.CommonId.REFRESH_ID; +import static com.android.base.app.ui.CommonId.STATE_ID; + +/** + * @author Ztiany + */ +final class RefreshableStateLayoutImpl implements RefreshStateLayout, StateLayoutConfig { + + private StateLayout mMultiStateView; + private RefreshView mRefreshView; + private RefreshView.RefreshHandler mRefreshHandler; + + static RefreshableStateLayoutImpl init(View layoutView) { + return new RefreshableStateLayoutImpl(layoutView); + } + + private RefreshableStateLayoutImpl(View layoutView) { + setupBaseUiLogic(layoutView); + setupRefreshLogic(layoutView); + } + + RefreshView getRefreshView() { + return mRefreshView; + } + + void setRefreshHandler(RefreshView.RefreshHandler refreshHandler) { + mRefreshHandler = refreshHandler; + } + + void setStateRetryListenerUnchecked(OnRetryActionListener retryActionListener) { + if (mMultiStateView != null) { + setStateRetryListener(retryActionListener); + } + } + + @SuppressWarnings("all") + private void setupBaseUiLogic(View layoutView) { + mMultiStateView = (StateLayout) layoutView.findViewById(STATE_ID); + } + + private void setupRefreshLogic(View layoutView) { + View refreshLayout = layoutView.findViewById(REFRESH_ID); + if (refreshLayout == null) { + return; + } + mRefreshView = RefreshViewFactory.createRefreshView(refreshLayout); + mRefreshView.setRefreshHandler(new RefreshView.RefreshHandler() { + @Override + public boolean canRefresh() { + return mRefreshHandler.canRefresh(); + } + + @Override + public void onRefresh() { + mRefreshHandler.onRefresh(); + } + }); + } + + @Override + public final void autoRefresh() { + if (mRefreshView != null) { + mRefreshView.autoRefresh(); + } + } + + @Override + public void showLoadingLayout() { + checkMultiStateView().showLoadingLayout(); + } + + @Override + public void showContentLayout() { + refreshCompleted(); + checkMultiStateView().showContentLayout(); + } + + @Override + public void showEmptyLayout() { + refreshCompleted(); + checkMultiStateView().showEmptyLayout(); + } + + @Override + public void showErrorLayout() { + refreshCompleted(); + checkMultiStateView().showErrorLayout(); + } + + @Override + public void showRequesting() { + checkMultiStateView().showRequesting(); + } + + @Override + public void showBlank() { + checkMultiStateView().showBlank(); + } + + @Override + public void showNetErrorLayout() { + refreshCompleted(); + checkMultiStateView().showNetErrorLayout(); + } + + @Override + public void showServerErrorLayout() { + refreshCompleted(); + checkMultiStateView().showNetErrorLayout(); + } + + @Override + public StateLayoutConfig getStateLayoutConfig() { + checkMultiStateView(); + return this; + } + + @Override + public void refreshCompleted() { + if (mRefreshView != null) { + mRefreshView.refreshCompleted(); + } + } + + @Override + public boolean isRefreshing() { + if (mRefreshView != null) { + return mRefreshView.isRefreshing(); + } + return false; + } + + @Override + public StateLayoutConfig setStateMessage(@RetryableState int state, CharSequence message) { + checkMultiStateView().getStateLayoutConfig().setStateMessage(state, message); + return this; + } + + @Override + public StateLayoutConfig setStateIcon(@RetryableState int state, Drawable drawable) { + checkMultiStateView().getStateLayoutConfig().setStateIcon(state, drawable); + return this; + } + + @Override + public StateLayoutConfig setStateIcon(@RetryableState int state, @DrawableRes int drawableId) { + checkMultiStateView().getStateLayoutConfig().setStateIcon(state, drawableId); + return this; + } + + @Override + public StateLayoutConfig setStateAction(@RetryableState int state, CharSequence actionText) { + checkMultiStateView().getStateLayoutConfig().setStateAction(state, actionText); + return this; + } + + @Override + public StateLayoutConfig setStateRetryListener(OnRetryActionListener retryActionListener) { + checkMultiStateView().getStateLayoutConfig().setStateRetryListener(retryActionListener); + return this; + } + + @Override + public StateLayoutConfig disableOperationWhenRequesting(boolean disable) { + checkMultiStateView().getStateLayoutConfig().disableOperationWhenRequesting(disable); + return this; + } + + private StateLayout checkMultiStateView() { + if (mMultiStateView == null) { + throw new IllegalStateException("Calling this function requires defining a view that implements StateLayout in the Layout"); + } + return mMultiStateView; + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/fragment/TabManager.java b/lib_base/src/main/java/com/android/base/app/fragment/TabManager.java new file mode 100644 index 0000000..fee2ea4 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/fragment/TabManager.java @@ -0,0 +1,207 @@ +package com.android.base.app.fragment; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class TabManager { + + @SuppressWarnings("WeakerAccess") public static final int ATTACH_DETACH = 1; + @SuppressWarnings("WeakerAccess") public static final int SHOW_HIDE = 2; + + private final FragmentManager mFragmentManager; + private final int mContainerId; + private final Tabs mMainTabs; + private final Context mContext; + + private FragmentInfo mCurrentFragmentInfo; + + private static final String CURRENT_ID_KET = "main_tab_id"; + private static final int NONE = -1; + private int mCurrentId = NONE; + private final int mOperationType; + + public TabManager(Context context, FragmentManager fragmentManager, Tabs tabs, int containerId) { + this(context, fragmentManager, tabs, containerId, ATTACH_DETACH); + } + + /** + * @param operationType {@link #ATTACH_DETACH} or {@link #SHOW_HIDE} + */ + @SuppressWarnings("WeakerAccess") + public TabManager(Context context, FragmentManager fragmentManager, Tabs tabs, int containerId, int operationType) { + if (operationType != ATTACH_DETACH && operationType != SHOW_HIDE) { + throw new IllegalArgumentException("the operationType must be ATTACH_DETACH or SHOW_HIDE"); + } + mMainTabs = tabs; + mContainerId = containerId; + mContext = context; + mFragmentManager = fragmentManager; + mOperationType = operationType; + } + + public final void setup(Bundle bundle) { + int pageId = mMainTabs.homePage().getPageId(); + if (bundle == null) { + switchPage(pageId); + } else { + mCurrentId = bundle.getInt(CURRENT_ID_KET, pageId); + restoreState(); + } + } + + private void restoreState() { + List pages = mMainTabs.getPages(); + for (FragmentInfo page : pages) { + page.setInstance(mFragmentManager.findFragmentByTag(page.getTag())); + if (mCurrentId == page.getPageId()) { + mCurrentFragmentInfo = page; + } + } + if (mCurrentId == NONE) { + doChangeTab(mMainTabs.homePage().getPageId()); + } + } + + private void switchPage(int pageId) { + if (mCurrentId == pageId) { + return; + } + FragmentTransaction ft = null; + if (mCurrentFragmentInfo != null) { + Fragment fragment = mCurrentFragmentInfo.getInstance(); + if (fragment != null) { + ft = mFragmentManager.beginTransaction(); + hideOrDetach(ft, fragment); + } + } + if (ft != null) { + ft.commit(); + } + doChangeTab(pageId); + } + + private void hideOrDetach(FragmentTransaction ft, Fragment fragment) { + if (mOperationType == SHOW_HIDE) { + ft.hide(fragment); + } else { + ft.detach(fragment); + } + } + + private void showOrAttach(FragmentTransaction fragmentTransaction, Fragment fragment) { + if (mOperationType == SHOW_HIDE) { + fragmentTransaction.show(fragment); + } else { + fragmentTransaction.attach(fragment); + } + } + + private void doChangeTab(int fragmentId) { + FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction(); + FragmentInfo fragmentInfo = mMainTabs.getFragmentInfo(fragmentId); + Fragment fragment = fragmentInfo.getInstance(); + if (fragment != null) { + showOrAttach(fragmentTransaction, fragment); + } else { + Fragment newFragment = fragmentInfo.newFragment(mContext); + fragmentInfo.setInstance(newFragment); + onFragmentCreated(fragmentId, newFragment); + fragmentTransaction.add(mContainerId, newFragment, fragmentInfo.getTag()); + } + mCurrentFragmentInfo = fragmentInfo; + mCurrentId = fragmentId; + fragmentTransaction.commit(); + } + + @SuppressWarnings("WeakerAccess") + public void selectTabByPosition(int position) { + switchPage(mMainTabs.getIdByPosition(position)); + } + + public void selectTabById(int tabId) { + selectTabByPosition(mMainTabs.getPositionById(tabId)); + } + + @SuppressWarnings("unused") + public int getCurrentPosition() { + return mMainTabs.getPositionById(mCurrentId); + } + + @SuppressWarnings("WeakerAccess,unused") + protected void onFragmentCreated(int id, Fragment newFragment) { + } + + public void onSaveInstanceState(Bundle bundle) { + bundle.putInt(CURRENT_ID_KET, mCurrentId); + } + + public static abstract class Tabs { + + private final List mPages; + + protected Tabs() { + mPages = new ArrayList<>(); + } + + protected void add(FragmentInfo page) { + mPages.add(page); + } + + FragmentInfo homePage() { + return mPages.get(0); + } + + public int size() { + return mPages.size(); + } + + FragmentInfo getFragmentInfo(int id) { + for (FragmentInfo page : mPages) { + if (page.getPageId() == id) { + return page; + } + } + throw new IllegalArgumentException("MainPages not has this pageId :" + id); + } + + /** + * @param clazz Fragment对应的clazz + * @return pagerId ,没有则返回-1 + */ + @SuppressWarnings("unused") + int getIdByClazz(Class clazz) { + for (FragmentInfo page : mPages) { + if (page.getClazz() == clazz) { + return page.getPageId(); + } + } + return -1; + } + + List getPages() { + return Collections.unmodifiableList(mPages); + } + + private int getPositionById(int tabId) { + int size = mPages.size(); + for (int i = 0; i < size; i++) { + if (mPages.get(i).getPageId() == tabId) { + return i; + } + } + return -1; + } + + private int getIdByPosition(int position) { + return mPages.get(position).getPageId(); + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/mvp/AbstractPresenter.kt b/lib_base/src/main/java/com/android/base/app/mvp/AbstractPresenter.kt new file mode 100644 index 0000000..edc66f6 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvp/AbstractPresenter.kt @@ -0,0 +1,44 @@ +package com.android.base.app.mvp + +import android.support.annotation.CallSuper +import java.lang.ref.WeakReference + +abstract class AbstractPresenter : IPresenter { + + private var _view: WeakReference? = null + + protected val view: V? + get() = if (_view != null) { + _view?.get() + } else null + + protected val isViewAttached: Boolean + get() = _view != null && _view?.get() != null + + final override fun bindView(view: V?) { + if (view == null) { + throw NullPointerException("Presenter bindView --> view is null") + } + if (_view != null) { + throw UnsupportedOperationException("Presenter bindView --> the view already bind") + } + _view = WeakReference(view) + } + + override fun onPostStart() {} + + override fun onPause() { + } + + override fun onResume() { + } + + @CallSuper + override fun onDestroy() { + if (_view != null) { + _view?.clear() + _view = null + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/mvp/IBaseView.kt b/lib_base/src/main/java/com/android/base/app/mvp/IBaseView.kt new file mode 100644 index 0000000..5553dc7 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvp/IBaseView.kt @@ -0,0 +1,3 @@ +package com.android.base.app.mvp + +interface IBaseView diff --git a/lib_base/src/main/java/com/android/base/app/mvp/IPresenter.kt b/lib_base/src/main/java/com/android/base/app/mvp/IPresenter.kt new file mode 100644 index 0000000..1c0009c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvp/IPresenter.kt @@ -0,0 +1,13 @@ +package com.android.base.app.mvp + + +interface IPresenter : Lifecycle { + + /** + * bind a view + * + * @param view V + */ + fun bindView(view: V?) + +} diff --git a/lib_base/src/main/java/com/android/base/app/mvp/Lifecycle.kt b/lib_base/src/main/java/com/android/base/app/mvp/Lifecycle.kt new file mode 100644 index 0000000..fa8ecc8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvp/Lifecycle.kt @@ -0,0 +1,31 @@ +package com.android.base.app.mvp + + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-04-18 16:23 + */ +interface Lifecycle { + + /** + * start the Lifecycle , initialize something, will be called only once + */ + fun onStart() + + /** + * will be called when view is ready. + */ + fun onPostStart() + + fun onResume() + + fun onPause() + + /** + * destroy the Lifecycle and release resource, will be called only once + */ + fun onDestroy() + +} + diff --git a/lib_base/src/main/java/com/android/base/app/mvp/PresenterBinder.kt b/lib_base/src/main/java/com/android/base/app/mvp/PresenterBinder.kt new file mode 100644 index 0000000..12214ce --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvp/PresenterBinder.kt @@ -0,0 +1,66 @@ +package com.android.base.app.mvp + +import android.os.Bundle +import android.support.v4.app.Fragment +import android.view.View +import com.android.base.app.fragment.FragmentDelegate +import com.android.base.app.fragment.FragmentDelegateOwner + + +class PresenterBinder constructor(private val lifecycle: Lifecycle) : FragmentDelegate { + + private var isCalled: Boolean = false + private var host: Fragment? = null + + override fun onAttachToFragment(fragment: Fragment) { + host = fragment + } + + override fun onDetachFromFragment() { + host = null + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + if (!isCalled) { + lifecycle.onStart() + val activity = host?.activity + activity?.findViewById(android.R.id.content)?.post { lifecycle.onPostStart() } + isCalled = true + } + } + + override fun onResume() { + lifecycle.onResume() + } + + override fun onPause() { + lifecycle.onPause() + } + + override fun onDestroy() { + lifecycle.onDestroy() + } + + companion object { + + /** + * @param v The MVP of the V + * @param p The MVP of the P + * @param The MVP of the V + */ + fun bind(fragmentDelegateOwner: FragmentDelegateOwner, v: V, p: IPresenter): PresenterBinder { + p.bindView(v) + val lifecycleDelegate = PresenterBinder(p) + fragmentDelegateOwner.addDelegate(lifecycleDelegate) + return lifecycleDelegate + } + } + +} + +fun FragmentDelegateOwner.bindPresenter(v: V, p: IPresenter): PresenterBinder { + p.bindView(v) + val lifecycleDelegate = PresenterBinder(p) + addDelegate(lifecycleDelegate) + return lifecycleDelegate +} diff --git a/lib_base/src/main/java/com/android/base/app/mvp/RxPresenter.kt b/lib_base/src/main/java/com/android/base/app/mvp/RxPresenter.kt new file mode 100644 index 0000000..d7b4ac8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvp/RxPresenter.kt @@ -0,0 +1,65 @@ +package com.android.base.app.mvp + +import android.support.annotation.CallSuper +import com.android.base.rx.LifecycleScopeProviderEx +import com.uber.autodispose.lifecycle.CorrespondingEventsFunction +import com.uber.autodispose.lifecycle.LifecycleEndedException +import com.uber.autodispose.lifecycle.LifecycleScopes +import io.reactivex.CompletableSource +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject + + +/** + * work with RxJava + * + * @author Ztiany + * Date : 2016-10-19 12:17 + * Email: 1169654504@qq.com + */ +abstract class RxPresenter : AbstractPresenter(), LifecycleScopeProviderEx { + + private val lifecycleSubject = BehaviorSubject.create() + + enum class LifecycleEvent { + START, + DESTROY + } + + @CallSuper + override fun onStart() { + lifecycleSubject.onNext(LifecycleEvent.START) + } + + @CallSuper + override fun onDestroy() { + lifecycleSubject.onNext(LifecycleEvent.DESTROY) + super@RxPresenter.onDestroy() + } + + final override fun lifecycle(): Observable { + return lifecycleSubject + } + + final override fun correspondingEvents(): CorrespondingEventsFunction { + return LIFECYCLE_CORRESPONDING_EVENTS + } + + final override fun peekLifecycle(): LifecycleEvent? { + return lifecycleSubject.value + } + + final override fun requestScope(): CompletableSource { + return LifecycleScopes.resolveScopeFromLifecycle(this) + } + + companion object { + internal val LIFECYCLE_CORRESPONDING_EVENTS: CorrespondingEventsFunction = CorrespondingEventsFunction { + return@CorrespondingEventsFunction when (it) { + LifecycleEvent.START -> LifecycleEvent.DESTROY + else -> throw LifecycleEndedException("Cannot bind to LifecycleEvent when outside of it.") + } + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/mvvm/ArchViewModel.kt b/lib_base/src/main/java/com/android/base/app/mvvm/ArchViewModel.kt new file mode 100644 index 0000000..d483faa --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvvm/ArchViewModel.kt @@ -0,0 +1,60 @@ +package com.android.base.app.mvvm + +import android.arch.lifecycle.ViewModel +import android.support.annotation.CallSuper +import com.android.base.rx.LifecycleScopeProviderEx +import com.uber.autodispose.lifecycle.CorrespondingEventsFunction +import com.uber.autodispose.lifecycle.LifecycleEndedException +import com.uber.autodispose.lifecycle.LifecycleScopes +import io.reactivex.CompletableSource +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject + +/** + * ArchViewModel work with Rx + * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-04-18 16:25 + */ +abstract class ArchViewModel : ViewModel(), LifecycleScopeProviderEx { + + private val archLifecycleSubject = BehaviorSubject.createDefault(ViewModelEvent.CREATED) + + enum class ViewModelEvent { + CREATED, CLEARED + } + + @CallSuper + override fun onCleared() { + archLifecycleSubject.onNext(ViewModelEvent.CLEARED) + super.onCleared() + } + + final override fun correspondingEvents(): CorrespondingEventsFunction { + return CORRESPONDING_EVENTS + } + + final override fun lifecycle(): Observable { + return archLifecycleSubject.hide() + } + + final override fun peekLifecycle(): ViewModelEvent? { + return archLifecycleSubject.value + } + + final override fun requestScope(): CompletableSource { + return LifecycleScopes.resolveScopeFromLifecycle(this) + } + + companion object { + private val CORRESPONDING_EVENTS = CorrespondingEventsFunction { event -> + when (event) { + ViewModelEvent.CREATED -> ViewModelEvent.CLEARED + else -> throw LifecycleEndedException( + "Cannot bind to ViewModel lifecycle after onCleared.") + } + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/mvvm/ViewModelEx.kt b/lib_base/src/main/java/com/android/base/app/mvvm/ViewModelEx.kt new file mode 100644 index 0000000..2079d2f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/mvvm/ViewModelEx.kt @@ -0,0 +1,32 @@ +package com.android.base.app.mvvm + +import android.arch.lifecycle.ViewModel +import android.arch.lifecycle.ViewModelProvider +import android.arch.lifecycle.ViewModelProviders +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentActivity + +inline fun Fragment.getViewModel(factory: ViewModelProvider.Factory? = null): VM { + return if (factory == null) { + ViewModelProviders.of(this)[VM::class.java] + } else { + ViewModelProviders.of(this, factory)[VM::class.java] + } +} + +inline fun Fragment.getViewModelFromActivity(factory: ViewModelProvider.Factory? = null): VM { + val activity = this.activity ?: throw IllegalStateException("fragment is not attach to activity") + return if (factory == null) { + ViewModelProviders.of(activity)[VM::class.java] + } else { + ViewModelProviders.of(activity, factory)[VM::class.java] + } +} + +inline fun FragmentActivity.getViewModel(factory: ViewModelProvider.Factory? = null): VM { + return if (factory == null) { + ViewModelProviders.of(this)[VM::class.java] + } else { + ViewModelProviders.of(this, factory)[VM::class.java] + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/ui/AutoPageNumber.java b/lib_base/src/main/java/com/android/base/app/ui/AutoPageNumber.java new file mode 100644 index 0000000..fab9309 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/AutoPageNumber.java @@ -0,0 +1,39 @@ +package com.android.base.app.ui; + +import com.android.base.adapter.DataManager; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-04-27 14:04 + */ +public class AutoPageNumber extends PageNumber { + + private final DataManager mDataManager; + private final RefreshListLayout mRefreshListLayout; + + public AutoPageNumber(RefreshListLayout refreshListLayout, DataManager dataManager) { + mRefreshListLayout = refreshListLayout; + mDataManager = dataManager; + } + + @Override + public int getCurrentPage() { + return calcPageNumber(mDataManager.getDataSize()); + } + + @Override + public int getLoadingPage() { + if (mRefreshListLayout.isRefreshing()) { + return getPageStart(); + } else { + return getCurrentPage() + 1; + } + } + + @Override + public int getItemCount() { + return mDataManager.getDataSize(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/CommonId.java b/lib_base/src/main/java/com/android/base/app/ui/CommonId.java new file mode 100644 index 0000000..706b161 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/CommonId.java @@ -0,0 +1,22 @@ +package com.android.base.app.ui; + + +import com.android.base.R; + +public class CommonId { + + private CommonId() { + throw new UnsupportedOperationException(); + } + + public static final int REFRESH_ID = R.id.base_refresh_layout; + public static final int STATE_ID = R.id.base_state_layout; + public static final int LIST_ID = R.id.base_list_layout; + + public static final int RETRY_TV_ID = R.id.base_retry_tv; + public static final int RETRY_IV_ID = R.id.base_retry_icon; + public static final int RETRY_BTN_ID = R.id.base_retry_btn; + + public static final int TOOLBAR_ID = R.id.common_toolbar; + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/LoadingView.java b/lib_base/src/main/java/com/android/base/app/ui/LoadingView.java new file mode 100644 index 0000000..078d760 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/LoadingView.java @@ -0,0 +1,28 @@ +package com.android.base.app.ui; + +import android.support.annotation.StringRes; + +/** + * 显示通用的 LoadingDialog 和 Message + * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2016-12-02 15:12 + */ +public interface LoadingView { + + void showLoadingDialog(); + + void showLoadingDialog(boolean cancelable); + + void showLoadingDialog(CharSequence message, boolean cancelable); + + void showLoadingDialog(@StringRes int messageId, boolean cancelable); + + void dismissLoadingDialog(); + + void showMessage(CharSequence message); + + void showMessage(@StringRes int messageId); + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/OnRetryActionListener.java b/lib_base/src/main/java/com/android/base/app/ui/OnRetryActionListener.java new file mode 100644 index 0000000..9bfc47f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/OnRetryActionListener.java @@ -0,0 +1,9 @@ +package com.android.base.app.ui; + +import static com.android.base.app.ui.StateLayoutConfig.*; + +public interface OnRetryActionListener { + + void onRetry(@RetryableState int state); + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/ui/PageNumber.java b/lib_base/src/main/java/com/android/base/app/ui/PageNumber.java new file mode 100644 index 0000000..b771698 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/PageNumber.java @@ -0,0 +1,99 @@ +package com.android.base.app.ui; + +/** + * @author Ztiany + * @version 1.0 + */ +public abstract class PageNumber { + + private static int PAGE_START = 1; + private static int PAGE_SIZE = 20; + + private int mPageStart; + private int mPageSize; + + private Object mPageToken; + + @SuppressWarnings("WeakerAccess,unused") + public PageNumber() { + this(PAGE_START, PAGE_SIZE); + } + + @SuppressWarnings("WeakerAccess") + public PageNumber(int pageStart, int pageSize) { + mPageStart = pageStart; + mPageSize = pageSize; + } + + @SuppressWarnings("unused") + public void setPageToken(Object pageToken) { + mPageToken = pageToken; + } + + @SuppressWarnings("unchecked,unused") + public T getPageToken() { + return (T) mPageToken; + } + + @SuppressWarnings("unused") + public int getPageSize() { + return mPageSize; + } + + @SuppressWarnings("WeakerAccess") + public int getPageStart() { + return mPageStart; + } + + public boolean hasMore(int size) { + return size >= mPageSize; + } + + /** + * 根据page size计算当前的页码 + */ + int calcPageNumber(int dataSize) { + /* s=1 s=0 + 19/20 = 0 1 0 + 21/20 = 1 2 1 + 54/20 = 2 3 2 + 64/20 = 3 4 3 + */ + int pageNumber; + int pageSize = mPageSize; + int pageStart = mPageStart; + if (pageStart == 0) { + pageNumber = (dataSize / pageSize) - 1; + pageNumber = pageNumber < 0 ? 0 : pageNumber; + } else if (pageStart == 1) { + pageNumber = (dataSize / pageSize); + pageNumber = pageNumber < 1 ? 1 : pageNumber; + } else { + throw new RuntimeException("pageStart must be 0 or 1"); + } + return pageNumber; + } + + @SuppressWarnings("unused") + public void changePageSetting(int pageStart, int pageSize) { + mPageStart = pageStart; + mPageSize = pageSize; + } + + public static void setDefaultPageStart(int pageSize) { + PAGE_START = pageSize; + } + + public static void setDefaultPageSize(int pageSize) { + PAGE_SIZE = pageSize; + } + + public abstract int getCurrentPage(); + + @SuppressWarnings("unused") + public abstract int getLoadingPage(); + + @SuppressWarnings("unused") + public abstract int getItemCount(); + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/RefreshListLayout.java b/lib_base/src/main/java/com/android/base/app/ui/RefreshListLayout.java new file mode 100644 index 0000000..27dec34 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/RefreshListLayout.java @@ -0,0 +1,31 @@ +package com.android.base.app.ui; + +import java.util.List; + +/** + * 列表视图行为 + * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-03-29 22:16 + */ +public interface RefreshListLayout extends RefreshStateLayout { + + void replaceData(List data); + + void addData(List data); + + PageNumber getPager(); + + boolean isEmpty(); + + void loadMoreCompleted(boolean hasMore); + + void loadMoreFailed(); + + boolean isLoadingMore(); + + @Override + boolean isRefreshing(); + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/RefreshLoadMoreView.java b/lib_base/src/main/java/com/android/base/app/ui/RefreshLoadMoreView.java new file mode 100644 index 0000000..260692c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/RefreshLoadMoreView.java @@ -0,0 +1,40 @@ +package com.android.base.app.ui; + +/** + *
对下拉刷新的抽象 + *
Email: 1169654504@qq.com + * + * @author Ztiany + * @version 1.0 + */ +public interface RefreshLoadMoreView { + + void autoRefresh(); + + void refreshCompleted(); + + void loadMoreCompleted(boolean hasMore); + + void loadMoreFailed(); + + void setRefreshHandler(RefreshHandler refreshHandler); + + void setLoadMoreHandler(LoadMoreHandler loadMoreHandler); + + boolean isRefreshing(); + + boolean isLoadingMore(); + + void setRefreshEnable(boolean enable); + + void setLoadMoreEnable(boolean enable); + + interface RefreshHandler { + void onRefresh(); + } + + interface LoadMoreHandler { + void onRefresh(); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/ui/RefreshLoadViewFactory.java b/lib_base/src/main/java/com/android/base/app/ui/RefreshLoadViewFactory.java new file mode 100644 index 0000000..c7ba4f8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/RefreshLoadViewFactory.java @@ -0,0 +1,31 @@ +package com.android.base.app.ui; + +import android.view.View; + +/** + * RefreshLoadMoreView Factory + * + * @author Ztiany + * Email: 1169654504@qq.com + * @version 1.0 + */ +public class RefreshLoadViewFactory { + + private static Factory sFactory; + + public static RefreshLoadMoreView createRefreshView(View view) { + if (sFactory != null) { + return sFactory.createRefreshView(view); + } + throw new IllegalArgumentException("RefreshLoadViewFactory does not support create RefreshLoadMoreView . the view :" + view); + } + + public static void registerFactory(Factory factory) { + sFactory = factory; + } + + public interface Factory { + RefreshLoadMoreView createRefreshView(View view); + } + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/RefreshStateLayout.java b/lib_base/src/main/java/com/android/base/app/ui/RefreshStateLayout.java new file mode 100644 index 0000000..7383475 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/RefreshStateLayout.java @@ -0,0 +1,11 @@ +package com.android.base.app.ui; + +public interface RefreshStateLayout extends StateLayout{ + + void autoRefresh(); + + void refreshCompleted(); + + boolean isRefreshing(); + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/RefreshView.java b/lib_base/src/main/java/com/android/base/app/ui/RefreshView.java new file mode 100644 index 0000000..68af62f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/RefreshView.java @@ -0,0 +1,32 @@ +package com.android.base.app.ui; + +/** + *
对下拉刷新的抽象 + *
Email: 1169654504@qq.com + * + * @author Ztiany + * @version 1.0 + */ +public interface RefreshView { + + void autoRefresh(); + + void refreshCompleted(); + + void setRefreshHandler(RefreshHandler refreshHandler); + + boolean isRefreshing(); + + void setRefreshEnable(boolean enable); + + abstract class RefreshHandler { + + public boolean canRefresh() { + return true; + } + + public abstract void onRefresh(); + + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/app/ui/RefreshViewFactory.java b/lib_base/src/main/java/com/android/base/app/ui/RefreshViewFactory.java new file mode 100644 index 0000000..f7009d0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/RefreshViewFactory.java @@ -0,0 +1,35 @@ +package com.android.base.app.ui; + +import android.support.v4.widget.SwipeRefreshLayout; +import android.view.View; + +/** + * RefreshView Factory + * + * @author Ztiany + * Email: 1169654504@qq.com + * @version 1.0 + */ +public class RefreshViewFactory { + + private static Factory sFactory; + + public static RefreshView createRefreshView(View view) { + + if (sFactory != null) { + return sFactory.createRefreshView(view); + } + if (view instanceof SwipeRefreshLayout) { + return new SwipeRefreshView((SwipeRefreshLayout) view); + } + throw new IllegalArgumentException("RefreshViewFactory does not support create RefreshView . the view :" + view); + } + + public static void registerFactory(Factory factory) { + sFactory = factory; + } + + public interface Factory { + RefreshView createRefreshView(View view); + } +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/StateLayout.java b/lib_base/src/main/java/com/android/base/app/ui/StateLayout.java new file mode 100644 index 0000000..44f7fb3 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/StateLayout.java @@ -0,0 +1,27 @@ +package com.android.base.app.ui; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-07-08 14:52 + */ +public interface StateLayout { + + void showContentLayout(); + + void showLoadingLayout(); + + void showEmptyLayout(); + + void showErrorLayout(); + + void showRequesting(); + + void showBlank(); + + void showNetErrorLayout(); + + void showServerErrorLayout(); + + StateLayoutConfig getStateLayoutConfig(); +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/StateLayoutConfig.java b/lib_base/src/main/java/com/android/base/app/ui/StateLayoutConfig.java new file mode 100644 index 0000000..052f34a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/StateLayoutConfig.java @@ -0,0 +1,63 @@ +package com.android.base.app.ui; + +import android.graphics.drawable.Drawable; +import android.support.annotation.DrawableRes; +import android.support.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-04-20 23:32 + */ +public interface StateLayoutConfig { + + int CONTENT = 0x01; + int LOADING = 0x02; + int ERROR = 0x03; + int EMPTY = 0x04; + int NET_ERROR = 0x05; + int BLANK = 0x06; + int REQUESTING = 0x07; + int SERVER_ERROR = 0x08; + + @IntDef({ + EMPTY, + ERROR, + NET_ERROR, + }) + @Retention(RetentionPolicy.SOURCE) + @interface RetryableState { + + } + + @IntDef({ + EMPTY, + ERROR, + CONTENT, + LOADING, + NET_ERROR, + BLANK, + REQUESTING, + SERVER_ERROR, + }) + @Retention(RetentionPolicy.SOURCE) + @interface ViewState { + + } + + StateLayoutConfig setStateMessage(@RetryableState int state, CharSequence message); + + StateLayoutConfig setStateIcon(@RetryableState int state, Drawable drawable); + + StateLayoutConfig setStateIcon(@RetryableState int state, @DrawableRes int drawableId); + + StateLayoutConfig setStateAction(@RetryableState int state, CharSequence actionText); + + StateLayoutConfig setStateRetryListener(OnRetryActionListener retryActionListener); + + StateLayoutConfig disableOperationWhenRequesting(boolean disable); + +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/SwipeRefreshView.java b/lib_base/src/main/java/com/android/base/app/ui/SwipeRefreshView.java new file mode 100644 index 0000000..5650b6c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/SwipeRefreshView.java @@ -0,0 +1,51 @@ +package com.android.base.app.ui; + +import android.support.v4.widget.SwipeRefreshLayout; + +class SwipeRefreshView implements RefreshView { + + private SwipeRefreshLayout mSwipeRefreshLayout; + private RefreshHandler mRefreshHandler; + + SwipeRefreshView(SwipeRefreshLayout swipeRefreshLayout) { + mSwipeRefreshLayout = swipeRefreshLayout; + } + + /** + * SwipeRefreshLayout + */ + @Override + public void autoRefresh() { + mSwipeRefreshLayout.setRefreshing(true); + doRefresh(); + } + + @Override + public void refreshCompleted() { + mSwipeRefreshLayout.setRefreshing(false); + } + + @Override + public void setRefreshHandler(final RefreshHandler refreshHandler) { + mRefreshHandler = refreshHandler; + mSwipeRefreshLayout.setOnRefreshListener(this::doRefresh); + } + + private void doRefresh() { + if (mRefreshHandler.canRefresh()) { + mRefreshHandler.onRefresh(); + } else { + mSwipeRefreshLayout.post(() -> mSwipeRefreshLayout.setRefreshing(false)); + } + } + + @Override + public boolean isRefreshing() { + return mSwipeRefreshLayout.isRefreshing(); + } + + @Override + public void setRefreshEnable(boolean enable) { + mSwipeRefreshLayout.setEnabled(enable); + } +} diff --git a/lib_base/src/main/java/com/android/base/app/ui/UIEx.kt b/lib_base/src/main/java/com/android/base/app/ui/UIEx.kt new file mode 100644 index 0000000..d13e9e0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/app/ui/UIEx.kt @@ -0,0 +1,153 @@ +@file:JvmName("UIKit") + +package com.android.base.app.ui + +import com.android.base.app.BaseKit +import com.android.base.utils.common.Checker + +fun RefreshListLayout.processListResultWithStatus(list: List?, onEmpty: (() -> Unit)? = null) { + if (isLoadingMore) { + if (!Checker.isEmpty(list)) { + addData(list) + } + } else { + replaceData(list) + refreshCompleted() + } + + if (pager != null) { + loadMoreCompleted(list != null && pager.hasMore(list.size)) + } + + if (isEmpty) { + if (onEmpty == null) { + showEmptyLayout() + } else { + onEmpty() + } + } else { + showContentLayout() + } +} + +fun RefreshListLayout.processListResultWithoutStatus(list: List?, onEmpty: (() -> Unit)? = null) { + if (isLoadingMore) { + if (!Checker.isEmpty(list)) { + addData(list) + } + } else { + replaceData(list) + refreshCompleted() + } + + if (pager != null) { + loadMoreCompleted(list != null && pager.hasMore(list.size)) + } + + if (onEmpty != null && isEmpty) { + onEmpty() + } + +} + +fun RefreshListLayout.submitListResultWithStatus(list: List?, hasMore: Boolean, onEmpty: (() -> Unit)? = null) { + if (isRefreshing) { + refreshCompleted() + } + + replaceData(list) + loadMoreCompleted(hasMore) + + if (isEmpty) { + if (onEmpty == null) { + showEmptyLayout() + } else { + onEmpty() + } + } else { + showContentLayout() + } +} + +fun RefreshListLayout.submitListResultWithoutStatus(list: List?, hasMore: Boolean, onEmpty: (() -> Unit)? = null) { + if (isRefreshing) { + refreshCompleted() + } + + replaceData(list) + loadMoreCompleted(hasMore) + + if (onEmpty != null && isEmpty) { + onEmpty() + } +} + +fun RefreshListLayout<*>.processListErrorWithStatus(throwable: Throwable) { + if (isRefreshing) { + refreshCompleted() + } + if (isLoadingMore) { + loadMoreFailed() + } + if (isEmpty) { + val errorTypeClassifier = BaseKit.get().errorClassifier() + if (errorTypeClassifier != null) { + when { + errorTypeClassifier.isNetworkError(throwable) -> showNetErrorLayout() + errorTypeClassifier.isServerError(throwable) -> showServerErrorLayout() + else -> showErrorLayout() + } + } else { + showErrorLayout() + } + } else { + showContentLayout() + } +} + +fun RefreshListLayout<*>.processListErrorWithoutStatus() { + if (isRefreshing) { + refreshCompleted() + } + if (isLoadingMore) { + loadMoreFailed() + } +} + +fun RefreshListLayout<*>.showLoadingIfEmpty() { + if (isEmpty) { + if (isRefreshing) { + showBlank() + } else { + showLoadingLayout() + } + } +} + +fun RefreshStateLayout.processResultWithStatus(t: T?, onResult: ((T) -> Unit)) { + if (isRefreshing) { + refreshCompleted() + } + if (t == null || (t is Collection<*> && t.isEmpty()) || (t is Map<*, *> && t.isEmpty())) { + showEmptyLayout() + } else { + onResult.invoke(t) + showContentLayout() + } +} + +fun RefreshStateLayout.processErrorWithStatus(throwable: Throwable) { + if (isRefreshing) { + refreshCompleted() + } + val errorTypeClassifier = BaseKit.get().errorClassifier() + if (errorTypeClassifier != null) { + when { + errorTypeClassifier.isNetworkError(throwable) -> showNetErrorLayout() + errorTypeClassifier.isServerError(throwable) -> showServerErrorLayout() + else -> showErrorLayout() + } + } else { + showErrorLayout() + } +} diff --git a/lib_base/src/main/java/com/android/base/concurrent/GoUtil.java b/lib_base/src/main/java/com/android/base/concurrent/GoUtil.java new file mode 100644 index 0000000..1fa539c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/concurrent/GoUtil.java @@ -0,0 +1,106 @@ +package com.android.base.concurrent; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.util.Log; + +import java.io.File; +import java.io.FileFilter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.regex.Pattern; + +/** + * @author MaTianyu + * @see
android-lite-go + */ +class GoUtil { + + private static final String TAG = GoUtil.class.getSimpleName(); + + private static final String PATH_CPU = "/sys/devices/system/cpu/"; + private static final String CPU_FILTER = "cpu[0-9]+"; + private static int CPU_CORES = 0; + + public static String formatDate(long millis) { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return format.format(new Date(millis)); + } + + /** + * Get available processors. + */ + public static int getProcessorsCount() { + return Runtime.getRuntime().availableProcessors(); + } + + /** + * Gets the number of cores available in this device, across all processors. + * Requires: Ability to peruse the filesystem at "/sys/devices/system/cpu" + * + * @return The number of cores, or available processors if failed to get result + */ + public static int getCoresNumbers() { + if (CPU_CORES > 0) { + return CPU_CORES; + } + //Private Class to display only CPU devices in the directory listing + class CpuFilter implements FileFilter { + @Override + public boolean accept(File pathname) { + //Check if filename is "cpu", followed by a single digit number + if (Pattern.matches(CPU_FILTER, pathname.getName())) { + return true; + } + return false; + } + } + try { + //Get directory containing CPU info + File dir = new File(PATH_CPU); + //Filter to only list the devices we care about + File[] files = dir.listFiles(new CpuFilter()); + //Return the number of cores (virtual CPU devices) + CPU_CORES = files.length; + } catch (Exception e) { + e.printStackTrace(); + } + if (CPU_CORES < 1) { + CPU_CORES = Runtime.getRuntime().availableProcessors(); + } + if (CPU_CORES < 1) { + CPU_CORES = 1; + } + Log.i(TAG, "CPU cores: " + CPU_CORES); + return CPU_CORES; + } + + public static AlertDialog.Builder dialogBuilder(Context context, String title, String msg) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + if (msg != null) { + builder.setMessage(msg); + } + if (title != null) { + builder.setTitle(title); + } + return builder; + } + + + public static Dialog showTips(Context context, String title, String des) { + return showTips(context, title, des, null, null); + } + + public static Dialog showTips(Context context, String title, String des, String btn, + DialogInterface.OnDismissListener dismissListener) { + AlertDialog.Builder builder = dialogBuilder(context, title, des); + builder.setCancelable(true); + builder.setPositiveButton(btn, null); + Dialog dialog = builder.show(); + dialog.setCanceledOnTouchOutside(true); + dialog.setOnDismissListener(dismissListener); + return dialog; + } +} diff --git a/lib_base/src/main/java/com/android/base/concurrent/JobExecutor.java b/lib_base/src/main/java/com/android/base/concurrent/JobExecutor.java new file mode 100644 index 0000000..eea4f92 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/concurrent/JobExecutor.java @@ -0,0 +1,37 @@ +package com.android.base.concurrent; + +import java.util.concurrent.Executor; + + +/** + * Smart执行器 + * + * @see android-lite-go + */ +public class JobExecutor { + + private static SmartExecutor smartExecutor; + + public static void setSchedulePolicy(SchedulePolicy policy) { + if (smartExecutor != null) { + smartExecutor.setSchedulePolicy(policy); + } + } + + public static void setOverloadPolicy(OverloadPolicy policy) { + if (smartExecutor != null) { + smartExecutor.setOverloadPolicy(policy); + } + } + + static { + smartExecutor = new SmartExecutor(); + smartExecutor.setSchedulePolicy(SchedulePolicy.LastInFirstRun); + smartExecutor.setOverloadPolicy(OverloadPolicy.DiscardOldTaskInQueue); + } + + public static Executor getJobExecutor() { + return smartExecutor; + } + +} diff --git a/lib_base/src/main/java/com/android/base/concurrent/OverloadPolicy.java b/lib_base/src/main/java/com/android/base/concurrent/OverloadPolicy.java new file mode 100644 index 0000000..39fd1cd --- /dev/null +++ b/lib_base/src/main/java/com/android/base/concurrent/OverloadPolicy.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016 litesuits.com + * + * 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.android.base.concurrent; + + +/** + * Policy of thread-pool-executor overload. + * + * @author MaTianyu + * @date 2015-04-23 + */ +public enum OverloadPolicy { + DiscardNewTaskInQueue, + DiscardOldTaskInQueue, + DiscardCurrentTask, + CallerRuns, + ThrowExecption +} diff --git a/lib_base/src/main/java/com/android/base/concurrent/PriorityRunnable.java b/lib_base/src/main/java/com/android/base/concurrent/PriorityRunnable.java new file mode 100644 index 0000000..bb26008 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/concurrent/PriorityRunnable.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 litesuits.com + * + * 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.android.base.concurrent; + +/** + * @author MaTianyu + * @date 2015-04-23 + */ +public abstract class PriorityRunnable implements Runnable { + + int priority; + + protected PriorityRunnable(int priority) { + this.priority = priority; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } +} diff --git a/lib_base/src/main/java/com/android/base/concurrent/SchedulePolicy.java b/lib_base/src/main/java/com/android/base/concurrent/SchedulePolicy.java new file mode 100644 index 0000000..16db5ef --- /dev/null +++ b/lib_base/src/main/java/com/android/base/concurrent/SchedulePolicy.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016 litesuits.com + * + * 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.android.base.concurrent; + +/** + * @author MaTianyu + * @date 2015-04-23 + */ +public enum SchedulePolicy { + LastInFirstRun, + FirstInFistRun +} diff --git a/lib_base/src/main/java/com/android/base/concurrent/SmartExecutor.java b/lib_base/src/main/java/com/android/base/concurrent/SmartExecutor.java new file mode 100644 index 0000000..d5e54a0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/concurrent/SmartExecutor.java @@ -0,0 +1,416 @@ +/* + * Copyright 2016 litesuits.com + * + * 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.android.base.concurrent; + +import android.util.Log; + +import java.util.LinkedList; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A smart thread pool executor, about {@link SmartExecutor}: + * + *
    + *
  • keep {@link #coreSize} tasks concurrent, and put them in {@link #runningList}, + * maximum number of running-tasks at the same time is {@link #coreSize}.
  • + *
  • when {@link #runningList} is full, put new task in {@link #waitingList} waiting for execution, + * maximum of waiting-tasks number is {@link #queueSize}.
  • + *
  • when {@link #waitingList} is full, new task is performed by {@link OverloadPolicy}.
  • + *
  • when running task is completed, take it out from {@link #runningList}.
  • + *
  • schedule next by {@link SchedulePolicy}, take next task out from {@link #waitingList} to execute, + * and so on until {@link #waitingList} is empty.
  • + * + *
+ * + * @author MaTianyu + * @date 2015-04-23 + */ + class SmartExecutor implements Executor { + private static final String TAG = SmartExecutor.class.getSimpleName(); + /** + * debug mode turn + */ + private boolean debug = false; + + private static final int CPU_CORE = GoUtil.getCoresNumbers(); + private static final int DEFAULT_CACHE_SENCOND = 5; + private static ThreadPoolExecutor threadPool; + private int coreSize = CPU_CORE; + private int queueSize = coreSize * 32; + private final Object lock = new Object(); + private LinkedList runningList = new LinkedList(); + private LinkedList waitingList = new LinkedList(); + private SchedulePolicy schedulePolicy = SchedulePolicy.FirstInFistRun; + private OverloadPolicy overloadPolicy = OverloadPolicy.DiscardOldTaskInQueue; + + + public SmartExecutor() { + initThreadPool(); + } + + public SmartExecutor(int coreSize, int queueSize) { + this.coreSize = coreSize; + this.queueSize = queueSize; + initThreadPool(); + } + + protected synchronized void initThreadPool() { + if (debug) { + Log.v(TAG, "SmartExecutor core-queue size: " + coreSize + " - " + queueSize + + " running-wait task: " + runningList.size() + " - " + waitingList.size()); + } + if (threadPool == null) { + threadPool = createDefaultThreadPool(); + } + } + + public static ThreadPoolExecutor createDefaultThreadPool() { + // 控制最多4个keep在pool中 + int corePoolSize = Math.min(4, CPU_CORE); + return new ThreadPoolExecutor( + corePoolSize, + Integer.MAX_VALUE, + DEFAULT_CACHE_SENCOND, TimeUnit.SECONDS, + new SynchronousQueue(), + new ThreadFactory() { + static final String NAME = "lite-"; + AtomicInteger IDS = new AtomicInteger(1); + + @Override + public Thread newThread(Runnable r) { + return new Thread(r, NAME + IDS.getAndIncrement()); + } + }, + new ThreadPoolExecutor.DiscardPolicy()); + } + + /** + * turn on or turn off debug mode + */ + public void setDebug(boolean debug) { + this.debug = debug; + } + + public boolean isDebug() { + return debug; + } + + public static void setThreadPool(ThreadPoolExecutor threadPool) { + SmartExecutor.threadPool = threadPool; + } + + public static ThreadPoolExecutor getThreadPool() { + return threadPool; + } + + public boolean cancelWaitingTask(Runnable command) { + boolean removed = false; + synchronized (lock) { + int size = waitingList.size(); + if (size > 0) { + for (int i = size - 1; i >= 0; i--) { + if (waitingList.get(i).getRealRunnable() == command) { + waitingList.remove(i); + removed = true; + } + } + } + } + return removed; + } + + interface WrappedRunnable extends Runnable { + Runnable getRealRunnable(); + } + + protected RunnableFuture newTaskFor(Runnable runnable, T value) { + return new FutureTask(runnable, value); + } + + protected RunnableFuture newTaskFor(Callable callable) { + return new FutureTask(callable); + } + + /** + * submit runnable + */ + public Future submit(Runnable task) { + RunnableFuture ftask = newTaskFor(task, null); + execute(ftask); + return ftask; + } + + /** + * Creates a {@code FutureTask} that will, upon running, execute the + * given {@code Runnable}, and arrange that {@code get} will return the + * given result on successful completion. + * + * @param task the runnable task + * @param result the result to return on successful completion. If + * you don't need a particular result, consider using + * constructions of the form: + * {@code Future f = new FutureTask(runnable, null)} + * @throws NullPointerException if the runnable is null + */ + public Future submit(Runnable task, T result) { + RunnableFuture ftask = newTaskFor(task, result); + execute(ftask); + return ftask; + } + + /** + * submit callable + */ + public Future submit(Callable task) { + RunnableFuture ftask = newTaskFor(task); + execute(ftask); + return ftask; + } + + + /** + * submit RunnableFuture task + */ + public void submit(RunnableFuture task) { + execute(task); + } + + + /** + * When {@link #execute(Runnable)} is called, {@link SmartExecutor} perform actions: + *
    + *
  1. if fewer than {@link #coreSize} tasks are running, post new task in {@link #runningList} and execute it immediately.
  2. + *
  3. if more than {@link #coreSize} tasks are running, and fewer than {@link #queueSize} tasks are waiting, put task in {@link #waitingList}.
  4. + *
  5. if more than {@link #queueSize} tasks are waiting ,schedule new task by {@link OverloadPolicy}
  6. + *
  7. if running task is completed, schedule next task by {@link SchedulePolicy} until {@link #waitingList} is empty.
  8. + *
+ */ + @Override + public void execute(final Runnable command) { + if (command == null) { + return; + } + + WrappedRunnable scheduler = new WrappedRunnable() { + @Override + public Runnable getRealRunnable() { + return command; + } + + public Runnable realRunnable; + + @Override + public void run() { + try { + command.run(); + } finally { + scheduleNext(this); + } + } + }; + + boolean callerRun = false; + synchronized (lock) { + //if (debug) { + // Log.v(TAG, "SmartExecutor core-queue size: " + coreSize + " - " + queueSize + // + " running-wait task: " + runningList.size() + " - " + waitingList.size()); + //} + if (runningList.size() < coreSize) { + runningList.add(scheduler); + threadPool.execute(scheduler); + //Log.v(TAG, "SmartExecutor task execute"); + } else if (waitingList.size() < queueSize) { + waitingList.addLast(scheduler); + //Log.v(TAG, "SmartExecutor task waiting"); + } else { + //if (debug) { + // Log.w(TAG, "SmartExecutor overload , policy is: " + overloadPolicy); + //} + switch (overloadPolicy) { + case DiscardNewTaskInQueue: + waitingList.pollLast(); + waitingList.addLast(scheduler); + break; + case DiscardOldTaskInQueue: + waitingList.pollFirst(); + waitingList.addLast(scheduler); + break; + case CallerRuns: + callerRun = true; + break; + case DiscardCurrentTask: + break; + case ThrowExecption: + throw new RuntimeException("Task rejected from lite smart executor. " + command.toString()); + default: + break; + } + } + //printThreadPoolInfo(); + } + if (callerRun) { + if (debug) { + Log.i(TAG, "SmartExecutor task running in caller thread"); + } + command.run(); + } + } + + private void scheduleNext(WrappedRunnable scheduler) { + synchronized (lock) { + boolean suc = runningList.remove(scheduler); + //if (debug) { + // Log.v(TAG, "Thread " + Thread.currentThread().getName() + // + " is completed. remove prior: " + suc + ", try schedule next.."); + //} + if (!suc) { + runningList.clear(); + Log.e(TAG, + "SmartExecutor scheduler remove failed, so clear all(running list) to avoid unpreditable error : " + scheduler); + } + if (waitingList.size() > 0) { + WrappedRunnable waitingRun; + switch (schedulePolicy) { + case LastInFirstRun: + waitingRun = waitingList.pollLast(); + break; + case FirstInFistRun: + waitingRun = waitingList.pollFirst(); + break; + default: + waitingRun = waitingList.pollLast(); + break; + } + if (waitingRun != null) { + runningList.add(waitingRun); + threadPool.execute(waitingRun); + Log.v(TAG, "Thread " + Thread.currentThread().getName() + " execute next task.."); + } else { + Log.e(TAG, + "SmartExecutor get a NULL task from waiting queue: " + Thread.currentThread().getName()); + } + } else { + if (debug) { + Log.v(TAG, "SmartExecutor: all tasks is completed. current thread: " + + Thread.currentThread().getName()); + //printThreadPoolInfo(); + } + } + } + } + + public void printThreadPoolInfo() { + if (debug) { + Log.i(TAG, "___________________________"); + Log.i(TAG, "state (shutdown - terminating - terminated): " + threadPool.isShutdown() + + " - " + threadPool.isTerminating() + " - " + threadPool.isTerminated()); + Log.i(TAG, "pool size (core - max): " + threadPool.getCorePoolSize() + + " - " + threadPool.getMaximumPoolSize()); + Log.i(TAG, "task (active - complete - total): " + threadPool.getActiveCount() + + " - " + threadPool.getCompletedTaskCount() + " - " + threadPool.getTaskCount()); + Log + .i(TAG, "waitingList size : " + threadPool.getQueue().size() + " , " + threadPool.getQueue()); + } + } + + public int getCoreSize() { + return coreSize; + } + + public int getRunningSize() { + return runningList.size(); + } + + public int getWaitingSize() { + return waitingList.size(); + } + + /** + * Set maximum number of concurrent tasks at the same time. + * Recommended core size is CPU count. + * + * @param coreSize number of concurrent tasks at the same time + * @return this + */ + public SmartExecutor setCoreSize(int coreSize) { + if (coreSize <= 0) { + throw new NullPointerException("coreSize can not <= 0 !"); + } + this.coreSize = coreSize; + if (debug) { + Log.v(TAG, "SmartExecutor core-queue size: " + coreSize + " - " + queueSize + + " running-wait task: " + runningList.size() + " - " + waitingList.size()); + } + return this; + } + + public int getQueueSize() { + return queueSize; + } + + /** + * Adjust maximum number of waiting queue size by yourself or based on phone performance. + * For example: CPU count * 32; + * + * @param queueSize waiting queue size + * @return this + */ + public SmartExecutor setQueueSize(int queueSize) { + if (queueSize < 0) { + throw new NullPointerException("queueSize can not < 0 !"); + } + + this.queueSize = queueSize; + if (debug) { + Log.v(TAG, "SmartExecutor core-queue size: " + coreSize + " - " + queueSize + + " running-wait task: " + runningList.size() + " - " + waitingList.size()); + } + return this; + } + + + public OverloadPolicy getOverloadPolicy() { + return overloadPolicy; + } + + public void setOverloadPolicy(OverloadPolicy overloadPolicy) { + if (overloadPolicy == null) { + throw new NullPointerException("OverloadPolicy can not be null !"); + } + this.overloadPolicy = overloadPolicy; + } + + public SchedulePolicy getSchedulePolicy() { + return schedulePolicy; + } + + public void setSchedulePolicy(SchedulePolicy schedulePolicy) { + if (schedulePolicy == null) { + throw new NullPointerException("SchedulePolicy can not be null !"); + } + this.schedulePolicy = schedulePolicy; + } + +} diff --git a/lib_base/src/main/java/com/android/base/data/Resource.java b/lib_base/src/main/java/com/android/base/data/Resource.java new file mode 100644 index 0000000..f0be229 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/data/Resource.java @@ -0,0 +1,119 @@ +package com.android.base.data; + + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-05-15 16:23 + */ +public class Resource { + + private final Throwable mError; + private final Status mStatus; + private final T mData; //data or default data + private final T mDefaultData; //data or default data + + private Resource(Throwable error, T data, T defaultData, Status status) { + mError = error; + mStatus = status; + mData = data; + mDefaultData = defaultData; + } + + public boolean isSuccess() { + return mStatus == Status.SUCCESS; + } + + public boolean isNoChange() { + return mStatus == Status.NOT_CHANGED; + } + + public boolean isLoading() { + return mStatus == Status.LOADING; + } + + public final boolean isError() { + return mStatus == Status.ERROR; + } + + public final boolean hasData() { + return mData != null; + } + + public static Resource success() { + return new Resource<>(null, null, null, Status.SUCCESS); + } + + public static Resource success(@Nullable T t) { + return new Resource<>(null, t, null, Status.SUCCESS); + } + + public static Resource error(@NonNull Throwable error) { + return error(error, null); + } + + public static Resource error(@NonNull Throwable error, T defaultValue) { + return new Resource<>(error, null, defaultValue, Status.ERROR); + } + + public static Resource loading() { + return loading(null); + } + + public static Resource loading(T defaultValue) { + return new Resource<>(null, null, defaultValue, Status.LOADING); + } + + /** + * 如果数据源(比如 Repository)缓存了上一次请求的数据,然后对其当前请求返回的数据,发现数据是一样的,可以使用此状态表示 + * + * @return Resource + */ + public static Resource noChange() { + return new Resource<>(null, null, null, Status.NOT_CHANGED); + } + + @NonNull + public T data() { + if (isError() || isLoading() || isNoChange()) { + throw new UnsupportedOperationException("This method can only be called when the state success"); + } + if (mData == null) { + throw new NullPointerException("Data is null"); + } + return mData; + } + + @Nullable + public T orElse(@Nullable T elseData) { + if (mData == null) { + return elseData; + } + return mData; + } + + @Nullable + public T defaultData() { + return mDefaultData; + } + + public Throwable error() { + return mError; + } + + @NonNull + @Override + public String toString() { + return "Resource{" + + "mError=" + mError + + ", mStatus=" + mStatus + + ", mData=" + mData + + ", mDefaultData=" + mDefaultData + + '}'; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/data/Resource.kt b/lib_base/src/main/java/com/android/base/data/Resource.kt new file mode 100644 index 0000000..2ac50d9 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/data/Resource.kt @@ -0,0 +1,45 @@ +package com.android.base.data + +import android.arch.lifecycle.LiveData + + +inline fun Resource.onLoading(onLoading: () -> Unit): Resource { + if (this.isLoading) { + onLoading() + } + return this +} + +inline fun Resource.onError(onError: (error: Throwable) -> Unit): Resource { + if (this.isError) { + onError(error()) + } + return this +} + +inline fun Resource.onNoChange(onNoChange: () -> Unit): Resource { + if (this.isNoChange) { + onNoChange() + } + return this +} + +/**success*/ +inline fun Resource.onSuccess(onSuccess: (data: T?) -> Unit): Resource { + if (this.isSuccess) { + onSuccess(this.orElse(null)) + } + return this +} + +/**success with data*/ +inline fun Resource.onSuccessWithData(onSuccess: (data: T) -> Unit): Resource { + if (this.isSuccess && this.hasData()) { + onSuccess(this.data()) + } + return this +} + +fun LiveData>.resourceData(): T? { + return if (value?.isSuccess == true) value?.orElse(null) else null +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/data/Status.java b/lib_base/src/main/java/com/android/base/data/Status.java new file mode 100644 index 0000000..9add2c2 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/data/Status.java @@ -0,0 +1,15 @@ +package com.android.base.data; + +/** + * 用于表示各种状态 + * + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-02-18 11:49 + */ +public enum Status { + LOADING, + ERROR, + SUCCESS, + NOT_CHANGED, +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/DisplayConfig.java b/lib_base/src/main/java/com/android/base/imageloader/DisplayConfig.java new file mode 100644 index 0000000..8c8fe3c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/DisplayConfig.java @@ -0,0 +1,137 @@ +package com.android.base.imageloader; + + +import android.graphics.drawable.Drawable; + +public class DisplayConfig { + + private boolean mCacheMemory = true; + private boolean mCacheDisk = true; + private int mTransform; + private int mScaleType; + + /*placeholder*/ + static final int NO_PLACE_HOLDER = -1; + private int mErrorPlaceholder = NO_PLACE_HOLDER; + private int mLoadingPlaceholder = NO_PLACE_HOLDER; + private Drawable mErrorDrawable = null; + private Drawable mLoadingDrawable = null; + + /*scale type*/ + public static final int SCALE_NONE = 0; + public static final int SCALE_CENTER_CROP = 1; + public static final int SCALE_FIT_CENTER = 2; + /*transform*/ + public static final int TRANSFORM_NONE = 1; + public static final int TRANSFORM_CIRCLE = 2; + public static final int TRANSFORM_ROUNDED_CORNERS = 3; + private int mRoundedCornersRadius; + /*animation*/ + public static final int ANIM_NONE = 1; + + private DisplayConfig() { + + } + + public static DisplayConfig create() { + return new DisplayConfig(); + } + + public DisplayConfig setErrorPlaceholder(int errorPlaceholder) { + mErrorPlaceholder = errorPlaceholder; + mErrorDrawable = null; + return this; + } + + public DisplayConfig setLoadingPlaceholder(int loadingPlaceholder) { + mLoadingPlaceholder = loadingPlaceholder; + mErrorDrawable = null; + return this; + } + + public Drawable getErrorDrawable() { + return mErrorDrawable; + } + + public Drawable getLoadingDrawable() { + return mLoadingDrawable; + } + + /** + * @param scaleType {@link #SCALE_CENTER_CROP} or{@link #SCALE_FIT_CENTER} + * @return DisplayConfig + */ + public DisplayConfig scaleType(int scaleType) { + mScaleType = scaleType; + return this; + } + + public DisplayConfig cacheMemory(boolean cacheMemory) { + mCacheMemory = cacheMemory; + return this; + } + + public DisplayConfig setCacheDisk(boolean cacheDisk) { + mCacheDisk = cacheDisk; + return this; + } + + public DisplayConfig setErrorDrawable(Drawable errorDrawable) { + mErrorDrawable = errorDrawable; + mErrorPlaceholder = NO_PLACE_HOLDER; + return this; + } + + public DisplayConfig setLoadingDrawable(Drawable loadingDrawable) { + mLoadingPlaceholder = NO_PLACE_HOLDER; + mLoadingDrawable = loadingDrawable; + return this; + } + + /** + * @param transform {@link #TRANSFORM_ROUNDED_CORNERS} or{@link #TRANSFORM_CIRCLE} + * @return DisplayConfig + */ + public DisplayConfig setTransform(int transform) { + mTransform = transform; + return this; + } + + public DisplayConfig setRoundedCornersRadius(int roundedCornersRadius) { + mRoundedCornersRadius = roundedCornersRadius; + return this; + } + + /////////////////////////////////////////////////////////////////////////// + // Getter + /////////////////////////////////////////////////////////////////////////// + + int getScaleType() { + return mScaleType; + } + + boolean isCacheMemory() { + return mCacheMemory; + } + + boolean isCacheDisk() { + return mCacheDisk; + } + + int getTransform() { + return mTransform; + } + + int getErrorPlaceholder() { + return mErrorPlaceholder; + } + + int getLoadingPlaceholder() { + return mLoadingPlaceholder; + } + + int getRoundedCornersRadius() { + return mRoundedCornersRadius; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/imageloader/GlideImageLoader.java b/lib_base/src/main/java/com/android/base/imageloader/GlideImageLoader.java new file mode 100644 index 0000000..df71392 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/GlideImageLoader.java @@ -0,0 +1,389 @@ +package com.android.base.imageloader; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.View; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.RequestManager; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.ImageViewTarget; +import com.bumptech.glide.request.target.SimpleTarget; +import com.bumptech.glide.request.transition.Transition; + +import java.util.concurrent.ExecutionException; + +class GlideImageLoader implements ImageLoader { + + private boolean canFragmentLoadImage(Fragment fragment) { + return fragment.isResumed() || fragment.isAdded() || fragment.isVisible(); + } + + @Override + public void display(Fragment fragment, ImageView imageView, final String url, final LoadListener loadListener) { + if (canFragmentLoadImage(fragment)) { + Glide.with(fragment).load(url).into(new InnerImageTarget(imageView, url, loadListener)); + } + } + + @Override + public void display(Fragment fragment, ImageView imageView, String url, DisplayConfig displayConfig, LoadListener loadListener) { + if (!canFragmentLoadImage(fragment)) { + return; + } + RequestBuilder requestBuilder = Glide.with(fragment).load(url); + if (displayConfig != null) { + RequestOptions requestOptions = buildRequestOptions(displayConfig); + requestBuilder.apply(requestOptions); + } + requestBuilder.into(new InnerImageTarget(imageView, url, loadListener)); + } + + @Override + public void display(ImageView imageView, String url, LoadListener loadListener) { + Glide.with(imageView.getContext()).load(url).into(new InnerImageTarget(imageView, url, loadListener)); + } + + @Override + public void display(ImageView imageView, String url, DisplayConfig config, LoadListener loadListener) { + RequestBuilder requestBuilder = Glide.with(imageView.getContext()).load(url); + if (config != null) { + RequestOptions requestOptions = buildRequestOptions(config); + requestBuilder.apply(requestOptions); + } + requestBuilder.into(new InnerImageTarget(imageView, url, loadListener)); + } + + + @Override + public void removeAllListener(String url) { + ProgressManager.getInstance().removeListener(url); + } + + @Override + public void addListener(@NonNull String url, @NonNull ProgressListener progressListener) { + ProgressManager.getInstance().addLoadListener(url, progressListener); + } + + @Override + public void setListener(String url, ProgressListener progressListener) { + ProgressManager.getInstance().setListener(url, progressListener); + } + + private class InnerImageTarget extends ImageViewTarget { + + private final LoadListener mLoadListener; + + InnerImageTarget(ImageView view, @SuppressWarnings("unused") String url, LoadListener loadListener) { + super(view); + mLoadListener = loadListener; + } + + @Override + protected void setResource(@Nullable Drawable resource) { + getView().setImageDrawable(resource); + } + + @Override + public void onResourceReady(Drawable resource, @Nullable Transition transition) { + super.onResourceReady(resource, transition); + mLoadListener.onLoadSuccess(resource); + } + + @Override + public void onLoadStarted(@Nullable Drawable placeholder) { + mLoadListener.onLoadStart(); + super.onLoadStarted(placeholder); + } + + @Override + public void onLoadFailed(@Nullable Drawable errorDrawable) { + super.onLoadFailed(errorDrawable); + mLoadListener.onLoadFail(); + } + } + + /////////////////////////////////////////////////////////////////////////// + // Progress end + /////////////////////////////////////////////////////////////////////////// + + @Override + public void display(Fragment fragment, ImageView imageView, String url) { + if (canFragmentLoadImage(fragment)) { + Glide.with(fragment).load(url).into(imageView); + } + } + + @Override + public void display(Fragment fragment, ImageView imageView, String url, DisplayConfig displayConfig) { + display(imageView, Glide.with(fragment).load(url), displayConfig); + } + + @Override + public void display(Fragment fragment, ImageView imageView, Source source) { + display(fragment, imageView, source, null); + } + + @Override + public void display(Fragment fragment, ImageView imageView, Source source, DisplayConfig displayConfig) { + if (!canFragmentLoadImage(fragment)) { + return; + } + RequestManager requestManager = Glide.with(fragment); + RequestBuilder drawableTypeRequest = setToRequest(requestManager, source); + display(imageView, drawableTypeRequest, displayConfig); + } + + @Override + public void display(ImageView imageView, String url) { + display(imageView, Glide.with(imageView.getContext()).load(url), null); + } + + @Override + public void display(ImageView imageView, String url, DisplayConfig config) { + display(imageView, Glide.with(imageView.getContext()).load(url), config); + } + + @Override + public void display(ImageView imageView, Source source) { + RequestBuilder drawableTypeRequest = setToRequest(Glide.with(imageView.getContext()), source); + display(imageView, drawableTypeRequest, null); + } + + @Override + public void display(ImageView imageView, Source source, DisplayConfig config) { + RequestBuilder drawableTypeRequest = setToRequest(Glide.with(imageView.getContext()), source); + display(imageView, drawableTypeRequest, config); + } + + private void display(ImageView imageView, RequestBuilder request, DisplayConfig displayConfig) { + if (displayConfig != null) { + RequestOptions requestOptions = buildRequestOptions(displayConfig); + request.apply(requestOptions); + } + request.into(imageView); + } + + /////////////////////////////////////////////////////////////////////////// + // pause and resume + /////////////////////////////////////////////////////////////////////////// + @Override + public void pause(Fragment fragment) { + Glide.with(fragment).pauseRequests(); + } + + @Override + public void resume(Fragment fragment) { + Glide.with(fragment).resumeRequests(); + } + + @Override + public void pause(Context context) { + Glide.with(context).pauseRequests(); + } + + @Override + public void resume(Context context) { + Glide.with(context).resumeRequests(); + } + + + /////////////////////////////////////////////////////////////////////////// + //Load Bitmap + /////////////////////////////////////////////////////////////////////////// + + @Override + public void preload(Context context, Source source) { + RequestManager requestManager = Glide.with(context); + setToRequest(requestManager, source).preload(); + } + + @Override + public void preload(Context context, Source source, int width, int height) { + RequestManager requestManager = Glide.with(context); + setToRequest(requestManager, source).preload(width, height); + } + + @Override + public void loadBitmap(Context context, Source source, boolean cache, final LoadListener bitmapLoadListener) { + loadBitmap(context, source, cache, 0, 0, bitmapLoadListener); + } + + @Override + public void loadBitmap(Fragment fragment, Source source, boolean cache, LoadListener bitmapLoadListener) { + loadBitmap(fragment, source, cache, 0, 0, bitmapLoadListener); + } + + @Override + public void loadBitmap(Fragment fragment, Source source, boolean cache, int width, int height, LoadListener bitmapLoadListener) { + loadBitmapInternal(Glide.with(fragment), source, cache, width, height, bitmapLoadListener); + } + + @Override + public Bitmap loadBitmap(Context context, Source source, boolean cache, int width, int height) { + RequestBuilder requestBuilder = Glide.with(context).asBitmap(); + requestBuilder = setToRequest(requestBuilder, source); + if (width > 0 && height > 0) { + try { + return requestBuilder.submit(width, height).get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } else { + try { + return requestBuilder.submit().get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + return null; + } + + @Override + public void loadBitmap(Context context, Source source, boolean cache, int width, int height, LoadListener bitmapLoadListener) { + loadBitmapInternal(Glide.with(context), source, cache, width, height, bitmapLoadListener); + } + + private void loadBitmapInternal(RequestManager requestManager, Source source, boolean cache, int width, int height, LoadListener bitmapLoadListener) { + RequestOptions requestOptions = new RequestOptions(); + if (cache) { + requestOptions.skipMemoryCache(false); + requestOptions.diskCacheStrategy(DiskCacheStrategy.ALL); + } else { + requestOptions.skipMemoryCache(true); + requestOptions.diskCacheStrategy(DiskCacheStrategy.NONE); + } + if (width > 0 && height > 0) { + requestOptions.override(width, height); + } + + RequestBuilder requestBuilder = requestManager.asBitmap().apply(requestOptions); + requestBuilder = setToRequest(requestBuilder, source); + requestBuilder.into(new SimpleTarget() { + @Override + public void onResourceReady(Bitmap resource, Transition transition) { + bitmapLoadListener.onLoadSuccess(resource); + } + + @Override + public void onLoadFailed(@Nullable Drawable errorDrawable) { + bitmapLoadListener.onLoadFail(); + } + + @Override + public void onLoadStarted(@Nullable Drawable placeholder) { + super.onLoadStarted(placeholder); + bitmapLoadListener.onLoadStart(); + } + }); + } + + @Override + public void clear(Context context) { + Glide.get(context).clearDiskCache(); + } + + @Override + public void clear(View view) { + Context context = view.getContext(); + if (context != null) { + Glide.with(context).clear(view); + } + } + + @Override + public void clear(Fragment fragment, View view) { + Glide.with(fragment).clear(view); + } + + /////////////////////////////////////////////////////////////////////////// + // Build request + /////////////////////////////////////////////////////////////////////////// + + private RequestOptions buildRequestOptions(DisplayConfig displayConfig) { + RequestOptions requestOptions = new RequestOptions(); + /*DiskCache*/ + if (displayConfig.isCacheDisk()) { + requestOptions.diskCacheStrategy(DiskCacheStrategy.ALL); + } else { + requestOptions.diskCacheStrategy(DiskCacheStrategy.NONE); + } + + /*MemoryCache*/ + if (displayConfig.isCacheMemory()) { + requestOptions.skipMemoryCache(false); + } else { + requestOptions.skipMemoryCache(true); + } + + /*SCALE_TYPE*/ + if (displayConfig.getScaleType() == DisplayConfig.SCALE_CENTER_CROP) { + requestOptions.centerCrop(); + } else if (displayConfig.getScaleType() == DisplayConfig.SCALE_FIT_CENTER) { + requestOptions.fitCenter(); + } + + /*transform*/ + if (displayConfig.getTransform() == DisplayConfig.TRANSFORM_CIRCLE) { + requestOptions.circleCrop(); + } else if (displayConfig.getTransform() == DisplayConfig.TRANSFORM_ROUNDED_CORNERS) { + requestOptions.transform(new RoundedCorners(displayConfig.getRoundedCornersRadius())); + } else if (displayConfig.getTransform() == DisplayConfig.TRANSFORM_NONE) { + requestOptions.dontTransform();//不做渐入渐出的转换 + } + + /*Placeholder*/ + if (displayConfig.getErrorPlaceholder() != DisplayConfig.NO_PLACE_HOLDER) { + requestOptions.error(displayConfig.getErrorPlaceholder()); + } + if (displayConfig.getErrorDrawable() != null) { + requestOptions.error(displayConfig.getErrorDrawable()); + } + + if (displayConfig.getLoadingPlaceholder() != DisplayConfig.NO_PLACE_HOLDER) { + requestOptions.placeholder(displayConfig.getLoadingPlaceholder()); + } + + if (displayConfig.getLoadingDrawable() != null) { + requestOptions.placeholder(displayConfig.getLoadingDrawable()); + } + return requestOptions; + } + + private RequestBuilder setToRequest(RequestManager requestManager, Source source) { + if (source.mFile != null) { + return requestManager.load(source.mFile); + } else if (source.mUrl != null) { + return requestManager.load(source.mUrl); + } else if (source.mResource != 0) { + return requestManager.load(source.mResource); + } else if (source.mUri != null) { + return requestManager.load(source.mUri); + } else { + throw new IllegalArgumentException("UnSupport source"); + } + } + + private RequestBuilder setToRequest(RequestBuilder requestBuilder, Source source) { + if (source.mFile != null) { + return requestBuilder.load(source.mFile); + } else if (source.mUrl != null) { + return requestBuilder.load(source.mUrl); + } else if (source.mResource != 0) { + return requestBuilder.load(source.mResource); + } else if (source.mUri != null) { + return requestBuilder.load(source.mUri); + } else { + throw new IllegalArgumentException("UnSupport source"); + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/ImageLoader.java b/lib_base/src/main/java/com/android/base/imageloader/ImageLoader.java new file mode 100644 index 0000000..ccd0c27 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ImageLoader.java @@ -0,0 +1,113 @@ +package com.android.base.imageloader; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.annotation.WorkerThread; +import android.support.v4.app.Fragment; +import android.view.View; +import android.widget.ImageView; + +// @formatter:off +public interface ImageLoader { + + void display(Fragment fragment, ImageView imageView, String url, LoadListener loadListener); + + void display(Fragment fragment, ImageView imageView, String url, DisplayConfig displayConfig, LoadListener loadListener); + + void display(ImageView imageView, String url, LoadListener loadListener); + + void display(ImageView imageView, String url, DisplayConfig config, LoadListener loadListener); + + void display(Fragment fragment, ImageView imageView, String url); + + void display(Fragment fragment, ImageView imageView, String url, DisplayConfig displayConfig); + + void display(Fragment fragment, ImageView imageView, Source source); + + void display(Fragment fragment, ImageView imageView, Source source, DisplayConfig displayConfig); + + void display(ImageView imageView, String url); + + void display(ImageView imageView, String url, DisplayConfig config); + + void display(ImageView imageView, Source source); + + void display(ImageView imageView, Source source, DisplayConfig config); + + + /////////////////////////////////////////////////////////////////////////// + // pause and resume + /////////////////////////////////////////////////////////////////////////// + void pause(Fragment fragment); + + void resume(Fragment fragment); + + void pause(Context context); + + void resume(Context context); + + /////////////////////////////////////////////////////////////////////////// + // preload + /////////////////////////////////////////////////////////////////////////// + void preload(Context context, Source source); + + void preload(Context context, Source source, int width, int height); + + /////////////////////////////////////////////////////////////////////////// + // LoadBitmap + /////////////////////////////////////////////////////////////////////////// + void loadBitmap(Context context, Source source, boolean cache, LoadListener bitmapLoadListener); + + void loadBitmap(Fragment fragment, Source source, boolean cache, LoadListener bitmapLoadListener); + + void loadBitmap(Context context, Source source, boolean cache, int width, int height, LoadListener bitmapLoadListener); + + void loadBitmap(Fragment fragment, Source source, boolean cache, int width, int height, LoadListener bitmapLoadListener); + + @WorkerThread + Bitmap loadBitmap(Context context, Source source, boolean cache, int width, int height); + + /////////////////////////////////////////////////////////////////////////// + // clear + /////////////////////////////////////////////////////////////////////////// + @WorkerThread + void clear(Context context); + + void clear(View view); + + void clear(Fragment fragment, View view); + + /////////////////////////////////////////////////////////////////////////// + // progress + /////////////////////////////////////////////////////////////////////////// + + /** + * 移除对应 URL 的所有监听器 + * + * @param url URL + */ + @UiThread + void removeAllListener(String url); + + /** + * 添加一个对应 URL 的监听器,针对相同的 URL 可以有多个 ProgressListener,但相同的对象可以不会被重复添加 + * + * @param url URL + * @param progressListener 监听器 + */ + @UiThread + void addListener(@NonNull String url, @NonNull ProgressListener progressListener); + + /** + * 添加一个对应 URL 的监听器,针对相同的 URL 只会有一个 ProgressListener,已经添加的 ProgressListener 会被新的替换,与{@link #addListener(String, ProgressListener)}是独立的 + * + * @param url URL + * @param progressListener 监听器,如果 progressListener 为 null,则表示移除 + */ + void setListener(String url, @Nullable ProgressListener progressListener); + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/imageloader/ImageLoaderFactory.java b/lib_base/src/main/java/com/android/base/imageloader/ImageLoaderFactory.java new file mode 100644 index 0000000..6017437 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ImageLoaderFactory.java @@ -0,0 +1,21 @@ +package com.android.base.imageloader; + +/** + *
+ *
+ * 
+ * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-03-27 18:09 + */ + +public class ImageLoaderFactory { + + private static final GlideImageLoader IMAGE_LOADER = new GlideImageLoader(); + + public static ImageLoader getImageLoader() { + return IMAGE_LOADER; + } + +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/LoadListener.java b/lib_base/src/main/java/com/android/base/imageloader/LoadListener.java new file mode 100644 index 0000000..059c164 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/LoadListener.java @@ -0,0 +1,14 @@ +package com.android.base.imageloader; + +public interface LoadListener { + + default void onLoadStart() { + } + + default void onLoadSuccess(T resource) { + } + + default void onLoadFail() { + } + +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/LoadListenerAdapter.java b/lib_base/src/main/java/com/android/base/imageloader/LoadListenerAdapter.java new file mode 100644 index 0000000..4d9604c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/LoadListenerAdapter.java @@ -0,0 +1,21 @@ +package com.android.base.imageloader; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-09-22 13:14 + */ +public abstract class LoadListenerAdapter implements LoadListener { + + @Override + public void onLoadSuccess(T resource) { + } + + @Override + public void onLoadFail() { + } + + @Override + public void onLoadStart() { + } +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/ProgressGlideModule.java b/lib_base/src/main/java/com/android/base/imageloader/ProgressGlideModule.java new file mode 100644 index 0000000..04e20cc --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ProgressGlideModule.java @@ -0,0 +1,38 @@ +package com.android.base.imageloader; + +import android.content.Context; +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.Registry; +import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.module.AppGlideModule; + +import java.io.InputStream; + +import okhttp3.OkHttpClient; + +public class ProgressGlideModule extends AppGlideModule { + + @Override + @CallSuper + public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { + //配置glide网络加载框架 + ProgressManager.getInstance().setRefreshTime(getRefreshTime()); + OkHttpClient.Builder builder = ProgressManager.getInstance().withProgress(new OkHttpClient.Builder()); + registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(builder.build())); + } + + @Override + public boolean isManifestParsingEnabled() { + //不使用清单配置的方式,减少初始化时间 + return false; + } + + protected int getRefreshTime() { + return 200; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/imageloader/ProgressInfo.java b/lib_base/src/main/java/com/android/base/imageloader/ProgressInfo.java new file mode 100644 index 0000000..a3b8009 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ProgressInfo.java @@ -0,0 +1,102 @@ +package com.android.base.imageloader; + + +import android.support.annotation.NonNull; + +public class ProgressInfo { + + //当前下载的总长度 + private long currentBytes; + //数据总长度 + private long contentLength; + //本次调用距离上一次被调用所间隔的毫秒值 + private long intervalTime; + //本次调用距离上一次被调用的间隔时间内下载的 byte 长度 + private long eachBytes; + //请求的 ID + private long id; + //进度是否完成 + private boolean finish; + + ProgressInfo(long id) { + this.id = id; + } + + void setCurrentBytes(long currentBytes) { + this.currentBytes = currentBytes; + } + + void setContentLength(long contentLength) { + this.contentLength = contentLength; + } + + void setIntervalTime(long intervalTime) { + this.intervalTime = intervalTime; + } + + void setEachBytes(long eachBytes) { + this.eachBytes = eachBytes; + } + + void setFinish(boolean finish) { + this.finish = finish; + } + + public long getCurrentBytes() { + return currentBytes; + } + + public long getContentLength() { + return contentLength; + } + + public long getIntervalTime() { + return intervalTime; + } + + public long getEachBytes() { + return eachBytes; + } + + public long getId() { + return id; + } + + public boolean isFinished() { + return finish; + } + + /** + * 获取下载比例(0 - 1) + */ + public float getProgress() { + if (getCurrentBytes() <= 0 || getContentLength() <= 0) { + return 0; + } + return ((1F * getCurrentBytes()) / getContentLength()); + } + + /** + * 获取上传或下载网络速度,单位为byte/s + */ + public long getSpeed() { + if (getEachBytes() <= 0 || getIntervalTime() <= 0) { + return 0; + } + return getEachBytes() * 1000 / getIntervalTime(); + } + + @NonNull + @Override + public String toString() { + return "ProgressInfo{" + + "id=" + id + + ", currentBytes=" + currentBytes + + ", contentLength=" + contentLength + + ", eachBytes=" + eachBytes + + ", intervalTime=" + intervalTime + + ", finish=" + finish + + '}'; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/imageloader/ProgressListener.java b/lib_base/src/main/java/com/android/base/imageloader/ProgressListener.java new file mode 100644 index 0000000..cb867be --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ProgressListener.java @@ -0,0 +1,13 @@ +package com.android.base.imageloader; + +/** + * @author Ztiany + * Date : 2018-08-20 10:40 + */ +public interface ProgressListener { + + void onProgress(String url, ProgressInfo progressInfo); + + default void onError(long id, String url, Throwable throwable){} + +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/ProgressManager.java b/lib_base/src/main/java/com/android/base/imageloader/ProgressManager.java new file mode 100644 index 0000000..54d30c8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ProgressManager.java @@ -0,0 +1,201 @@ +package com.android.base.imageloader; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.support.annotation.UiThread; + +import com.android.base.utils.common.Checker; +import com.android.base.utils.common.StringChecker; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Response; + +final class ProgressManager { + + private static volatile ProgressManager mProgressManager; + private static final int DEFAULT_REFRESH_TIME = 200; + + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final Map>> mMultiResponseListeners; + private final Map> mExclusiveResponseListeners; + private final ResponseProgressInterceptor mInterceptor; + private int mRefreshTime = DEFAULT_REFRESH_TIME;//进度刷新时间(单位ms),避免高频率调用 + + static ProgressManager getInstance() { + if (mProgressManager == null) { + synchronized (ProgressManager.class) { + if (mProgressManager == null) { + mProgressManager = new ProgressManager(); + } + } + } + return mProgressManager; + } + + private ProgressManager() { + mMultiResponseListeners = new HashMap<>(); + mExclusiveResponseListeners = new HashMap<>(); + this.mInterceptor = new ResponseProgressInterceptor(); + } + + OkHttpClient.Builder withProgress(OkHttpClient.Builder builder) { + return builder.addNetworkInterceptor(mInterceptor); + } + + @UiThread + private void notifyResponseProgress(String url, ProgressInfo progressInfo) { + //multi + List> weakReferences = mMultiResponseListeners.get(url); + if (!Checker.isEmpty(weakReferences)) { + for (WeakReference weakReference : weakReferences) { + ProgressListener progressListener = weakReference.get(); + if (progressListener != null) { + progressListener.onProgress(url, progressInfo); + } + } + } + //Exclusive + WeakReference listenerWeakReference = mExclusiveResponseListeners.get(url); + if (listenerWeakReference != null) { + ProgressListener progressListener = listenerWeakReference.get(); + if (progressListener != null) { + progressListener.onProgress(url, progressInfo); + } + } + } + + @UiThread + private void notifyResponseError(String url, long id, Exception e) { + //multi + List> weakReferences = mMultiResponseListeners.get(url); + if (!Checker.isEmpty(weakReferences)) { + for (WeakReference weakReference : weakReferences) { + ProgressListener progressListener = weakReference.get(); + if (progressListener != null) { + progressListener.onError(id, url, e); + } + } + } + //Exclusive + WeakReference listenerWeakReference = mExclusiveResponseListeners.get(url); + if (listenerWeakReference != null) { + ProgressListener progressListener = listenerWeakReference.get(); + if (progressListener != null) { + progressListener.onError(id, url, e); + } + } + } + + /** + * 设置每次被调用的间隔时间,单位毫秒 + */ + void setRefreshTime(int refreshTime) { + if (refreshTime < 0) { + throw new IllegalArgumentException("the refreshTime must be >= 0"); + } + mRefreshTime = refreshTime; + } + + /////////////////////////////////////////////////////////////////////////// + //listener + /////////////////////////////////////////////////////////////////////////// + @UiThread + void addLoadListener(String url, ProgressListener listener) { + //check + if (StringChecker.isEmpty(url)) { + throw new NullPointerException("url cannot be null"); + } + if (listener == null) { + throw new NullPointerException("ProgressListener cannot be null"); + } + + List> progressListeners = mMultiResponseListeners.get(url); + //make list if need + if (progressListeners == null) { + progressListeners = new ArrayList<>(); + mMultiResponseListeners.put(url, progressListeners); + } + //add direct + if (progressListeners.isEmpty()) { + progressListeners.add(new WeakReference<>(listener)); + } else { + //Prevent duplication + boolean containers = false; + ProgressListener reference; + for (WeakReference progressListener : progressListeners) { + reference = progressListener.get(); + if (reference == listener) { + containers = true; + break; + } + } + if (!containers) { + progressListeners.add(new WeakReference<>(listener)); + } + } + } + + @UiThread + @SuppressWarnings("WeakerAccess") + public void setListener(String url, ProgressListener progressListener) { + if (StringChecker.isEmpty(url)) { + throw new NullPointerException("url cannot be null"); + } + if (progressListener == null) { + mExclusiveResponseListeners.remove(url); + } else { + mExclusiveResponseListeners.put(url, new WeakReference<>(progressListener)); + } + } + + @UiThread + void removeListener(String url) { + mMultiResponseListeners.remove(url); + } + + + /////////////////////////////////////////////////////////////////////////// + //ResponseProgressInterceptor + /////////////////////////////////////////////////////////////////////////// + private class ResponseProgressInterceptor implements Interceptor { + + @NonNull + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + return wrapResponseBody(chain.proceed(chain.request())); + } + + private Response wrapResponseBody(Response response) { + if (response == null || response.body() == null) { + return response; + } + final String key = response.request().url().toString(); + ProgressResponseBody progressResponseBody = new ProgressResponseBody(response.body(), mRefreshTime) { + @Override + void onProgress(ProgressInfo progressInfo) { + runOnUIThread(() -> notifyResponseProgress(key, progressInfo)); + } + + @Override + protected void onError(long id, Exception e) { + runOnUIThread(() -> notifyResponseError(key, id, e)); + } + }; + return response.newBuilder().body(progressResponseBody).build(); + } + } + + private void runOnUIThread(Runnable runnable) { + mHandler.post(runnable); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/imageloader/ProgressResponseBody.java b/lib_base/src/main/java/com/android/base/imageloader/ProgressResponseBody.java new file mode 100644 index 0000000..932de64 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/ProgressResponseBody.java @@ -0,0 +1,97 @@ +package com.android.base.imageloader; + +import android.os.SystemClock; +import android.support.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import okio.ForwardingSource; +import okio.Okio; +import okio.Source; + +abstract class ProgressResponseBody extends ResponseBody { + + private final int mRefreshTime; + private final ResponseBody mDelegate; + private BufferedSource mBufferedSource; + private final ProgressInfo mProgressInfo; + + ProgressResponseBody(ResponseBody responseBody, int refreshTime) { + this.mDelegate = responseBody; + this.mRefreshTime = refreshTime; + this.mProgressInfo = new ProgressInfo(System.currentTimeMillis()); + } + + @Override + public MediaType contentType() { + return mDelegate.contentType(); + } + + @Override + public long contentLength() { + return mDelegate.contentLength(); + } + + @NonNull + @Override + public BufferedSource source() { + if (mBufferedSource == null) { + mBufferedSource = Okio.buffer(source(mDelegate.source())); + } + return mBufferedSource; + } + + private Source source(Source source) { + return new ForwardingSource(source) { + private long mContentLength; + private long totalBytesRead = 0L; + private long lastRefreshTime = 0L; //最后一次刷新的时间 + + @Override + public long read(@NonNull Buffer sink, long byteCount) throws IOException { + long bytesRead; + try { + bytesRead = super.read(sink, byteCount); + } catch (IOException e) { + e.printStackTrace(); + onError(mProgressInfo.getId(), e); + throw e; + } + + if (mContentLength == 0) { + mContentLength = contentLength(); + mProgressInfo.setContentLength(mContentLength); + } + + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += bytesRead != -1 ? bytesRead : 0; + long curTime = SystemClock.elapsedRealtime(); + long intervalTime = curTime - lastRefreshTime; + + if (intervalTime >= mRefreshTime || bytesRead == -1 || totalBytesRead == mContentLength) { + + boolean finish = bytesRead == -1 && totalBytesRead == mContentLength; + + mProgressInfo.setCurrentBytes(totalBytesRead); + mProgressInfo.setIntervalTime(intervalTime); + mProgressInfo.setFinish(finish); + mProgressInfo.setEachBytes(bytesRead); + + onProgress(mProgressInfo); + + lastRefreshTime = curTime; + } + return bytesRead; + } + }; + } + + protected abstract void onError(long id, Exception e); + + abstract void onProgress(ProgressInfo progressInfo); + +} diff --git a/lib_base/src/main/java/com/android/base/imageloader/Source.java b/lib_base/src/main/java/com/android/base/imageloader/Source.java new file mode 100644 index 0000000..02a7c64 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/imageloader/Source.java @@ -0,0 +1,43 @@ +package com.android.base.imageloader; + +import android.net.Uri; + +import java.io.File; + + +public class Source { + + String mUrl; + File mFile; + int mResource; + Uri mUri; + + public static Source create(String url) { + Source source = new Source(); + source.mUrl = url; + return source; + } + + public static Source createWithPath(String path) { + return create(new File(path)); + } + + public static Source createWithUri(Uri uri) { + Source source = new Source(); + source.mUri = uri; + return source; + } + + public static Source create(File file) { + Source source = new Source(); + source.mFile = file; + return source; + } + + public static Source create(int resource) { + Source source = new Source(); + source.mResource = resource; + return source; + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/OnItemClickListener.java b/lib_base/src/main/java/com/android/base/interfaces/OnItemClickListener.java new file mode 100644 index 0000000..1ba71b8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/OnItemClickListener.java @@ -0,0 +1,24 @@ +package com.android.base.interfaces; + +import android.view.View; + +import timber.log.Timber; + + +public abstract class OnItemClickListener implements View.OnClickListener { + + @Override + @SuppressWarnings("unchecked") + public final void onClick(View v) { + Object tag = v.getTag(); + if (tag == null) { + Timber.w("OnItemClickListener tag is null , view = " + v); + return; + } + onClick(v, (T) tag); + } + + public abstract void onClick(View view, T t); + + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/OnItemLongClickListener.java b/lib_base/src/main/java/com/android/base/interfaces/OnItemLongClickListener.java new file mode 100644 index 0000000..d1fb782 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/OnItemLongClickListener.java @@ -0,0 +1,25 @@ +package com.android.base.interfaces; + +import android.view.View; + +import timber.log.Timber; + + + +public abstract class OnItemLongClickListener implements View.OnLongClickListener { + + @Override + @SuppressWarnings("unchecked") + public final boolean onLongClick(View v) { + Object tag = v.getTag(); + if (tag == null) { + Timber.w("OnItemLongClickListener tag is null , view = " + v); + return false; + } + return onClick(v, (T) tag); + } + + public abstract boolean onClick(View view, T t); + + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/ActivityLifecycleCallbacksAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/ActivityLifecycleCallbacksAdapter.java new file mode 100644 index 0000000..436fcd8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/ActivityLifecycleCallbacksAdapter.java @@ -0,0 +1,38 @@ +package com.android.base.interfaces.adapter; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + + +public interface ActivityLifecycleCallbacksAdapter extends Application.ActivityLifecycleCallbacks { + + @Override + default void onActivityCreated(Activity activity, Bundle savedInstanceState) { + } + + @Override + default void onActivityStarted(Activity activity) { + } + + @Override + default void onActivityResumed(Activity activity) { + } + + @Override + default void onActivityPaused(Activity activity) { + } + + @Override + default void onActivityStopped(Activity activity) { + } + + @Override + default void onActivitySaveInstanceState(Activity activity, Bundle outState) { + } + + @Override + default void onActivityDestroyed(Activity activity) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/DrawerListenerAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/DrawerListenerAdapter.java new file mode 100644 index 0000000..f54d556 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/DrawerListenerAdapter.java @@ -0,0 +1,25 @@ +package com.android.base.interfaces.adapter; + +import android.support.v4.widget.DrawerLayout; +import android.view.View; + + +public interface DrawerListenerAdapter extends DrawerLayout.DrawerListener { + + @Override + default void onDrawerSlide(View drawerView, float slideOffset) { + } + + @Override + default void onDrawerOpened(View drawerView) { + } + + @Override + default void onDrawerClosed(View drawerView) { + } + + @Override + default void onDrawerStateChanged(int newState) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/OnPageChangeListenerAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/OnPageChangeListenerAdapter.java new file mode 100644 index 0000000..b7f81af --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/OnPageChangeListenerAdapter.java @@ -0,0 +1,24 @@ +package com.android.base.interfaces.adapter; + +import android.support.v4.view.ViewPager; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2017-12-12 17:35 + */ +public interface OnPageChangeListenerAdapter extends ViewPager.OnPageChangeListener { + + @Override + default void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + @Override + default void onPageSelected(int position) { + } + + @Override + default void onPageScrollStateChanged(int state) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/OnSeekBarChangeListenerAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/OnSeekBarChangeListenerAdapter.java new file mode 100644 index 0000000..8f985df --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/OnSeekBarChangeListenerAdapter.java @@ -0,0 +1,24 @@ +package com.android.base.interfaces.adapter; + +import android.widget.SeekBar; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-04-18 16:29 + */ +public interface OnSeekBarChangeListenerAdapter extends SeekBar.OnSeekBarChangeListener { + + @Override + default void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + } + + @Override + default void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + default void onStopTrackingTouch(SeekBar seekBar) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/OnTabSelectedListenerAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/OnTabSelectedListenerAdapter.java new file mode 100644 index 0000000..c386e7a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/OnTabSelectedListenerAdapter.java @@ -0,0 +1,19 @@ +package com.android.base.interfaces.adapter; + +import android.support.design.widget.TabLayout; + +public interface OnTabSelectedListenerAdapter extends TabLayout.OnTabSelectedListener { + + @Override + default void onTabSelected(TabLayout.Tab tab) { + } + + @Override + default void onTabUnselected(TabLayout.Tab tab) { + } + + @Override + default void onTabReselected(TabLayout.Tab tab) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/TextWatcherAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/TextWatcherAdapter.java new file mode 100644 index 0000000..a27d7c5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/TextWatcherAdapter.java @@ -0,0 +1,25 @@ +package com.android.base.interfaces.adapter; + +import android.text.Editable; +import android.text.TextWatcher; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-07-05 18:01 + */ +public interface TextWatcherAdapter extends TextWatcher { + + @Override + default void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + default void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + default void afterTextChanged(Editable s) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/interfaces/adapter/TransitionListenerAdapter.java b/lib_base/src/main/java/com/android/base/interfaces/adapter/TransitionListenerAdapter.java new file mode 100644 index 0000000..7d4bafb --- /dev/null +++ b/lib_base/src/main/java/com/android/base/interfaces/adapter/TransitionListenerAdapter.java @@ -0,0 +1,31 @@ +package com.android.base.interfaces.adapter; + +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.transition.Transition; + + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public interface TransitionListenerAdapter extends Transition.TransitionListener { + + @Override + default void onTransitionStart(Transition transition) { + } + + @Override + default void onTransitionEnd(Transition transition) { + } + + @Override + default void onTransitionCancel(Transition transition) { + } + + @Override + default void onTransitionPause(Transition transition) { + } + + @Override + default void onTransitionResume(Transition transition) { + } + +} diff --git a/lib_base/src/main/java/com/android/base/kotlin/AttrStyleEx.kt b/lib_base/src/main/java/com/android/base/kotlin/AttrStyleEx.kt new file mode 100644 index 0000000..424905b --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/AttrStyleEx.kt @@ -0,0 +1,38 @@ +package com.android.base.kotlin + +import android.content.Context +import android.content.ContextWrapper +import android.content.res.TypedArray +import android.support.annotation.AttrRes +import android.support.annotation.StyleRes +import android.util.TypedValue +import android.view.ContextThemeWrapper +import android.view.View + +/** 属性相关扩展:https://github.com/Kotlin/anko/issues/16*/ +val View.contextThemeWrapper: ContextThemeWrapper + get() = context.contextThemeWrapper + +val Context.contextThemeWrapper: ContextThemeWrapper + get() = when (this) { + is ContextThemeWrapper -> this + is ContextWrapper -> baseContext.contextThemeWrapper + else -> throw IllegalStateException("Context is not an Activity, can't get theme: $this") + } + +@StyleRes +fun View.attrStyle(@AttrRes attrColor: Int): Int = contextThemeWrapper.attrStyle(attrColor) +@StyleRes +private fun ContextThemeWrapper.attrStyle(@AttrRes attrRes: Int): Int = + attr(attrRes) { + it.getResourceId(0, 0) + } + +private fun ContextThemeWrapper.attr(@AttrRes attrRes: Int, block: (TypedArray)->R): R { + val typedValue = TypedValue() + if (!theme.resolveAttribute(attrRes, typedValue, true)) throw IllegalArgumentException("$attrRes is not resolvable") + val a = obtainStyledAttributes(typedValue.data, intArrayOf(attrRes)) + val result = block(a) + a.recycle() + return result +} diff --git a/lib_base/src/main/java/com/android/base/kotlin/ContextEx.kt b/lib_base/src/main/java/com/android/base/kotlin/ContextEx.kt new file mode 100644 index 0000000..7efe49b --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/ContextEx.kt @@ -0,0 +1,15 @@ +package com.android.base.kotlin + +import android.content.Context +import android.graphics.drawable.Drawable +import android.support.annotation.ColorRes +import android.support.annotation.DrawableRes +import android.support.v4.content.ContextCompat + +fun Context.colorFromId(@ColorRes id: Int): Int { + return ContextCompat.getColor(this, id) +} + +fun Context.drawableFromId(@DrawableRes id: Int): Drawable? { + return ContextCompat.getDrawable(this, id) +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/DialogEx.kt b/lib_base/src/main/java/com/android/base/kotlin/DialogEx.kt new file mode 100644 index 0000000..353d45b --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/DialogEx.kt @@ -0,0 +1,13 @@ +package com.android.base.kotlin + +import android.app.Dialog + +/** + *@author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-03-04 10:46 + */ +fun Dialog.noCancelable(): Dialog { + this.setCancelable(false) + return this +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/FragmentEx.kt b/lib_base/src/main/java/com/android/base/kotlin/FragmentEx.kt new file mode 100644 index 0000000..ae21e7f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/FragmentEx.kt @@ -0,0 +1,18 @@ +package com.android.base.kotlin + +import android.graphics.drawable.Drawable +import android.support.annotation.ColorRes +import android.support.annotation.DrawableRes +import android.support.v4.app.Fragment +import android.support.v4.content.ContextCompat + + +fun Fragment.colorFromId(@ColorRes colorRes: Int): Int { + val safeContext = context ?: return 0 + return ContextCompat.getColor(safeContext, colorRes) +} + +fun Fragment.drawableFromId(@DrawableRes colorRes: Int): Drawable? { + val safeContext = context ?: return null + return ContextCompat.getDrawable(safeContext, colorRes) +} diff --git a/lib_base/src/main/java/com/android/base/kotlin/KtViewHolder.kt b/lib_base/src/main/java/com/android/base/kotlin/KtViewHolder.kt new file mode 100644 index 0000000..2637902 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/KtViewHolder.kt @@ -0,0 +1,15 @@ +package com.android.base.kotlin + +import android.view.View +import com.android.base.adapter.recycler.ViewHolder +import kotlinx.android.extensions.CacheImplementation +import kotlinx.android.extensions.ContainerOptions +import kotlinx.android.extensions.LayoutContainer + +/** + *@author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-12 17:07 + */ +@ContainerOptions(cache = CacheImplementation.SPARSE_ARRAY) +open class KtViewHolder(override val containerView: View) : ViewHolder(containerView), LayoutContainer diff --git a/lib_base/src/main/java/com/android/base/kotlin/LangEx.kt b/lib_base/src/main/java/com/android/base/kotlin/LangEx.kt new file mode 100644 index 0000000..fb97338 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/LangEx.kt @@ -0,0 +1,58 @@ +package com.android.base.kotlin + +/** + * 参考: + * - [Boolean扩展](https://blog.kotliner.cn/2017/06/19/interesting-booleanext/) + */ +sealed class Ext constructor(val boolean: Boolean) + +/** 如果该对象不是null,则执行action */ +fun T?.ifNonNull(action: T.() -> E): Ext { + if (this != null) { + return WithData(action()) + } + return Otherwise +} + +/** 如果该对象是null,则执行action */ +fun T?.ifNull(action: () -> E) { + if (this == null) { + action() + } +} + +inline fun Boolean.yes(block: () -> T): Ext = when { + this -> { + WithData(block()) + } + else -> Otherwise +} + +inline fun Boolean.no(block: () -> T) = when { + this -> Otherwise + else -> { + WithData(block()) + } +} + +object Otherwise : Ext(true) + +class WithData(val data: T) : Ext(false) + +/**除此以外*/ +inline infix fun Ext.otherwise(block: () -> T): T { + return when (this) { + is Otherwise -> block() + is WithData -> this.data + } +} + +inline operator fun Boolean.invoke(block: () -> T) = yes(block) + +fun Any?.javaClassName(): String { + return if (this == null) { + "" + } else { + this::class.java.name + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/LiveDataEx.kt b/lib_base/src/main/java/com/android/base/kotlin/LiveDataEx.kt new file mode 100644 index 0000000..610a2d0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/LiveDataEx.kt @@ -0,0 +1,23 @@ +package com.android.base.kotlin + +import android.arch.lifecycle.MutableLiveData + +fun MutableLiveData.init(t: T): MutableLiveData { + this.postValue(t) + return this +} + +fun MutableLiveData.touchOff() { + this.postValue(this.value) +} + +fun MutableLiveData>.append(list: List) { + val value = this.value + if (value == null) { + this.postValue(list) + } else { + val mutableList = value.toMutableList() + mutableList.addAll(list) + this.postValue(mutableList) + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/MenuEx.kt b/lib_base/src/main/java/com/android/base/kotlin/MenuEx.kt new file mode 100644 index 0000000..022a49a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/MenuEx.kt @@ -0,0 +1,16 @@ +package com.android.base.kotlin + +import android.view.MenuItem + +fun MenuItem.alwaysShow(): MenuItem { + setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + return this +} + +fun MenuItem.setSimpleClickListener(onClick: (MenuItem) -> Unit): MenuItem { + setOnMenuItemClickListener { + onClick(it) + true + } + return this +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/RecyclerViewEx.kt b/lib_base/src/main/java/com/android/base/kotlin/RecyclerViewEx.kt new file mode 100644 index 0000000..fcfcfb1 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/RecyclerViewEx.kt @@ -0,0 +1,29 @@ +package com.android.base.kotlin + +import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView + +fun RecyclerView.verticalLinearLayoutManager(): LinearLayoutManager { + val linearLayoutManager = LinearLayoutManager(context) + layoutManager = linearLayoutManager + return linearLayoutManager +} + +fun RecyclerView.horizontalLinearlLayoutManager(): LinearLayoutManager { + val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + layoutManager = linearLayoutManager + return linearLayoutManager +} + +fun RecyclerView.verticalLinearLayoutManager(span: Int): GridLayoutManager { + val gridLayoutManager = GridLayoutManager(context, span) + layoutManager = gridLayoutManager + return gridLayoutManager +} + +fun RecyclerView.horizontalLinearlLayoutManager(span: Int): GridLayoutManager { + val gridLayoutManager = GridLayoutManager(context, span, GridLayoutManager.HORIZONTAL, false) + layoutManager = gridLayoutManager + return gridLayoutManager +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/ResourceEx.kt b/lib_base/src/main/java/com/android/base/kotlin/ResourceEx.kt new file mode 100644 index 0000000..114457e --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/ResourceEx.kt @@ -0,0 +1,21 @@ +package com.android.base.kotlin + +import android.content.res.TypedArray + +inline fun T.use(block: (T) -> R): R { + var recycled = false + try { + return block(this) + } catch (e: Exception) { + recycled = true + try { + this?.recycle() + } catch (exception: Exception) { + } + throw e + } finally { + if (!recycled) { + this?.recycle() + } + } +} diff --git a/lib_base/src/main/java/com/android/base/kotlin/SizeEx.kt b/lib_base/src/main/java/com/android/base/kotlin/SizeEx.kt new file mode 100644 index 0000000..b99dc04 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/SizeEx.kt @@ -0,0 +1,27 @@ +package com.android.base.kotlin + +import android.content.Context +import android.support.v4.app.Fragment +import android.support.v7.widget.RecyclerView +import android.view.View +import com.android.base.utils.android.UnitConverter + +fun Context.dip(value: Int): Int = UnitConverter.dpToPx(value) +fun Context.dip(value: Float): Float = UnitConverter.dpToPx(value) +fun Context.sp(value: Int): Int = UnitConverter.spToPx(value) +fun Context.sp(value: Float): Float = UnitConverter.spToPx(value) + +fun Fragment.dip(value: Int): Int = UnitConverter.dpToPx(value) +fun Fragment.dip(value: Float): Float = UnitConverter.dpToPx(value) +fun Fragment.sp(value: Int): Int = UnitConverter.spToPx(value) +fun Fragment.sp(value: Float): Float = UnitConverter.spToPx(value) + +fun View.dip(value: Int): Int = context.dip(value) +fun View.dip(value: Float): Float = context.dip(value) +fun View.sp(value: Int): Int = context.sp(value) +fun View.sp(value: Float): Float = context.sp(value) + +fun RecyclerView.ViewHolder.dip(value: Int): Int = itemView.dip(value) +fun RecyclerView.ViewHolder.dip(value: Float): Float = itemView.dip(value) +fun RecyclerView.ViewHolder.sp(value: Int): Int = itemView.dip(value) +fun RecyclerView.ViewHolder.sp(value: Float): Float = itemView.dip(value) \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/TextViewEx.kt b/lib_base/src/main/java/com/android/base/kotlin/TextViewEx.kt new file mode 100644 index 0000000..8587bc5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/TextViewEx.kt @@ -0,0 +1,121 @@ +package com.android.base.kotlin + +import android.graphics.drawable.Drawable +import android.support.annotation.DrawableRes +import android.text.Editable +import android.text.TextWatcher +import android.widget.Button +import android.widget.EditText +import android.widget.TextView +import com.android.base.interfaces.adapter.TextWatcherAdapter + +inline fun TextView.textWatcher(init: KTextWatcher.() -> Unit) = addTextChangedListener(KTextWatcher().apply(init)) + +class KTextWatcher : TextWatcher { + + val TextView.isEmpty + get() = text.isEmpty() + + val TextView.isNotEmpty + get() = text.isNotEmpty() + + val TextView.isBlank + get() = text.isBlank() + + val TextView.isNotBlank + get() = text.isNotBlank() + + private var _beforeTextChanged: ((CharSequence?, Int, Int, Int) -> Unit)? = null + private var _onTextChanged: ((CharSequence?, Int, Int, Int) -> Unit)? = null + private var _afterTextChanged: ((Editable?) -> Unit)? = null + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + _beforeTextChanged?.invoke(s, start, count, after) + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + _onTextChanged?.invoke(s, start, before, count) + } + + override fun afterTextChanged(s: Editable?) { + _afterTextChanged?.invoke(s) + } + + fun beforeTextChanged(listener: (CharSequence?, Int, Int, Int) -> Unit) { + _beforeTextChanged = listener + } + + fun onTextChanged(listener: (CharSequence?, Int, Int, Int) -> Unit) { + _onTextChanged = listener + } + + fun afterTextChanged(listener: (Editable?) -> Unit) { + _afterTextChanged = listener + } + +} + +fun TextView.topDrawable(@DrawableRes id: Int) { + this.setCompoundDrawablesWithIntrinsicBounds(0, id, 0, 0) +} + +fun TextView.leftDrawable(@DrawableRes id: Int) { + this.setCompoundDrawablesWithIntrinsicBounds(id, 0, 0, 0) +} + +fun TextView.rightDrawable(@DrawableRes id: Int) { + this.setCompoundDrawablesWithIntrinsicBounds(0, 0, id, 0) +} + +fun TextView.bottomDrawable(@DrawableRes id: Int) { + this.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, id) +} + +fun TextView.topDrawable(drawable: Drawable, retain: Boolean = false) { + if (retain) { + val compoundDrawables = this.compoundDrawables + this.setCompoundDrawablesWithIntrinsicBounds(compoundDrawables[0], drawable, compoundDrawables[2], compoundDrawables[3]) + } else { + this.setCompoundDrawablesWithIntrinsicBounds(null, drawable, null, null) + } +} + +fun TextView.leftDrawable(drawable: Drawable, retain: Boolean = false) { + if (retain) { + val compoundDrawables = this.compoundDrawables + this.setCompoundDrawablesWithIntrinsicBounds(drawable, compoundDrawables[1], compoundDrawables[2], compoundDrawables[3]) + } else { + this.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) + } +} + +fun TextView.rightDrawable(drawable: Drawable, retain: Boolean = false) { + if (retain) { + val compoundDrawables = this.compoundDrawables + this.setCompoundDrawablesWithIntrinsicBounds(compoundDrawables[0], compoundDrawables[1], drawable, compoundDrawables[3]) + } else { + this.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null) + } +} + +fun TextView.bottomDrawable(drawable: Drawable, retain: Boolean = false) { + if (retain) { + val compoundDrawables = this.compoundDrawables + this.setCompoundDrawablesWithIntrinsicBounds(compoundDrawables[0], compoundDrawables[1], compoundDrawables[2], drawable) + } else { + this.setCompoundDrawablesWithIntrinsicBounds(null, null, null, drawable) + } +} + +fun TextView.clearComponentDrawable() { + this.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null) +} + +fun Button.enable(et: EditText, checker: (s: CharSequence?) -> Boolean) { + val btn = this + et.addTextChangedListener(object : TextWatcherAdapter { + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + btn.isEnabled = checker(s) + } + }) +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/kotlin/UtilsEx.kt b/lib_base/src/main/java/com/android/base/kotlin/UtilsEx.kt new file mode 100644 index 0000000..c086ae5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/UtilsEx.kt @@ -0,0 +1,30 @@ +package com.android.base.kotlin + +import com.android.base.utils.android.compat.AndroidVersion + + +inline fun ifSDKAbove(sdkVersion: Int, block: () -> Unit) { + if (AndroidVersion.above(sdkVersion)) { + block() + } +} + +inline fun ifSDKAt(sdkVersion: Int, block: () -> Unit) { + if (AndroidVersion.at(sdkVersion)) { + block() + } +} + +inline fun ifSDKAtLeast(sdkVersion: Int, block: () -> Unit) { + if (AndroidVersion.atLeast(sdkVersion)) { + block() + } +} + +inline fun ignoreCrash(code: () -> Unit) { + try { + code() + } catch (e: Exception) { + e.printStackTrace() + } +} diff --git a/lib_base/src/main/java/com/android/base/kotlin/ViewEx.kt b/lib_base/src/main/java/com/android/base/kotlin/ViewEx.kt new file mode 100644 index 0000000..9b9ca68 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/kotlin/ViewEx.kt @@ -0,0 +1,148 @@ +package com.android.base.kotlin + +import android.graphics.drawable.Drawable +import android.support.annotation.ColorRes +import android.support.annotation.DrawableRes +import android.support.v4.content.ContextCompat +import android.support.v4.view.ViewCompat +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import com.android.base.rx.subscribeIgnoreError +import com.android.base.utils.android.ViewUtils +import com.android.base.utils.android.compat.AndroidVersion.atLeast +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit + +fun View.visibleOrGone(visible: Boolean) { + if (visible) { + this.visibility = View.VISIBLE + } else { + this.visibility = View.GONE + } +} + +fun View.visibleOrInvisible(visible: Boolean) { + if (visible) { + this.visibility = View.VISIBLE + } else { + this.visibility = View.INVISIBLE + } +} + +fun View.visible() { + this.visibility = View.VISIBLE +} + +fun View.invisible() { + this.visibility = View.INVISIBLE +} + +fun View.gone() { + this.visibility = View.GONE +} + +fun View.realContext() = ViewUtils.getRealContext(this) + +inline fun View.doOnLayoutAvailable(crossinline block: () -> Unit) { + //如果 view 已经通过至少一个布局,则返回true,因为它最后一次附加到窗口或从窗口分离。 + ViewCompat.isLaidOut(this).yes { + block() + }.otherwise { + addOnLayoutChangeListener(object : View.OnLayoutChangeListener { + override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) { + removeOnLayoutChangeListener(this) + block() + } + }) + } +} + +inline fun T.onGlobalLayoutOnce(crossinline action: T.() -> Unit) { + val t: T = this + t.viewTreeObserver + .addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + action.invoke(t) + if (atLeast(16)) { + viewTreeObserver.removeOnGlobalLayoutListener(this) + } else { + @Suppress("DEPRECATION") + viewTreeObserver.removeGlobalOnLayoutListener(this) + } + } + }) +} + +fun View.paddingAll(padding: Int) { + this.setPadding(padding, padding, padding, padding) +} + +fun View.setPaddingLeft(padding: Int) { + this.setPadding(padding, paddingTop, paddingRight, paddingBottom) +} + +fun View.setPaddRight(padding: Int) { + this.setPadding(paddingLeft, paddingTop, padding, paddingBottom) +} + +fun View.setPaddingTop(padding: Int) { + this.setPadding(paddingLeft, padding, paddingRight, paddingBottom) +} + +fun View.setPaddingBottom(padding: Int) { + this.setPadding(paddingLeft, paddingTop, paddingRight, padding) +} + +fun View.colorFromId(@ColorRes colorRes: Int): Int { + val safeContext = context ?: return 0 + return ContextCompat.getColor(safeContext, colorRes) +} + +fun View.drawableFromId(@DrawableRes colorRes: Int): Drawable? { + val safeContext = context ?: return null + return ContextCompat.getDrawable(safeContext, colorRes) +} + +fun View.setWidth(width: Int) { + val params = layoutParams ?: ViewGroup.LayoutParams(0, 0) + params.width = width + layoutParams = params +} + +fun View.setHeight(height: Int) { + val params = layoutParams ?: ViewGroup.LayoutParams(0, 0) + params.height = height + layoutParams = params +} + +fun View.setSize(width: Int, height: Int) { + val params = layoutParams ?: ViewGroup.LayoutParams(0, 0) + params.width = width + params.height = height + layoutParams = params +} + +fun View.onDebouncedClick(onClick: (View) -> Unit) { + onClickObservable(300) + .subscribeIgnoreError { onClick(this) } +} + +fun View.onDebouncedClick(milliseconds: Long, onClick: (View) -> Unit) { + onClickObservable(milliseconds) + .subscribeIgnoreError { onClick(this) } +} + +fun View.onClickObservable(): Observable { + return onClickObservable(300) +} + +fun View.onClickObservable(milliseconds: Long): Observable { + return RxView.clicks(this) + .debounce(milliseconds, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) +} + +inline val ViewGroup.views get() = (0 until childCount).map { getChildAt(it) } \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/permission/AndPermissionFragment.java b/lib_base/src/main/java/com/android/base/permission/AndPermissionFragment.java new file mode 100644 index 0000000..2344e39 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/AndPermissionFragment.java @@ -0,0 +1,78 @@ +package com.android.base.permission; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-04-16 17:48 + */ +public class AndPermissionFragment extends Fragment { + + static AndPermissionFragment newInstance() { + return new AndPermissionFragment(); + } + + private final Handler mHandler = new Handler(); + private final Runnable mRunnable = this::startChecked; + + private AndPermissionRequester mRequester; + private boolean mIsActivityReady = false; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mIsActivityReady = true; + } + + @Override + public void onResume() { + super.onResume(); + mIsActivityReady = true; + } + + @Override + public void onPause() { + super.onPause(); + mHandler.removeCallbacks(mRunnable); + mIsActivityReady = false; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (mRequester != null) { + mRequester.onActivityResult(requestCode, resultCode, data); + } + } + + void setRequester(AndPermissionRequester requester) { + mRequester = requester; + } + + void startAsk() { + startChecked(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mHandler.removeCallbacks(mRunnable); + } + + private void startChecked() { + if (mIsActivityReady) { + if (mRequester != null) { + mRequester.onAutoPermissionFragmentReady(this); + } + mHandler.removeCallbacks(mRunnable); + } else { + mHandler.post(mRunnable); + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/permission/AndPermissionRequester.java b/lib_base/src/main/java/com/android/base/permission/AndPermissionRequester.java new file mode 100644 index 0000000..c34b529 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/AndPermissionRequester.java @@ -0,0 +1,205 @@ +package com.android.base.permission; + +import android.content.Intent; +import android.net.Uri; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; + +import com.github.dmstocking.optional.java.util.function.Consumer; +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.PermissionRequest; + +import java.util.List; + + +/** + *
+ *      1: 使用该类申请权限,当所有的权限都通过时回调权限获取成功,否则回调权限获取失败。
+ *      2:不要同时调用requestPermission方法多次!!!以保证一个完整的流程。
+ * 获取权限流程,以申请相机权限为例:
+ *          1先检查自身是否有相机权限
+ *          2如果有我们的app已经有了相机权限,则可以直接使用相机相关功能了
+ *          3如果没有权限我们就需要请求权限了,但是还需要处理不再询问的设置
+ *              3.1如果shouldShowRequestPermissionRationale返回false,则说明接下来的对话框不包含”不再询问“选择框,我们可以直接申请权限
+ *              3.2如果shouldShowRequestPermissionRationale返回true,我们最好先弹出一个对话框来说明我们需要权限来做什么,让用户来选择是否继续授予权限,如果用户允许继续授予权限则继续申请权限
+ *          4不管权限是否授予给我们的App,都可以在onRequestPermissionsResult的回调中获取结果,我们再问一次
+ * 
+ * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-01-11 15:09 + */ +public class AndPermissionRequester { + + private final FragmentActivity mActivity; + private boolean mAskAgain = true; + private boolean mShowReason = true; + private boolean mShowTips = false; + private IPermissionUIProvider mPermissionUIProvider; + private Consumer> mOnGranted; + private Consumer> mOnDenied; + private static final int REQUEST_PERMISSION_FOR_SETTING = 999; + private List mDeniedPermission; + private String[] mPerms; + + private AndPermissionRequester(FragmentActivity activity) { + if (activity == null) { + throw new NullPointerException("activity is null"); + } + mActivity = activity; + } + + public static AndPermissionRequester with(Fragment fragment) { + return new AndPermissionRequester(fragment.getActivity()); + } + + public static AndPermissionRequester with(FragmentActivity activity) { + return new AndPermissionRequester(activity); + } + + public AndPermissionRequester permission(String... permissions) { + if (permissions == null || permissions.length == 0) { + throw new IllegalArgumentException(); + } + mPerms = permissions; + return this; + } + + public AndPermissionRequester askAgain(boolean askAgain) { + mAskAgain = askAgain; + return this; + } + + public AndPermissionRequester showReason(boolean showReason) { + mShowReason = showReason; + return this; + } + + public AndPermissionRequester showDeniedTips(boolean showTips) { + mShowTips = showTips; + return this; + } + + public AndPermissionRequester customUI(@NonNull IPermissionUIProvider uiProvider) { + mPermissionUIProvider = uiProvider; + return this; + } + + public AndPermissionRequester onGranted(@NonNull Consumer> onGranted) { + this.mOnGranted = onGranted; + return this; + } + + public AndPermissionRequester onDenied(@NonNull Consumer> onDenied) { + this.mOnDenied = onDenied; + return this; + } + + public void request() { + if (mPerms != null) { + doPermissionRequest(); + } else { + throw new IllegalStateException("no permission set"); + } + } + + private void doPermissionRequest() { + PermissionRequest permissionRequest = AndPermission.with(mActivity).runtime().permission(mPerms); + + if (mShowReason) { + permissionRequest = permissionRequest.rationale((context, data, executor) -> + getPermissionUIProvider().showPermissionRationaleDialog( + mActivity, + data.toArray(new String[0]), + (dialog, which) -> executor.execute(), + (dialog, which) -> executor.cancel())); + } + + permissionRequest + .onGranted(data -> { + if (mOnGranted != null) { + mOnGranted.accept(data); + } + }) + .onDenied(permissions -> { + if (mAskAgain) { + doAskAgain(permissions); + } else { + if (mOnDenied != null) { + mOnDenied.accept(permissions); + } + } + }) + .start(); + } + + /** + * 询问是否去设置界面 + */ + private void doAskAgain(List permissions) { + getPermissionUIProvider().showAskAgainDialog(mActivity, permissions.toArray(new String[0]), + (dialog, which) -> openSettings(permissions),/*去设置界面*/ + (dialog, which) -> { + if (mOnDenied != null) { + mOnDenied.accept(permissions);/*通知权限被拒绝*/ + } + }); + } + + private void openSettings(List permissions) { + FragmentManager supportFragmentManager = mActivity.getSupportFragmentManager(); + AndPermissionFragment fragment = (AndPermissionFragment) supportFragmentManager.findFragmentByTag(AndPermissionFragment.class.getName()); + if (fragment == null) { + fragment = AndPermissionFragment.newInstance(); + fragment.setRequester(this); + supportFragmentManager + .beginTransaction() + .add(fragment, AndPermissionFragment.class.getName()) + .commitNowAllowingStateLoss(); + } else { + fragment.setRequester(this); + } + + mDeniedPermission = permissions; + fragment.startAsk(); + } + + void onAutoPermissionFragmentReady(AndPermissionFragment autoPermissionFragment) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", mActivity.getPackageName(), null); + intent.setData(uri); + try { + autoPermissionFragment.startActivityForResult(intent, REQUEST_PERMISSION_FOR_SETTING, null); + } catch (Exception ignore) { + } + } + + @SuppressWarnings("unused") + void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_PERMISSION_FOR_SETTING) {//申请权限 + if (!AndUtils.hasPermission(mActivity, mPerms)) {//Setting界面回来之后,没有授予权限 + if (mOnDenied != null) { + mOnDenied.accept(mDeniedPermission); + } + if (mShowTips) { + getPermissionUIProvider().showPermissionDeniedTip(mActivity, mDeniedPermission.toArray(new String[0])); + } + } else { + if (mOnGranted != null) { + mOnGranted.accept(mDeniedPermission);//所有权限被获取 + } + } + } + } + + private IPermissionUIProvider getPermissionUIProvider() { + if (mPermissionUIProvider == null) { + mPermissionUIProvider = PermissionUIProviderFactory.getPermissionUIProvider(); + } + return mPermissionUIProvider; + } + +} diff --git a/lib_base/src/main/java/com/android/base/permission/AndUtils.java b/lib_base/src/main/java/com/android/base/permission/AndUtils.java new file mode 100644 index 0000000..f69666a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/AndUtils.java @@ -0,0 +1,21 @@ +package com.android.base.permission; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.yanzhenjie.permission.checker.DoubleChecker; + +/** + * @author Ztiany + * Date : 2018-09-07 11:38 + */ +class AndUtils { + + /** + * direct check permissions + */ + static boolean hasPermission(@NonNull Context context, @NonNull String... permissions) { + return new DoubleChecker().hasPermission(context, permissions); + } + +} diff --git a/lib_base/src/main/java/com/android/base/permission/AutoPermissionFragment.java b/lib_base/src/main/java/com/android/base/permission/AutoPermissionFragment.java new file mode 100644 index 0000000..53a662c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/AutoPermissionFragment.java @@ -0,0 +1,95 @@ +package com.android.base.permission; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-04-16 17:48 + */ +public class AutoPermissionFragment extends Fragment { + + static AutoPermissionFragment newInstance() { + return new AutoPermissionFragment(); + } + + private final Handler mHandler = new Handler(); + private final Runnable mRunnable = this::startChecked; + + private AutoPermissionFragmentCallback mRequester; + private boolean mIsActivityReady = false; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mIsActivityReady = true; + } + + @Override + public void onResume() { + super.onResume(); + mIsActivityReady = true; + } + + @Override + public void onPause() { + super.onPause(); + mHandler.removeCallbacks(mRunnable); + mIsActivityReady = false; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (mRequester != null) { + mRequester.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + mRequester = null; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (mRequester != null) { + mRequester.onActivityResult(requestCode, resultCode, data); + } + } + + void startRequest() { + startChecked(); + } + + private void startChecked() { + if (mIsActivityReady) { + if (mRequester != null) { + mRequester.onReady(); + } + mHandler.removeCallbacks(mRunnable); + } else { + mHandler.post(mRunnable); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + mHandler.removeCallbacks(mRunnable); + } + + void setRequester(AutoPermissionFragmentCallback requester) { + mRequester = requester; + } + + interface AutoPermissionFragmentCallback{ + void onReady(); + + void onActivityResult(int requestCode, int resultCode, Intent data); + + void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults); + } + +} diff --git a/lib_base/src/main/java/com/android/base/permission/AutoPermissionRequester.java b/lib_base/src/main/java/com/android/base/permission/AutoPermissionRequester.java new file mode 100644 index 0000000..bb5548a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/AutoPermissionRequester.java @@ -0,0 +1,145 @@ +package com.android.base.permission; + +import android.content.Intent; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; + +import com.android.base.utils.android.ActFragWrapper; + +import static com.android.base.permission.PermissionCode.PERMISSION_REQUESTER_CODE; + +/** + *
+ *      1: 使用该类申请权限,当所有的权限都通过时回调权限获取成功,否则回调权限获取失败。
+ *      2:不要同时调用requestPermission方法多次!!!以保证一个完整的流程。
+ * 获取权限流程,以申请相机权限为例:
+ *          1先检查自身是否有相机权限
+ *          2如果有我们的app已经有了相机权限,则可以直接使用相机相关功能了
+ *          3如果没有权限我们就需要请求权限了,但是还需要处理不再询问的设置
+ *              3.1如果shouldShowRequestPermissionRationale返回false,则说明接下来的对话框不包含”不再询问“选择框,我们可以直接申请权限
+ *              3.2如果shouldShowRequestPermissionRationale返回true,我们最好先弹出一个对话框来说明我们需要权限来做什么,让用户来选择是否继续授予权限,如果用户允许继续授予权限则继续申请权限
+ *          4不管权限是否授予给我们的App,都可以在onRequestPermissionsResult的回调中获取结果,我们再问一次
+ * 
+ * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-01-11 15:09 + */ +public class AutoPermissionRequester { + + private final FragmentActivity mActivity; + private String[] mPerms; + private boolean mAskAgain = true; + + private IPermissionUIProvider mPermissionUIProvider; + private PermissionCallback mPermissionCallback; + private PermissionRequester mPermissionRequester; + + private OnAllPermissionGrantedListener mOnAllPermissionGrantedListener; + private OnPermissionDeniedListener mOnPermissionDeniedListener; + + private AutoPermissionFragment.AutoPermissionFragmentCallback mAutoPermissionFragmentCallback; + + private AutoPermissionRequester(FragmentActivity activity) { + mActivity = activity; + if (mActivity == null) { + throw new NullPointerException(); + } + } + + public static AutoPermissionRequester with(Fragment fragment) { + return new AutoPermissionRequester(fragment.getActivity()); + } + + public static AutoPermissionRequester with(FragmentActivity activity) { + return new AutoPermissionRequester(activity); + } + + public AutoPermissionRequester permission(String... permissions) { + if (permissions == null || permissions.length == 0) { + throw new IllegalArgumentException(); + } + mPerms = permissions; + return this; + } + + public AutoPermissionRequester askAgain(boolean askAgain) { + mAskAgain = askAgain; + return this; + } + + public AutoPermissionRequester customUI(IPermissionUIProvider uiProvider) { + mPermissionUIProvider = uiProvider; + return this; + } + + public AutoPermissionRequester onGranted(OnAllPermissionGrantedListener listener) { + mOnAllPermissionGrantedListener = listener; + return this; + } + + public AutoPermissionRequester onDenied(OnPermissionDeniedListener listener) { + mOnPermissionDeniedListener = listener; + return this; + } + + public void request() { + mPermissionCallback = new PermissionCallback(mOnPermissionDeniedListener, mOnAllPermissionGrantedListener); + startRequest(); + } + + private void startRequest() { + FragmentManager supportFragmentManager = mActivity.getSupportFragmentManager(); + AutoPermissionFragment fragment = (AutoPermissionFragment) supportFragmentManager.findFragmentByTag(AutoPermissionFragment.class.getName()); + + if (fragment == null) { + fragment = AutoPermissionFragment.newInstance(); + + mPermissionRequester = new PermissionRequester(ActFragWrapper.create(fragment), mPermissionCallback, mAskAgain, mPermissionUIProvider); + fragment.setRequester(getCallback()); + + supportFragmentManager.beginTransaction() + .add(fragment, AutoPermissionFragment.class.getName()) + .commitNowAllowingStateLoss(); + + } else { + fragment.setRequester(getCallback()); + mPermissionRequester = new PermissionRequester(ActFragWrapper.create(fragment), mPermissionCallback, mAskAgain, mPermissionUIProvider); + } + + fragment.startRequest(); + } + + private AutoPermissionFragment.AutoPermissionFragmentCallback getCallback() { + if (mAutoPermissionFragmentCallback == null) { + return mAutoPermissionFragmentCallback = new AutoPermissionFragment.AutoPermissionFragmentCallback() { + @Override + public void onReady() { + if (mPermissionRequester != null) { + mPermissionRequester.requestPermission(PERMISSION_REQUESTER_CODE, mPerms); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (PERMISSION_REQUESTER_CODE != requestCode) { + if (mPermissionRequester != null) { + mPermissionRequester.onActivityResult(requestCode, resultCode, data); + } + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (mPermissionRequester != null) { + mPermissionRequester.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + }; + } + return mAutoPermissionFragmentCallback; + } + +} diff --git a/lib_base/src/main/java/com/android/base/permission/DefaultPermissionUIProvider.java b/lib_base/src/main/java/com/android/base/permission/DefaultPermissionUIProvider.java new file mode 100644 index 0000000..6ddd97f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/DefaultPermissionUIProvider.java @@ -0,0 +1,99 @@ +package com.android.base.permission; + +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Color; +import android.support.annotation.NonNull; +import android.support.v7.app.AlertDialog; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; + +import com.android.base.R; +import com.blankj.utilcode.util.AppUtils; +import com.blankj.utilcode.util.ToastUtils; +import com.yanzhenjie.permission.Permission; + +import java.util.Arrays; +import java.util.List; + +class DefaultPermissionUIProvider implements IPermissionUIProvider { + + private static final String COLOR_STRING = "#FF4081"; + + @Override + public void showPermissionRationaleDialog(Context context, final String[] permission, + final DialogInterface.OnClickListener onContinueListener, DialogInterface.OnClickListener onCancelListener) { + + AlertDialog dialog = new AlertDialog.Builder(context) + .setMessage(DefaultPermissionResourceProvider.createPermissionRationaleText(context, permission)) + .setCancelable(false) + .setPositiveButton(R.string.Base_Confirm, onContinueListener) + .setNegativeButton(R.string.Base_Cancel, onCancelListener) + .create(); + + dialog.show(); + } + + @Override + public void showAskAgainDialog(Context context, final String[] permission, + DialogInterface.OnClickListener onToSetPermissionListener, + DialogInterface.OnClickListener onCancelListener) { + + AlertDialog dialog = new AlertDialog.Builder(context) + .setMessage(DefaultPermissionResourceProvider.createPermissionAskAgainText(context, permission)) + .setCancelable(false) + .setPositiveButton(R.string.Base_to_set_permission, onToSetPermissionListener) + .setNegativeButton(R.string.Base_Cancel, onCancelListener) + .create(); + dialog.show(); + } + + @Override + public void showPermissionDeniedTip(Context contexts, String[] permission) { + ToastUtils.showShort(DefaultPermissionResourceProvider.createPermissionDeniedTip(contexts, permission)); + } + + private static final class DefaultPermissionResourceProvider { + + static CharSequence createPermissionRationaleText(Context context, @NonNull String[] perms) { + String permissionText = createPermissionText(context, Arrays.asList(perms)); + return tintText(context.getString(R.string.Base_request_permission_rationale, permissionText), permissionText); + } + + static CharSequence createPermissionAskAgainText(Context context, @NonNull String[] permission) { + String permissionText = createPermissionText(context, Arrays.asList(permission)); + String appName = AppUtils.getAppName(); + String content = context.getString(R.string.Base_permission_denied_ask_again_rationale, appName, permissionText); + return tintText(content, permissionText); + } + + static CharSequence createPermissionDeniedTip(Context context, String[] permission) { + String permissionText = createPermissionText(context, Arrays.asList(permission)); + return tintText(context.getString( + R.string.Base_permission_denied, permissionText), permissionText); + } + + private static CharSequence tintText(String content, String perms) { + SpannableStringBuilder ssb = new SpannableStringBuilder(content); + int indexPerm = content.indexOf(perms); + ssb.setSpan(new ForegroundColorSpan(Color.parseColor(COLOR_STRING)), indexPerm, indexPerm + perms.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + return ssb; + } + + private static String createPermissionText(Context context, @NonNull List perms) { + List permList = Permission.transformText(context, perms); + String characterSeg = context.getString(R.string.Base_character_seg); + StringBuilder sb = new StringBuilder(); + int size = permList.size(); + for (int i = 0; i < size; i++) { + sb.append(permList.get(i)); + if (i < size - 1) { + sb.append(characterSeg); + } + } + return sb.toString(); + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/permission/EasyPermissions.java b/lib_base/src/main/java/com/android/base/permission/EasyPermissions.java new file mode 100644 index 0000000..7455382 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/EasyPermissions.java @@ -0,0 +1,219 @@ +/* + * Copyright Google Inc. All Rights Reserved. + * + * 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.android.base.permission; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.provider.Settings; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import timber.log.Timber; + +/** + *
+ *   See:
+ *          https://github.com/googlesamples/easypermissions
+ *          http://droidyue.com/blog/2016/01/17/understanding-marshmallow-runtime-permission/index.html
+ *          http://jijiaxin89.com/2015/08/30/Android-s-Runtime-Permission/
+ *
+ * Utility to request and check System permissions for apps targeting Android M (API >= 23).
+ *
+ * 
+ */ +final class EasyPermissions { + + interface PermissionCaller { + + void onPortionPermissionsGranted(boolean allGranted, int requestCode, List perms); + + void onPermissionsDenied(int requestCode, List perms); + + /** + * @return PermissionCaller must is Fragment(app and support) or Activity + */ + Object getRequester(); + + IPermissionUIProvider getPermissionUIProvider(); + } + + static private Context getContext(PermissionCaller permissionCaller) { + Object object = permissionCaller.getRequester(); + if (object instanceof Activity) { + return (Activity) object; + } else if (object instanceof Fragment) { + return ((Fragment) object).getActivity(); + } else if (object instanceof android.app.Fragment) { + return ((android.app.Fragment) object).getActivity(); + } else { + throw new RuntimeException("PermissionCaller getRequester mu"); + } + } + + /** + * 是否有权限 + */ + static boolean hasPermissions(Context context, String... perms) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + Timber.w("hasPermissions: API version < M, returning true by default"); + return true; + } + for (String perm : perms) { + boolean hasPerm = (ContextCompat.checkSelfPermission(context, perm) == + PackageManager.PERMISSION_GRANTED); + if (!hasPerm) { + return false; + } + } + return true; + } + + + /** + * 请求权限 + */ + static void requestPermissions(final PermissionCaller permissionCaller, final int requestCode, final String... perms) { + + if (hasPermissions(getContext(permissionCaller), perms)) { + permissionCaller.onPortionPermissionsGranted(true, requestCode, Arrays.asList(perms)); + return; + } + + final String[] filterPerms = filter(getContext(permissionCaller), perms);//过滤已有权限 + + //判断是否有不再询问的复选框 + boolean shouldShowRationale = false; + for (String filterPerm : filterPerms) { + shouldShowRationale = shouldShowRationale || shouldShowRequestPermissionRationale(permissionCaller, filterPerm); + } + + if (shouldShowRationale) { + permissionCaller.getPermissionUIProvider().showPermissionRationaleDialog( + getContext(permissionCaller), filterPerms, + (dialog, which) -> executePermissionsRequest(permissionCaller, filterPerms, requestCode), + (dialog, which) -> permissionCaller.onPermissionsDenied(requestCode, Arrays.asList(filterPerms))); + return; + } + //没有不再询问的复选框,直接申请 + executePermissionsRequest(permissionCaller, filterPerms, requestCode); + } + + /** + * 过滤掉已有的权限 + */ + @TargetApi(23) + static String[] filter(Context context, String[] perms) { + List permList = new ArrayList<>(); + for (String perm : perms) { + boolean hasPerm = (ContextCompat.checkSelfPermission(context, perm) == + PackageManager.PERMISSION_GRANTED); + if (!hasPerm) { + permList.add(perm); + } + } + return permList.toArray(new String[0]); + } + + /** + * 处理申请权限后的结果 + */ + static void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults, PermissionCaller permissionCaller) { + // Make a collection of granted and denied permissions from the request. + ArrayList granted = new ArrayList<>(); + ArrayList denied = new ArrayList<>(); + for (int i = 0; i < permissions.length; i++) { + String perm = permissions[i]; + if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + granted.add(perm); + } else { + denied.add(perm); + } + } + // Report denied permissions, if any. + if (!denied.isEmpty()) { + permissionCaller.onPermissionsDenied(requestCode, denied); + } + // Report granted permissions, if any. + if (!granted.isEmpty()) { + boolean allGranted = denied.isEmpty(); + permissionCaller.onPortionPermissionsGranted(allGranted, requestCode, granted); + } + } + + /** + * 这个方法需要在onPermissionDenial后调用,如果此时shouldShowRationale为false,表示用户已经选择了不再询问切拒绝了授予权限,需要到应用详情界面设置权限 + */ + static boolean checkDeniedPermissionsNeverAskAgain(final PermissionCaller permissionCaller, List deniedPerms) { + boolean shouldShowRationale = false; + for (String perm : deniedPerms) { + shouldShowRationale = shouldShowRequestPermissionRationale(permissionCaller, perm); + } + return !shouldShowRationale; + } + + /** + * 获取一个到详情界面的intent,去设置权限 + * + * @param context 上下文 + * @return Intent + */ + static Intent getIntentForPermission(Context context) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", context.getPackageName(), null); + intent.setData(uri); + return intent; + } + + /** + * 帮我们判断接下来的对话框是否包含”不再询问“选择框。 + * + * @return true表示有 + */ + @TargetApi(23) + private static boolean shouldShowRequestPermissionRationale(PermissionCaller permissionCaller, String perm) { + if (permissionCaller.getRequester() instanceof Activity) { + return ActivityCompat.shouldShowRequestPermissionRationale((Activity) permissionCaller.getRequester(), perm); + } else if (permissionCaller.getRequester() instanceof Fragment) { + return ((Fragment) permissionCaller.getRequester()).shouldShowRequestPermissionRationale(perm); + } else + return permissionCaller.getRequester() instanceof android.app.Fragment + && ((android.app.Fragment) permissionCaller.getRequester()).shouldShowRequestPermissionRationale(perm); + } + + /** + * 执行申请权限 + */ + @TargetApi(23) + private static void executePermissionsRequest(PermissionCaller permissionCaller, String[] perms, int requestCode) { + if (permissionCaller.getRequester() instanceof Activity) { + ActivityCompat.requestPermissions((Activity) permissionCaller.getRequester(), perms, requestCode); + } else if (permissionCaller.getRequester() instanceof Fragment) { + ((Fragment) permissionCaller.getRequester()).requestPermissions(perms, requestCode); + } else if (permissionCaller.getRequester() instanceof android.app.Fragment) { + ((android.app.Fragment) permissionCaller.getRequester()).requestPermissions(perms, requestCode); + } + } +} diff --git a/lib_base/src/main/java/com/android/base/permission/IPermissionUIProvider.java b/lib_base/src/main/java/com/android/base/permission/IPermissionUIProvider.java new file mode 100644 index 0000000..a92ff3d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/IPermissionUIProvider.java @@ -0,0 +1,37 @@ +package com.android.base.permission; + +import android.content.Context; +import android.content.DialogInterface; + + +public interface IPermissionUIProvider { + + /** + * 显示需要获取权限的原因,询问是否继续 + * + * @param context 上下文 + * @param permission 需要的权限 + * @param onContinueListener 继续 + * @param onCancelListener 取消 + */ + void showPermissionRationaleDialog(Context context, final String[] permission, final DialogInterface.OnClickListener onContinueListener, DialogInterface.OnClickListener onCancelListener); + + /** + * 拒绝权限后,询问是否去设置界面授予应用权限 + * + * @param context 上下文 + * @param permission 需要的权限 + * @param onContinueListener 继续 + * @param onCancelListener 取消 + */ + void showAskAgainDialog(Context context, final String[] permission, DialogInterface.OnClickListener onContinueListener, DialogInterface.OnClickListener onCancelListener); + + /** + * 权限被拒绝后,展示一个提示消息,比如 toast + * + * @param contexts 上下文 + * @param permission 被拒绝的权限 + */ + void showPermissionDeniedTip(Context contexts, String[] permission); + +} diff --git a/lib_base/src/main/java/com/android/base/permission/OnAllPermissionGrantedListener.java b/lib_base/src/main/java/com/android/base/permission/OnAllPermissionGrantedListener.java new file mode 100644 index 0000000..41e4bfc --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/OnAllPermissionGrantedListener.java @@ -0,0 +1,10 @@ +package com.android.base.permission; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-07-02 13:51 + */ +public interface OnAllPermissionGrantedListener { + void onAllPermissionGranted(); +} diff --git a/lib_base/src/main/java/com/android/base/permission/OnPermissionDeniedListener.java b/lib_base/src/main/java/com/android/base/permission/OnPermissionDeniedListener.java new file mode 100644 index 0000000..2fea808 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/OnPermissionDeniedListener.java @@ -0,0 +1,12 @@ +package com.android.base.permission; + +import java.util.List; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-07-02 13:50 + */ +public interface OnPermissionDeniedListener { + void onPermissionDenied(List permissions); +} diff --git a/lib_base/src/main/java/com/android/base/permission/Permission.java b/lib_base/src/main/java/com/android/base/permission/Permission.java new file mode 100644 index 0000000..8b90ffd --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/Permission.java @@ -0,0 +1,209 @@ +package com.android.base.permission; + +import android.content.Context; +import android.os.Build; + +import com.android.base.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * copy from https://github.com/yanzhenjie/AndPermission + */ +public final class Permission { + + + public static final String READ_CALENDAR = "android.permission.READ_CALENDAR"; + public static final String WRITE_CALENDAR = "android.permission.WRITE_CALENDAR"; + + public static final String CAMERA = "android.permission.CAMERA"; + + public static final String READ_CONTACTS = "android.permission.READ_CONTACTS"; + public static final String WRITE_CONTACTS = "android.permission.WRITE_CONTACTS"; + public static final String GET_ACCOUNTS = "android.permission.GET_ACCOUNTS"; + + public static final String ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION"; + public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"; + + public static final String RECORD_AUDIO = "android.permission.RECORD_AUDIO"; + + public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; + public static final String CALL_PHONE = "android.permission.CALL_PHONE"; + public static final String READ_CALL_LOG = "android.permission.READ_CALL_LOG"; + public static final String WRITE_CALL_LOG = "android.permission.WRITE_CALL_LOG"; + public static final String ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL"; + static final String ADD_VOICEMAIL_MANIFEST = "android.permission.ADD_VOICEMAIL"; + public static final String USE_SIP = "android.permission.USE_SIP"; + public static final String PROCESS_OUTGOING_CALLS = "android.permission.PROCESS_OUTGOING_CALLS"; + public static final String READ_PHONE_NUMBERS = "android.permission.READ_PHONE_NUMBERS"; + public static final String ANSWER_PHONE_CALLS = "android.permission.ANSWER_PHONE_CALLS"; + + public static final String BODY_SENSORS = "android.permission.BODY_SENSORS"; + + public static final String SEND_SMS = "android.permission.SEND_SMS"; + public static final String RECEIVE_SMS = "android.permission.RECEIVE_SMS"; + public static final String READ_SMS = "android.permission.READ_SMS"; + public static final String RECEIVE_WAP_PUSH = "android.permission.RECEIVE_WAP_PUSH"; + public static final String RECEIVE_MMS = "android.permission.RECEIVE_MMS"; + + public static final String READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE"; + public static final String WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE"; + + public static final class Group { + + public static final String[] CALENDAR = new String[]{Permission.READ_CALENDAR, Permission.WRITE_CALENDAR}; + + public static final String[] CAMERA = new String[]{Permission.CAMERA}; + + public static final String[] CONTACTS = new String[]{Permission.READ_CONTACTS, Permission.WRITE_CONTACTS, + Permission.GET_ACCOUNTS}; + + public static final String[] LOCATION = new String[]{Permission.ACCESS_FINE_LOCATION, + Permission.ACCESS_COARSE_LOCATION}; + + public static final String[] MICROPHONE = new String[]{Permission.RECORD_AUDIO}; + + public static final String[] PHONE; + + static { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + PHONE = new String[]{Permission.READ_PHONE_STATE, Permission.CALL_PHONE, Permission.READ_CALL_LOG, + Permission.WRITE_CALL_LOG, Permission.ADD_VOICEMAIL, Permission.USE_SIP, + Permission.PROCESS_OUTGOING_CALLS, Permission.READ_PHONE_NUMBERS, Permission.ANSWER_PHONE_CALLS}; + } else { + PHONE = new String[]{Permission.READ_PHONE_STATE, Permission.CALL_PHONE, Permission.READ_CALL_LOG, + Permission.WRITE_CALL_LOG, Permission.ADD_VOICEMAIL, Permission.USE_SIP, + Permission.PROCESS_OUTGOING_CALLS}; + } + } + + public static final String[] SENSORS = new String[]{Permission.BODY_SENSORS}; + + public static final String[] SMS = new String[]{Permission.SEND_SMS, Permission.RECEIVE_SMS, + Permission.READ_SMS, Permission.RECEIVE_WAP_PUSH, Permission.RECEIVE_MMS}; + + public static final String[] STORAGE = new String[]{Permission.READ_EXTERNAL_STORAGE, + Permission.WRITE_EXTERNAL_STORAGE}; + } + + /** + * Turn permissions into text. + */ + public static List transformText(Context context, String... permissions) { + return transformText(context, Arrays.asList(permissions)); + } + + /** + * Turn permissions into text. + */ + public static List transformText(Context context, String[]... groups) { + List permissionList = new ArrayList<>(); + for (String[] group : groups) { + permissionList.addAll(Arrays.asList(group)); + } + return transformText(context, permissionList); + } + + /** + * Turn permissions into text. + */ + public static List transformText(Context context, List permissions) { + List textList = new ArrayList<>(); + for (String permission : permissions) { + switch (permission) { + case Permission.READ_CALENDAR: + case Permission.WRITE_CALENDAR: { + String message = context.getString(R.string.Base_permission_name_calendar); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + + case Permission.CAMERA: { + String message = context.getString(R.string.Base_permission_name_camera); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.READ_CONTACTS: + case Permission.WRITE_CONTACTS: { + String message = context.getString(R.string.Base_permission_name_contacts); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.GET_ACCOUNTS: { + String message = context.getString(R.string.Base_permission_name_accounts); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.ACCESS_FINE_LOCATION: + case Permission.ACCESS_COARSE_LOCATION: { + String message = context.getString(R.string.Base_permission_name_location); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.RECORD_AUDIO: { + String message = context.getString(R.string.Base_permission_name_microphone); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.READ_PHONE_STATE: + case Permission.CALL_PHONE: + case Permission.READ_CALL_LOG: + case Permission.WRITE_CALL_LOG: + case Permission.ADD_VOICEMAIL: + case Permission.ADD_VOICEMAIL_MANIFEST: + case Permission.USE_SIP: + case Permission.PROCESS_OUTGOING_CALLS: + case Permission.READ_PHONE_NUMBERS: + case Permission.ANSWER_PHONE_CALLS: { + String message = context.getString(R.string.Base_permission_name_phone); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.BODY_SENSORS: { + String message = context.getString(R.string.Base_permission_name_sensors); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.SEND_SMS: + case Permission.RECEIVE_SMS: + case Permission.READ_SMS: + case Permission.RECEIVE_WAP_PUSH: + case Permission.RECEIVE_MMS: { + String message = context.getString(R.string.Base_permission_name_sms); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + case Permission.READ_EXTERNAL_STORAGE: + case Permission.WRITE_EXTERNAL_STORAGE: { + String message = context.getString(R.string.Base_permission_name_storage); + if (!textList.contains(message)) { + textList.add(message); + } + break; + } + } + } + return textList; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/permission/PermissionCallback.java b/lib_base/src/main/java/com/android/base/permission/PermissionCallback.java new file mode 100644 index 0000000..5d7f5e0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/PermissionCallback.java @@ -0,0 +1,27 @@ +package com.android.base.permission; // Callback + +import java.util.List; + +class PermissionCallback { + + private final OnPermissionDeniedListener mOnPermissionDeniedListener; + private final OnAllPermissionGrantedListener mOnAllPermissionGrantedListener; + + PermissionCallback(OnPermissionDeniedListener onPermissionDeniedListener, OnAllPermissionGrantedListener onAllPermissionGrantedListener) { + mOnPermissionDeniedListener = onPermissionDeniedListener; + mOnAllPermissionGrantedListener = onAllPermissionGrantedListener; + } + + void onPermissionDenied(List strings) { + if (mOnPermissionDeniedListener != null) { + mOnPermissionDeniedListener.onPermissionDenied(strings); + } + } + + void onAllPermissionGranted() { + if (mOnAllPermissionGrantedListener != null) { + mOnAllPermissionGrantedListener.onAllPermissionGranted(); + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/permission/PermissionCode.java b/lib_base/src/main/java/com/android/base/permission/PermissionCode.java new file mode 100644 index 0000000..305bad7 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/PermissionCode.java @@ -0,0 +1,8 @@ +package com.android.base.permission; + +final class PermissionCode { + + static final int REQUEST_PERMISSION_FOR_SETTING = 999; + static final int PERMISSION_REQUESTER_CODE = 15086; + +} diff --git a/lib_base/src/main/java/com/android/base/permission/PermissionRequester.java b/lib_base/src/main/java/com/android/base/permission/PermissionRequester.java new file mode 100644 index 0000000..ce6b49c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/PermissionRequester.java @@ -0,0 +1,73 @@ +package com.android.base.permission; + +import android.content.Intent; +import android.support.annotation.NonNull; + +import com.android.base.utils.android.ActFragWrapper; + +import java.util.Arrays; + +import static com.android.base.permission.PermissionCode.REQUEST_PERMISSION_FOR_SETTING; + +/** + *
+ *      1:使用{@link #requestPermission(int, String...)}来申请权限,当所有的权限都通过时回调权限获取成功,否则回调权限获取失败。
+ *      2:不要同时调用requestPermission方法多次!!!以保证一个完整的流程。
+ * 获取权限流程,以申请相机权限为例:
+ *          1先检查自身是否有相机权限
+ *          2如果有我们的app已经有了相机权限,则可以直接使用相机相关功能了
+ *          3如果没有权限我们就需要请求权限了,但是还需要处理不再询问的设置
+ *              3.1如果shouldShowRequestPermissionRationale返回false,则说明接下来的对话框不包含”不再询问“选择框,我们可以直接申请权限
+ *              3.2如果shouldShowRequestPermissionRationale返回true,我们最好先弹出一个对话框来说明我们需要权限来做什么,让用户来选择是否继续授予权限,如果用户允许继续授予权限则继续申请权限
+ *          4不管权限是否授予给我们的App,都可以在onRequestPermissionsResult的回调中获取结果,我们再问一次
+ * 
+ * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-01-11 15:09 + */ +class PermissionRequester { + + private final ActFragWrapper mActFragWrapper; + private String[] mPerms; + private PermissionCallback mPermissionCallback; + private EasyPermissions.PermissionCaller mPermissionCaller; + private final boolean mAskAgain; + private final IPermissionUIProvider mPermissionUIProvider; + + PermissionRequester(ActFragWrapper actFragWrapper, PermissionCallback permissionCallback, boolean askAgain, IPermissionUIProvider permissionUIProvider) { + mPermissionCallback = permissionCallback; + mActFragWrapper = actFragWrapper; + mAskAgain = askAgain; + mPermissionUIProvider = permissionUIProvider; + } + + private EasyPermissions.PermissionCaller getPermissionCaller() { + if (mPermissionCaller == null) { + mPermissionCaller = new PermissionRequesterImpl(mPermissionCallback, mActFragWrapper, mAskAgain, mPermissionUIProvider); + } + return mPermissionCaller; + } + + void requestPermission(int requestCode, String... perms) { + mPerms = perms; + EasyPermissions.requestPermissions(getPermissionCaller(), requestCode, mPerms); + } + + void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, getPermissionCaller()); + } + + void onActivityResult(int requestCode, @SuppressWarnings("unused") int resultCode, @SuppressWarnings("unused") Intent data) { + if (requestCode == REQUEST_PERMISSION_FOR_SETTING) {//申请权限 + if (!EasyPermissions.hasPermissions(mActFragWrapper.getContext(), mPerms)) {//Setting界面回来之后,没有授予权限 + String[] filter = EasyPermissions.filter(mActFragWrapper.getContext(), mPerms); + mPermissionCallback.onPermissionDenied(Arrays.asList(filter));//权限被拒绝 + mPermissionCaller.getPermissionUIProvider().showPermissionDeniedTip(mActFragWrapper.getContext(), filter); + } else { + mPermissionCallback.onAllPermissionGranted();//所有权限被获取 + } + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/permission/PermissionRequesterImpl.java b/lib_base/src/main/java/com/android/base/permission/PermissionRequesterImpl.java new file mode 100644 index 0000000..161a7c5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/PermissionRequesterImpl.java @@ -0,0 +1,80 @@ +package com.android.base.permission; + +import android.content.Intent; + +import com.android.base.utils.android.ActFragWrapper; + +import java.util.List; + +import static com.android.base.permission.PermissionCode.REQUEST_PERMISSION_FOR_SETTING; + + +class PermissionRequesterImpl implements EasyPermissions.PermissionCaller { + + private final boolean mShouldAskAgain; + private ActFragWrapper mContextWrapper; + private PermissionCallback mPermissionCallback; + private IPermissionUIProvider mIPermissionUIProvider; + + PermissionRequesterImpl(PermissionCallback permissionCallback, ActFragWrapper contextWrapper, boolean shouldAskAgain, IPermissionUIProvider iPermissionUIProvider) { + mPermissionCallback = permissionCallback; + mContextWrapper = contextWrapper; + mShouldAskAgain = shouldAskAgain; + mIPermissionUIProvider = iPermissionUIProvider; + } + + @Override + public Object getRequester() { + if (mContextWrapper.getFragment() != null) { + return mContextWrapper.getFragment(); + } + return mContextWrapper.getContext(); + } + + @Override + public IPermissionUIProvider getPermissionUIProvider() { + if (mIPermissionUIProvider == null) { + return PermissionUIProviderFactory.getPermissionUIProvider(); + } + return mIPermissionUIProvider; + } + + /** + * 只获取到部分权限则不管,会在onPermissionsDenied和onPermissionAllGranted中处理 + */ + @Override + public void onPortionPermissionsGranted(boolean allGranted, int requestCode, List perms) { + // do nothing + if (allGranted) { + mPermissionCallback.onAllPermissionGranted(); + } + } + + @Override + public void onPermissionsDenied(final int requestCode, final List perms) { + if (!mShouldAskAgain) { + notifyPermissionDenied(perms); + return; + } + //ask again + boolean again = EasyPermissions.checkDeniedPermissionsNeverAskAgain(this, perms); + if (again) { + getPermissionUIProvider() + .showAskAgainDialog(mContextWrapper.getContext(), perms.toArray(new String[0]), + (dialog, which) -> { + Intent intentForPermission = EasyPermissions.getIntentForPermission(mContextWrapper.getContext()); + mContextWrapper.startActivityForResult(intentForPermission, REQUEST_PERMISSION_FOR_SETTING, null); + }, + (dialog, which) -> notifyPermissionDenied(perms)); + } else { + notifyPermissionDenied(perms); + } + } + + private void notifyPermissionDenied(List perms) { + mPermissionCallback.onPermissionDenied(perms); + getPermissionUIProvider().showPermissionDeniedTip(mContextWrapper.getContext(), perms.toArray(new String[0])); + } + + +} diff --git a/lib_base/src/main/java/com/android/base/permission/PermissionUIProviderFactory.java b/lib_base/src/main/java/com/android/base/permission/PermissionUIProviderFactory.java new file mode 100644 index 0000000..82a56a9 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/permission/PermissionUIProviderFactory.java @@ -0,0 +1,19 @@ +package com.android.base.permission; + + +public class PermissionUIProviderFactory { + + private static IPermissionUIProvider sIPermissionUIProvider; + + static IPermissionUIProvider getPermissionUIProvider() { + if (sIPermissionUIProvider == null) { + sIPermissionUIProvider = new DefaultPermissionUIProvider(); + } + return sIPermissionUIProvider; + } + + public static void registerPermissionUIProvider(IPermissionUIProvider iPermissionUIProvider) { + sIPermissionUIProvider = iPermissionUIProvider; + } + +} diff --git a/lib_base/src/main/java/com/android/base/receiver/NetStateReceiver.java b/lib_base/src/main/java/com/android/base/receiver/NetStateReceiver.java new file mode 100644 index 0000000..0fced23 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/receiver/NetStateReceiver.java @@ -0,0 +1,130 @@ +package com.android.base.receiver; + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkInfo; +import android.net.NetworkInfo.State; + +import com.android.base.utils.android.compat.AndroidVersion; +import com.blankj.utilcode.util.NetworkUtils; + +import timber.log.Timber; + +import static com.android.base.receiver.NetworkState.STATE_GPRS; +import static com.android.base.receiver.NetworkState.STATE_NONE; +import static com.android.base.receiver.NetworkState.STATE_WIFI; + + +/** + * 网络监听,接下来会使用eventBus来分发网络状态,对网络状态感兴趣的可以对网络状态进行订阅,需要权限: + *
{@code
+ *
+ *   
+ *          
+ *               
+ *               
+ *               
+ *          
+ *      
+ * }
+ *
+ * 
+ * + * @author Ztiany + * email 1169654504@qq.com + * date 2015-12-08 14:50 + * @see Android:检测网络状态&监听网络变化 + */ +public class NetStateReceiver extends BroadcastReceiver { + + private static NetworkState mStatus = null; + + private State state_wifi = null; + private State state_gprs = null; + + @Override + public void onReceive(Context context, Intent intent) { + NetworkState tempStatus; + + // 获得网络连接服务 + ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + + getState(connManager); + + if (null != state_wifi && State.CONNECTED == state_wifi) { // 判断是否正在使用WIFI网络 + tempStatus = STATE_WIFI; + Timber.d("mStatus=" + mStatus + "改变后的网络为WIFI"); + } else if (null != state_gprs && State.CONNECTED == state_gprs) { // 判断是否正在使用GPRS网络 + tempStatus = STATE_GPRS; + Timber.d("mStatus=" + mStatus + "改变后的网络为GPRS"); + } else { + tempStatus = STATE_NONE; + Timber.d("mStatus=" + mStatus + "改变后的网络为无连接"); + } + + if (mStatus != tempStatus) { + Timber.d("mStatus与改变后的网络不同,网络真的改变了"); + //在此处 通知网络更新 + NetworkState.notify(tempStatus); + } else { + Timber.d("mStatus与改变后的网络相同,不处理"); + } + + mStatus = tempStatus; + + state_wifi = null; + state_gprs = null; + } + + @SuppressLint("MissingPermission") + private void getState(ConnectivityManager connManager) { + + if (AndroidVersion.atLeast(23)) { + + //获取所有网络连接的信息 + Network[] networks = connManager.getAllNetworks(); + NetworkInfo networkInfo; + for (Network network : networks) { + //获取ConnectivityManager对象对应的NetworkInfo对象 + networkInfo = connManager.getNetworkInfo(network); + if (networkInfo == null) { + continue; + } + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + state_wifi = networkInfo.getState(); + } else if (networkInfo.getType() == ConnectivityManager.TYPE_MOBILE) { + state_gprs = networkInfo.getState(); + } + if (state_wifi != null && state_gprs != null) { + break; + } + } + + if (!NetworkUtils.isConnected()) { + Timber.d("api 23 after getState check isConnected = false"); + state_wifi = null; + state_gprs = null; + } + + Timber.d("api 23 after getState state_wifi = " + state_wifi + " state_gprs = " + state_gprs); + } else { + + try { + state_wifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI).getState(); // 获取网络连接状态 + } catch (Exception e) { + Timber.d("测试机没有WIFI模块"); + } + try { + state_gprs = connManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE).getState(); // 获取网络连接状态 + } catch (Exception e) { + Timber.d("测试机没有GPRS模块"); + } + Timber.d("api 23 before getState state_wifi = " + state_wifi + " state_gprs = " + state_gprs); + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/receiver/NetworkState.java b/lib_base/src/main/java/com/android/base/receiver/NetworkState.java new file mode 100644 index 0000000..66e57ad --- /dev/null +++ b/lib_base/src/main/java/com/android/base/receiver/NetworkState.java @@ -0,0 +1,27 @@ +package com.android.base.receiver; + +import io.reactivex.Flowable; +import io.reactivex.processors.BehaviorProcessor; + +public enum NetworkState { + + STATE_WIFI, + STATE_GPRS, + STATE_NONE; + + private static final BehaviorProcessor PROCESSOR = BehaviorProcessor.create(); + + public static Flowable observableState() { + return PROCESSOR; + } + + public boolean isConnected() { + return this != STATE_NONE; + } + + static void notify(NetworkState networkState) { + PROCESSOR.onNext(networkState); + } + + +} diff --git a/lib_base/src/main/java/com/android/base/rx/AutoDispose.kt b/lib_base/src/main/java/com/android/base/rx/AutoDispose.kt new file mode 100644 index 0000000..eb3e92d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/AutoDispose.kt @@ -0,0 +1,103 @@ +@file:JvmName("AutoDisposeUtils") + +package com.android.base.rx + +import android.arch.lifecycle.Lifecycle +import android.arch.lifecycle.LifecycleOwner +import com.uber.autodispose.* +import com.uber.autodispose.AutoDispose.autoDisposable +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider +import io.reactivex.* +import io.reactivex.disposables.Disposable +import timber.log.Timber + +fun Flowable.bindLifecycle(lifecycleOwner: LifecycleOwner): FlowableSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner))) +} + +fun Flowable.bindLifecycle(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event): FlowableSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner, event))) +} + +fun Observable.bindLifecycle(lifecycleOwner: LifecycleOwner): ObservableSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner))) +} + +fun Observable.bindLifecycle(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event): ObservableSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner, event))) +} + +fun Completable.bindLifecycle(lifecycleOwner: LifecycleOwner): CompletableSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner))) +} + +fun Completable.bindLifecycle(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event): CompletableSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner, event))) +} + +fun Maybe.bindLifecycle(lifecycleOwner: LifecycleOwner): MaybeSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner))) +} + +fun Maybe.bindLifecycle(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event): MaybeSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner, event))) +} + +fun Single.bindLifecycle(lifecycleOwner: LifecycleOwner): SingleSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner))) +} + +fun Single.bindLifecycle(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event): SingleSubscribeProxy { + return this.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner, event))) +} + +fun ObservableSubscribeProxy.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun FlowableSubscribeProxy.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun SingleSubscribeProxy.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun MaybeSubscribeProxy.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun CompletableSubscribeProxy.subscribed(): Disposable = this.subscribe(RxKit.logCompletedHandler(), RxKit.logErrorHandler()) + +fun ObservableSubscribeProxy.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun FlowableSubscribeProxy.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun SingleSubscribeProxy.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun MaybeSubscribeProxy.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun CompletableSubscribeProxy.subscribeIgnoreError(action: () -> Unit): Disposable = this.subscribe( + { + action() + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/FlowableRetryDelay.java b/lib_base/src/main/java/com/android/base/rx/FlowableRetryDelay.java new file mode 100644 index 0000000..ea96c3e --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/FlowableRetryDelay.java @@ -0,0 +1,48 @@ +package com.android.base.rx; + +import android.support.annotation.Nullable; + +import org.reactivestreams.Publisher; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Flowable; +import io.reactivex.functions.Function; +import timber.log.Timber; + +public class FlowableRetryDelay implements Function, Publisher> { + + private final int mMaxRetries; + private final long mRetryDelayMillis; + @Nullable + private RetryChecker mRetryChecker; + private int mRetryCount = 0; + + @SuppressWarnings("unused") + public FlowableRetryDelay(final int maxRetries, final long retryDelayMillis) { + this(maxRetries, retryDelayMillis, null); + } + + public FlowableRetryDelay(final int maxRetries, final long retryDelayMillis, @Nullable RetryChecker retryChecker) { + mMaxRetries = maxRetries; + mRetryDelayMillis = retryDelayMillis; + mRetryChecker = retryChecker != null ? retryChecker : throwable -> true; + } + + @Override + public Publisher apply(Flowable throwableFlowable) { + return throwableFlowable.flatMap((Function>) throwable -> { + if (mRetryChecker != null && !mRetryChecker.verify(throwable)) { + return Flowable.error(throwable); + } + mRetryCount++; + Timber.i(new Date() + " 自动重试" + (mRetryCount + 1) + "次,在" + Thread.currentThread() + "线程"); + if (mRetryCount <= mMaxRetries) { + return Flowable.timer(mRetryDelayMillis, TimeUnit.MILLISECONDS); + } + return Flowable.error(throwable); + }); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/LifecycleScopeProviderEx.kt b/lib_base/src/main/java/com/android/base/rx/LifecycleScopeProviderEx.kt new file mode 100644 index 0000000..8a7b65f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/LifecycleScopeProviderEx.kt @@ -0,0 +1,80 @@ +package com.android.base.rx + +import android.arch.lifecycle.Lifecycle +import android.arch.lifecycle.LifecycleOwner +import com.uber.autodispose.* +import com.uber.autodispose.lifecycle.LifecycleScopeProvider +import io.reactivex.* + +/** + *@author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-05-10 14:41 + */ +interface LifecycleScopeProviderEx : LifecycleScopeProvider { + + fun Flowable.autoDispose(): FlowableSubscribeProxy { + return this.`as`(AutoDispose.autoDisposable(this@LifecycleScopeProviderEx)) + } + + fun Observable.autoDispose(): ObservableSubscribeProxy { + return this.`as`(AutoDispose.autoDisposable(this@LifecycleScopeProviderEx)) + } + + fun Completable.autoDispose(): CompletableSubscribeProxy { + return this.`as`(AutoDispose.autoDisposable(this@LifecycleScopeProviderEx)) + } + + fun Maybe.autoDispose(): MaybeSubscribeProxy { + return this.`as`(AutoDispose.autoDisposable(this@LifecycleScopeProviderEx)) + } + + fun Single.autoDispose(): SingleSubscribeProxy { + return this.`as`(AutoDispose.autoDisposable(this@LifecycleScopeProviderEx)) + } + +} + +interface LifecycleOwnerEx : LifecycleOwner { + + fun Flowable.autoDispose(): FlowableSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx) + } + + fun Observable.autoDispose(): ObservableSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx) + } + + fun Completable.autoDispose(): CompletableSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx) + } + + fun Maybe.autoDispose(): MaybeSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx) + } + + fun Single.autoDispose(): SingleSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx) + } + + fun Flowable.autoDispose(event: Lifecycle.Event): FlowableSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx, event) + } + + fun Observable.autoDispose(event: Lifecycle.Event): ObservableSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx, event) + } + + fun Completable.autoDispose(event: Lifecycle.Event): CompletableSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx, event) + } + + fun Maybe.autoDispose(event: Lifecycle.Event): MaybeSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx, event) + } + + fun Single.autoDispose(event: Lifecycle.Event): SingleSubscribeProxy { + return this.bindLifecycle(this@LifecycleOwnerEx, event) + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/ObservableRetryDelay.java b/lib_base/src/main/java/com/android/base/rx/ObservableRetryDelay.java new file mode 100644 index 0000000..8bdac37 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/ObservableRetryDelay.java @@ -0,0 +1,47 @@ +package com.android.base.rx; + +import android.support.annotation.Nullable; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.functions.Function; +import timber.log.Timber; + +public class ObservableRetryDelay implements Function, ObservableSource> { + + private final int mMaxRetries; + private final long mRetryDelayMillis; + @Nullable + private RetryChecker mRetryChecker; + private int mRetryCount = 0; + + @SuppressWarnings("unused") + public ObservableRetryDelay(final int maxRetries, final long retryDelayMillis) { + this(maxRetries, retryDelayMillis, null); + } + + public ObservableRetryDelay(final int maxRetries, final long retryDelayMillis, @Nullable RetryChecker retryChecker) { + mMaxRetries = maxRetries; + mRetryDelayMillis = retryDelayMillis; + mRetryChecker = retryChecker != null ? retryChecker : throwable -> true; + } + + @Override + public ObservableSource apply(Observable throwableObservable) { + return throwableObservable.flatMap((Function>) throwable -> { + if (mRetryChecker != null && !mRetryChecker.verify(throwable)) { + return Observable.error(throwable); + } + mRetryCount++; + Timber.i(new Date() + " 自动重试" + (mRetryCount + 1) + "次,在" + Thread.currentThread() + "线程"); + if (mRetryCount <= mMaxRetries) { + return Observable.timer(mRetryDelayMillis, TimeUnit.MILLISECONDS); + } + return Observable.error(throwable); + }); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/RetryChecker.java b/lib_base/src/main/java/com/android/base/rx/RetryChecker.java new file mode 100644 index 0000000..545ed51 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/RetryChecker.java @@ -0,0 +1,7 @@ +package com.android.base.rx; + +public interface RetryChecker { + + boolean verify(Throwable throwable); + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/RxBus.java b/lib_base/src/main/java/com/android/base/rx/RxBus.java new file mode 100644 index 0000000..663ddb4 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/RxBus.java @@ -0,0 +1,83 @@ +package com.android.base.rx; + +import android.support.annotation.NonNull; + +import java.util.UUID; + +import io.reactivex.Flowable; +import io.reactivex.processors.PublishProcessor; + + +/** + * 不要跨大范围的使用 RxBus,比较推荐的模式为针对特定模块定义特定的事件分发类: + *
+ *     {@code
+ *      public class FooEvents{
+ *
+ *                  public Observable subscribeFooEvents(){
+ *                             return RxBus.getDefault().toObservable(FooEvents.class);
+ *                  }
+ *
+ *                  public void sendFooEvent(FooEvents event){
+ *                            RxBus.getDefault().send(event);
+ *                  }
+ *              }
+ *     }
+ * 
+ */ +public class RxBus { + + private static final RxBus BUS = new RxBus(); + + public static RxBus getDefault() { + return BUS; + } + + public static RxBus newInstance() { + return new RxBus(); + } + + //All other Publisher and Subject methods are thread-safe by design. + private static final PublishProcessor mBus = PublishProcessor.create(); + + private final String COMMON_EVENT_IDENTIFY; + + private RxBus() { + COMMON_EVENT_IDENTIFY = UUID.randomUUID().toString(); + } + + public boolean hasObservers() { + return mBus.hasSubscribers(); + } + + public void send(@NonNull final Object event) { + mBus.onNext(new ObjectHolder(COMMON_EVENT_IDENTIFY, event)); + } + + public Flowable toObservable(Class tClass) { + return toObservable(COMMON_EVENT_IDENTIFY, tClass); + } + + public void send(@NonNull String identify, @NonNull final Object event) { + mBus.onNext(new ObjectHolder(identify, event)); + } + + public Flowable toObservable(@NonNull final String identify, final Class tClass) { + return mBus.ofType(ObjectHolder.class) + .filter(objectHolder -> objectHolder.identify.equals(identify) && tClass == objectHolder.event.getClass()) + .map(objectHolder -> tClass.cast(objectHolder.event)); + } + + private static class ObjectHolder { + + private final String identify; + private final Object event; + + ObjectHolder(String identify, Object event) { + this.identify = identify; + this.event = event; + } + + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/RxEx.kt b/lib_base/src/main/java/com/android/base/rx/RxEx.kt new file mode 100644 index 0000000..9ede2b5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/RxEx.kt @@ -0,0 +1,116 @@ +package com.android.base.rx + +import io.reactivex.* +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import timber.log.Timber + +fun Single.observeOnUI(): Single = this.observeOn(AndroidSchedulers.mainThread()) +fun Maybe.observeOnUI(): Maybe = this.observeOn(AndroidSchedulers.mainThread()) +fun Observable.observeOnUI(): Observable = this.observeOn(AndroidSchedulers.mainThread()) +fun Flowable.observeOnUI(): Flowable = this.observeOn(AndroidSchedulers.mainThread()) +fun Completable.observeOnUI(): Completable = this.observeOn(AndroidSchedulers.mainThread()) + +fun Observable.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun Flowable.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun Single.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun Maybe.subscribeIgnoreError(action: (T) -> Unit): Disposable = this.subscribe( + { + action(it) + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +fun Completable.subscribeIgnoreError(action: () -> Unit): Disposable = this.subscribe( + { + action() + }, + { + Timber.e(it, "Kotlin Extends ignoreError: ") + } +) + +inline fun RxBus.toObservable(): Flowable = this.toObservable(A::class.java) +inline fun RxBus.toObservable(tag: String): Flowable = this.toObservable(tag, A::class.java) + +fun Observable.io2UI(): Observable = this.compose(RxKit.io2UI()) +fun Observable.newThread2UI(): Observable = this.compose(RxKit.newThread2UI()) +fun Observable.computation2UI(): Observable = this.compose(RxKit.computation2UI()) + +fun Flowable.io2UI(): Flowable = this.compose(RxKit.io2UI()) +fun Flowable.newThread2UI(): Flowable = this.compose(RxKit.newThread2UI()) +fun Flowable.computation2UI(): Flowable = this.compose(RxKit.computation2UI()) + +fun Single.io2UI(): Single = this.compose(RxKit.io2UI()) +fun Single.newThread2UI(): Single = this.compose(RxKit.newThread2UI()) +fun Single.computation2UI(): Single = this.compose(RxKit.computation2UI()) + +fun Maybe.io2UI(): Maybe = this.compose(RxKit.io2UI()) +fun Maybe.newThread2UI(): Maybe = this.compose(RxKit.newThread2UI()) +fun Maybe.computation2UI(): Maybe = this.compose(RxKit.computation2UI()) + +fun Completable.io2UI(): Completable = this.compose(RxKit.io2UI()) +fun Completable.newThread2UI(): Completable = this.compose(RxKit.newThread2UI()) +fun Completable.computation2UI(): Completable = this.compose(RxKit.computation2UI()) + +fun Observable.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun Flowable.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun Single.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun Maybe.subscribed(): Disposable = this.subscribe(RxKit.logResultHandler(), RxKit.logErrorHandler()) +fun Completable.subscribed(): Disposable = this.subscribe(RxKit.logCompletedHandler(), RxKit.logErrorHandler()) + + +fun Flowable.retryWhen(maxRetries: Int, retryDelayMillis: Long, retryChecker: RetryChecker? = null): Flowable { + return this.retryWhen(FlowableRetryDelay(maxRetries, retryDelayMillis, retryChecker)) +} + +fun Observable.retryWhen(maxRetries: Int, retryDelayMillis: Long, retryChecker: RetryChecker? = null): Observable { + return this.retryWhen(ObservableRetryDelay(maxRetries, retryDelayMillis, retryChecker)) +} + +fun Completable.retryWhen(maxRetries: Int, retryDelayMillis: Long, retryChecker: RetryChecker? = null): Completable { + return this.retryWhen(FlowableRetryDelay(maxRetries, retryDelayMillis, retryChecker)) +} + +fun Single.retryWhen(maxRetries: Int, retryDelayMillis: Long, retryChecker: RetryChecker? = null): Single { + return this.retryWhen(FlowableRetryDelay(maxRetries, retryDelayMillis, retryChecker)) +} + +fun Maybe.retryWhen(maxRetries: Int, retryDelayMillis: Long, retryChecker: RetryChecker? = null): Maybe { + return this.retryWhen(FlowableRetryDelay(maxRetries, retryDelayMillis, retryChecker)) +} + +operator fun CompositeDisposable?.plusAssign(disposable: Disposable) { + this?.add(disposable) +} + +fun Disposable.addTo(compositeDisposable: CompositeDisposable?) { + compositeDisposable?.add(this) +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/RxKit.java b/lib_base/src/main/java/com/android/base/rx/RxKit.java new file mode 100644 index 0000000..c1db177 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/RxKit.java @@ -0,0 +1,119 @@ +package com.android.base.rx; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; +import timber.log.Timber; + +/** + * @author Ztiany + * date : 2016-03-19 23:09 + * email: 1169654504@qq.com + */ +public class RxKit { + + /** + *
+     *     {@code private CompositeSubscription mCompositeSubscription;
+     *
+     *       protected void addSubscription(Subscription subscription)
+     *                     CompositeSubscription newCompositeSubIfUnsubscribed =
+     *                     RxUtils .getNewCompositeSubIfUnsubscribed(mCompositeSubscription);
+     *                     mCompositeSubscription = newCompositeSubIfUnsubscribed;
+     *                     newCompositeSubIfUnsubscribed.add(subscription);
+     *
+     *        private void unsubscribe() {
+     *                    RxUtils.unsubscribeIfNotNull(mCompositeSubscription);
+     *        }
+     * }
+     * 
+ */ + public static void unsubscribeIfNotNull(Disposable disposable) { + if (disposable != null && !disposable.isDisposed()) { + disposable.dispose(); + } + } + + public static CompositeDisposable getNewCompositeSubIfUnsubscribed(CompositeDisposable disposable) { + if (disposable == null || disposable.isDisposed()) { + return new CompositeDisposable(); + } + return disposable; + } + + private static final Action LOG_ACTION0 = () -> Timber.d("RxUtils LOG_ACTION0 call() called"); + + private static final Consumer LOG_ACTION1 = obj -> { + if (obj instanceof Throwable) { + Timber.e((Throwable) obj, "RxUtils PRINT_ACTION1 call() called with: error"); + } else { + Timber.d("RxUtils ERROR_ACTION call() called with: result = [" + obj + "]"); + } + }; + + @SuppressWarnings("unchecked") + public static Consumer logErrorHandler() { + return (Consumer) LOG_ACTION1; + } + + public static Action logCompletedHandler() { + return LOG_ACTION0; + } + + @SuppressWarnings("unchecked") + public static Consumer logResultHandler() { + return (Consumer) LOG_ACTION1; + } + + private static final Subscriber LOG_SUBSCRIBER = new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + Timber.d("onSubscribe() called with: s = [" + s + "]"); + } + + @Override + public void onNext(Object o) { + Timber.d("onNext() called with: o = [" + o + "]"); + } + + @Override + public void onError(Throwable t) { + Timber.d("onError() called with: t = [" + t + "]"); + } + + @Override + public void onComplete() { + Timber.d("onComplete() called"); + } + }; + + @SuppressWarnings("unchecked") + public static Subscriber logSubscriber() { + return (Subscriber) LOG_SUBSCRIBER; + } + + private static final ThreadTransformer IO_2_UI_SCHEDULERS_TRANSFORMER = ThreadTransformer.newInstance(ThreadTransformer.IO_UI); + private static final ThreadTransformer COMPUTATION_2_UI_SCHEDULERS_TRANSFORMER = ThreadTransformer.newInstance(ThreadTransformer.COMPUTATION_UI); + private static final ThreadTransformer NEW_THREAD_2_UI_SCHEDULERS_TRANSFORMER = ThreadTransformer.newInstance(ThreadTransformer.NEW_THREAD_UI); + + @SuppressWarnings("unchecked") + public static ThreadTransformer io2UI() { + return IO_2_UI_SCHEDULERS_TRANSFORMER; + } + + @SuppressWarnings("unchecked") + public static ThreadTransformer computation2UI() { + return COMPUTATION_2_UI_SCHEDULERS_TRANSFORMER; + } + + @SuppressWarnings("unchecked") + public static ThreadTransformer newThread2UI() { + return NEW_THREAD_2_UI_SCHEDULERS_TRANSFORMER; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/rx/SchedulerProvider.kt b/lib_base/src/main/java/com/android/base/rx/SchedulerProvider.kt new file mode 100644 index 0000000..5070884 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/SchedulerProvider.kt @@ -0,0 +1,46 @@ +@file:JvmName("SchedulerProviders") +package com.android.base.rx + +import io.reactivex.Scheduler +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + + +/** + * Allow providing different types of [Scheduler]s. + */ +interface SchedulerProvider { + + fun computation(): Scheduler + + fun io(): Scheduler + + fun ui(): Scheduler + + fun database(): Scheduler + +} + +fun newDefaultSchedulerProvider(): SchedulerProvider { + return DefaultSchedulerProvider() +} + +private class DefaultSchedulerProvider : SchedulerProvider { + + override fun computation(): Scheduler { + return Schedulers.computation() + } + + override fun io(): Scheduler { + return Schedulers.io() + } + + override fun ui(): Scheduler { + return AndroidSchedulers.mainThread() + } + + override fun database(): Scheduler { + return Schedulers.single() + } + +} diff --git a/lib_base/src/main/java/com/android/base/rx/ThreadTransformer.java b/lib_base/src/main/java/com/android/base/rx/ThreadTransformer.java new file mode 100644 index 0000000..a83f93c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/rx/ThreadTransformer.java @@ -0,0 +1,85 @@ +package com.android.base.rx; + +import org.reactivestreams.Publisher; + +import io.reactivex.Completable; +import io.reactivex.CompletableSource; +import io.reactivex.CompletableTransformer; +import io.reactivex.Flowable; +import io.reactivex.FlowableTransformer; +import io.reactivex.Maybe; +import io.reactivex.MaybeSource; +import io.reactivex.MaybeTransformer; +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.ObservableTransformer; +import io.reactivex.Scheduler; +import io.reactivex.Single; +import io.reactivex.SingleSource; +import io.reactivex.SingleTransformer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-08-10 00:03 + */ +public final class ThreadTransformer implements ObservableTransformer, + FlowableTransformer, + SingleTransformer, + MaybeTransformer, + CompletableTransformer { + + static ThreadTransformer newInstance(int type) { + return new ThreadTransformer<>(type); + } + + static final int IO_UI = 1; + static final int COMPUTATION_UI = 2; + static final int NEW_THREAD_UI = 3; + + private final int mType; + + private ThreadTransformer(int type) { + mType = type; + if (!(mType == IO_UI || mType == COMPUTATION_UI || mType == NEW_THREAD_UI)) { + throw new IllegalArgumentException("type error: " + type); + } + } + + @Override + public CompletableSource apply(Completable upstream) { + return upstream.subscribeOn(getScheduler()).observeOn(AndroidSchedulers.mainThread()); + } + + @Override + public Publisher apply(Flowable upstream) { + return upstream.subscribeOn(getScheduler()).observeOn(AndroidSchedulers.mainThread()); + } + + @Override + public MaybeSource apply(Maybe upstream) { + return upstream.subscribeOn(getScheduler()).observeOn(AndroidSchedulers.mainThread()); + } + + @Override + public ObservableSource apply(Observable upstream) { + return upstream.subscribeOn(getScheduler()).observeOn(AndroidSchedulers.mainThread()); + } + + @Override + public SingleSource apply(Single upstream) { + return upstream.subscribeOn(getScheduler()).observeOn(AndroidSchedulers.mainThread()); + } + + private Scheduler getScheduler() { + if (mType == IO_UI) { + return Schedulers.io(); + } else if (mType == COMPUTATION_UI) { + return Schedulers.computation(); + } else { + return Schedulers.newThread(); + } + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/BaseUtils.java b/lib_base/src/main/java/com/android/base/utils/BaseUtils.java new file mode 100644 index 0000000..0a0650f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/BaseUtils.java @@ -0,0 +1,45 @@ +package com.android.base.utils; + +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.util.DisplayMetrics; + +import com.blankj.utilcode.util.Utils; + +/** + * 依赖 Content 的其他工具类都由 BaseUtils 提供 + */ +public class BaseUtils { + + public static void init(Context context) { + Utils.init(context); + } + + public static Context getAppContext() { + return Utils.getApp(); + } + + public static Resources getResources() { + return BaseUtils.getAppContext().getResources(); + } + + public static Resources.Theme getTheme() { + return BaseUtils.getAppContext().getTheme(); + } + + public static AssetManager getAssets() { + return BaseUtils.getAppContext().getAssets(); + } + + @SuppressWarnings("unused") + public static Configuration getConfiguration() { + return BaseUtils.getResources().getConfiguration(); + } + + public static DisplayMetrics getDisplayMetrics() { + return BaseUtils.getResources().getDisplayMetrics(); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/ActFragWrapper.java b/lib_base/src/main/java/com/android/base/utils/android/ActFragWrapper.java new file mode 100644 index 0000000..72bce69 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/ActFragWrapper.java @@ -0,0 +1,68 @@ +package com.android.base.utils.android; + +import android.app.Activity; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; + +/** + * 包含了Activity和Fragment的相关操作 + */ +public class ActFragWrapper { + + private Fragment mFragment; + private Activity mActivity; + + public static ActFragWrapper create(Activity activity) { + ActFragWrapper context = new ActFragWrapper(); + context.mActivity = activity; + return context; + } + + public Context getContext() { + if (mActivity != null) { + return mActivity; + } else { + return mFragment.getActivity(); + } + } + + public Fragment getFragment() { + return mFragment; + } + + public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { + if (mActivity != null) { + mActivity.startActivityForResult(intent, requestCode, options); + } else { + mFragment.startActivityForResult(intent, requestCode, options); + } + } + + public static ActFragWrapper create(Fragment fragment) { + ActFragWrapper context = new ActFragWrapper(); + context.mFragment = fragment; + return context; + } + + public void startService(Intent intent) { + if (mActivity != null) { + mActivity.startService(intent); + } else { + FragmentActivity activity = mFragment.getActivity(); + activity.startService(intent); + } + } + + public void stopService(Class payPalServiceClass) { + if (mActivity != null) { + mActivity.stopService(new Intent(mActivity, payPalServiceClass)); + } else { + mFragment.getActivity().stopService(new Intent(mFragment.getActivity(), payPalServiceClass)); + } + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/ClipboardUtils.java b/lib_base/src/main/java/com/android/base/utils/android/ClipboardUtils.java new file mode 100644 index 0000000..efc7c50 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/ClipboardUtils.java @@ -0,0 +1,88 @@ +package com.android.base.utils.android; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import com.android.base.utils.BaseUtils; + +public class ClipboardUtils { + + private ClipboardUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 复制文本到剪贴板 + * + * @param text 文本 + */ + public static void copyText(CharSequence text) { + ClipboardManager clipboard = (ClipboardManager) BaseUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText("text", text)); + } + + /** + * 获取剪贴板的文本 + * + * @return 剪贴板的文本 + */ + public static CharSequence getText() { + ClipboardManager clipboard = (ClipboardManager) BaseUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = clipboard.getPrimaryClip(); + if (clip != null && clip.getItemCount() > 0) { + return clip.getItemAt(0).coerceToText(BaseUtils.getAppContext()); + } + return null; + } + + /** + * 复制uri到剪贴板 + * + * @param uri uri + */ + public static void copyUri(Uri uri) { + ClipboardManager clipboard = (ClipboardManager) BaseUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newUri(BaseUtils.getAppContext().getContentResolver(), "uri", uri)); + } + + /** + * 获取剪贴板的uri + * + * @return 剪贴板的uri + */ + public static Uri getUri() { + ClipboardManager clipboard = (ClipboardManager) BaseUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = clipboard.getPrimaryClip(); + if (clip != null && clip.getItemCount() > 0) { + return clip.getItemAt(0).getUri(); + } + return null; + } + + /** + * 复制意图到剪贴板 + * + * @param intent 意图 + */ + public static void copyIntent(Intent intent) { + ClipboardManager clipboard = (ClipboardManager) BaseUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newIntent("intent", intent)); + } + + /** + * 获取剪贴板的意图 + * + * @return 剪贴板的意图 + */ + public static Intent getIntent() { + ClipboardManager clipboard = (ClipboardManager) BaseUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = clipboard.getPrimaryClip(); + if (clip != null && clip.getItemCount() > 0) { + return clip.getItemAt(0).getIntent(); + } + return null; + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/DebugUtils.java b/lib_base/src/main/java/com/android/base/utils/android/DebugUtils.java new file mode 100644 index 0000000..3e3a909 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/DebugUtils.java @@ -0,0 +1,32 @@ +package com.android.base.utils.android; + +import android.os.StrictMode; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-10-12 18:32 + */ +public class DebugUtils { + + private DebugUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 开启严苛模式 + */ + public static void startStrictMode() { + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyLog() + .build()); + + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog() + .penaltyDeathOnNetwork() + .build()); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/DevicesUtils.java b/lib_base/src/main/java/com/android/base/utils/android/DevicesUtils.java new file mode 100644 index 0000000..b4e2c26 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/DevicesUtils.java @@ -0,0 +1,127 @@ +package com.android.base.utils.android; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.os.Build; +import android.support.annotation.RequiresPermission; +import android.util.Log; + +import com.android.base.utils.android.compat.AndroidVersion; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.UUID; + +import timber.log.Timber; + +public class DevicesUtils { + + private DevicesUtils() { + throw new UnsupportedOperationException("no need instantiation"); + } + + public static String getModel() { + String model = Build.MODEL; + if (model != null) { + model = model.trim().replaceAll("\\s*", ""); + } else { + model = ""; + } + return model; + } + + /** + * for getDeviceId,DevicesId 形式: + *
+     *      00000000-1082-436b-ffff-ffffc2e337a1
+     *      ffffffff-a987-897a-0000-000000de11f7
+     * 
+ */ + private static final String M_SZ_DEV_ID_SHORT = "35" + + Build.BOARD.length() % 10 + Build.BRAND.length() % 10 + + Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 + + Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 + + Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 + + Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 + + Build.TAGS.length() % 10 + Build.TYPE.length() % 10 + + Build.USER.length() % 10; //13 位 + + /** + * 参考:http://blog.csdn.net/nugongahou110/article/details/47003257 + * 参考:https://stackoverflow.com/questions/2785485/is-there-a-unique-android-device-id/2853253#2853253 + * 参考:https://www.jianshu.com/p/b6f4b0aca6b0 + * + * @return device id + */ + @RequiresPermission(value = Manifest.permission.READ_PHONE_STATE) + public static String getDeviceId() { + String serial = null; + try { + if (AndroidVersion.atLeast(26)) { + //add in api 26, need Permission.READ_PHONE_STATE + serial = Build.getSerial(); + } else { + serial = Build.class.getField("SERIAL").get(null).toString(); + } + } catch (Exception e) { + Timber.e(e,"getDeviceId"); + } + if (serial == null) { + //serial 需要一个初始化 + serial = "serial"; // 随便一个初始化 + } + //使用硬件信息拼凑出来的15位号码 + return new UUID(M_SZ_DEV_ID_SHORT.hashCode(), serial.hashCode()).toString(); + } + + + @SuppressLint("ObsoleteSdkInt") + public static String printSystemInfo() { + Date date = new Date(System.currentTimeMillis()); + @SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String time = dateFormat.format(date); + StringBuilder sb = new StringBuilder(); + sb.append("_______ 系统信息 ").append(time).append(" ______________"); + sb.append("\nID :").append(Build.ID); + sb.append("\nBRAND :").append(Build.BRAND); + sb.append("\nMODEL :").append(Build.MODEL); + sb.append("\nRELEASE :").append(Build.VERSION.RELEASE); + sb.append("\nSDK :").append(Build.VERSION.SDK); + + sb.append("\n_______ OTHER _______"); + sb.append("\nBOARD :").append(Build.BOARD); + sb.append("\nPRODUCT :").append(Build.PRODUCT); + sb.append("\nDEVICE :").append(Build.DEVICE); + sb.append("\nFINGERPRINT :").append(Build.FINGERPRINT); + sb.append("\nHOST :").append(Build.HOST); + sb.append("\nTAGS :").append(Build.TAGS); + sb.append("\nTYPE :").append(Build.TYPE); + sb.append("\nTIME :").append(Build.TIME); + sb.append("\nINCREMENTAL :").append(Build.VERSION.INCREMENTAL); + + sb.append("\n_______ CUPCAKE-3 _______"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) { + sb.append("\nDISPLAY :").append(Build.DISPLAY); + } + + sb.append("\n_______ DONUT-4 _______"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.DONUT) { + sb.append("\nSDK_INT :").append(Build.VERSION.SDK_INT); + sb.append("\nMANUFACTURER :").append(Build.MANUFACTURER); + sb.append("\nBOOTLOADER :").append(Build.BOOTLOADER); + sb.append("\nCPU_ABI :").append(Build.CPU_ABI); + sb.append("\nCPU_ABI2 :").append(Build.CPU_ABI2); + sb.append("\nHARDWARE :").append(Build.HARDWARE); + sb.append("\nUNKNOWN :").append(Build.UNKNOWN); + sb.append("\nCODENAME :").append(Build.VERSION.CODENAME); + } + + sb.append("\n_______ GINGERBREAD-9 _______"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + sb.append("\nSERIAL :").append(Build.SERIAL); + } + Log.i("DEVICES", sb.toString()); + return sb.toString(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/DirectoryUtils.java b/lib_base/src/main/java/com/android/base/utils/android/DirectoryUtils.java new file mode 100644 index 0000000..8eae586 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/DirectoryUtils.java @@ -0,0 +1,69 @@ +package com.android.base.utils.android; + +import android.os.Environment; +import android.support.annotation.NonNull; + +import com.android.base.utils.BaseUtils; + +import java.io.File; + +import timber.log.Timber; + +public class DirectoryUtils { + + private DirectoryUtils() { + } + + /** + * 获取SD卡上私有的外部存储 + * + * @return /storage/emulated/0/Android/data/包名/cache/ + */ + public static File getAppExternalCacheStorage() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state)) { + return BaseUtils.getAppContext().getExternalCacheDir(); + } else { + return BaseUtils.getAppContext().getCacheDir(); + } + } + + /** + * 获取SD卡上外部存储 + * + * @return /storage/emulated/0/ + */ + public static File getExternalStorage() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state)) { + return Environment.getExternalStorageDirectory(); + } else { + return BaseUtils.getAppContext().getCacheDir(); + } + } + + /** + * 获取公共的外部存储目录 + * + * @param type {@link Environment#DIRECTORY_DOWNLOADS}, + * {@link Environment#DIRECTORY_DCIM}, ect + * @return DIRECTORY_DCIM = /storage/sdcard0/DCIM , + * DIRECTORY_DOWNLOADS = /storage/sdcard0/Download ...ect + */ + public static File getExternalStoragePublicDirectory(@NonNull String type) { + String state = Environment.getExternalStorageState(); + File dir; + if (Environment.MEDIA_MOUNTED.equals(state)) { + dir = Environment.getExternalStoragePublicDirectory(type); + } else { + dir = new File(BaseUtils.getAppContext().getCacheDir(), type); + } + if (dir != null && !dir.exists()) { + boolean mkdirs = dir.mkdirs(); + Timber.d("getExternalStoragePublicDirectory type = " + type + " mkdirs = " + mkdirs); + } + return dir; + } + + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/ResourceUtils.java b/lib_base/src/main/java/com/android/base/utils/android/ResourceUtils.java new file mode 100644 index 0000000..6e0ce78 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/ResourceUtils.java @@ -0,0 +1,82 @@ +package com.android.base.utils.android; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.support.annotation.ArrayRes; +import android.support.annotation.AttrRes; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; + +import com.android.base.utils.BaseUtils; + + +public class ResourceUtils { + + private ResourceUtils() { + throw new UnsupportedOperationException("no need instantiation"); + } + + /** + * @param name 资源的名称,如 ic_launcher 或者 com.example.android/drawable/ic_launcher(这是,下面两个参数可以省略) + * @param defType 资源的类型,如 drawable + * @param defPackage 包名 + * @return 资源id + */ + public static int getResource(String name, String defType, String defPackage) { + return BaseUtils.getResources().getIdentifier(name, defType, defPackage); + } + + public static CharSequence getText(@StringRes int id) { + return BaseUtils.getResources().getText(id); + } + + public static String getString(@StringRes int id) { + return BaseUtils.getResources().getString(id); + } + + public static String getString(@StringRes int id, Object... formatArgs) { + return BaseUtils.getResources().getString(id, formatArgs); + } + + public static String[] getStringArray(@ArrayRes int id) { + return BaseUtils.getResources().getStringArray(id); + } + + + public static int[] getIntArray(@ArrayRes int id) { + return BaseUtils.getResources().getIntArray(id); + } + + + public static Uri createUriByResource(int id) { + return Uri.parse("android.resource://" + BaseUtils.getAppContext().getPackageName() + "/" + id); + } + + public static Uri createUriByAssets(String path) { + return Uri.parse("file:///android_asset/" + path); + } + + public static int getStyledColor(@NonNull Context context, @AttrRes int attr) { + TypedArray a = context.obtainStyledAttributes(null, new int[]{attr}); + try { + return a.getColor(0, 0x000000); + } finally { + a.recycle(); + } + } + + public static Drawable getStyledDrawable(@NonNull Context context, @AttrRes int attr) { + TypedArray a = context.obtainStyledAttributes(null, new int[]{attr}); + try { + return a.getDrawable(0); + } finally { + a.recycle(); + } + } + + public static int getDimensPixelSize(int dimenId) { + return BaseUtils.getResources().getDimensionPixelSize(dimenId); + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/SoftKeyboardUtils.java b/lib_base/src/main/java/com/android/base/utils/android/SoftKeyboardUtils.java new file mode 100644 index 0000000..af6bdf0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/SoftKeyboardUtils.java @@ -0,0 +1,85 @@ +package com.android.base.utils.android; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class SoftKeyboardUtils { + + private SoftKeyboardUtils() { + throw new UnsupportedOperationException("no need instantiation"); + } + + public static void toggleSoftInput(Context context) { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); + } + + public static boolean showSoftInput(View view) { + InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.showSoftInput(view, InputMethodManager.SHOW_FORCED); + } + + public static boolean showSoftInput(Activity activity) { + View view = activity.getCurrentFocus(); + if (view != null) { + InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + return imm.showSoftInput(view, InputMethodManager.SHOW_FORCED); + } + return false; + } + + public static boolean hideSoftInput(View view) { + InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + public static boolean hideSoftInput(Activity activity) { + if (activity.getCurrentFocus() != null) { + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(), 0); + } + return false; + } + + public static boolean isActive(Context context) { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.isActive(); + } + + @SuppressWarnings("all") + public static void showErrorImmediately(String error, TextView editText) { + editText.setError(error); + try { + Field mEditor = TextView.class.getDeclaredField("mEditor"); + if (mEditor != null) { + mEditor.setAccessible(true); + Object editor = mEditor.get(editText); + if (editor != null) { + Method showError = editor.getClass().getDeclaredMethod("showError"); + if (showError != null) { + showError.setAccessible(true); + showError.invoke(editor); + } + } + } + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/SpCache.java b/lib_base/src/main/java/com/android/base/utils/android/SpCache.java new file mode 100644 index 0000000..5219e5a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/SpCache.java @@ -0,0 +1,209 @@ +package com.android.base.utils.android; + +import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; +import android.util.Log; + +import com.android.base.utils.BaseUtils; + +import java.lang.reflect.Method; + +import timber.log.Timber; + +/** + * modified from hongyangAndroid/SpCache + */ +public class SpCache { + + private static final String TAG = SpCache.class.getSimpleName(); + + private final SharedPreferences mSharedPreferences; + private final boolean mUseApply; + + public SpCache(String prefFileName) { + this(BaseUtils.getAppContext(), prefFileName, true); + } + + public SpCache(String prefFileName, boolean useApply) { + this(BaseUtils.getAppContext(), prefFileName, useApply); + } + + public SpCache(Context context, String prefFileName, boolean useApply) { + if (TextUtils.isEmpty(prefFileName)) { + throw new NullPointerException("SpCache get fileName = null"); + } + mSharedPreferences = context.getSharedPreferences(prefFileName, Context.MODE_PRIVATE); + mUseApply = useApply; + } + + //put + public SpCache putInt(String key, int val) { + return put(key, val); + } + + public SpCache putLong(String key, long val) { + return put(key, val); + } + + public SpCache putString(String key, String val) { + return put(key, val); + } + + public SpCache putBoolean(String key, boolean val) { + return put(key, val); + } + + public SpCache putFloat(String key, float val) { + return put(key, val); + } + + //get + public int getInt(String key, int defaultVal) { + return (int) (get(key, defaultVal)); + } + + public long getLong(String key, long defaultVal) { + return (long) (get(key, defaultVal)); + } + + public String getString(String key, String defaultVal) { + return (String) (get(key, defaultVal)); + } + + public boolean getBoolean(String key, boolean defaultVal) { + return (boolean) (get(key, defaultVal)); + } + + public float getFloat(String key, float defaultVal) { + return (float) (get(key, defaultVal)); + } + + //contains + public boolean contains(String key) { + return getSharedPreferences().contains(key); + } + + //remove + public SpCache remove(String key) { + return _remove(key); + } + + private SpCache _remove(String key) { + SharedPreferences.Editor editor = getSharedPreferences().edit(); + editor.remove(key); + SharedPreferencesCompat.apply(editor, mUseApply); + return this; + } + + //clear + public SpCache clear() { + return _clear(); + } + + private SpCache _clear() { + SharedPreferences.Editor editor = getSharedPreferences().edit(); + editor.clear(); + SharedPreferencesCompat.apply(editor, mUseApply); + return this; + } + + private SpCache put(String key, T t) { + SharedPreferences.Editor editor = getSharedPreferences().edit(); + if (t instanceof String) { + editor.putString(key, (String) t); + } else if (t instanceof Integer) { + editor.putInt(key, (Integer) t); + } else if (t instanceof Boolean) { + editor.putBoolean(key, (Boolean) t); + } else if (t instanceof Float) { + editor.putFloat(key, (Float) t); + } else if (t instanceof Long) { + editor.putLong(key, (Long) t); + } else { + Timber.d("you may be put a invalid object :" + t); + editor.putString(key, t.toString()); + } + SharedPreferencesCompat.apply(editor, mUseApply); + return this; + } + + + private Object readDisk(String key, Object defaultObject) { + Log.e("TAG", "readDisk"); + SharedPreferences sp = getSharedPreferences(); + + if (defaultObject instanceof String) { + return sp.getString(key, (String) defaultObject); + } else if (defaultObject instanceof Integer) { + return sp.getInt(key, (Integer) defaultObject); + } else if (defaultObject instanceof Boolean) { + return sp.getBoolean(key, (Boolean) defaultObject); + } else if (defaultObject instanceof Float) { + return sp.getFloat(key, (Float) defaultObject); + } else if (defaultObject instanceof Long) { + return sp.getLong(key, (Long) defaultObject); + } + Log.e(TAG, "you can not read object , which class is " + defaultObject.getClass().getSimpleName()); + return null; + + } + + private Object get(String key, Object defaultVal) { + return readDisk(key, defaultVal); + } + + /** + * 创建一个解决SharedPreferencesCompat.apply方法的一个兼容类 + * + * @author zhy + */ + private static class SharedPreferencesCompat { + private static final Method sApplyMethod = findApplyMethod(); + + /** + * 反射查找apply的方法 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Method findApplyMethod() { + try { + Class clz = SharedPreferences.Editor.class; + return clz.getMethod("apply"); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 如果找到则使用apply执行,否则使用commit + */ + public static void apply(final SharedPreferences.Editor editor, boolean useApply) { + if (useApply) { + try { + if (sApplyMethod != null) { + sApplyMethod.invoke(editor); + } + } catch (Exception e) { + e.printStackTrace(); + editor.commit(); + } + } else { + editor.commit(); + } + } + } + + private SharedPreferences getSharedPreferences() { + return mSharedPreferences; + } + + public void registerOnSharedPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener) { + getSharedPreferences().registerOnSharedPreferenceChangeListener(listener); + } + + public void unregisterOnSharedPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener) { + getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener); + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/TintUtils.java b/lib_base/src/main/java/com/android/base/utils/android/TintUtils.java new file mode 100644 index 0000000..75e1c89 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/TintUtils.java @@ -0,0 +1,74 @@ +package com.android.base.utils.android; + +import android.content.res.ColorStateList; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.support.v4.graphics.drawable.DrawableCompat; + +/** + * usage: + *
+ *     {@code
+ *
+ *              int[][] states = new int[][]{
+ *                  new int[]{android.R.attr.state_selected}, // pressed
+ *                  new int[]{-android.R.attr.state_selected}  // unpressed
+ *             };
+ *
+ *              int[] colors = new int[]{
+ *                  Color.BLACK,
+ *                  Color.GRAY
+ *              };
+ *
+ *              ColorStateList colorStateList = new ColorStateList(states, colors);
+ *              Drawable aaa = ContextCompat.getDrawable(getContext(), R.drawable.aaa);
+ *              Drawable bbb = ContextCompat.getDrawable(getContext(), R.drawable.bbb);
+ *              Drawable aaaTint = TintUtils.tint(aaa, colorStateList);
+ *              Drawable bbbTint = TintUtils.tint(bbb, colorStateList);
+ *              aaaTv.setCompoundDrawablesWithIntrinsicBounds(null, aaaTint, null, null);
+ *              bbbTv.setCompoundDrawablesWithIntrinsicBounds(null, bbbTint, null, null);
+ * 
+ */ +public class TintUtils { + + private TintUtils() { + } + + public static Drawable tint(Drawable originDrawable, int color) { + return tint(originDrawable, ColorStateList.valueOf(color)); + } + + public static Drawable tint(Drawable originDrawable, int color, PorterDuff.Mode tintMode) { + return tint(originDrawable, ColorStateList.valueOf(color), tintMode); + } + + public static Drawable tint(Drawable originDrawable, ColorStateList colorStateList) { + return tint(originDrawable, colorStateList, null); + } + + public static Drawable tint(Drawable originDrawable, ColorStateList colorStateList, PorterDuff.Mode tintMode) { + Drawable tintDrawable = DrawableCompat.wrap(originDrawable); + if (tintMode != null) { + DrawableCompat.setTintMode(tintDrawable, tintMode); + } + DrawableCompat.setTintList(tintDrawable, colorStateList); + return tintDrawable; + } + + public static Bitmap tintBitmap(Bitmap inBitmap, int tintColor) { + if (inBitmap == null) { + return null; + } + Bitmap outBitmap = Bitmap.createBitmap(inBitmap.getWidth(), inBitmap.getHeight(), inBitmap.getConfig()); + Canvas canvas = new Canvas(outBitmap); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setColorFilter(new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(inBitmap, 0, 0, paint); + return outBitmap; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/UnitConverter.java b/lib_base/src/main/java/com/android/base/utils/android/UnitConverter.java new file mode 100644 index 0000000..1d53afd --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/UnitConverter.java @@ -0,0 +1,72 @@ +package com.android.base.utils.android; + + +import android.util.DisplayMetrics; +import android.util.TypedValue; + +import com.android.base.utils.BaseUtils; + +public class UnitConverter { + + private UnitConverter() { + throw new UnsupportedOperationException("no need instantiation"); + } + + public static float dpToPx(float dp) { + return dp * BaseUtils.getDisplayMetrics().density; + } + + public static int dpToPx(int dp) { + return (int) (dp * BaseUtils.getDisplayMetrics().density + 0.5f); + } + + public static float pxToDp(float px) { + return px / BaseUtils.getDisplayMetrics().density; + } + + public static int pxToDp(int px) { + return (int) (px / BaseUtils.getDisplayMetrics().density + 0.5f); + } + + public static float spToPx(float sp) { + return sp * BaseUtils.getDisplayMetrics().scaledDensity; + } + + public static int spToPx(int sp) { + return (int) (sp * BaseUtils.getDisplayMetrics().scaledDensity + 0.5f); + } + + public static float pxToSp(float px) { + return px / BaseUtils.getDisplayMetrics().scaledDensity; + } + + public static int pxToSp(int px) { + return (int) (px / BaseUtils.getDisplayMetrics().scaledDensity + 0.5f); + } + + /** + * 各种单位转换,该方法存在于{@link TypedValue} 中 + * + * @param unit 单位 + * @param value 值 + * @return 转换结果 + */ + public static float applyDimension(int unit, float value) { + DisplayMetrics metrics = BaseUtils.getDisplayMetrics(); + switch (unit) { + case TypedValue.COMPLEX_UNIT_PX: + return value; + case TypedValue.COMPLEX_UNIT_DIP: + return value * metrics.density; + case TypedValue.COMPLEX_UNIT_SP: + return value * metrics.scaledDensity; + case TypedValue.COMPLEX_UNIT_PT: + return value * metrics.xdpi * (1.0f / 72); + case TypedValue.COMPLEX_UNIT_IN: + return value * metrics.xdpi; + case TypedValue.COMPLEX_UNIT_MM: + return value * metrics.xdpi * (1.0f / 25.4f); + } + return 0; + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/VideoUtils.java b/lib_base/src/main/java/com/android/base/utils/android/VideoUtils.java new file mode 100644 index 0000000..acac226 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/VideoUtils.java @@ -0,0 +1,24 @@ +package com.android.base.utils.android; + +import android.content.Context; +import android.graphics.Bitmap; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.support.annotation.WorkerThread; + +public class VideoUtils { + + private VideoUtils() { + } + + @WorkerThread + public static Bitmap createVideoThumbnail(final Context context, final Uri uri) { + Bitmap bitmap; + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(context, uri); + bitmap = retriever.getFrameAtTime(); + retriever.release(); + return bitmap; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/ViewUtils.java b/lib_base/src/main/java/com/android/base/utils/android/ViewUtils.java new file mode 100644 index 0000000..c5d15e4 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/ViewUtils.java @@ -0,0 +1,207 @@ +package com.android.base.utils.android; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Picture; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.IdRes; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; +import android.widget.TextView; + +public class ViewUtils { + + private ViewUtils() { + } + + private static final int ACTION_VISIBLE = 0x01; + private static final int ACTION_GONE = 0x02; + private static final int ACTION_INVISIBLE = 0x03; + private static final int ACTION_DISABLE = 0x04; + private static final int ACTION_ENABLE = 0x05; + + public static boolean measureWithMaxSize(View view) { + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + if (layoutParams == null || (layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT && layoutParams.height == ViewGroup.LayoutParams.MATCH_PARENT)) { + return false; + } + int size = 1 << 30 - 1;//即后30位 + int measureSpec = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.AT_MOST); + view.measure(measureSpec, measureSpec); + return true; + } + + public static boolean measureWithScreenSize(View view) { + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + if (layoutParams == null || (layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT && layoutParams.height == ViewGroup.LayoutParams.MATCH_PARENT)) { + return false; + } + view.measure( + View.MeasureSpec.makeMeasureSpec(WindowUtils.getScreenWidth(), View.MeasureSpec.AT_MOST), + View.MeasureSpec.makeMeasureSpec(WindowUtils.getScreenHeight(), View.MeasureSpec.AT_MOST)); + return true; + } + + public static boolean measureWithSize(View view, int width, int height) { + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + if (layoutParams == null || (layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT && layoutParams.height == ViewGroup.LayoutParams.MATCH_PARENT)) { + return false; + } + view.measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.AT_MOST), + View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST)); + return true; + } + + + public static void disable(View view1, View view2) { + view1.setEnabled(false); + view2.setEnabled(false); + } + + public static void disable(View view1, View view2, View view3) { + view1.setEnabled(false); + view2.setEnabled(false); + view3.setEnabled(false); + } + + public static void disable(View view1, View view2, View view3, View... views) { + view1.setEnabled(false); + view2.setEnabled(false); + view3.setEnabled(false); + doAction(ACTION_DISABLE, views); + } + + public static void enable(View view1, View view2) { + view1.setEnabled(true); + view2.setEnabled(true); + } + + public static void enable(View view1, View view2, View view3) { + view1.setEnabled(true); + view2.setEnabled(true); + view3.setEnabled(true); + } + + public static void enable(View view1, View view2, View view3, View... views) { + view1.setEnabled(true); + view2.setEnabled(true); + view3.setEnabled(true); + doAction(ACTION_ENABLE, views); + } + + + public static void gone(View view1, View view2) { + view1.setVisibility(View.GONE); + view2.setVisibility(View.GONE); + } + + public static void gone(View view1, View view2, View view3) { + view1.setVisibility(View.GONE); + view2.setVisibility(View.GONE); + view3.setVisibility(View.GONE); + } + + public static void gone(View view1, View view2, View view3, View... views) { + view1.setVisibility(View.GONE); + view2.setVisibility(View.GONE); + view3.setVisibility(View.GONE); + doAction(ACTION_GONE, views); + } + + public static void visible(View view1, View view2) { + view1.setVisibility(View.VISIBLE); + view2.setVisibility(View.VISIBLE); + } + + public static void visible(View view1, View view2, View view3) { + view1.setVisibility(View.VISIBLE); + view2.setVisibility(View.VISIBLE); + view3.setVisibility(View.VISIBLE); + } + + public static void visible(View view1, View view2, View view3, View... views) { + view1.setVisibility(View.VISIBLE); + view2.setVisibility(View.VISIBLE); + view3.setVisibility(View.VISIBLE); + doAction(ACTION_VISIBLE, views); + } + + public static void invisible(View view1, View view2) { + view1.setVisibility(View.INVISIBLE); + view2.setVisibility(View.INVISIBLE); + } + + public static void invisible(View view1, View view2, View view3) { + view1.setVisibility(View.INVISIBLE); + view2.setVisibility(View.INVISIBLE); + view3.setVisibility(View.INVISIBLE); + } + + public static void invisible(View view1, View view2, View view3, View... views) { + view1.setVisibility(View.INVISIBLE); + view2.setVisibility(View.INVISIBLE); + view3.setVisibility(View.INVISIBLE); + doAction(ACTION_INVISIBLE, views); + } + + private static void doAction(int action, View... views) { + for (View view : views) { + if (action == ACTION_GONE) { + view.setVisibility(View.GONE); + } else if (action == ACTION_INVISIBLE) { + view.setVisibility(View.INVISIBLE); + } else if (action == ACTION_VISIBLE) { + view.setVisibility(View.VISIBLE); + } else if (action == ACTION_ENABLE) { + view.setEnabled(true); + } else if (action == ACTION_DISABLE) { + view.setEnabled(false); + } + } + } + + public static void setBackgroundDrawable(View view, Drawable drawable) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + view.setBackground(drawable); + } else { + view.setBackgroundDrawable(drawable); + } + } + + public static V find(View view, @IdRes int viewId) { + @SuppressWarnings("unchecked") + V v = (V) view.findViewById(viewId); + return v; + } + + public static Bitmap captureBitmapFromWebView(WebView webView) { + Picture snapShot = webView.capturePicture(); + Bitmap bmp = Bitmap.createBitmap(snapShot.getWidth(), snapShot.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bmp); + snapShot.draw(canvas); + return bmp; + } + + public static void clearTextDrawable(TextView textView) { + textView.setCompoundDrawables(null, null, null, null); + } + + @Nullable + public static FragmentActivity getRealContext(View view) { + Context context = view.getContext(); + while (context instanceof android.content.ContextWrapper) { + if (context instanceof FragmentActivity) { + return (FragmentActivity) context; + } + context = ((android.content.ContextWrapper) context).getBaseContext(); + } + return null; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/WebViewUtils.java b/lib_base/src/main/java/com/android/base/utils/android/WebViewUtils.java new file mode 100644 index 0000000..984d5d9 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/WebViewUtils.java @@ -0,0 +1,123 @@ +package com.android.base.utils.android; + +import android.content.Context; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.WebView; + +/** + *
+ * 关于内存泄漏:最好的方式还是开启独立的进程
+ *
+ * 关于Cookie:
+ *
+ *          之前同步 cookie 需要用到 CookieSyncManager 类,现在这个类已经被抛弃了。
+ *          现在WebView已经可以在需要的时候自动同步 cookie 了,
+ *          所以不再需要创建 CookieSyncManager 类的对象来进行强制性的同步 cookie 了。
+ *          现在只需要获得 CookieManager 的对象将 cookie 设置进去就可以了。
+ *
+ *          从服务器的返回头中取出 cookie 根据Http请求的客户端不同,获取 cookie 的方式也不同。比如以HttpURLCollection
+ *
+ *          
+ *               String cookieStr = conn.getHeaderField("Set-Cookie");
+ *          
+ *
+ *          步骤:
+ *                   1:利用HttpClient进行api请求登录,然后获取cookie
+ *                   2:调用syncCookie分发把cookie写入到WebCookie的数据库中
+ * 
+ */ +public class WebViewUtils { + + private static final String TAG = WebViewUtils.class.getSimpleName(); + + private WebViewUtils() { + throw new UnsupportedOperationException(); + } + + public static void destroy(WebView webView) { + try { + if (webView == null) { + return; + } + webView.setWebChromeClient(null); + webView.setWebViewClient(null); + webView.onPause(); + if (webView.getParent() != null) { + ((ViewGroup) webView.getParent()).removeAllViews(); + } + webView.setVisibility(View.GONE);//解决崩溃问题 Receiver not Register + webView.removeAllViews(); + if (webView.getHandler() != null) { + webView.getHandler().removeCallbacks(null); + } + webView.pauseTimers(); + webView.destroy(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 将cookie设置到WebView, 客户端通过以下代码设置cookie,如果两次设置相同,会覆盖上一次的 + * + * @param url 要加载的 url + * @param cookie 要同步的 cookie + */ + public static void syncCookie(String url, String cookie, Context context) { + /* + 注意: + 1,同步 cookie 要在 WebView 加载 url 之前,否则 WebView 无法获得相应的 cookie,也就无法通过验证。 + 2,cookie应该被及时更新,否则很可能导致WebView拿的是旧的session id和服务器进行通信。 + 3,CookieManager会将这个Cookie存入该应用程序data/data/package_name/app_WebView/Cookies.db + */ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + CookieSyncManager.createInstance(context); + } + CookieManager cookieManager = CookieManager.getInstance(); + cookieManager.setAcceptCookie(true); + cookieManager.setCookie(url, cookie);//如果没有特殊需求,这里只需要将session id以"key=value"形式作为cookie即可 + CookieSyncManager.getInstance().sync(); + } + + /** + * 获取指定 url 的cookie + *
+     * 打开网页,WebView从数据库中读取该cookie值,放到http请求的头部,传递到服务器
+     * 
+ */ + public static String getCookie(String url) { + CookieManager cookieManager = CookieManager.getInstance(); + return cookieManager.getCookie(url); + } + + public static void clearCookie(Context context) { + // 这个两个在 API level 21 被抛弃 + CookieSyncManager.createInstance(context); + CookieManager.getInstance().removeSessionCookie(); + CookieManager.getInstance().removeAllCookie(); + CookieSyncManager.getInstance().sync(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + clearCookie21(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private static void clearCookie21() { + // 推荐使用这两个, level 21 新加的 + // 移除所有过期 cookie + CookieManager.getInstance().removeSessionCookies(aBoolean -> { + Log.d(TAG, "clearCookie21 removeSessionCookies:" + aBoolean); + }); + // 移除所有的 cookie + CookieManager.getInstance().removeAllCookies(aBoolean -> { + Log.d(TAG, "clearCookie21 removeAllCookies:" + aBoolean); + }); + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/WindowUtils.java b/lib_base/src/main/java/com/android/base/utils/android/WindowUtils.java new file mode 100644 index 0000000..48f182f --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/WindowUtils.java @@ -0,0 +1,157 @@ +package com.android.base.utils.android; + +import android.app.Activity; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.util.DisplayMetrics; +import android.view.Surface; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import com.android.base.utils.BaseUtils; +import com.android.base.utils.android.compat.SystemBarCompat; + +/** + * 窗口工具箱 + */ +public final class WindowUtils { + + /** + * Don't let anyone instantiate this class. + */ + private WindowUtils() { + throw new Error("Do not need instantiate!"); + } + + /** + * 获取当前窗口的旋转角度 + */ + public static int getDisplayRotation(Activity activity) { + switch (activity.getWindowManager().getDefaultDisplay().getRotation()) { + case Surface.ROTATION_0: + return 0; + case Surface.ROTATION_90: + return 90; + case Surface.ROTATION_180: + return 180; + case Surface.ROTATION_270: + return 270; + default: + return 0; + } + } + + /** + * 当前是否是横屏 + */ + public static boolean isLandscape(Context context) { + return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + } + + /** + * 当前是否是竖屏 + */ + public static boolean isPortrait(Context context) { + return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + } + + public static int getScreenWidth() { + WindowManager wm = (WindowManager) BaseUtils.getAppContext().getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics outMetrics = new DisplayMetrics(); + if (wm != null) { + wm.getDefaultDisplay().getMetrics(outMetrics); + } + return outMetrics.widthPixels; + } + + public static int getScreenHeight() { + WindowManager wm = (WindowManager) BaseUtils.getAppContext().getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics outMetrics = new DisplayMetrics(); + if (wm != null) { + wm.getDefaultDisplay().getMetrics(outMetrics); + } + return outMetrics.heightPixels; + } + + /** + * FullScreen: + *
+     *          4.0 以及之前设置全屏:
+     *              1:使用全屏的主题
+     *              2:getWindow().addFlag(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+     * 
+ */ + public static void setFullScreen(AppCompatActivity appCompatActivity, boolean fullScreen) { + Window win = appCompatActivity.getWindow(); + WindowManager.LayoutParams winParams = win.getAttributes(); + final int bits = WindowManager.LayoutParams.FLAG_FULLSCREEN; + if (fullScreen) { + winParams.flags |= bits; + } else { + winParams.flags &= ~bits; + } + win.setAttributes(winParams); + ActionBar supportActionBar = appCompatActivity.getSupportActionBar(); + if (supportActionBar != null) { + if (fullScreen) { + supportActionBar.hide(); + } else { + supportActionBar.show(); + } + } + } + + /** + * 获取当前屏幕截图,包含状态栏 + * + * @param activity activity + * @return Bitmap + */ + public static Bitmap captureWithStatusBar(Activity activity) { + View view = activity.getWindow().getDecorView(); + view.setDrawingCacheEnabled(true); + view.buildDrawingCache(); + Bitmap bmp = view.getDrawingCache(); + DisplayMetrics dm = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(dm); + Bitmap ret = Bitmap.createBitmap(bmp, 0, 0, dm.widthPixels, dm.heightPixels); + view.destroyDrawingCache(); + return ret; + } + + /** + * 获取当前屏幕截图,不包含状态栏 + * + * @param activity activity + * @return Bitmap + */ + public static Bitmap captureWithoutStatusBar(Activity activity) { + View view = activity.getWindow().getDecorView(); + view.setDrawingCacheEnabled(true); + view.buildDrawingCache(); + Bitmap bmp = view.getDrawingCache(); + int statusBarHeight = SystemBarCompat.getStatusBarHeight(activity); + DisplayMetrics dm = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(dm); + Bitmap ret = Bitmap.createBitmap(bmp, 0, statusBarHeight, dm.widthPixels, dm.heightPixels - statusBarHeight); + view.destroyDrawingCache(); + return ret; + } + + /** + * 判断是否锁屏 + * + * @return {@code true}: 是
{@code false}: 否 + */ + public static boolean isScreenLock() { + KeyguardManager km = (KeyguardManager) BaseUtils.getAppContext().getSystemService(Context.KEYGUARD_SERVICE); + return km != null && km.inKeyguardRestrictedInputMode(); + } + + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/XAppUtils.java b/lib_base/src/main/java/com/android/base/utils/android/XAppUtils.java new file mode 100644 index 0000000..bd3cd0b --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/XAppUtils.java @@ -0,0 +1,129 @@ +package com.android.base.utils.android; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.v4.content.FileProvider; +import android.webkit.MimeTypeMap; + +import com.android.base.utils.common.FileUtils; +import com.android.base.utils.common.StringChecker; + +import java.io.File; +import java.util.List; + +import timber.log.Timber; + +/** + * 对 {@link com.blankj.utilcode.util.AppUtils} 的补充 + */ +public class XAppUtils { + + private XAppUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 安装App,支持 Android 6.0 FileProvider。 + * + * @param context caller + * @param file 文件 + * @param authority FileProvider authorities, default is {@code PackageName + ".fileProvider"} + */ + public static boolean installApp(Context context, File file, @Nullable String authority) { + if (file == null || !file.exists()) { + return false; + } + try { + if (Build.VERSION.SDK_INT <= 23) { + context.startActivity(getInstallAppIntent23(file)); + } else { + Intent intent = getInstallAppIntent24(context, file, StringChecker.isEmpty(authority) ? (context.getPackageName() + ".fileProvider") : authority); + context.startActivity(intent); + } + Timber.d("installApp open activity successfully"); + return true; + } catch (Exception e) { + Timber.e(e, "installApp"); + } + return false; + } + + /** + * 获取安装App(支持7.0)的意图 + * + * @param file 文件 + * @return intent + */ + @NonNull + @RequiresApi(24) + private static Intent getInstallAppIntent24(Context context, File file, String authority) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + Uri contentUri = FileProvider.getUriForFile(context, authority, file); + intent.setDataAndType(contentUri, MimeTypeMap.getSingleton().getMimeTypeFromExtension(FileUtils.getFileExtension(file))); + + // 然后全部授权 + List resolveLists = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resolveLists) { + String packageName = resolveInfo.activityInfo.packageName; + context.grantUriPermission(packageName, contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + + return intent; + } + + /** + * 获取安装App(支持6.0)的意图 + * + * @param file 文件 + * @return intent + */ + private static Intent getInstallAppIntent23(File file) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + String type; + if (Build.VERSION.SDK_INT < 23) { + type = "application/vnd.android.package-archive"; + } else { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FileUtils.getFileExtension(file)); + } + intent.setDataAndType(Uri.fromFile(file), type); + return intent; + } + + /** + * 用来判断服务是否运行 + * + * @param context 上下文 + * @param serviceName 判断的服务名字 + * @return true 在运行 false 不在运行 + */ + public static boolean isServiceRunning(Context context, String serviceName) { + boolean isRunning = false; + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + if (activityManager == null) { + return false; + } + List serviceList = activityManager.getRunningServices(100); + if (serviceList.isEmpty()) { + return false; + } + for (int i = 0; i < serviceList.size(); i++) { + if (serviceName.equals(serviceList.get(i).service.getClassName())) { + isRunning = true; + break; + } + } + return isRunning; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/XIntentUtils.java b/lib_base/src/main/java/com/android/base/utils/android/XIntentUtils.java new file mode 100644 index 0000000..95c1186 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/XIntentUtils.java @@ -0,0 +1,150 @@ +package com.android.base.utils.android; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.provider.CalendarContract; +import android.provider.Settings; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; + +import static android.content.ContentValues.TAG; + +/** + * 对 {@link com.blankj.utilcode.util.IntentUtils} 的补充 + */ +public class XIntentUtils { + + public static final String GOOGLE_PLAY_PACKAGE_NAME = "com.android.vending"; + public static final String WANDOUJIA_PACKAGE_NAME = "com.wandoujia.phoenix2"; + public static final String TENCENT_PACKAGE_NAME = "com.tencent.android.qqdownloader"; + + /** + * 打开应用市场 + * + * @param context 上下文 + * @return 是否成功 + */ + public static boolean openMarket(Context context) { + Intent intent = new Intent(Intent.ACTION_VIEW); + //跳转到应用市场,非GooglePlay市场一般情况也实现了这个接口 + intent.setData(Uri.parse("market://details?id=" + context.getPackageName())); + //存在手机里没安装应用市场的情况,跳转会包异常,做一个接收判断 + if (intent.resolveActivity(context.getPackageManager()) != null) { //可以接收 + context.startActivity(intent); + return true; + } else { + return false; + } + } + + /** + * 优先打开规定的应用市场 + * + * @param context 上下文 + * @param specificMarkets 你规定的能打开app的包名,优先打开规定的包名 + * @return true表示打开成功 + */ + public static boolean openSpecificMarket(Context context, String... specificMarkets) { + String appId = context.getPackageName(); + Intent rateIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + appId)); + boolean marketFound = false; + // find all applications able to handle our rateIntent + final List otherApps = context.getPackageManager().queryIntentActivities(rateIntent, 0); + List specifiedPackage = Arrays.asList(specificMarkets); + for (ResolveInfo otherApp : otherApps) { + // look for Google Play application + if (specifiedPackage.contains(otherApp.activityInfo.applicationInfo.packageName)) { + ActivityInfo otherAppActivity = otherApp.activityInfo; + ComponentName componentName = new ComponentName(otherAppActivity.applicationInfo.packageName, otherAppActivity.name); + // make sure it does NOT open in the stack of your activity + rateIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // task reparenting if needed + rateIntent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + // if the Google Play was already open in a search result + // this make sure it still go to the app page you requested + rateIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + // this make sure only the Google Play app is allowed to + // intercept the intent + rateIntent.setComponent(componentName); + context.startActivity(rateIntent); + marketFound = true; + break; + } + } + return marketFound || openMarket(context); + } + + /** + * @param email email email必须放到数组中 + * @param subject 主题 + * @param text 发送的内容 + */ + public static void sendEmail(Context context, String[] email, String subject, String text) { + Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND); + emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + emailIntent.setType("toastMessage/rfc822"); + emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, email); + emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject); + emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, text); + if (emailIntent.resolveActivity(context.getPackageManager()) != null) { + context.startActivity(emailIntent); + } + } + + /** + * 参考:https://developer.android.com/guide/topics/providers/calendar-provider.html?hl=zh-cn#intent-insert + * + * @param context 上下文 + * @param beginTime 开始时间 + * @param endTime 结束时间 + * @param title 标题 + * @param description 描述 + * @param eventLocation 位置 + * @param emails extra 字段提供以逗号分隔的受邀者电子邮件地址列表。 + * @return 是否添加成功 + */ + public static boolean insertEvent(Context context, Calendar beginTime, Calendar endTime, String title, String description, String eventLocation, @Nullable String emails) { + emails = TextUtils.isEmpty(emails) ? "" : emails; + Intent intent = new Intent(Intent.ACTION_INSERT) + .setData(CalendarContract.Events.CONTENT_URI) + .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, beginTime.getTimeInMillis()) + .putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endTime.getTimeInMillis()) + .putExtra(CalendarContract.Events.TITLE, title) + .putExtra(CalendarContract.Events.DESCRIPTION, description) + .putExtra(CalendarContract.Events.EVENT_LOCATION, eventLocation) + .putExtra(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY) + .putExtra(Intent.EXTRA_EMAIL, emails); + ComponentName componentName = intent.resolveActivity(context.getPackageManager()); + if (componentName != null) { + context.startActivity(intent); + return true; + } else { + Log.d(TAG, "insertEvent() fail"); + return false; + } + } + + /** + * 打开网络设置界面 + */ + public static boolean networkSettings(Context context) { + try { + Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + return true; + } catch (Exception ignore) { + return false; + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/anim/ActivityAnimUtils.java b/lib_base/src/main/java/com/android/base/utils/android/anim/ActivityAnimUtils.java new file mode 100644 index 0000000..f5fc128 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/anim/ActivityAnimUtils.java @@ -0,0 +1,72 @@ +package com.android.base.utils.android.anim; + +import android.app.Activity; +import android.app.ActivityOptions; +import android.content.Intent; +import android.support.annotation.AnimRes; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.ActivityOptionsCompat; +import android.support.v7.app.AppCompatActivity; +import android.transition.Explode; +import android.transition.Fade; +import android.transition.Slide; +import android.view.View; +import android.view.Window; + +import com.android.base.utils.android.compat.AndroidVersion; + +public class ActivityAnimUtils { + + private ActivityAnimUtils() { + throw new UnsupportedOperationException("no need instantiation"); + } + + public static void startActivity(Activity activity, Intent intent, View share) { + if (AndroidVersion.atLeast(21)) { + ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(activity, share, share.getTransitionName()); + activity.startActivity(intent, options.toBundle()); + } else {//小于5.0,使用makeScaleUpAnimation + ActivityOptionsCompat options = ActivityOptionsCompat.makeScaleUpAnimation(share, share.getWidth() / 2, share.getHeight() / 2, 0, 0); + ActivityCompat.startActivity(activity, intent, options.toBundle()); + } + } + + /////////////////////////////////////////////////////////////////////////// + // Enter And Exit Transition + /////////////////////////////////////////////////////////////////////////// + + public static final int TYPE_EXPLODE = 1; + public static final int TYPE_SLIDE = 2; + public static final int TYPE_FADE = 3; + + /** + * @param activity context + * @param type {@link #TYPE_EXPLODE},{@link #TYPE_SLIDE},{@link #TYPE_FADE} + */ + private static void setTransition(Activity activity, int type) { + if (AndroidVersion.atLeast(21)) { + Window window = activity.getWindow(); + window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS); + switch (type) { + case TYPE_EXPLODE: + window.setEnterTransition(new Explode()); + window.setExitTransition(new Explode()); + break; + case TYPE_SLIDE: + window.setEnterTransition(new Slide()); + window.setExitTransition(new Slide()); + break; + case TYPE_FADE: + window.setEnterTransition(new Fade()); + window.setExitTransition(new Fade()); + break; + } + } + } + + public static void finishWithAnimation(AppCompatActivity activity, @AnimRes int enterAnim, @AnimRes int exitAnim) { + activity.supportFinishAfterTransition(); + activity.overridePendingTransition(enterAnim, exitAnim); + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/compat/AndroidVersion.java b/lib_base/src/main/java/com/android/base/utils/android/compat/AndroidVersion.java new file mode 100644 index 0000000..b096bdd --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/compat/AndroidVersion.java @@ -0,0 +1,37 @@ +package com.android.base.utils.android.compat; + +import android.os.Build; + +/** + * 版本判断 + */ +public class AndroidVersion { + + private AndroidVersion() { + throw new UnsupportedOperationException(); + } + + /** + * @param sdkVersion 要求的版本 + * @return true when the caller API version is > level + */ + public static boolean above(int sdkVersion) { + return Build.VERSION.SDK_INT > sdkVersion; + } + + /** + * @param sdkVersion 要求的版本 + * @return true when the caller API version >= level + */ + public static boolean atLeast(int sdkVersion) { + return Build.VERSION.SDK_INT >= sdkVersion; + } + + /** + * 当前系统版本 == level + */ + public static boolean at(int sdkVersion) { + return Build.VERSION.SDK_INT == sdkVersion; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/compat/SystemBarCompat.java b/lib_base/src/main/java/com/android/base/utils/android/compat/SystemBarCompat.java new file mode 100644 index 0000000..19c4468 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/compat/SystemBarCompat.java @@ -0,0 +1,363 @@ +package com.android.base.utils.android.compat; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.Point; +import android.os.Build; +import android.support.annotation.ColorInt; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.Display; +import android.view.Gravity; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import timber.log.Timber; + +/** + * SystemBar工具类 + * + *

+ * other useful utils: + *

  • https://github.com/Zackratos/UltimateBar
  • + *
  • https://github.com/niorgai/StatusBarCompat
  • + *
  • https://github.com/laobie/StatusBarUtil
  • + *
  • https://github.com/msdx/status-bar-compat
  • + *

    + * + * @author Ztiany + * Date : 2016-03-16 21:52 + */ +public class SystemBarCompat { + + private SystemBarCompat() { + throw new UnsupportedOperationException(); + } + + private static final String STATUS_BAR_HEIGHT_RES_NAME = "status_bar_height"; + private static final String NAV_BAR_HEIGHT_RES_NAME = "navigation_bar_height"; + + /////////////////////////////////////////////////////////////////////////// + // Kitkat + /////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentStatusOn19(Activity activity) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + setTranslucentSystemUi(activity, true, false); + } + } + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentNavigationOn19(Activity activity) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + setTranslucentSystemUi(activity, false, true); + } + } + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentOn19(Activity activity) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + setTranslucentSystemUi(activity, true, true); + } + } + + @SuppressWarnings("WeakerAccess,unused") + public static View setStatusBarColorOn19(Activity activity, @ColorInt int color) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); + return setupStatusBarView(activity, decorView, color); + } + return null; + } + + /////////////////////////////////////////////////////////////////////////// + // After L + /////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentStatusAfter19(Activity activity) { + if (!AndroidVersion.above(20)) { + return; + } + setTranslucentSystemUi(activity, true, false); + } + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentNavigationAfter19(Activity activity) { + if (!AndroidVersion.above(20)) { + return; + } + setTranslucentSystemUi(activity, false, true); + } + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentAfter19(Activity activity) { + if (!AndroidVersion.above(20)) { + return; + } + setTranslucentSystemUi(activity, true, true); + } + + public static void setupStatusBarColorAfter19(Activity activity, @ColorInt int color) { + if (!AndroidVersion.above(20)) { + return; + } + Window window = activity.getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + activity.getWindow().setStatusBarColor(color); + } + + public static void setupNavigationBarColorAfter19(Activity activity, @ColorInt int color) { + if (!AndroidVersion.above(20)) { + return; + } + Window window = activity.getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setNavigationBarColor(color); + } + + /////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////// + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void setTranslucentSystemUi(Activity activity, boolean status, boolean navigation) { + Window win = activity.getWindow(); + setTranslucentSystemUi(win, status, navigation); + } + + public static void setStatusBarColor(Activity activity, @ColorInt int color) { + setStatusBarColorOn19(activity, color); + setupStatusBarColorAfter19(activity, color); + } + + public static void setTranslucentSystemUi(Window win, boolean status, boolean navigation) { + if (!AndroidVersion.atLeast(19)) { + return; + } + WindowManager.LayoutParams winParams = win.getAttributes(); + int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; + if (status) { + winParams.flags |= bits; + } else { + winParams.flags &= ~bits; + } + bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; + if (navigation) { + winParams.flags |= bits; + } else { + winParams.flags &= ~bits; + } + win.setAttributes(winParams); + } + + /** + * 适用于4.4,在 rootView 中添加一个与 StatusBar 高度一样的 View,用于对状态栏着色 + * + * @param context 上下文 + * @param rootView 用于添加着色View的根View + * @param color 着色 + * @return 被添加的View + */ + @SuppressWarnings("WeakerAccess,unused") + public static View setupStatusBarView(Context context, ViewGroup rootView, @ColorInt int color) { + View mStatusBarTintView = new View(context); + mStatusBarTintView.setBackgroundColor(color); + FrameLayout.LayoutParams mStatusBarParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, getStatusBarHeight(context)); + mStatusBarParams.gravity = Gravity.TOP; + mStatusBarTintView.setLayoutParams(mStatusBarParams); + rootView.addView(mStatusBarTintView, 0); + return mStatusBarTintView; + } + + /** + * 获取状态栏高度 + */ + @SuppressWarnings("WeakerAccess,unused") + public static int getStatusBarHeight(Context context) { + int result = 0; + int resourceId = context.getResources().getIdentifier(STATUS_BAR_HEIGHT_RES_NAME, "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + return result; + } + + /** + * 获取NavigationBar高度 + * + * @param context 上下文 + */ + @SuppressWarnings("WeakerAccess,unused") + public static int getNavigationBarHeight(Context context) { + int navigationBarHeight = 0; + Resources rs = context.getResources(); + int id = rs.getIdentifier(NAV_BAR_HEIGHT_RES_NAME, "dimen", "android"); + if (id > 0 && hasNavigationBar(context)) { + navigationBarHeight = rs.getDimensionPixelSize(id); + } + return navigationBarHeight; + } + + /** + * 获取是否存在 NavigationBar + * + * @see detect-soft-navigation-bar-availability-in-android-device-progmatically, Android APP适配全面屏手机的技术要点 + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean hasNavigationBar(Context context) { + if (!hasSoftKeys(context)) { + Timber.d("hasSoftKeys = false"); + return false; + } + + Timber.d("hasSoftKeys = true"); + + WindowManager systemService; + if (context instanceof Activity) { + systemService = ((Activity) context).getWindowManager(); + } else { + systemService = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + } + + Point realSize = new Point(); + Point screenSize = new Point(); + boolean hasNavBar = false; + DisplayMetrics metrics = new DisplayMetrics(); + + systemService.getDefaultDisplay().getRealMetrics(metrics); + realSize.x = metrics.widthPixels; + realSize.y = metrics.heightPixels; + systemService.getDefaultDisplay().getSize(screenSize); + + if (realSize.y != screenSize.y) { + Timber.d("realSize.y = %d screenSize.y = %d", realSize.y, screenSize.y); + int difference = realSize.y - screenSize.y; + int navBarHeight = 0; + Resources resources = context.getResources(); + int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); + if (resourceId > 0) { + navBarHeight = resources.getDimensionPixelSize(resourceId); + } + Timber.d("navBarHeight = %d ", navBarHeight); + if (navBarHeight != 0) { + if (difference >= navBarHeight) { + hasNavBar = true; + } + } + + } + Timber.d("hasNavigationBar = " + hasNavBar); + return hasNavBar; + } + + /** + * 判断有没有导航栏,参考:https://github.com/roughike/BottomBar/blob/master/bottom-bar/src/main/java/com/roughike/bottombar/NavbarUtils.java + * + * @param context 上下文 + * @return true表示有 + */ + @SuppressLint("ObsoleteSdkInt") + private static boolean hasSoftKeys(Context context) { + + boolean hasSoftwareKeys = true; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Display d = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + + DisplayMetrics realDisplayMetrics = new DisplayMetrics(); + d.getRealMetrics(realDisplayMetrics); + + int realHeight = realDisplayMetrics.heightPixels; + int realWidth = realDisplayMetrics.widthPixels; + + DisplayMetrics displayMetrics = new DisplayMetrics(); + d.getMetrics(displayMetrics); + + int displayHeight = displayMetrics.heightPixels; + int displayWidth = displayMetrics.widthPixels; + + hasSoftwareKeys = (realWidth - displayWidth) > 0 || (realHeight - displayHeight) > 0; + + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + boolean hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey(); + boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); + hasSoftwareKeys = !hasMenuKey && !hasBackKey; + } + return hasSoftwareKeys; + } + + /** + * 获取ActionBar高度 + * + * @param activity activity + * @return ActionBar高度 + */ + @SuppressWarnings("WeakerAccess,unused") + public static int getActionBarHeight(Activity activity) { + TypedValue tv = new TypedValue(); + if (activity.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) { + return TypedValue.complexToDimensionPixelSize(tv.data, activity.getResources().getDisplayMetrics()); + } + return 0; + } + + /////////////////////////////////////////////////////////////////////////// + // View Flags + /////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentStatusViaViewFlags(Activity activity) { + setTranslucentSystemUiViaViewFlags(activity, true, false); + } + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentNavigationViaViewFlags(Activity activity) { + setTranslucentSystemUiViaViewFlags(activity, false, true); + } + + @SuppressWarnings("WeakerAccess,unused") + public static void setTranslucentSystemUiViaViewFlags(Activity activity) { + setTranslucentSystemUiViaViewFlags(activity, true, true); + } + + private static void setTranslucentSystemUiViaViewFlags(Activity activity, boolean status, boolean navigation) { + Window window = activity.getWindow(); + if (AndroidVersion.atLeast(21)) { + if (status && !navigation) { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + if (navigation && !status) { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + } + if (navigation && status) { + window.clearFlags( + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + } + setupStatusBarColorAfter19(activity, Color.TRANSPARENT); + setupNavigationBarColorAfter19(activity, Color.TRANSPARENT); + } else if (AndroidVersion.at(19)) { + setTranslucentSystemUi(window, status, navigation); + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/orientation/AbsOrientationProvider.java b/lib_base/src/main/java/com/android/base/utils/android/orientation/AbsOrientationProvider.java new file mode 100644 index 0000000..28b5a91 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/orientation/AbsOrientationProvider.java @@ -0,0 +1,108 @@ +package com.android.base.utils.android.orientation; + +import android.content.Context; +import android.hardware.SensorManager; +import android.view.Surface; +import android.view.WindowManager; + +abstract class AbsOrientationProvider implements OrientationProvider { + + + private Listener mListener; + private int[] worldAxisForDeviceAxisXy; + private final WindowManager mWindowManager; + SensorManager mSensorManager; + private boolean mIsInvertAxle; + private float[] mAdjustedRotationMatrix = new float[9]; + private float[] mOrientation = new float[3]; + + + AbsOrientationProvider(Context context) { + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + worldAxisForDeviceAxisXy = new int[2]; + mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + } + + + @Override + public final void startListening(Listener listener) { + if (listener == null) { + return; + } + if (mListener == listener) { + return; + } + if (mListener != null) { + mListener = null; + stopListening(); + } + mListener = listener; + onStartListening(); + } + + protected abstract void onStartListening(); + + + @Override + public final void stopListening() { + mListener = null; + onStopListening(); + } + + protected abstract void onStopListening(); + + + void determineOrientation(float[] rotationMatrix) { + setWorldAxisForDeviceAxis(); + SensorManager.remapCoordinateSystem(rotationMatrix, worldAxisForDeviceAxisXy[0], + worldAxisForDeviceAxisXy[1], mAdjustedRotationMatrix); + // Transform rotation matrix into azimuth/pitch/roll + SensorManager.getOrientation(mAdjustedRotationMatrix, mOrientation); + // Convert radians to degrees + // Convert radians to degrees + float pitch = (float) Math.toDegrees(mOrientation[1]);//pitch z轴与水平面的夹角 范围在-90 到 90° ,手机的任何一面垂直于水平面即为0°,z轴指向下方是为角度为正直 + float roll = (float) Math.toDegrees(mOrientation[2]);//roll 绕y轴 + float azimuth = (float) Math.toDegrees(mOrientation[0]);//roll 绕y轴 + if (mIsInvertAxle) { + pitch = -pitch; + roll = -roll; + azimuth = -azimuth; + } + notifyOrientation(azimuth, pitch, roll); + } + + + private void setWorldAxisForDeviceAxis() { + switch (mWindowManager.getDefaultDisplay().getRotation()) { + case Surface.ROTATION_0: + default: + worldAxisForDeviceAxisXy[0] = SensorManager.AXIS_X; + worldAxisForDeviceAxisXy[1] = SensorManager.AXIS_Z; + break; + case Surface.ROTATION_90: + worldAxisForDeviceAxisXy[0] = SensorManager.AXIS_Z; + worldAxisForDeviceAxisXy[1] = SensorManager.AXIS_MINUS_X; + break; + case Surface.ROTATION_180: + worldAxisForDeviceAxisXy[0] = SensorManager.AXIS_MINUS_X; + worldAxisForDeviceAxisXy[1] = SensorManager.AXIS_MINUS_Z; + break; + case Surface.ROTATION_270: + worldAxisForDeviceAxisXy[0] = SensorManager.AXIS_MINUS_Z; + worldAxisForDeviceAxisXy[1] = SensorManager.AXIS_X; + break; + } + } + + private void notifyOrientation(float azimuth, float pitch, float roll) { + if (mListener != null) { + mListener.onOrientationChanged(azimuth, pitch, roll); + } + } + + + @Override + public void invertAxle(boolean isInvertAxle) { + mIsInvertAxle = isInvertAxle; + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/orientation/AccelerometerMagneticOrientationProvider.java b/lib_base/src/main/java/com/android/base/utils/android/orientation/AccelerometerMagneticOrientationProvider.java new file mode 100644 index 0000000..a6a5e34 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/orientation/AccelerometerMagneticOrientationProvider.java @@ -0,0 +1,88 @@ +package com.android.base.utils.android.orientation; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +import com.android.base.utils.android.orientation.filter.MeanFilterSmoothing; + + +class AccelerometerMagneticOrientationProvider extends AbsOrientationProvider implements SensorEventListener { + + + private float[] mAccelerationValues = new float[3]; + private float[] mMagneticValues = new float[3]; + + private float[] mRotationMatrix = new float[9]; + private final Sensor mAccelerometerSensor; + private final Sensor mMagneticFieldSensor; + + private MeanFilterSmoothing mAccelerometerFilter; + private MeanFilterSmoothing mMagneticFieldFilter; + + + AccelerometerMagneticOrientationProvider(Context context) { + super(context); + mAccelerometerSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + mMagneticFieldSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + + mAccelerometerFilter = new MeanFilterSmoothing(); + mMagneticFieldFilter = new MeanFilterSmoothing(); + + mAccelerometerFilter.setTimeConstant(0.1F); + mMagneticFieldFilter.setTimeConstant(0.1F); + } + + + @Override + protected void onStartListening() { + mSensorManager.registerListener(this, mAccelerometerSensor, SensorManager.SENSOR_DELAY_UI); + mSensorManager.registerListener(this, mMagneticFieldSensor, SensorManager.SENSOR_DELAY_UI); + } + + @Override + protected void onStopListening() { + mSensorManager.unregisterListener(this); + mSensorManager.unregisterListener(this, mAccelerometerSensor); + mSensorManager.unregisterListener(this, mMagneticFieldSensor); + } + + private boolean generateRotationMatrix() { + if (mAccelerationValues != null && mMagneticValues != null) { + boolean rotationMatrixGenerated; + rotationMatrixGenerated = + SensorManager.getRotationMatrix(mRotationMatrix, null, + mAccelerationValues, + mMagneticValues); + return rotationMatrixGenerated; + } + return false; + } + + + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor == mAccelerometerSensor) { + + System.arraycopy(event.values, 0, mAccelerationValues, 0, + event.values.length); + mAccelerationValues = mAccelerometerFilter + .addSamples(mAccelerationValues); + } else if (event.sensor == mMagneticFieldSensor) { + + System.arraycopy(event.values, 0, mMagneticValues, 0, + event.values.length); + mMagneticValues = mMagneticFieldFilter.addSamples(mMagneticValues); + } + if (generateRotationMatrix()) { + determineOrientation(mRotationMatrix); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/orientation/Orientation.java b/lib_base/src/main/java/com/android/base/utils/android/orientation/Orientation.java new file mode 100644 index 0000000..491f808 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/orientation/Orientation.java @@ -0,0 +1,58 @@ +package com.android.base.utils.android.orientation; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorManager; + +import java.util.List; + + +/** + * 手机旋转方向提供,参考这个项目 + */ +public class Orientation { + + public static OrientationProvider newInstance(Context context) { + if (hasRotationVector(context)) { + return new RotationVectorOrientationProvider(context); + } else if (hasAccelerometer(context) && hasMagnetometer(context)) { + return new AccelerometerMagneticOrientationProvider(context); + } else { + return new OrientationProvider() { + @Override + public void startListening(Listener listener) { + + } + + @Override + public void invertAxle(boolean isInvert) { + + } + + @Override + public void stopListening() { + + } + }; + } + } + + + private static boolean hasMagnetometer(Context context) { + SensorManager mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + List listSensors = mSensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD); + return !listSensors.isEmpty(); + } + + private static boolean hasAccelerometer(Context context) { + SensorManager mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + List listSensors = mSensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER); + return !listSensors.isEmpty(); + } + + private static boolean hasRotationVector(Context context) { + SensorManager mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + List listSensors = mSensorManager.getSensorList(Sensor.TYPE_ROTATION_VECTOR); + return !listSensors.isEmpty(); + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/orientation/OrientationProvider.java b/lib_base/src/main/java/com/android/base/utils/android/orientation/OrientationProvider.java new file mode 100644 index 0000000..32c90f5 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/orientation/OrientationProvider.java @@ -0,0 +1,16 @@ +package com.android.base.utils.android.orientation; + +public interface OrientationProvider { + + void startListening(Listener listener); + + void invertAxle(boolean isInvert); + + void stopListening(); + + interface Listener { + void onOrientationChanged(float azimuth, float pitch, float roll); + } + + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/android/orientation/RotationVectorOrientationProvider.java b/lib_base/src/main/java/com/android/base/utils/android/orientation/RotationVectorOrientationProvider.java new file mode 100644 index 0000000..fba330a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/orientation/RotationVectorOrientationProvider.java @@ -0,0 +1,48 @@ +package com.android.base.utils.android.orientation; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +class RotationVectorOrientationProvider extends AbsOrientationProvider implements SensorEventListener { + + private final Sensor mSensor; + private int mLastAccuracy; + private float[] Q = new float[9]; + + RotationVectorOrientationProvider(Context context) { + super(context); + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); + } + + @Override + protected void onStartListening() { + mSensorManager.registerListener(RotationVectorOrientationProvider.this, mSensor, SensorManager.SENSOR_DELAY_UI); + } + + @Override + protected void onStopListening() { + mSensorManager.unregisterListener(this); + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor == mSensor) { + if (mLastAccuracy != SensorManager.SENSOR_STATUS_UNRELIABLE) { + SensorManager.getRotationMatrixFromVector(Q, event.values); + determineOrientation(Q); + } + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + if (sensor == mSensor) { + if (mLastAccuracy != accuracy) { + mLastAccuracy = accuracy; + } + } + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/orientation/filter/MeanFilterSmoothing.java b/lib_base/src/main/java/com/android/base/utils/android/orientation/filter/MeanFilterSmoothing.java new file mode 100644 index 0000000..edb0c1d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/orientation/filter/MeanFilterSmoothing.java @@ -0,0 +1,143 @@ +package com.android.base.utils.android.orientation.filter; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/* + * Acceleration Explorer + * Copyright (C) 2013-2015, Kaleb Kircher - Kircher Engineering, LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Implements a mean filter designed to smooth the data points based on a time + * constant in units of seconds. The mean filter will average the samples that + * occur over a period defined by the time constant... the number of samples + * that are averaged is known as the filter window. The approach allows the + * filter window to be defined over a period of time, instead of a fixed number + * of samples. This is important on Android devices that are equipped with + * different hardware sensors that output samples at different frequencies and + * also allow the developer to generally specify the output frequency. Defining + * the filter window in terms of the time constant allows the mean filter to + * applied to all sensor outputs with the same relative filter window, + * regardless of sensor frequency. + * + * @author Kaleb + * @version %I%, %G% + */ +public class MeanFilterSmoothing { + private float timeConstant = 1; + private float startTime = 0; + private float timestamp = 0; + private float hz = 0; + + private int count = 0; + // The size of the mean filters rolling window. + private int filterWindow = 20; + + private boolean dataInit; + + private ArrayList> dataLists; + + /** + * Initialize a new MeanFilter object. + */ + public MeanFilterSmoothing() { + dataLists = new ArrayList<>(); + dataInit = false; + } + + public void setTimeConstant(float timeConstant) { + this.timeConstant = timeConstant; + } + + public void reset() { + startTime = 0; + timestamp = 0; + count = 0; + hz = 0; + } + + /** + * Filter the data. + * + * @param data iterator contains input the data. + * @return the filtered output data. + */ + public float[] addSamples(float[] data) { + // Initialize the start time. + if (startTime == 0) { + startTime = System.nanoTime(); + } + + timestamp = System.nanoTime(); + + // Find the sample period (between updates) and convert from + // nanoseconds to seconds. Note that the sensor delivery rates can + // individually vary by a relatively large time frame, so we use an + // averaging technique with the number of sensor updates to + // determine the delivery rate. + hz = (count++ / ((timestamp - startTime) / 1000000000.0f)); + + filterWindow = (int) (hz * timeConstant); + + for (int i = 0; i < data.length; i++) { + // Initialize the data structures for the data set. + if (!dataInit) { + dataLists.add(new LinkedList()); + } + + dataLists.get(i).addLast(data[i]); + + if (dataLists.get(i).size() > filterWindow) { + dataLists.get(i).removeFirst(); + } + } + + dataInit = true; + + float[] means = new float[dataLists.size()]; + + for (int i = 0; i < dataLists.size(); i++) { + means[i] = getMean(dataLists.get(i)); + } + + return means; + } + + /** + * Get the mean of the data set. + * + * @param data the data set. + * @return the mean of the data set. + */ + private float getMean(List data) { + float m = 0; + float count = 0; + + for (int i = 0; i < data.size(); i++) { + m += data.get(i).floatValue(); + count++; + } + + if (count != 0) { + m = m / count; + } + + return m; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/text/HtmlBuilder.java b/lib_base/src/main/java/com/android/base/utils/android/text/HtmlBuilder.java new file mode 100644 index 0000000..0be23a0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/text/HtmlBuilder.java @@ -0,0 +1,582 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * 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.android.base.utils.android.text; + +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.text.Html; +import android.text.Spanned; +import android.widget.TextView; + +import java.util.Iterator; +import java.util.LinkedList; + +/** + *

    Build a valid HTML string for a {@link TextView}.

    + * + *

    Example usage:

    + * + *
    + * 
    + * HtmlBuilder html = new HtmlBuilder()
    + * .p()
    + * .b()
    + * .font().color("red").face("sans-serif-condensed").text("Hello").close()
    + * .close()
    + * .close();
    + *
    + * // html.toString():
    + * // <p><b><font color="red" face="sans-serif-condensed">Hello</font></b></p>
    + *
    + * yourEditText.setText(html.toSpan());
    + * 
    + * 
    + * + *

    HTML Tags Supported by {@link TextView}:

    + * + *
      + *
    • <a href="...">
    • + *
    • <b>
    • + *
    • <big>
    • + *
    • <blockquote>
    • + *
    • <br>
    • + *
    • <cite>
    • + *
    • <dfn>
    • + *
    • <div align="...">
    • + *
    • <em>
    • + *
    • <font color="..." face="...">
    • + *
    • <h1>
    • + *
    • <h2>
    • + *
    • <h3>
    • + *
    • <h4>
    • + *
    • <h5>
    • + *
    • <h6>
    • + *
    • <i>
    • + *
    • <img src="...">
    • + *
    • <p>
    • + *
    • <small>
    • + *
    • <strike>
    • + *
    • <strong>
    • + *
    • <sub>
    • + *
    • <sup>
    • + *
    • <tt>
    • + *
    • <u>
    • + *
    • <ul> (Android 7.0+)
    • + *
    • <li> (Android 7.0+)
    • + *
    + * + * @see HtmlBuilder + */ +public class HtmlBuilder { + + private final StringBuilder html = new StringBuilder(); + + private final LinkedList tags = new LinkedList<>(); + + public HtmlBuilder open(String element, String data) { + tags.add(element); + html.append('<'); + html.append(element); + if (data != null) { + html.append(' ').append(data); + } + html.append('>'); + return this; + } + + public HtmlBuilder open(String element) { + return open(element, null); + } + + public HtmlBuilder close(String element) { + html.append("'); + for (Iterator iterator = tags.iterator(); iterator.hasNext(); ) { + if (iterator.next().equals(element)) { + iterator.remove(); + break; + } + } + return this; + } + + public HtmlBuilder close() { + if (tags.isEmpty()) { + return this; + } + html.append("'); + return this; + } + + public HtmlBuilder close(char element) { + return close(String.valueOf(element)); + } + + public HtmlBuilder append(boolean b) { + html.append(b); + return this; + } + + public HtmlBuilder append(char c) { + html.append(c); + return this; + } + + public HtmlBuilder append(int i) { + html.append(i); + return this; + } + + public HtmlBuilder append(long l) { + html.append(l); + return this; + } + + public HtmlBuilder append(float f) { + html.append(f); + return this; + } + + public HtmlBuilder append(double d) { + html.append(d); + return this; + } + + public HtmlBuilder append(Object obj) { + html.append(obj); + return this; + } + + public HtmlBuilder append(String str) { + html.append(str); + return this; + } + + public HtmlBuilder append(StringBuffer sb) { + html.append(sb); + return this; + } + + public HtmlBuilder append(char[] chars) { + html.append(chars); + return this; + } + + public HtmlBuilder append(char[] str, int offset, int len) { + html.append(str, offset, len); + return this; + } + + public HtmlBuilder append(CharSequence csq) { + html.append(csq); + return this; + } + + public HtmlBuilder append(CharSequence csq, int start, int end) { + html.append(csq, start, end); + return this; + } + + public HtmlBuilder append(Tag tag) { + html.append(tag.toString()); + return this; + } + + public HtmlBuilder a(String href, String text) { + return append(String.format("%s", href, text)); + } + + public HtmlBuilder b() { + return open("b"); + } + + public HtmlBuilder b(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder big() { + return open("big"); + } + + public HtmlBuilder big(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder blockquote() { + return open("blockquote"); + } + + public HtmlBuilder blockquote(String text) { + html.append("
    ").append(text).append("
    "); + return this; + } + + public HtmlBuilder br() { + html.append("
    "); + return this; + } + + public HtmlBuilder cite() { + return open("cite"); + } + + public HtmlBuilder cite(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder dfn() { + return open("dfn"); + } + + public HtmlBuilder dfn(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder div() { + return open("div"); + } + + public HtmlBuilder div(String align) { + html.append(String.format("
    ", align)); + tags.add("div"); + return this; + } + + public HtmlBuilder em() { + return open("em"); + } + + public HtmlBuilder em(String text) { + html.append("").append(text).append(""); + return this; + } + + public Font font() { + return new Font(this); + } + + public HtmlBuilder font(int color, String text) { + return font().color(color).text(text).close(); + } + + public HtmlBuilder font(String face, String text) { + return font().face(face).text(text).close(); + } + + public HtmlBuilder h1() { + return open("h1"); + } + + public HtmlBuilder h1(String text) { + html.append("

    ").append(text).append("

    "); + return this; + } + + public HtmlBuilder h2() { + return open("h2"); + } + + public HtmlBuilder h2(String text) { + html.append("

    ").append(text).append("

    "); + return this; + } + + public HtmlBuilder h3() { + return open("h3"); + } + + public HtmlBuilder h3(String text) { + html.append("

    ").append(text).append("

    "); + return this; + } + + public HtmlBuilder h4() { + return open("h4"); + } + + public HtmlBuilder h4(String text) { + html.append("

    ").append(text).append("

    "); + return this; + } + + public HtmlBuilder h5() { + return open("h5"); + } + + public HtmlBuilder h5(String text) { + html.append("
    ").append(text).append("
    "); + return this; + } + + public HtmlBuilder h6() { + return open("h6"); + } + + public HtmlBuilder h6(String text) { + html.append("
    ").append(text).append("
    "); + return this; + } + + public HtmlBuilder i() { + return open("i"); + } + + public HtmlBuilder i(String text) { + html.append("").append(text).append(""); + return this; + } + + public Img img() { + return new Img(this); + } + + public HtmlBuilder img(String src) { + return img().src(src).close(); + } + + public HtmlBuilder p() { + return open("p"); + } + + public HtmlBuilder p(String text) { + html.append("

    ").append(text).append("

    "); + return this; + } + + public HtmlBuilder small() { + return open("small"); + } + + public HtmlBuilder small(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder strike() { + return open("strike"); + } + + public HtmlBuilder strike(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder strong() { + return open("strong"); + } + + public HtmlBuilder strong(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder sub() { + return open("sub"); + } + + public HtmlBuilder sub(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder sup() { + return open("sup"); + } + + public HtmlBuilder sup(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder tt() { + return open("tt"); + } + + public HtmlBuilder tt(String text) { + html.append("").append(text).append(""); + return this; + } + + public HtmlBuilder u() { + return open("u"); + } + + public HtmlBuilder u(String text) { + html.append("").append(text).append(""); + return this; + } + + @RequiresApi(Build.VERSION_CODES.N) + public HtmlBuilder ul() { + return open("ul"); + } + + @RequiresApi(Build.VERSION_CODES.N) + public HtmlBuilder li() { + return open("li"); + } + + @RequiresApi(Build.VERSION_CODES.N) + public HtmlBuilder li(String text) { + html.append("
  • ").append(text).append("
  • "); + return this; + } + + @RequiresApi(Build.VERSION_CODES.N) + public Spanned build(int flags) { + return Html.fromHtml(html.toString(), flags); + } + + public Spanned build() { + //noinspection deprecation + return Html.fromHtml(html.toString()); + } + + @Override + public String toString() { + return html.toString(); + } + + public static class Tag { + + final HtmlBuilder builder; + final String element; + String separator = ""; + + public Tag(HtmlBuilder builder, String element) { + this.builder = builder; + this.element = element; + open(); + } + + protected void open() { + builder.append('<').append(element).append(' '); + } + + public HtmlBuilder close() { + return builder.append("'); + } + + @Override + public String toString() { + return builder.toString(); + } + + } + + public static class Font extends Tag { + + public Font() { + this(new HtmlBuilder()); + } + + public Font(HtmlBuilder builder) { + super(builder, "font"); + } + + public Font size(int size) { + builder.append(separator).append("size=\"").append(size).append('\"'); + separator = " "; + return this; + } + + public Font size(String size) { + builder.append(separator).append("size=\"").append(size).append('\"'); + separator = " "; + return this; + } + + public Font color(int color) { + return color(String.format("#%06X", (0xFFFFFF & color))); + } + + public Font color(String color) { + builder.append(separator).append("color=\"").append(color).append('\"'); + separator = " "; + return this; + } + + public Font face(String face) { + builder.append(separator).append("face=\"").append(face).append('\"'); + separator = " "; + return this; + } + + public Font text(String text) { + builder.append('>').append(text); + return this; + } + + } + + public static class Img extends Tag { + + public Img() { + this(new HtmlBuilder()); + } + + public Img(HtmlBuilder builder) { + super(builder, "img"); + } + + public Img src(String src) { + builder.append(separator).append("src=\"").append(src).append('\"'); + separator = " "; + return this; + } + + public Img alt(String alt) { + builder.append(separator).append("alt=\"").append(alt).append('\"'); + separator = " "; + return this; + } + + public Img height(String height) { + builder.append(separator).append("height=\"").append(height).append('\"'); + separator = " "; + return this; + } + + public Img height(int height) { + builder.append(separator).append("height=\"").append(height).append('\"'); + separator = " "; + return this; + } + + public Img width(String width) { + builder.append(separator).append("width=\"").append(width).append('\"'); + separator = " "; + return this; + } + + public Img width(int width) { + builder.append(separator).append("width=\"").append(width).append('\"'); + separator = " "; + return this; + } + + @Override + public HtmlBuilder close() { + return builder.append('>'); + } + + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/android/text/RoundedBackgroundSpan.java b/lib_base/src/main/java/com/android/base/utils/android/text/RoundedBackgroundSpan.java new file mode 100644 index 0000000..79856aa --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/android/text/RoundedBackgroundSpan.java @@ -0,0 +1,42 @@ +package com.android.base.utils.android.text; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.text.style.ReplacementSpan; + +/** + * 圆角背景Span + */ +public class RoundedBackgroundSpan extends ReplacementSpan { + + private int mPadding; + private int mBackgroundColor; + private int mTextColor; + private int mCorner; + + public RoundedBackgroundSpan(int backgroundColor, int textColor, int padding, int corner) { + super(); + mBackgroundColor = backgroundColor; + mTextColor = textColor; + mCorner = corner; + mPadding = padding; + } + + @Override + public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { + return (int) (mPadding + paint.measureText(text.subSequence(start, end).toString()) + mPadding); + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { + float width = paint.measureText(text.subSequence(start, end).toString()); + RectF rect = new RectF(x, top, x + width + (2 * mPadding), bottom); + paint.setColor(mBackgroundColor); + canvas.drawRoundRect(rect, mCorner, mCorner, paint); + paint.setColor(mTextColor); + canvas.drawText(text, start, end, x + mPadding, y, paint); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/common/Checker.java b/lib_base/src/main/java/com/android/base/utils/common/Checker.java new file mode 100644 index 0000000..e2f0429 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/common/Checker.java @@ -0,0 +1,59 @@ +package com.android.base.utils.common; + +import java.util.Collection; +import java.util.Map; + +/** + * 对象检查工具 + */ +public class Checker { + + private Checker() { + + } + + public static boolean isEmpty(Collection data) { + return data == null || data.size() == 0; + } + + public static boolean notEmpty(Collection data) { + return !isEmpty(data); + } + + public static boolean isNull(Object o) { + return o == null; + } + + public static boolean isEmpty(Map map) { + return map == null || map.size() == 0; + } + + public static boolean notEmpty(Map map) { + return !isEmpty(map); + } + + public static boolean isEmpty(T[] t) { + return t == null || t.length == 0; + } + + public static boolean notEmpty(T[] t) { + return !isEmpty(t); + } + + public static T requireNonNull(T obj) { + if (obj == null) + throw new NullPointerException(); + return obj; + } + + public static T requireNonNull(T obj, String message) { + if (obj == null) + throw new NullPointerException(message); + return obj; + } + + public static boolean nonNull(Object obj) { + return obj != null; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/common/CloseUtils.java b/lib_base/src/main/java/com/android/base/utils/common/CloseUtils.java new file mode 100644 index 0000000..3515087 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/common/CloseUtils.java @@ -0,0 +1,62 @@ +package com.android.base.utils.common; + +import java.io.Closeable; +import java.io.IOException; + + +public class CloseUtils { + + private CloseUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 安静关闭IO + */ + public static void closeIO(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * 关闭IO + */ + public static void closeIOQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException ignored) { + } + } + } + + /** + * 关闭IO + * + * @param closeables closeable + */ + public static void closeIO(Closeable... closeables) { + if (closeables == null) return; + for (Closeable closeable : closeables) { + closeIO(closeable); + } + } + + /** + * 安静关闭IO + * + * @param closeables closeable + */ + public static void closeIOQuietly(Closeable... closeables) { + if (closeables == null) return; + for (Closeable closeable : closeables) { + closeIOQuietly(closeable); + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/common/CollectionUtils.java b/lib_base/src/main/java/com/android/base/utils/common/CollectionUtils.java new file mode 100644 index 0000000..1332c74 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/common/CollectionUtils.java @@ -0,0 +1,31 @@ +package com.android.base.utils.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +public class CollectionUtils { + + private CollectionUtils() { + throw new UnsupportedOperationException(); + } + + public static ArrayList toArrayList(List list) { + if (list == null) { + return new ArrayList<>(0); + } + if (list instanceof ArrayList) { + return (ArrayList) list; + } + return new ArrayList<>(list); + } + + public static List emptyIfNull(List list) { + if (list == null) { + return Collections.emptyList(); + } + return list; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/common/FileUtils.java b/lib_base/src/main/java/com/android/base/utils/common/FileUtils.java new file mode 100644 index 0000000..2096e20 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/common/FileUtils.java @@ -0,0 +1,90 @@ +package com.android.base.utils.common; + +import android.util.Log; + +import java.io.File; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2016-11-30 16:29 + */ +public class FileUtils { + + private static final String TAG = FileUtils.class.getSimpleName(); + + private FileUtils() { + throw new UnsupportedOperationException("no need instantiation"); + } + + public static boolean makeFilePath(File file) { + if (file == null) { + return false; + } + File parent = file.getParentFile(); + return parent.exists() || makeDir(parent); + } + + private static boolean makeDir(File file) { + return file != null && (file.exists() || file.mkdirs()); + } + + public static long sizeOfFile(File file) { + if (file == null || !file.exists()) { + return 0; + } + if (file.isDirectory()) { + long length = 0; + for (File subFile : file.listFiles()) { + length += sizeOfFile(subFile); + } + return length; + } else { + return file.length(); + } + } + + public static void deleteFile(File file) { + if (file.isDirectory()) { + for (File subFile : file.listFiles()) { + deleteFile(subFile); + } + } else { + boolean delete = file.delete(); + Log.d(TAG, "delete:" + delete); + } + } + + public static boolean isFileExists(File file) { + return file != null && file.isFile() && file.exists(); + } + + /** + * 获取全路径中的文件拓展名 + * + * @param file 文件 + * @return 文件拓展名 + */ + public static String getFileExtension(File file) { + if (file == null) return null; + return getFileExtension(file.getPath()); + } + + /** + * 获取全路径中的文件拓展名 + * + * @param filePath 文件路径 + * @return 文件拓展名 + */ + public static String getFileExtension(String filePath) { + if (StringChecker.isEmpty(filePath)) { + return filePath; + } + int lastPoi = filePath.lastIndexOf('.'); + int lastSep = filePath.lastIndexOf(File.separator); + if (lastPoi == -1 || lastSep >= lastPoi) { + return ""; + } + return filePath.substring(lastPoi + 1); + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/common/StringChecker.java b/lib_base/src/main/java/com/android/base/utils/common/StringChecker.java new file mode 100644 index 0000000..eba3232 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/common/StringChecker.java @@ -0,0 +1,126 @@ +package com.android.base.utils.common; + +import java.util.regex.Pattern; + +public class StringChecker { + + private StringChecker() { + throw new UnsupportedOperationException("no need instantiation"); + } + + private static final String CHINA_PHONE_REG = "^1\\d{10}$"; + private static final String ID_CARD_REG = "[1-9]\\d{13,16}[a-zA-Z0-9]{1}"; + private static final String EMAIL_REG = "\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*"; + private static final String DIGIT_REG = "\\-?[1-9]\\d+\""; + private static final String URL_REG = "(https?://(w{3}\\.)?)?\\w+\\.\\w+(\\.[a-zA-Z]+)*(:\\d{1,5})?(/\\w*)*(\\??(.+=.*)?(&.+=.*)?)?"; + private static final String CHINESE_REGEX = "^[\u4E00-\u9FA5]+$"; + private static final String DECIMALS_REG = "\\-?[1-9]\\d+(\\.\\d+)?"; + private static final String LETTERS_REG = ".*[a-zA-Z]++.*"; + private static final String DIGITAL_REG = ".*[0-9]++.*"; + private static final String DIGITAL_LETTER_ONLY_REG = "^[A-Za-z0-9]+$"; + + /** + * 验证中国的手机号 + * + * @return 验证成功返回 true,验证失败返回 false + */ + public static boolean isChinaPhoneNumber(String mobile) { + return !isEmpty(mobile) && Pattern.matches(CHINA_PHONE_REG, mobile); + } + + public static boolean containsLetter(String text) { + return !isEmpty(text) && Pattern.matches(LETTERS_REG, text); + } + + public static boolean containsDigital(String text) { + return !isEmpty(text) && Pattern.matches(DIGITAL_REG, text); + } + + public static boolean containsDigtalLetterOnly(String text) { + return !isEmpty(text) && Pattern.matches(DIGITAL_LETTER_ONLY_REG, text); + } + + /** + * 验证身份证号码 + * + * @param idCard 居民身份证号码15位或18位,最后一位可能是数字或字母 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean isIdCard(String idCard) { + return !isEmpty(idCard) && Pattern.matches(ID_CARD_REG, idCard); + } + + /** + * 验证Email + * + * @param email email地址,格式:zhangsan@sina.com,zhangsan@xxx.com.cn,xxx代表邮件服务商 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean isEmail(String email) { + return !isEmpty(email) && Pattern.matches(EMAIL_REG, email); + } + + /** + * 验证整数(正整数和负整数) + * + * @param digit 一位或多位0-9之间的整数 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean isDigit(String digit) { + return !isEmpty(digit) && Pattern.matches(DIGIT_REG, digit); + } + + /** + * 验证整数和浮点数(正负整数和正负浮点数) + * + * @param decimals 一位或多位0-9之间的浮点数,如:1.23,233.30 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean isDecimals(String decimals) { + return !isEmpty(decimals) && Pattern.matches(DECIMALS_REG, decimals); + } + + /** + * 验证中文 + * + * @param chinese 中文字符 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean isChinese(String chinese) { + return !isEmpty(chinese) && Pattern.matches(CHINESE_REGEX, chinese); + } + + /** + * 验证URL地址 + * + * @param url url + * @return 验证成功返回true,验证失败返回false + */ + public static boolean isURL(String url) { + return !isEmpty(url) && Pattern.matches(URL_REG, url); + } + + /** + * 获取字符串的字符个数 + */ + public static int getCharLength(String string) { + if (isEmpty(string)) { + return 0; + } + return string.trim().toCharArray().length; + } + + public static boolean isCharLength(String string, int length) { + return null != string && ((isEmpty(string) && length == 0) || (string.trim().toCharArray().length == length)); + } + + public static boolean isLengthIn(String string, int min, int max) { + int length = string == null ? 0 : string.length(); + return length <= max && length >= min; + } + + public static boolean isEmpty(CharSequence str) { + return str == null || str.toString().trim().length() == 0; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/AESCipherStrategy.java b/lib_base/src/main/java/com/android/base/utils/security/AESCipherStrategy.java new file mode 100644 index 0000000..9cfa9c7 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/AESCipherStrategy.java @@ -0,0 +1,42 @@ +package com.android.base.utils.security; + +import com.android.base.utils.security.util.AESUtils; + +import java.io.UnsupportedEncodingException; + +/** + * Reference: https://github.com/zhengxiaopeng/Rocko-Android-Demos + */ +public class AESCipherStrategy extends CipherStrategy { + + private String key; + + public AESCipherStrategy(String key) { + this.key = key; + } + + @Override + public String encrypt(String content) { + byte[] encryptByte = new byte[0]; + try { + encryptByte = AESUtils.encryptData(content.getBytes(CHARSET), key); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return encodeConvert(encryptByte); + } + + @Override + public String decrypt(String encryptContent) { + byte[] encrypByte = decodeConvert(encryptContent); + byte[] decryptByte = AESUtils.decryptData(encrypByte, key); + String result = ""; + try { + result = new String(decryptByte, CHARSET); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return result; + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/CipherStrategy.java b/lib_base/src/main/java/com/android/base/utils/security/CipherStrategy.java new file mode 100644 index 0000000..77774a0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/CipherStrategy.java @@ -0,0 +1,43 @@ +package com.android.base.utils.security; + + +import android.util.Base64; + +/** + * Reference: https://github.com/zhengxiaopeng/Rocko-Android-Demos + */ +abstract class CipherStrategy { + + final static String CHARSET = "UTF-8"; + + /** + * 将加密内容的 Base64 编码转换为二进制内容 + */ + protected byte[] decodeConvert(String str) { + return Base64.decode(str, Base64.DEFAULT); + } + + /** + * 对加密后的二进制结果转换为 Base64 编码 + */ + protected String encodeConvert(byte[] bytes) { + return new String(Base64.encode(bytes, Base64.DEFAULT)); + } + + /** + * 对字符串进行加密 + * + * @param content 需要加密的字符串 + * @return + */ + public abstract String encrypt(String content); + + /** + * 对字符串进行解密 + * + * @param encryptContent 加密内容的 Base64 编码 + * @return + */ + public abstract String decrypt(String encryptContent); + +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/DESCipherStrategy.java b/lib_base/src/main/java/com/android/base/utils/security/DESCipherStrategy.java new file mode 100644 index 0000000..10e48a3 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/DESCipherStrategy.java @@ -0,0 +1,47 @@ +package com.android.base.utils.security; + + +import com.android.base.utils.security.util.DESUtils; + +import java.io.UnsupportedEncodingException; + +/** + * Reference: https://github.com/zhengxiaopeng/Rocko-Android-Demos + */ +public class DESCipherStrategy extends CipherStrategy { + + private String key;// 解密密码 + + /** + * @param key 加解密的 key + */ + public DESCipherStrategy(String key) { + this.key = key; + } + + @Override + public String encrypt(String content) { + byte[] encryptByte = null; + try { + encryptByte = DESUtils.encrypt(content.getBytes(CHARSET), key); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return encodeConvert(encryptByte); + } + + @Override + public String decrypt(String encryptContent) { + byte[] encrypByte = decodeConvert(encryptContent); + byte[] decryptByte = DESUtils.decrypt(encrypByte, key); + String result = ""; + try { + result = new String(decryptByte, CHARSET); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return result; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/RSACipherStrategy.java b/lib_base/src/main/java/com/android/base/utils/security/RSACipherStrategy.java new file mode 100644 index 0000000..99490b6 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/RSACipherStrategy.java @@ -0,0 +1,70 @@ +package com.android.base.utils.security; + +import com.android.base.utils.security.util.RSAUtils; + +import java.io.InputStream; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * Reference: https://github.com/zhengxiaopeng/Rocko-Android-Demos + */ +public class RSACipherStrategy extends CipherStrategy { + + private PublicKey mPublicKey; + private PrivateKey mPrivateKey; + + public void initPublicKey(String publicKeyContentStr) { + try { + mPublicKey = RSAUtils.loadPublicKey(publicKeyContentStr); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void initPublicKey(InputStream publicKeyIs) { + try { + mPublicKey = RSAUtils.loadPublicKey(publicKeyIs); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void initPrivateKey(String privateKeyContentStr) { + try { + mPrivateKey = RSAUtils.loadPrivateKey(privateKeyContentStr); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void initPrivateKey(InputStream privateIs) { + try { + mPrivateKey = RSAUtils.loadPrivateKey(privateIs); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public String encrypt(String content) { + if (mPublicKey == null) { + throw new NullPointerException("PublicKey is null, please initialize it first"); + } + byte[] encryptByte = RSAUtils.encryptData(content.getBytes(), mPublicKey); + + return encodeConvert(encryptByte); + } + + @Override + public String decrypt(String encryptContent) { + if (mPrivateKey == null) { + throw new NullPointerException("PrivateKey is null, please initialize it first"); + } + byte[] encryptByte = decodeConvert(encryptContent); + byte[] decryptByte = RSAUtils.decryptData(encryptByte, mPrivateKey); + + return new String(decryptByte); + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/SimpleRsa.java b/lib_base/src/main/java/com/android/base/utils/security/SimpleRsa.java new file mode 100644 index 0000000..c8c43c1 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/SimpleRsa.java @@ -0,0 +1,87 @@ +package com.android.base.utils.security; + +import android.util.Base64; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.Cipher; + + +public class SimpleRsa { + + private static final String ALGORITHM = "RSA"; + private static final String TRANSFORMATION = "RSA/ECB/PKCS1Padding"; + + /** + * 得到公钥 + * + * @param algorithm 算法 + * @param bysKey key + * @return 公钥 + */ + private static PublicKey getPublicKeyFromX509(String algorithm, String bysKey) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { + byte[] decodedKey = Base64.decode(bysKey, Base64.DEFAULT); + X509EncodedKeySpec x509 = new X509EncodedKeySpec(decodedKey); + KeyFactory keyFactory = KeyFactory.getInstance(algorithm, "BC"); + return keyFactory.generatePublic(x509); + } + + /** + * 使用公钥加密 + * + * @param content 加密字符串 + * @return 加密后的字符串 + */ + public static String encryptByPublic(String content, String RSA_PUBLIC_KEY) { + try { + PublicKey pubKey = getPublicKeyFromX509(ALGORITHM, RSA_PUBLIC_KEY); + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, pubKey); + byte plaintext[] = content.getBytes("UTF-8"); + byte[] output = cipher.doFinal(plaintext); + return new String(Base64.encode(output, Base64.DEFAULT)); + } catch (Exception e) { + return null; + } + } + + /** + * 使用公钥解密 + * + * @param content 密文 + * @return 解密后的字符串 + */ + public static String decryptByPublic(String content, String RSA_PUBLIC_KEY) { + try { + PublicKey pubkey = getPublicKeyFromX509(ALGORITHM, RSA_PUBLIC_KEY); + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, pubkey); + InputStream ins = new ByteArrayInputStream(Base64.decode(content, Base64.DEFAULT)); + ByteArrayOutputStream writer = new ByteArrayOutputStream(); + byte[] buf = new byte[128]; + int bufl; + while ((bufl = ins.read(buf)) != -1) { + byte[] block; + if (buf.length == bufl) { + block = buf; + } else { + block = new byte[bufl]; + System.arraycopy(buf, 0, block, 0, bufl); + } + writer.write(cipher.doFinal(block)); + } + return new String(writer.toByteArray(), "utf-8"); + } catch (Exception e) { + return null; + } + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/security/util/AESUtils.java b/lib_base/src/main/java/com/android/base/utils/security/util/AESUtils.java new file mode 100644 index 0000000..c75275d --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/util/AESUtils.java @@ -0,0 +1,177 @@ +/* + * Copyright 2015 Rocko (http://rocko.xyz) . + * + * 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.android.base.utils.security.util; + +import java.io.UnsupportedEncodingException; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +/** + * AES 对称加密 + */ +public class AESUtils { + /** + * 算法/模式/填充 * + */ + private static final String CipherMode = "AES"; + + /** + * 创建密钥 + * + * @param password 例如:"0123456701234567" 128位 16*8
    + * 所有密钥长度不能超过16字符中文占两个。192 24; 256 32 + * @return SecretKeySpec 实例 + */ + private static SecretKeySpec generateAESKey(String password) { + byte[] data = null; + StringBuilder sb = new StringBuilder(); + sb.append(password); + while (sb.length() < 16) + sb.append("0"); + if (sb.length() > 16) + sb.setLength(16); + try { + data = sb.toString().getBytes("UTF-8"); + return new SecretKeySpec(data, "AES"); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 加密字节数据 + * + * @param content 需要加密的字节数组 + * @param password 密钥 128 <16个字节 192 <24,256 <32个字节 + * @return 加密完后的字节数组 + */ + public static byte[] encryptData(byte[] content, String password) { + try { + SecretKeySpec key = generateAESKey(password); + Cipher cipher = Cipher.getInstance(CipherMode); + cipher.init(Cipher.ENCRYPT_MODE, key); + byte[] result = cipher.doFinal(content); + return result; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 加密(结果为16进制字符串), UTF-8 编码 + * + * @param content 要加密的字符串 + * @param password 密钥 + * @return 加密后的16进制字符串 + */ + public static String encryptData(String content, String password) { + byte[] data = null; + try { + data = content.getBytes("UTF-8"); + } catch (Exception e) { + e.printStackTrace(); + } + data = encryptData(data, password); + String result = byte2hex(data); + return result; + } + + /** + * 解密字节数组 UTF-8 + * + * @param content + * @param password + * @return + */ + public static byte[] decryptData(byte[] content, String password) { + try { + SecretKeySpec key = generateAESKey(password); + Cipher cipher = Cipher.getInstance(CipherMode); + cipher.init(Cipher.DECRYPT_MODE, key); + return cipher.doFinal(content); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 解密16进制的字符串为字符串 * + */ + public static String decryptData(String content, String password) { + byte[] data = null; + try { + data = hex2byte(content); + } catch (Exception e) { + e.printStackTrace(); + } + data = decryptData(data, password); + if (data == null) + return null; + String result = null; + try { + result = new String(data, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return result; + } + + /** + * 字节数组转成16进制字符串 + * + * @param b + * @return 16进制字符串 + */ + public static String byte2hex(byte[] b) { // 一个字节的数, + StringBuffer sb = new StringBuffer(b.length * 2); + String tmp = ""; + for (int n = 0; n < b.length; n++) { + // 整数转成十六进制表示 + tmp = (Integer.toHexString(b[n] & 0XFF)); + if (tmp.length() == 1) { + sb.append("0"); + } + sb.append(tmp); + } + return sb.toString().toUpperCase(); // 转成大写 + } + + /** + * 将hex字符串转换成字节数组 * + * + * @param inputString 16进制的字符串 + * @return 字节数组 + */ + public static byte[] hex2byte(String inputString) { + if (inputString == null || inputString.length() < 2) { + return new byte[0]; + } + inputString = inputString.toLowerCase(); + int l = inputString.length() / 2; + byte[] result = new byte[l]; + for (int i = 0; i < l; ++i) { + String tmp = inputString.substring(2 * i, 2 * i + 2); + result[i] = (byte) (Integer.parseInt(tmp, 16) & 0xFF); + } + return result; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/utils/security/util/DESUtils.java b/lib_base/src/main/java/com/android/base/utils/security/util/DESUtils.java new file mode 100644 index 0000000..dd06852 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/util/DESUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Rocko (http://rocko.xyz) . + * + * 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.android.base.utils.security.util; + +import java.security.SecureRandom; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; + +/** + * Created by Administrator on 2015/11/11. + */ +public class DESUtils { + + /** + * 加密 + * + * @param bytesContent 待加密内容 + * @param key 加密的密钥 + * @return + */ + public static byte[] encrypt(byte[] bytesContent, String key) { + try { + SecureRandom random = new SecureRandom(); + DESKeySpec desKey = new DESKeySpec(key.getBytes()); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey securekey = keyFactory.generateSecret(desKey); + Cipher cipher = Cipher.getInstance("DES"); + cipher.init(Cipher.ENCRYPT_MODE, securekey, random); + byte[] result = cipher.doFinal(bytesContent); + return result; + } catch (Throwable e) { + e.printStackTrace(); + } + return null; + } + + /** + * 解密 + * + * @param content 待解密内容 + * @param key 解密的密钥 + * @return + */ + public static byte[] decrypt(byte[] content, String key) { + try { + SecureRandom random = new SecureRandom(); + DESKeySpec desKey = new DESKeySpec(key.getBytes()); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey securekey = keyFactory.generateSecret(desKey); + Cipher cipher = Cipher.getInstance("DES"); + cipher.init(Cipher.DECRYPT_MODE, securekey, random); + return cipher.doFinal(content); + } catch (Throwable e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/util/MD5Util.java b/lib_base/src/main/java/com/android/base/utils/security/util/MD5Util.java new file mode 100644 index 0000000..fad4e41 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/util/MD5Util.java @@ -0,0 +1,113 @@ +package com.android.base.utils.security.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class MD5Util { + + private final static String[] strDigits = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"}; + + private static String byteToArrayString(byte bByte) { + int iRet = bByte; + if (iRet < 0) { + iRet += 256; + } + int iD1 = iRet / 16; + int iD2 = iRet % 16; + return strDigits[iD1] + strDigits[iD2]; + } + + private static String byteToString(byte[] bByte) { + StringBuilder sBuffer = new StringBuilder(); + for (byte aBByte : bByte) { + sBuffer.append(byteToArrayString(aBByte)); + } + return sBuffer.toString(); + } + + /** + * 32 位 MD5加密 + * + * @param str 待加密的字符串 + * @return result + */ + public static String encrypt(String str) { + String result = null; + try { + result = str; + MessageDigest md = MessageDigest.getInstance("MD5"); + result = byteToString(md.digest(str.getBytes())); + } catch (NoSuchAlgorithmException ex) { + ex.printStackTrace(); + } + return result; + } + + + public static String encrypt(String algorithm, String str) { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + md.update(str.getBytes()); + StringBuilder sb = new StringBuilder(); + byte[] bytes = md.digest(); + for (int i = 0; i < bytes.length; i++) { + int b = bytes[i] & 0xFF; + if (b < 0x10) { + sb.append('0'); + } + sb.append(Integer.toHexString(b)); + } + return sb.toString(); + } catch (Exception e) { + return ""; + } + } + + public static String hashKeyForDisk(String key) { + String cacheKey; + try { + final MessageDigest mDigest = MessageDigest.getInstance("MD5"); + mDigest.update(key.getBytes()); + cacheKey = bytesToHexString(mDigest.digest()); + } catch (NoSuchAlgorithmException e) { + cacheKey = String.valueOf(key.hashCode()); + } + return cacheKey; + } + + private static String bytesToHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(0xFF & bytes[i]); + if (hex.length() == 1) { + sb.append('0'); + } + sb.append(hex); + } + return sb.toString(); + } + + + public static String getMd5Value(String secret) { + try { + MessageDigest bmd5 = MessageDigest.getInstance("MD5"); + bmd5.update(secret.getBytes()); + int i; + StringBuffer buf = new StringBuffer(); + byte[] b = bmd5.digest(); + for (int offset = 0; offset < b.length; offset++) { + i = b[offset]; + if (i < 0) + i += 256; + if (i < 16) + buf.append("0"); + buf.append(Integer.toHexString(i)); + } + return buf.toString(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return ""; + } + +} diff --git a/lib_base/src/main/java/com/android/base/utils/security/util/RSAUtils.java b/lib_base/src/main/java/com/android/base/utils/security/util/RSAUtils.java new file mode 100644 index 0000000..8b72374 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/utils/security/util/RSAUtils.java @@ -0,0 +1,288 @@ +/* + * Copyright 2015 Rocko (http://rocko.xyz) . + * + * 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.android.base.utils.security.util; + +import android.util.Base64; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.Cipher; + + +/** + * @author Mr.Zheng + * @date 2014年8月22日 下午1:44:23 + */ +public final class RSAUtils { + private final static String KEY_PAIR = "RSA"; + private final static String CIPHER = "RSA/ECB/PKCS1Padding"; + + /** + * 随机生成RSA密钥对(默认密钥长度为1024) + * + * @return + */ + public static KeyPair generateRSAKeyPair() { + return generateRSAKeyPair(1024); + } + + /** + * 随机生成RSA密钥对 + * + * @param keyLength 密钥长度,范围:512~2048
    + * 一般1024 + * @return + */ + public static KeyPair generateRSAKeyPair(int keyLength) { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(KEY_PAIR); + kpg.initialize(keyLength); + return kpg.genKeyPair(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 用公钥加密
    + * 每次加密的字节数,不能超过密钥的长度值减去11 + * + * @param data 需加密数据的byte数据 + * @param publicKey 公钥 + * @return 加密后的byte型数据 + */ + public static byte[] encryptData(byte[] data, PublicKey publicKey) { + try { + Cipher cipher = Cipher.getInstance(CIPHER); + // 编码前设定编码方式及密钥 + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + // 传入编码数据并返回编码结果 + return cipher.doFinal(data); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 用私钥解密 + * + * @param encryptedData 经过encryptedData()加密返回的byte数据 + * @param privateKey 私钥 + * @return + */ + public static byte[] decryptData(byte[] encryptedData, PrivateKey privateKey) { + try { + Cipher cipher = Cipher.getInstance(CIPHER); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + return cipher.doFinal(encryptedData); + } catch (Exception e) { + return null; + } + } + + /** + * 通过公钥byte[](publicKey.getEncoded())将公钥还原,适用于RSA算法 + * + * @param keyBytes + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + public static PublicKey getPublicKey(byte[] keyBytes) throws NoSuchAlgorithmException, + InvalidKeySpecException { + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR); + PublicKey publicKey = keyFactory.generatePublic(keySpec); + return publicKey; + } + + /** + * 通过私钥byte[]将公钥还原,适用于RSA算法 + * + * @param keyBytes + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + public static PrivateKey getPrivateKey(byte[] keyBytes) throws NoSuchAlgorithmException, + InvalidKeySpecException { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR); + PrivateKey privateKey = keyFactory.generatePrivate(keySpec); + return privateKey; + } + + /** + * 使用N、e值还原公钥 + * + * @param modulus + * @param publicExponent + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + public static PublicKey getPublicKey(String modulus, String publicExponent) + throws NoSuchAlgorithmException, InvalidKeySpecException { + BigInteger bigIntModulus = new BigInteger(modulus); + BigInteger bigIntPrivateExponent = new BigInteger(publicExponent); + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(bigIntModulus, bigIntPrivateExponent); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR); + PublicKey publicKey = keyFactory.generatePublic(keySpec); + return publicKey; + } + + /** + * 使用N、d值还原私钥 + * + * @param modulus + * @param privateExponent + * @return + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + public static PrivateKey getPrivateKey(String modulus, String privateExponent) + throws NoSuchAlgorithmException, InvalidKeySpecException { + BigInteger bigIntModulus = new BigInteger(modulus); + BigInteger bigIntPrivateExponent = new BigInteger(privateExponent); + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(bigIntModulus, bigIntPrivateExponent); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR); + PrivateKey privateKey = keyFactory.generatePrivate(keySpec); + return privateKey; + } + + /** + * 从字符串中加载公钥 + * + * @param publicKeyStr 公钥数据字符串 + * @throws Exception 加载公钥时产生的异常 + */ + public static PublicKey loadPublicKey(String publicKeyStr) throws Exception { + try { + byte[] buffer = Base64.decode(publicKeyStr, Base64.DEFAULT); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR, "BC"); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException e) { + throw new Exception("无此算法"); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + throw new Exception("公钥非法" + e.getLocalizedMessage()); + } catch (NullPointerException e) { + throw new Exception("公钥数据为空"); + } + } + + /** + * 从字符串中加载私钥
    + * 加载时使用的是PKCS8EncodedKeySpec(PKCS#8编码的Key指令)。 + * + * @param privateKeyStr + * @return + * @throws Exception + */ + public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception { + try { + byte[] buffer = Base64.decode(privateKeyStr, Base64.DEFAULT); + // X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR); + return keyFactory.generatePrivate(keySpec); + } catch (NoSuchAlgorithmException e) { + throw new Exception("无此算法"); + } catch (InvalidKeySpecException e) { + throw new Exception("私钥非法"); + } catch (NullPointerException e) { + throw new Exception("私钥数据为空"); + } + } + + /** + * 从文件中输入流中加载公钥 + * + * @param in 公钥输入流 + * @throws Exception 加载公钥时产生的异常 + */ + public static PublicKey loadPublicKey(InputStream in) throws Exception { + try { + return loadPublicKey(readKey(in)); + } catch (IOException e) { + throw new Exception("公钥数据流读取错误"); + } catch (NullPointerException e) { + throw new Exception("公钥输入流为空"); + } + } + + /** + * 从文件中加载私钥 + * + * @param + * @return 是否成功 + * @throws Exception + */ + public static PrivateKey loadPrivateKey(InputStream in) throws Exception { + try { + return loadPrivateKey(readKey(in)); + } catch (IOException e) { + throw new Exception("私钥数据读取错误"); + } catch (NullPointerException e) { + throw new Exception("私钥输入流为空"); + } + } + + /** + * 读取密钥信息 + * -------------------- + * CONTENT + * -------------------- + * + * @param in + * @return + * @throws IOException + */ + private static String readKey(InputStream in) throws IOException { + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + String readLine = null; + StringBuilder sb = new StringBuilder(); + while ((readLine = br.readLine()) != null) { + if (readLine.charAt(0) == '-') { + continue; + } else { + sb.append(readLine); + sb.append('\r'); + } + } + + return sb.toString(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/MultiStateView.java b/lib_base/src/main/java/com/android/base/widget/MultiStateView.java new file mode 100644 index 0000000..2898281 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/MultiStateView.java @@ -0,0 +1,381 @@ +package com.android.base.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.android.base.R; +import com.android.base.app.ui.StateLayoutConfig; +import com.android.base.app.ui.StateLayoutConfig.ViewState; + +import static com.android.base.app.ui.StateLayoutConfig.CONTENT; +import static com.android.base.app.ui.StateLayoutConfig.EMPTY; +import static com.android.base.app.ui.StateLayoutConfig.ERROR; +import static com.android.base.app.ui.StateLayoutConfig.LOADING; +import static com.android.base.app.ui.StateLayoutConfig.NET_ERROR; +import static com.android.base.app.ui.StateLayoutConfig.REQUESTING; +import static com.android.base.app.ui.StateLayoutConfig.SERVER_ERROR; + +/** + * View that contains 7 different states: Content, Error/NetError/ServerError, Empty, and Loading/Request. + */ +public class MultiStateView extends FrameLayout { + + private LayoutInflater mInflater; + private SparseArray mChildren; + private View mContentView; + private boolean mDisableOperationWhenRequesting = true; + + @Nullable + private StateListener mListener; + @ViewState + private int mViewState = CONTENT; + + public MultiStateView(Context context) { + this(context, null); + } + + public MultiStateView(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs, 0); + } + + public MultiStateView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(attrs, defStyle); + } + + private void init(AttributeSet attrs, int defStyle) { + mChildren = new SparseArray<>(); + mInflater = LayoutInflater.from(getContext()); + + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MultiStateView, defStyle, 0); + + int loadingViewResId = a.getResourceId(R.styleable.MultiStateView_msv_loadingView, -1); + int requestingViewResId = a.getResourceId(R.styleable.MultiStateView_msv_requestingView, -1); + int emptyViewResId = a.getResourceId(R.styleable.MultiStateView_msv_emptyView, -1); + int errorViewResId = a.getResourceId(R.styleable.MultiStateView_msv_errorView, -1); + int netErrorViewResId = a.getResourceId(R.styleable.MultiStateView_msv_net_errorView, -1); + int serverErrorViewResId = a.getResourceId(R.styleable.MultiStateView_msv_net_errorView, -1); + + mChildren.put(LOADING, new ViewHolder(loadingViewResId)); + mChildren.put(REQUESTING, new ViewHolder(requestingViewResId)); + mChildren.put(EMPTY, new ViewHolder(emptyViewResId)); + mChildren.put(ERROR, new ViewHolder(errorViewResId)); + mChildren.put(NET_ERROR, new ViewHolder(netErrorViewResId)); + mChildren.put(SERVER_ERROR, new ViewHolder(serverErrorViewResId)); + + mDisableOperationWhenRequesting = a.getBoolean(R.styleable.MultiStateView_msv_disable_when_requesting, true); + + ensureInitState(a.getInt(R.styleable.MultiStateView_msv_viewState, CONTENT)); + + a.recycle(); + } + + private void ensureInitState(int viewState) { + /* + + + + + + + + */ + switch (viewState) { + case 2: + mViewState = LOADING; + break; + case 3: + mViewState = EMPTY; + break; + case 4: + mViewState = ERROR; + break; + case 5: + mViewState = NET_ERROR; + break; + case 6: + mViewState = SERVER_ERROR; + break; + case 7: + mViewState = REQUESTING; + break; + case 1: + default: + mViewState = CONTENT; + break; + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mContentView == null) { + throw new IllegalArgumentException("Content view is not defined"); + } + setView(); + } + + /* All of the addView methods have been overridden so that it can obtain the content view via XML + It is NOT recommended to add views into MultiStateView via the addView methods, but rather use + any of the setViewForState methods to set views for their given ContentViewState accordingly */ + @Override + public void addView(View child) { + if (isValidContentView(child)) { + mContentView = child; + } + super.addView(child); + } + + @Override + public void addView(View child, int index) { + if (isValidContentView(child)) { + mContentView = child; + } + super.addView(child, index); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (isValidContentView(child)) { + mContentView = child; + } + super.addView(child, index, params); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (isValidContentView(child)) { + mContentView = child; + } + super.addView(child, params); + } + + @Override + public void addView(View child, int width, int height) { + if (isValidContentView(child)) { + mContentView = child; + } + super.addView(child, width, height); + } + + @Override + protected boolean addViewInLayout(View child, int index, ViewGroup.LayoutParams params) { + if (isValidContentView(child)) { + mContentView = child; + } + return super.addViewInLayout(child, index, params); + } + + @Override + protected boolean addViewInLayout(View child, int index, ViewGroup.LayoutParams params, boolean preventRequestLayout) { + if (isValidContentView(child)) { + mContentView = child; + } + return super.addViewInLayout(child, index, params, preventRequestLayout); + } + + /** + * Returns the {@link View} associated with the {@link ViewState} + * + * @param state The {@link ViewState} with to return the view for + * @return The {@link View} associated with the {@link ViewState}, null if no view is present + */ + @Nullable + @SuppressWarnings("unused") + public View getView(@ViewState int state) { + if (state == StateLayoutConfig.BLANK) { + return null; + } + return ensureStateView(state); + } + + private View ensureStateView(@ViewState int state) { + ViewHolder viewHolder = mChildren.get(state); + if (viewHolder == null) { + throw new NullPointerException("the ViewHolder is null, state = " + state); + } + + if (viewHolder.mView != null) { + return viewHolder.mView; + } + + int viewLayoutId = viewHolder.mViewLayoutId; + + if (viewLayoutId > 0) { + View newView = mInflater.inflate(viewLayoutId, this, false); + newView.setTag(R.id.base_tag_multi_state_view, state); + addView(newView, newView.getLayoutParams()); + if (mListener != null) { + mListener.onStateInflated(state, newView); + } + if (mViewState != state) { + newView.setVisibility(GONE); + } + viewHolder.mView = newView; + return newView; + } else { + throw new IllegalStateException("the view layout id is invalidate, layout id = " + viewLayoutId + " state = " + state); + } + } + + /** + * Returns the current {@link ViewState} + * + * @return ContentViewState + */ + @ViewState + @SuppressWarnings("unused") + public int getViewState() { + return mViewState; + } + + /** + * Sets the current {@link ViewState} + * + * @param state The {@link ViewState} to set {@link MultiStateView} to + */ + public void setViewState(@ViewState int state) { + if (state != mViewState) { + mViewState = state; + setView(); + if (mListener != null) { + mListener.onStateChanged(mViewState); + } + } + } + + /** + * Shows the {@link View} based on the {@link ViewState} + */ + private void setView() { + if (mViewState == StateLayoutConfig.BLANK) { + int size = mChildren.size(); + View view; + for (int i = 0; i < size; i++) { + view = mChildren.valueAt(i).mView; + if (view != null) { + view.setVisibility(View.GONE); + } + } + return; + } + + View curStateView = ensureStateView(mViewState); + int size = mChildren.size(); + for (int i = 0; i < size; i++) { + ViewHolder viewHolder = mChildren.valueAt(i); + if (viewHolder.mView == null) { + continue; + } + if (viewHolder.mView != curStateView) { + if (mViewState == REQUESTING && viewHolder.mView != mContentView) { + viewHolder.mView.setVisibility(GONE); + } else { + viewHolder.mView.setVisibility(GONE); + } + } + } + curStateView.setVisibility(VISIBLE); + + if (mViewState == REQUESTING) { + curStateView.setOnTouchListener(mDisableOperationWhenRequesting ? NO_ACTION_TOUCH_LISTENER : null); + } + + } + + /** + * Checks if the given {@link View} is valid for the Content View + * + * @param view The {@link View} to check + */ + private boolean isValidContentView(View view) { + if (mContentView != null && mContentView != view) { + return false; + } + Object tag = view.getTag(R.id.base_tag_multi_state_view); + if (tag == null) { + mChildren.put(CONTENT, new ViewHolder(view, 0)); + return true; + } + if (tag instanceof Integer) { + int viewTag = (Integer) tag; + if (viewTag != LOADING && viewTag != EMPTY && viewTag != ERROR && viewTag != NET_ERROR && viewTag != SERVER_ERROR && viewTag != REQUESTING) { + mChildren.put(CONTENT, new ViewHolder(view, 0)); + return true; + } else { + return false; + } + } + mChildren.put(CONTENT, new ViewHolder(view, 0)); + return true; + } + + public void setDisableOperationWhenRequesting(boolean disableOperationWhenRequesting) { + mDisableOperationWhenRequesting = disableOperationWhenRequesting; + ViewHolder viewHolder = mChildren.get(LOADING); + if (viewHolder == null || viewHolder.mView == null) { + return; + } + if (disableOperationWhenRequesting) { + if (mViewState == REQUESTING) { + viewHolder.mView.setOnTouchListener(NO_ACTION_TOUCH_LISTENER); + } + } else { + viewHolder.mView.setOnTouchListener(null); + } + } + + /** + * Sets the {@link StateListener} for the view + * + * @param listener The {@link StateListener} that will receive callbacks + */ + public void setStateListener(StateListener listener) { + mListener = listener; + } + + public interface StateListener { + /** + * Callback for when the {@link ViewState} has changed + * + * @param viewState The {@link ViewState} that was switched to + */ + void onStateChanged(@ViewState int viewState); + + /** + * Callback for when a {@link ViewState} has been inflated + * + * @param viewState The {@link ViewState} that was inflated + * @param view The {@link View} that was inflated + */ + void onStateInflated(@ViewState int viewState, @NonNull View view); + } + + @SuppressLint("ClickableViewAccessibility") + private static final OnTouchListener NO_ACTION_TOUCH_LISTENER = (v, event) -> true; + + private class ViewHolder { + private View mView; + private int mViewLayoutId; + + public ViewHolder(int viewLayoutId) { + this(null, viewLayoutId); + } + + public ViewHolder(View view, int viewLayoutId) { + mView = view; + mViewLayoutId = viewLayoutId; + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/ScrollChildSwipeRefreshLayout.java b/lib_base/src/main/java/com/android/base/widget/ScrollChildSwipeRefreshLayout.java new file mode 100644 index 0000000..8ca7795 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/ScrollChildSwipeRefreshLayout.java @@ -0,0 +1,87 @@ +package com.android.base.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.support.v4.view.ViewCompat; +import android.support.v4.widget.SwipeRefreshLayout; +import android.util.AttributeSet; +import android.view.View; + +import com.android.base.R; + + +/** + * Extends {@link SwipeRefreshLayout} to support non-direct descendant scrolling views. + *

    + * {@link SwipeRefreshLayout} works as expected when a scroll view is a direct child: it triggers + * the refresh only when the view is on top. This class adds a way (@link #setScrollUpChild} to + * define which view controls this behavior. + */ +public class ScrollChildSwipeRefreshLayout extends SwipeRefreshLayout { + + private View mScrollUpChild; + private int mTargetId; + private boolean mRestoreRefreshStatus; + + public ScrollChildSwipeRefreshLayout(Context context) { + this(context, null); + } + + public ScrollChildSwipeRefreshLayout(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ScrollChildSwipeRefreshLayout); + mTargetId = typedArray.getResourceId(R.styleable.ScrollChildSwipeRefreshLayout_srl_target_id, 0); + int colorSchemeArrayId = typedArray.getResourceId(R.styleable.ScrollChildSwipeRefreshLayout_srl_color_scheme, 0); + mRestoreRefreshStatus = typedArray.getBoolean(R.styleable.ScrollChildSwipeRefreshLayout_srl_restore_refresh_status, true); + typedArray.recycle(); + if (colorSchemeArrayId != 0) { + setColors(colorSchemeArrayId); + } + } + + private void setColors(int colorSchemeArrayId) { + TypedArray colorsTypeArray = getResources().obtainTypedArray(colorSchemeArrayId); + int indexCount = colorsTypeArray.length(); + if (indexCount != 0) { + int colors[] = new int[indexCount]; + for (int i = 0; i < indexCount; i++) { + colors[i] = colorsTypeArray.getColor(i, Color.BLACK); + } + setColorSchemeColors(colors); + } + colorsTypeArray.recycle(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + if (mTargetId != 0) { + mScrollUpChild = findViewById(mTargetId); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mRestoreRefreshStatus && isRefreshing()) { + //show animation + setRefreshing(false); + setRefreshing(true); + } + } + + @Override + public boolean canChildScrollUp() { + if (mScrollUpChild != null) { + return ViewCompat.canScrollVertically(mScrollUpChild, -1); + } + return super.canChildScrollUp(); + } + + @SuppressWarnings("unused") + public void setScrollUpChild(View view) { + mScrollUpChild = view; + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/SimpleLayout.java b/lib_base/src/main/java/com/android/base/widget/SimpleLayout.java new file mode 100644 index 0000000..a6fd29c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/SimpleLayout.java @@ -0,0 +1,89 @@ +package com.android.base.widget; + +import android.content.Context; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * a SimpleLayout look like FrameLayout + */ +public class SimpleLayout extends ViewGroup { + + public SimpleLayout(Context context) { + super(context); + } + + public SimpleLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SimpleLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public SimpleLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int maxHeight = 0; + int maxWidth = 0; + int childState = 0; + + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); + maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); + childState = combineMeasuredStates(childState, child.getMeasuredState()); + } + } + + maxWidth += getPaddingLeft() + getPaddingRight(); + maxHeight += getPaddingTop() + getPaddingBottom(); + + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), + resolveSizeAndState(maxHeight, heightMeasureSpec, + childState << MEASURED_HEIGHT_STATE_SHIFT)); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams(); + child.layout(getPaddingLeft() + params.leftMargin, + getPaddingTop() + params.topMargin, + getPaddingRight() + child.getMeasuredWidth() + params.rightMargin, + getPaddingBottom() + child.getMeasuredHeight() + params.bottomMargin); + } + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new MarginLayoutParams(getContext(), attrs); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + @Override + protected LayoutParams generateLayoutParams(LayoutParams p) { + return new MarginLayoutParams(p); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/SimpleMultiStateView.java b/lib_base/src/main/java/com/android/base/widget/SimpleMultiStateView.java new file mode 100644 index 0000000..3f47d7e --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/SimpleMultiStateView.java @@ -0,0 +1,145 @@ +package com.android.base.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.View; + +import com.android.base.R; +import com.android.base.app.ui.StateLayout; +import com.android.base.app.ui.StateLayoutConfig; +import com.android.base.utils.common.StringChecker; + +import timber.log.Timber; + +import static com.android.base.app.ui.StateLayoutConfig.CONTENT; +import static com.android.base.app.ui.StateLayoutConfig.EMPTY; +import static com.android.base.app.ui.StateLayoutConfig.ERROR; +import static com.android.base.app.ui.StateLayoutConfig.LOADING; +import static com.android.base.app.ui.StateLayoutConfig.ViewState; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-04-21 10:21 + */ +public class SimpleMultiStateView extends MultiStateView implements StateLayout { + + private StateActionProcessor mStateActionProcessor; + private StateListener mStateListener; + + public SimpleMultiStateView(Context context) { + this(context, null); + } + + public SimpleMultiStateView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SimpleMultiStateView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setListener(); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SimpleMultiStateView, defStyle, defStyle); + + initProcessor(typedArray); + + mStateActionProcessor.onInitialize(this); + mStateActionProcessor.onParseAttrs(typedArray); + + typedArray.recycle(); + } + + private void initProcessor(TypedArray typedArray) { + String processorPath = typedArray.getString(R.styleable.SimpleMultiStateView_msv_state_processor); + + if (!StringChecker.isEmpty(processorPath)) { + try { + Class processorClass = Class.forName(processorPath); + mStateActionProcessor = (StateActionProcessor) processorClass.newInstance(); + } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { + e.printStackTrace(); + Timber.e("initProcessor() called can not instance processor: " + processorPath); + } + } + + if (mStateActionProcessor == null) { + mStateActionProcessor = new StateActionProcessor(); + } + } + + private void setListener() { + super.setStateListener(new StateListener() { + @Override + public void onStateChanged(@StateLayoutConfig.ViewState int viewState) { + if (mStateListener != null) { + mStateListener.onStateChanged(viewState); + } + } + + @Override + public void onStateInflated(@StateLayoutConfig.ViewState int viewState, @NonNull android.view.View view) { + if (mStateListener != null) { + mStateListener.onStateInflated(viewState, view); + } + processStateInflated(viewState, view); + } + }); + } + + private void processStateInflated(@ViewState int viewState, @NonNull View view) { + mStateActionProcessor.processStateInflated(viewState, view); + } + + @Override + public void setStateListener(StateListener listener) { + mStateListener = listener; + } + + @Override + public void showContentLayout() { + setViewState(CONTENT); + } + + @Override + public void showLoadingLayout() { + setViewState(LOADING); + } + + @Override + public void showEmptyLayout() { + setViewState(EMPTY); + } + + @Override + public void showErrorLayout() { + setViewState(ERROR); + } + + @Override + public void showRequesting() { + setViewState(StateLayoutConfig.REQUESTING); + } + + @Override + public void showBlank() { + setViewState(StateLayoutConfig.BLANK); + } + + @Override + public void showNetErrorLayout() { + setViewState(StateLayoutConfig.NET_ERROR); + } + + @Override + public void showServerErrorLayout() { + setViewState(StateLayoutConfig.SERVER_ERROR); + } + + @Override + public StateLayoutConfig getStateLayoutConfig() { + return mStateActionProcessor.getStateLayoutConfigImpl(); + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/StateActionProcessor.java b/lib_base/src/main/java/com/android/base/widget/StateActionProcessor.java new file mode 100644 index 0000000..1001b67 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/StateActionProcessor.java @@ -0,0 +1,197 @@ +package com.android.base.widget; + +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.base.R; +import com.android.base.app.ui.CommonId; +import com.android.base.app.ui.OnRetryActionListener; +import com.android.base.app.ui.StateLayoutConfig; + +import static com.android.base.app.ui.StateLayoutConfig.EMPTY; +import static com.android.base.app.ui.StateLayoutConfig.ERROR; +import static com.android.base.app.ui.StateLayoutConfig.NET_ERROR; +import static com.android.base.app.ui.StateLayoutConfig.SERVER_ERROR; + + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-05-17 17:08 + */ +public class StateActionProcessor implements StateProcessor { + + private OnRetryActionListener mOnRetryActionListener; + + private ViewInfo mEmptyViewInfo; + private ViewInfo mErrorViewInfo; + private ViewInfo mNetErrorViewInfo; + private ViewInfo mServerErrorViewInfo; + + private SimpleMultiStateView mSimpleMultiStateView; + + @Override + public void onInitialize(SimpleMultiStateView simpleMultiStateView) { + mSimpleMultiStateView = simpleMultiStateView; + } + + @Override + public void onParseAttrs(TypedArray typedArray) { + mErrorViewInfo = new ViewInfo(ERROR); + mErrorViewInfo.mDrawable = typedArray.getDrawable(R.styleable.SimpleMultiStateView_msv_errorImg); + mErrorViewInfo.mMessage = typedArray.getText(R.styleable.SimpleMultiStateView_msv_errorText); + mErrorViewInfo.mActionText = typedArray.getText(R.styleable.SimpleMultiStateView_msv_errorAction); + + mEmptyViewInfo = new ViewInfo(EMPTY); + mEmptyViewInfo.mDrawable = typedArray.getDrawable(R.styleable.SimpleMultiStateView_msv_emptyImg); + mEmptyViewInfo.mMessage = typedArray.getText(R.styleable.SimpleMultiStateView_msv_emptyText); + mEmptyViewInfo.mActionText = typedArray.getText(R.styleable.SimpleMultiStateView_msv_emptyAction); + + mNetErrorViewInfo = new ViewInfo(NET_ERROR); + mNetErrorViewInfo.mDrawable = typedArray.getDrawable(R.styleable.SimpleMultiStateView_msv_net_errorImg); + mNetErrorViewInfo.mMessage = typedArray.getText(R.styleable.SimpleMultiStateView_msv_net_errorText); + mNetErrorViewInfo.mActionText = typedArray.getText(R.styleable.SimpleMultiStateView_msv_net_errorAction); + + mServerErrorViewInfo = new ViewInfo(SERVER_ERROR); + mServerErrorViewInfo.mDrawable = typedArray.getDrawable(R.styleable.SimpleMultiStateView_msv_server_errorImg); + mServerErrorViewInfo.mMessage = typedArray.getText(R.styleable.SimpleMultiStateView_msv_server_errorText); + mServerErrorViewInfo.mActionText = typedArray.getText(R.styleable.SimpleMultiStateView_msv_server_errorAction); + } + + @Override + public void processStateInflated(@StateLayoutConfig.ViewState int viewState, @NonNull View view) { + if (viewState == ERROR) { + mErrorViewInfo.setStateView(view); + } else if (viewState == EMPTY) { + mEmptyViewInfo.setStateView(view); + } else if (viewState == NET_ERROR) { + mNetErrorViewInfo.setStateView(view); + } else if (viewState == SERVER_ERROR) { + mServerErrorViewInfo.setStateView(view); + } + } + + private StateActionProcessor.ViewInfo getViewInfoForState(@StateLayoutConfig.ViewState int viewState) { + if (viewState == EMPTY) { + return mEmptyViewInfo; + } else if (viewState == ERROR) { + return mErrorViewInfo; + } else if (viewState == NET_ERROR) { + return mNetErrorViewInfo; + } else if (viewState == SERVER_ERROR) { + return mServerErrorViewInfo; + } + throw new IllegalArgumentException("no viewInfo in this state"); + } + + @Override + public StateLayoutConfig getStateLayoutConfigImpl() { + return mStateLayoutConfig; + } + + class ViewInfo { + + private final int mState; + private Drawable mDrawable; + private CharSequence mMessage; + private CharSequence mActionText; + private View mStateView; + private TextView mMessageTv; + private ImageView mIconTv; + private Button mActionBtn; + + private ViewInfo(int state) { + mState = state; + } + + void setStateView(View stateView) { + mStateView = stateView; + mIconTv = mStateView.findViewById(CommonId.RETRY_IV_ID); + mMessageTv = mStateView.findViewById(CommonId.RETRY_TV_ID); + mActionBtn = mStateView.findViewById(CommonId.RETRY_BTN_ID); + mActionBtn.setOnClickListener(v -> { + if (mOnRetryActionListener != null) { + mOnRetryActionListener.onRetry(mState); + } + }); + setActionText(mActionText); + setMessage(mMessage); + setDrawable(mDrawable); + } + + void setDrawable(Drawable drawable) { + mDrawable = drawable; + if (mIconTv != null) { + mIconTv.setImageDrawable(drawable); + } + } + + void setMessage(CharSequence message) { + mMessage = message; + if (mMessageTv != null) { + mMessageTv.setText(mMessage); + } + } + + void setActionText(CharSequence actionText) { + mActionText = actionText; + if (mActionBtn == null) { + return; + } + mActionBtn.setText(mActionText); + if (TextUtils.isEmpty(mActionText)) { + mActionBtn.setVisibility(View.GONE); + } else { + mActionBtn.setVisibility(View.VISIBLE); + } + } + } + + private StateLayoutConfig mStateLayoutConfig = new StateLayoutConfig() { + + @Override + public StateLayoutConfig setStateMessage(@ViewState int state, CharSequence message) { + getViewInfoForState(state).setMessage(message); + return this; + } + + @Override + public StateLayoutConfig setStateIcon(@ViewState int state, Drawable drawable) { + getViewInfoForState(state).setDrawable(drawable); + return this; + } + + @Override + public StateLayoutConfig setStateIcon(@ViewState int state, @DrawableRes int drawableId) { + getViewInfoForState(state).setDrawable(ContextCompat.getDrawable(mSimpleMultiStateView.getContext(), drawableId)); + return this; + } + + @Override + public StateLayoutConfig setStateAction(@ViewState int state, CharSequence actionText) { + getViewInfoForState(state).setActionText(actionText); + return this; + } + + @Override + public StateLayoutConfig setStateRetryListener(OnRetryActionListener retryActionListener) { + mOnRetryActionListener = retryActionListener; + return this; + } + + @Override + public StateLayoutConfig disableOperationWhenRequesting(boolean disable) { + mSimpleMultiStateView.setDisableOperationWhenRequesting(disable); + return this; + } + }; + +} diff --git a/lib_base/src/main/java/com/android/base/widget/StateProcessor.java b/lib_base/src/main/java/com/android/base/widget/StateProcessor.java new file mode 100644 index 0000000..f094a73 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/StateProcessor.java @@ -0,0 +1,24 @@ +package com.android.base.widget; + +import android.content.res.TypedArray; +import android.support.annotation.NonNull; +import android.view.View; + +import com.android.base.app.ui.StateLayoutConfig; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-05-17 17:25 + */ +interface StateProcessor { + + void onInitialize(SimpleMultiStateView simpleMultiStateView); + + void onParseAttrs(TypedArray typedArray); + + void processStateInflated(@StateLayoutConfig.ViewState int viewState, @NonNull View view); + + StateLayoutConfig getStateLayoutConfigImpl(); + +} diff --git a/lib_base/src/main/java/com/android/base/widget/pulltozoom/NestedScrollView.java b/lib_base/src/main/java/com/android/base/widget/pulltozoom/NestedScrollView.java new file mode 100644 index 0000000..21dde69 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/pulltozoom/NestedScrollView.java @@ -0,0 +1,1937 @@ +package com.android.base.widget.pulltozoom; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.InputDeviceCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.NestedScrollingChild; +import android.support.v4.view.NestedScrollingChildHelper; +import android.support.v4.view.NestedScrollingParent; +import android.support.v4.view.NestedScrollingParentHelper; +import android.support.v4.view.ScrollingView; +import android.support.v4.view.VelocityTrackerCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.support.v4.widget.EdgeEffectCompat; +import android.support.v4.widget.ScrollerCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.ScrollView; + +import java.util.List; + +class NestedScrollView extends FrameLayout implements NestedScrollingParent, NestedScrollingChild, ScrollingView { + + static final int ANIMATED_SCROLL_GAP = 250; + static final float MAX_SCROLL_FACTOR = 0.5f; + + private static final String TAG = NestedScrollView.class.getSimpleName(); + + /** + * Interface definition for a callback to be invoked when the scroll + * X or Y positions of a view change. + *

    + *

    This version of the interface works on all versions of Android, back to API v4.

    + * + * @see #setOnScrollChangeListener(OnScrollChangeListener) + */ + public interface OnScrollChangeListener { + /** + * Called when the scroll position of a view changes. + * + * @param v The view whose scroll position has changed. + * @param scrollX Current horizontal scroll origin. + * @param scrollY Current vertical scroll origin. + * @param oldScrollX Previous horizontal scroll origin. + * @param oldScrollY Previous vertical scroll origin. + */ + void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY); + } + + private long mLastScroll; + + private final Rect mTempRect = new Rect(); + private ScrollerCompat mScroller; + private EdgeEffectCompat mEdgeGlowTop; + private EdgeEffectCompat mEdgeGlowBottom; + + /** + * Position of the last motion event. + */ + private int mLastMotionY; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + private boolean mIsLaidOut = false; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + /** + * True if the subscribeUser is currently dragging this ScrollView around. This is + * not the same as 'is being flinged', which can be checked by + * mScroller.isFinished() (flinging begins when the subscribeUser lifts his finger). + */ + private boolean mIsBeingDragged = false; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + /** + * When set to true, the scroll view measure its child to make it fill the currently + * visible area. + */ + private boolean mFillViewport; + + /** + * Whether arrow scrolling is animated. + */ + private boolean mSmoothScrollingEnabled = true; + + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + + /** + * Used during scrolling to retrieve the new offset within the window. + */ + private final int[] mScrollOffset = new int[2]; + private final int[] mScrollConsumed = new int[2]; + private int mNestedYOffset; + + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + private SavedState mSavedState; + + private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); + + private static final int[] SCROLLVIEW_STYLEABLE = new int[]{ + android.R.attr.fillViewport + }; + + private final NestedScrollingParentHelper mParentHelper; + private final NestedScrollingChildHelper mChildHelper; + + private float mVerticalScrollFactor; + + private OnScrollChangeListener mOnScrollChangeListener; + + public NestedScrollView(Context context) { + this(context, null); + } + + public NestedScrollView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initScrollView(); + + final TypedArray a = context.obtainStyledAttributes( + attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0); + + setFillViewport(a.getBoolean(0, false)); + + a.recycle(); + + mParentHelper = new NestedScrollingParentHelper(this); + mChildHelper = new NestedScrollingChildHelper(this); + + // ...because why else would you be using this widget? + setNestedScrollingEnabled(true); + + ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); + } + + // NestedScrollingChild + + @Override + public void setNestedScrollingEnabled(boolean enabled) { + mChildHelper.setNestedScrollingEnabled(enabled); + } + + @Override + public boolean isNestedScrollingEnabled() { + return mChildHelper.isNestedScrollingEnabled(); + } + + @Override + public boolean startNestedScroll(int axes) { + return mChildHelper.startNestedScroll(axes); + } + + @Override + public void stopNestedScroll() { + mChildHelper.stopNestedScroll(); + } + + @Override + public boolean hasNestedScrollingParent() { + return mChildHelper.hasNestedScrollingParent(); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); + } + + // NestedScrollingParent + + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; + } + + @Override + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { + mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + } + + @Override + public void onStopNestedScroll(View target) { + mParentHelper.onStopNestedScroll(target); + stopNestedScroll(); + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed) { + final int oldScrollY = getScrollY(); + scrollBy(0, dyUnconsumed); + final int myConsumed = getScrollY() - oldScrollY; + final int myUnconsumed = dyUnconsumed - myConsumed; + dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); + } + + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + dispatchNestedPreScroll(dx, dy, consumed, null); + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (!consumed) { + flingWithNestedDispatch((int) velocityY); + return true; + } + return false; + } + + @Override + public boolean onNestedPreFling(View target, float velocityX, float velocityY) { + return dispatchNestedPreFling(velocityX, velocityY); + } + + @Override + public int getNestedScrollAxes() { + return mParentHelper.getNestedScrollAxes(); + } + + // ScrollView import + + public boolean shouldDelayChildPressedState() { + return true; + } + + @Override + protected float getTopFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int scrollY = getScrollY(); + if (scrollY < length) { + return scrollY / (float) length; + } + + return 1.0f; + } + + @Override + protected float getBottomFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int bottomEdge = getHeight() - getPaddingBottom(); + final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (span < length) { + return span / (float) length; + } + + return 1.0f; + } + + /** + * @return The maximum amount this scroll view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmount() { + return (int) (MAX_SCROLL_FACTOR * getHeight()); + } + + private void initScrollView() { + mScroller = ScrollerCompat.create(getContext(), null); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setWillNotDraw(false); + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + @Override + public void addView(View child) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child); + } + + @Override + public void addView(View child, int index) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, params); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index, params); + } + + /** + * Register a callback to be invoked when the scroll X or Y positions of + * this view change. + *

    This version of the method works on all versions of Android, back to API v4.

    + * + * @param l The listener to notify when the scroll X or Y position changes. + * @see View#getScrollX() + * @see View#getScrollY() + */ + public void setOnScrollChangeListener(OnScrollChangeListener l) { + mOnScrollChangeListener = l; + } + + /** + * @return Returns true this ScrollView can be scrolled + */ + private boolean canScroll() { + View child = getChildAt(0); + if (child != null) { + int childHeight = child.getHeight(); + return getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); + } + return false; + } + + /** + * Indicates whether this ScrollView's content is stretched to fill the viewport. + * + * @return True if the content fills the viewport, false otherwise. + * @attr name android:fillViewport + */ + public boolean isFillViewport() { + return mFillViewport; + } + + /** + * Set whether this ScrollView should stretch its content height to fill the viewport or not. + * + * @param fillViewport True to stretch the content's height to the viewport's + * boundaries, false otherwise. + * @attr name android:fillViewport + */ + public void setFillViewport(boolean fillViewport) { + if (fillViewport != mFillViewport) { + mFillViewport = fillViewport; + requestLayout(); + } + } + + /** + * @return Whether arrow scrolling will animate its transition. + */ + public boolean isSmoothScrollingEnabled() { + return mSmoothScrollingEnabled; + } + + /** + * Set whether arrow scrolling will animate its transition. + * + * @param smoothScrollingEnabled whether arrow scrolling will animate its transition + */ + public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { + mSmoothScrollingEnabled = smoothScrollingEnabled; + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + + if (mOnScrollChangeListener != null) { + mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (!mFillViewport) { + return; + } + + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == MeasureSpec.UNSPECIFIED) { + return; + } + + if (getChildCount() > 0) { + final View child = getChildAt(0); + int height = getMeasuredHeight(); + if (child.getMeasuredHeight() < height) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeft() + getPaddingRight(), lp.width); + height -= getPaddingTop(); + height -= getPaddingBottom(); + int childHeightMeasureSpec = + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + mTempRect.setEmpty(); + + if (!canScroll()) { + if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, + currentFocused, View.FOCUS_DOWN); + return nextFocused != null + && nextFocused != this + && nextFocused.requestFocus(View.FOCUS_DOWN); + } + return false; + } + + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_UP); + } else { + handled = fullScroll(View.FOCUS_UP); + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_DOWN); + } else { + handled = fullScroll(View.FOCUS_DOWN); + } + break; + case KeyEvent.KEYCODE_SPACE: + pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); + break; + } + } + + return handled; + } + + private boolean inChild(int x, int y) { + if (getChildCount() > 0) { + final int scrollY = getScrollY(); + final View child = getChildAt(0); + return !(y < child.getTop() - scrollY + || y >= child.getBottom() - scrollY + || x < child.getLeft() + || x >= child.getRight()); + } + return false; + } + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + private void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + recycleVelocityTracker(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + /* + * Shortcut the most recurring case: the subscribeUser is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { + return true; + } + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the subscribeUser has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = ev.findPointerIndex(activePointerId); + if (pointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + activePointerId + + " in onInterceptTouchEvent"); + break; + } + + final int y = (int) ev.getY(pointerIndex); + final int yDiff = Math.abs(y - mLastMotionY); + if (yDiff > mTouchSlop + && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { + mIsBeingDragged = true; + mLastMotionY = y; + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + mNestedYOffset = 0; + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + final int y = (int) ev.getY(); + if (!inChild((int) ev.getX(), (int) y)) { + mIsBeingDragged = false; + recycleVelocityTracker(); + break; + } + + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionY = y; + mActivePointerId = ev.getPointerId(0); + + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + /* + * If being flinged and subscribeUser touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. We need to call computeScrollOffset() first so that + * isFinished() is correct. + */ + mScroller.computeScrollOffset(); + mIsBeingDragged = !mScroller.isFinished(); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + /* Release the drag */ + mIsBeingDragged = false; + mActivePointerId = INVALID_POINTER; + recycleVelocityTracker(); + if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { + ViewCompat.postInvalidateOnAnimation(this); + } + stopNestedScroll(); + break; + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + initVelocityTrackerIfNotExists(); + + MotionEvent vtev = MotionEvent.obtain(ev); + + final int actionMasked = MotionEventCompat.getActionMasked(ev); + + if (actionMasked == MotionEvent.ACTION_DOWN) { + mNestedYOffset = 0; + } + vtev.offsetLocation(0, mNestedYOffset); + + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: { + if (getChildCount() == 0) { + return false; + } + if ((mIsBeingDragged = !mScroller.isFinished())) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + + /* + * If being flinged and subscribeUser touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + break; + } + case MotionEvent.ACTION_MOVE: + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + if (activePointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); + break; + } + + final int y = (int) ev.getY(activePointerIndex); + int deltaY = mLastMotionY - y; + if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { + deltaY -= mScrollConsumed[1]; + vtev.offsetLocation(0, mScrollOffset[1]); + mNestedYOffset += mScrollOffset[1]; + } + if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + mIsBeingDragged = true; + if (deltaY > 0) { + deltaY -= mTouchSlop; + } else { + deltaY += mTouchSlop; + } + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + mLastMotionY = y - mScrollOffset[1]; + + final int oldY = getScrollY(); + final int range = getScrollRange(); + final int overscrollMode = getOverScrollMode(); + boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS + || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + + // Calling overScrollByCompat will call onOverScrolled, which + // calls onScrollChanged if applicable. + if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, + 0, true) && !hasNestedScrollingParent()) { + // Break our velocity if we hit a scroll barrier. + mVelocityTracker.clear(); + } + + final int scrolledDeltaY = getScrollY() - oldY; + final int unconsumedY = deltaY - scrolledDeltaY; + if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { + mLastMotionY -= mScrollOffset[1]; + vtev.offsetLocation(0, mScrollOffset[1]); + mNestedYOffset += mScrollOffset[1]; + } else if (canOverscroll) { + ensureGlows(); + final int pulledToY = oldY + deltaY; + if (pulledToY < 0) { + mEdgeGlowTop.onPull((float) deltaY / getHeight(), + ev.getX(activePointerIndex) / getWidth()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (pulledToY > range) { + mEdgeGlowBottom.onPull((float) deltaY / getHeight(), + 1.f - ev.getX(activePointerIndex) + / getWidth()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + if (mEdgeGlowTop != null + && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker, + mActivePointerId); + + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + flingWithNestedDispatch(-initialVelocity); + } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, + getScrollRange())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + mActivePointerId = INVALID_POINTER; + endDrag(); + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged && getChildCount() > 0) { + if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, + getScrollRange())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + mActivePointerId = INVALID_POINTER; + endDrag(); + break; + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mLastMotionY = (int) ev.getY(index); + mActivePointerId = ev.getPointerId(index); + break; + } + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); + break; + } + + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(vtev); + } + vtev.recycle(); + return true; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = (ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK) + >> MotionEventCompat.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionY = (int) ev.getY(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEventCompat.ACTION_SCROLL: { + if (!mIsBeingDragged) { + final float vscroll = MotionEventCompat.getAxisValue(event, + MotionEventCompat.AXIS_VSCROLL); + if (vscroll != 0) { + final int delta = (int) (vscroll * getVerticalScrollFactorCompat()); + final int range = getScrollRange(); + int oldScrollY = getScrollY(); + int newScrollY = oldScrollY - delta; + if (newScrollY < 0) { + newScrollY = 0; + } else if (newScrollY > range) { + newScrollY = range; + } + if (newScrollY != oldScrollY) { + super.scrollTo(getScrollX(), newScrollY); + return true; + } + } + } + } + } + } + return false; + } + + private float getVerticalScrollFactorCompat() { + if (mVerticalScrollFactor == 0) { + TypedValue outValue = new TypedValue(); + final Context context = getContext(); + if (!context.getTheme().resolveAttribute( + android.R.attr.listPreferredItemHeight, outValue, true)) { + throw new IllegalStateException( + "Expected theme to define listPreferredItemHeight."); + } + mVerticalScrollFactor = outValue.getDimension( + context.getResources().getDisplayMetrics()); + } + return mVerticalScrollFactor; + } + + @Override + protected void onOverScrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + super.scrollTo(scrollX, scrollY); + } + + protected boolean overScrollByCompat(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverScrollX, int maxOverScrollY, + boolean isTouchEvent) { + final int overScrollMode = getOverScrollMode(); + final boolean canScrollHorizontal = + computeHorizontalScrollRange() > computeHorizontalScrollExtent(); + final boolean canScrollVertical = + computeVerticalScrollRange() > computeVerticalScrollExtent(); + final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS + || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); + final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS + || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); + + int newScrollX = scrollX + deltaX; + if (!overScrollHorizontal) { + maxOverScrollX = 0; + } + + int newScrollY = scrollY + deltaY; + if (!overScrollVertical) { + maxOverScrollY = 0; + } + + // Clamp values if at the limits and record + final int left = -maxOverScrollX; + final int right = maxOverScrollX + scrollRangeX; + final int top = -maxOverScrollY; + final int bottom = maxOverScrollY + scrollRangeY; + + boolean clampedX = false; + if (newScrollX > right) { + newScrollX = right; + clampedX = true; + } else if (newScrollX < left) { + newScrollX = left; + clampedX = true; + } + + boolean clampedY = false; + if (newScrollY > bottom) { + newScrollY = bottom; + clampedY = true; + } else if (newScrollY < top) { + newScrollY = top; + clampedY = true; + } + + if (clampedY) { + mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange()); + } + + onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); + + return clampedX || clampedY; + } + + int getScrollRange() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop())); + } + return scrollRange; + } + + /** + *

    + * Finds the next focusable component that fits in the specified bounds. + *

    + * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found + * @param bottom the bottom offset of the bounds in which a focusable must + * be found + * @return the next focusable component in the bounds or null if none can + * be found + */ + private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { + + List focusables = getFocusables(View.FOCUS_FORWARD); + View focusCandidate = null; + + /* + * A fully contained focusable is one where its top is below the bound's + * top, and its bottom is above the bound's bottom. A partially + * contained focusable is one where some part of it is within the + * bounds, but it also has some part that is not within bounds. A fully contained + * focusable is preferred to a partially contained focusable. + */ + boolean foundFullyContainedFocusable = false; + + int count = focusables.size(); + for (int i = 0; i < count; i++) { + View view = focusables.get(i); + int viewTop = view.getTop(); + int viewBottom = view.getBottom(); + + if (top < viewBottom && viewTop < bottom) { + /* + * the focusable is in the target area, it is a candidate for + * focusing + */ + + final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom); + + if (focusCandidate == null) { + /* No candidate, take this one */ + focusCandidate = view; + foundFullyContainedFocusable = viewIsFullyContained; + } else { + final boolean viewIsCloserToBoundary = + (topFocus && viewTop < focusCandidate.getTop()) + || (!topFocus && viewBottom > focusCandidate.getBottom()); + + if (foundFullyContainedFocusable) { + if (viewIsFullyContained && viewIsCloserToBoundary) { + /* + * We're dealing with only fully contained views, so + * it has to be closer to the boundary to beat our + * candidate + */ + focusCandidate = view; + } + } else { + if (viewIsFullyContained) { + /* Any fully contained view beats a partially contained view */ + focusCandidate = view; + foundFullyContainedFocusable = true; + } else if (viewIsCloserToBoundary) { + /* + * Partially contained view beats another partially + * contained view if it's closer + */ + focusCandidate = view; + } + } + } + } + } + + return focusCandidate; + } + + /** + *

    Handles scrolling in response to a "page up/down" shortcut press. This + * method will scroll the view by one page up or down and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

    + * + * @param direction the scroll direction: {@link View#FOCUS_UP} + * to go one page up or + * {@link View#FOCUS_DOWN} to go one page down + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean pageScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + if (down) { + mTempRect.top = getScrollY() + height; + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + if (mTempRect.top + height > view.getBottom()) { + mTempRect.top = view.getBottom() - height; + } + } + } else { + mTempRect.top = getScrollY() - height; + if (mTempRect.top < 0) { + mTempRect.top = 0; + } + } + mTempRect.bottom = mTempRect.top + height; + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + *

    Handles scrolling in response to a "home/end" shortcut press. This + * method will scroll the view to the top or bottom and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

    + * + * @param direction the scroll direction: {@link View#FOCUS_UP} + * to go the top of the view or + * {@link View#FOCUS_DOWN} to go the bottom + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean fullScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + mTempRect.top = 0; + mTempRect.bottom = height; + + if (down) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.bottom = view.getBottom() + getPaddingBottom(); + mTempRect.top = mTempRect.bottom - height; + } + } + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + *

    Scrolls the view to make the area defined by top and + * bottom visible. This method attempts to give the focus + * to a component visible in this area. If no component can be focused in + * the new visible area, the focus is reclaimed by this ScrollView.

    + * + * @param direction the scroll direction: {@link View#FOCUS_UP} + * to go upward, {@link View#FOCUS_DOWN} to downward + * @param top the top offset of the new area to be made visible + * @param bottom the bottom offset of the new area to be made visible + * @return true if the key event is consumed by this method, false otherwise + */ + private boolean scrollAndFocus(int direction, int top, int bottom) { + boolean handled = true; + + int height = getHeight(); + int containerTop = getScrollY(); + int containerBottom = containerTop + height; + boolean up = direction == View.FOCUS_UP; + + View newFocused = findFocusableViewInBounds(up, top, bottom); + if (newFocused == null) { + newFocused = this; + } + + if (top >= containerTop && bottom <= containerBottom) { + handled = false; + } else { + int delta = up ? (top - containerTop) : (bottom - containerBottom); + doScrollY(delta); + } + + if (newFocused != findFocus()) newFocused.requestFocus(direction); + + return handled; + } + + /** + * Handle scrolling in response to an up or down arrow click. + * + * @param direction The direction corresponding to the arrow key that was + * pressed + * @return True if we consumed the event, false otherwise + */ + public boolean arrowScroll(int direction) { + + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); + + final int maxJump = getMaxScrollAmount(); + + if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScrollY(scrollDelta); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + if (getChildCount() > 0) { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight() - getPaddingBottom(); + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) { + return false; + } + doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); + } + + if (currentFocused != null && currentFocused.isFocused() + && isOffScreen(currentFocused)) { + // previously focused item still has focus and is off screen, give + // it up (take it back to ourselves) + // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are + // sure to + // get it) + final int descendantFocusability = getDescendantFocusability(); // save + setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); + requestFocus(); + setDescendantFocusability(descendantFocusability); // restore + } + return true; + } + + /** + * @return whether the descendant of this scroll view is scrolled off + * screen. + */ + private boolean isOffScreen(View descendant) { + return !isWithinDeltaOfScreen(descendant, 0, getHeight()); + } + + /** + * @return whether the descendant of this scroll view is within delta + * pixels of being on the screen. + */ + private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) { + descendant.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(descendant, mTempRect); + + return (mTempRect.bottom + delta) >= getScrollY() + && (mTempRect.top - delta) <= (getScrollY() + height); + } + + /** + * Smooth scroll by a Y delta + * + * @param delta the number of pixels to scroll by on the Y axis + */ + private void doScrollY(int delta) { + if (delta != 0) { + if (mSmoothScrollingEnabled) { + smoothScrollBy(0, delta); + } else { + scrollBy(0, delta); + } + } + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param dx the number of pixels to scroll by on the X axis + * @param dy the number of pixels to scroll by on the Y axis + */ + public final void smoothScrollBy(int dx, int dy) { + if (getChildCount() == 0) { + // Nothing to do. + return; + } + long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; + if (duration > ANIMATED_SCROLL_GAP) { + final int height = getHeight() - getPaddingBottom() - getPaddingTop(); + final int bottom = getChildAt(0).getHeight(); + final int maxY = Math.max(0, bottom - height); + final int scrollY = getScrollY(); + dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; + + mScroller.startScroll(getScrollX(), scrollY, 0, dy); + ViewCompat.postInvalidateOnAnimation(this); + } else { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + scrollBy(dx, dy); + } + mLastScroll = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Like {@link #scrollTo}, but scroll smoothly instead of immediately. + * + * @param x the position where to scroll on the X axis + * @param y the position where to scroll on the Y axis + */ + public final void smoothScrollTo(int x, int y) { + smoothScrollBy(x - getScrollX(), y - getScrollY()); + } + + /** + *

    The scroll range of a scroll view is the overall height of all of its + * children.

    + * + * @hide + */ + @Override + public int computeVerticalScrollRange() { + final int count = getChildCount(); + final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop(); + if (count == 0) { + return contentHeight; + } + + int scrollRange = getChildAt(0).getBottom(); + final int scrollY = getScrollY(); + final int overscrollBottom = Math.max(0, scrollRange - contentHeight); + if (scrollY < 0) { + scrollRange -= scrollY; + } else if (scrollY > overscrollBottom) { + scrollRange += scrollY - overscrollBottom; + } + + return scrollRange; + } + + /** + * @hide + */ + @Override + public int computeVerticalScrollOffset() { + return Math.max(0, super.computeVerticalScrollOffset()); + } + + /** + * @hide + */ + @Override + public int computeVerticalScrollExtent() { + return super.computeVerticalScrollExtent(); + } + + /** + * @hide + */ + @Override + public int computeHorizontalScrollRange() { + return super.computeHorizontalScrollRange(); + } + + /** + * @hide + */ + @Override + public int computeHorizontalScrollOffset() { + return super.computeHorizontalScrollOffset(); + } + + /** + * @hide + */ + @Override + public int computeHorizontalScrollExtent() { + return super.computeHorizontalScrollExtent(); + } + + @Override + protected void measureChild(View child, int parentWidthMeasureSpec, + int parentHeightMeasureSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + + getPaddingRight(), lp.width); + + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + + widthUsed, lp.width); + final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + final int range = getScrollRange(); + final int overscrollMode = getOverScrollMode(); + final boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS + || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + + overScrollByCompat(x - oldX, y - oldY, oldX, oldY, 0, range, + 0, 0, false); + + if (canOverscroll) { + ensureGlows(); + if (y <= 0 && oldY > 0) { + mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (y >= range && oldY < range) { + mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); + } + } + } + } + } + + /** + * Scrolls the view to the given child. + * + * @param child the RefreshView to scroll to + */ + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + + if (scrollDelta != 0) { + scrollBy(0, scrollDelta); + } + } + + /** + * If rect is off screen, scroll just enough to get it (or at least the + * first screen size chunk of it) on screen. + * + * @param rect The rectangle. + * @param immediate True to scroll immediately without animation + * @return true if scrolling was performed + */ + private boolean scrollToChildRect(Rect rect, boolean immediate) { + final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); + final boolean scroll = delta != 0; + if (scroll) { + if (immediate) { + scrollBy(0, delta); + } else { + smoothScrollBy(0, delta); + } + } + return scroll; + } + + /** + * Compute the amount to scroll in the Y direction in order to get + * a rectangle completely on the screen (or, if taller than the screen, + * at least the first screen size chunk of it). + * + * @param rect The rect. + * @return The scroll delta. + */ + protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { + if (getChildCount() == 0) return 0; + + int height = getHeight(); + int screenTop = getScrollY(); + int screenBottom = screenTop + height; + + int fadingEdge = getVerticalFadingEdgeLength(); + + // leave room for top fading edge as long as rect isn't at very top + if (rect.top > 0) { + screenTop += fadingEdge; + } + + // leave room for bottom fading edge as long as rect isn't at very bottom + if (rect.bottom < getChildAt(0).getHeight()) { + screenBottom -= fadingEdge; + } + + int scrollYDelta = 0; + + if (rect.bottom > screenBottom && rect.top > screenTop) { + // need to move down to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + + if (rect.height() > height) { + // just enough to get screen size chunk on + scrollYDelta += (rect.top - screenTop); + } else { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - screenBottom); + } + + // make sure we aren't scrolling beyond the end of our content + int bottom = getChildAt(0).getBottom(); + int distanceToBottom = bottom - screenBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + + } else if (rect.top < screenTop && rect.bottom < screenBottom) { + // need to move up to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.height() > height) { + // screen size chunk + scrollYDelta -= (screenBottom - rect.bottom); + } else { + // entire rect at top + scrollYDelta -= (screenTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our content + scrollYDelta = Math.max(scrollYDelta, -getScrollY()); + } + return scrollYDelta; + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mIsLayoutDirty) { + scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); + } + + + /** + * When looking for focus in children of a scroll view, need to be a little + * more careful not to give focus to something that is scrolled off screen. + *

    + * This is more expensive than the default {@link ViewGroup} + * implementation, otherwise this behavior might have been made the default. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + + // convert from forward / backward notation to up / down / left / right + // (ugh). + if (direction == View.FOCUS_FORWARD) { + direction = View.FOCUS_DOWN; + } else if (direction == View.FOCUS_BACKWARD) { + direction = View.FOCUS_UP; + } + + final View nextFocus = previouslyFocusedRect == null + ? FocusFinder.getInstance().findNextFocus(this, null, direction) + : FocusFinder.getInstance().findNextFocusFromRect( + this, previouslyFocusedRect, direction); + + if (nextFocus == null) { + return false; + } + + if (isOffScreen(nextFocus)) { + return false; + } + + return nextFocus.requestFocus(direction, previouslyFocusedRect); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, + boolean immediate) { + // offset into coordinate space of this scroll view + rectangle.offset(child.getLeft() - child.getScrollX(), + child.getTop() - child.getScrollY()); + + return scrollToChildRect(rectangle, immediate); + } + + @Override + public void requestLayout() { + mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mIsLayoutDirty = false; + // Give a child focus if it needs it + if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { + scrollToChild(mChildToScrollTo); + } + mChildToScrollTo = null; + + if (!mIsLaidOut) { + if (mSavedState != null) { + scrollTo(getScrollX(), mSavedState.scrollPosition); + mSavedState = null; + } // mScrollY default value is "0" + + final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; + final int scrollRange = Math.max(0, + childHeight - (b - t - getPaddingBottom() - getPaddingTop())); + + // Don't forget to clamp + if (getScrollY() > scrollRange) { + scrollTo(getScrollX(), scrollRange); + } else if (getScrollY() < 0) { + scrollTo(getScrollX(), 0); + } + } + + // Calling this with the present values causes it to re-claim them + scrollTo(getScrollX(), getScrollY()); + mIsLaidOut = true; + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + mIsLaidOut = false; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + View currentFocused = findFocus(); + if (null == currentFocused || this == currentFocused) { + return; + } + + // If the currently-focused view was visible on the screen when the + // screen was at the old height, then scroll the screen to make that + // view visible with the new screen height. + if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { + currentFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScrollY(scrollDelta); + } + } + + /** + * Return true if child is a descendant of parent, (or equal to the parent). + */ + private static boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + /** + * Fling the scroll view + * + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/cursor is moving down the screen, + * which means we want to scroll towards the top. + */ + public void fling(int velocityY) { + if (getChildCount() > 0) { + int height = getHeight() - getPaddingBottom() - getPaddingTop(); + int bottom = getChildAt(0).getHeight(); + + mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, + Math.max(0, bottom - height), 0, height / 2); + + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private void flingWithNestedDispatch(int velocityY) { + final int scrollY = getScrollY(); + final boolean canFling = (scrollY > 0 || velocityY > 0) + && (scrollY < getScrollRange() || velocityY < 0); + if (!dispatchNestedPreFling(0, velocityY)) { + dispatchNestedFling(0, velocityY, canFling); + if (canFling) { + fling(velocityY); + } + } + } + + private void endDrag() { + mIsBeingDragged = false; + + recycleVelocityTracker(); + stopNestedScroll(); + + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + } + } + + /** + * {@inheritDoc} + *

    + *

    This version also clamps the scrolling to the bounds of our child. + */ + @Override + public void scrollTo(int x, int y) { + // we rely on the fact the RefreshView.scrollBy calls scrollTo. + if (getChildCount() > 0) { + View child = getChildAt(0); + x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); + y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); + if (x != getScrollX() || y != getScrollY()) { + super.scrollTo(x, y); + } + } + } + + private void ensureGlows() { + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + if (mEdgeGlowTop == null) { + Context context = getContext(); + mEdgeGlowTop = new EdgeEffectCompat(context); + mEdgeGlowBottom = new EdgeEffectCompat(context); + } + } else { + mEdgeGlowTop = null; + mEdgeGlowBottom = null; + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mEdgeGlowTop != null) { + final int scrollY = getScrollY(); + if (!mEdgeGlowTop.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + + canvas.translate(getPaddingLeft(), Math.min(0, scrollY)); + mEdgeGlowTop.setSize(width, getHeight()); + if (mEdgeGlowTop.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowBottom.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + final int height = getHeight(); + + canvas.translate(-width + getPaddingLeft(), + Math.max(getScrollRange(), scrollY) + height); + canvas.rotate(180, width, 0); + mEdgeGlowBottom.setSize(width, height); + if (mEdgeGlowBottom.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + } + } + + private static int clamp(int n, int my, int child) { + if (my >= child || n < 0) { + /* my >= child is this case: + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * + * n < 0 is this case: + * |------ me ------| + * |-------- child --------| + * |-- mScrollX --| + */ + return 0; + } + if ((my + n) > child) { + /* this case: + * |------ me ------| + * |------ child ------| + * |-- mScrollX --| + */ + return child - my; + } + return n; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mSavedState = ss; + requestLayout(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPosition = getScrollY(); + return ss; + } + + static class SavedState extends BaseSavedState { + public int scrollPosition; + + SavedState(Parcelable superState) { + super(superState); + } + + SavedState(Parcel source) { + super(source); + scrollPosition = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(scrollPosition); + } + + @Override + public String toString() { + return "HorizontalScrollView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " scrollPosition=" + scrollPosition + "}"; + } + + public static final Creator CREATOR = + new Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + static class AccessibilityDelegate extends AccessibilityDelegateCompat { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle arguments) { + if (super.performAccessibilityAction(host, action, arguments)) { + return true; + } + final NestedScrollView nsvHost = (NestedScrollView) host; + if (!nsvHost.isEnabled()) { + return false; + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { + final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() + - nsvHost.getPaddingTop(); + final int targetScrollY = Math.min(nsvHost.getScrollY() + viewportHeight, + nsvHost.getScrollRange()); + if (targetScrollY != nsvHost.getScrollY()) { + nsvHost.smoothScrollTo(0, targetScrollY); + return true; + } + } + return false; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { + final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() + - nsvHost.getPaddingTop(); + final int targetScrollY = Math.max(nsvHost.getScrollY() - viewportHeight, 0); + if (targetScrollY != nsvHost.getScrollY()) { + nsvHost.smoothScrollTo(0, targetScrollY); + return true; + } + } + return false; + } + return false; + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final NestedScrollView nsvHost = (NestedScrollView) host; + info.setClassName(ScrollView.class.getName()); + if (nsvHost.isEnabled()) { + final int scrollRange = nsvHost.getScrollRange(); + if (scrollRange > 0) { + info.setScrollable(true); + if (nsvHost.getScrollY() > 0) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } + if (nsvHost.getScrollY() < scrollRange) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } + } + } + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + final NestedScrollView nsvHost = (NestedScrollView) host; + event.setClassName(ScrollView.class.getName()); + final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); + final boolean scrollable = nsvHost.getScrollRange() > 0; + record.setScrollable(scrollable); + record.setScrollX(nsvHost.getScrollX()); + record.setScrollY(nsvHost.getScrollY()); + record.setMaxScrollX(nsvHost.getScrollX()); + record.setMaxScrollY(nsvHost.getScrollRange()); + } + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/pulltozoom/PullToZoomScrollView.java b/lib_base/src/main/java/com/android/base/widget/pulltozoom/PullToZoomScrollView.java new file mode 100644 index 0000000..02a1dc7 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/pulltozoom/PullToZoomScrollView.java @@ -0,0 +1,250 @@ +package com.android.base.widget.pulltozoom; + +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.DecelerateInterpolator; + +import com.android.base.R; + +/** + *

    + *     {@code
    + *     
    + *
    + *         
    + *
    + *             //header
    + *             
    + *
    + *                  //image view to pull zoom
    + *                 
    + *
    + *              
    + *
    + *              //content
    + *              
    + *
    + *          
    + *
    + *         
    + *     }
    + * 
    + */ +public class PullToZoomScrollView extends NestedScrollView { + + private OnScrollListener mScrollListener; + + private View mZoomView; + private int mZoomViewId; + + private View mContainerView; + private int mContainerViewId; + + private int mMaxZoomHeight; + private int mOriginContainerViewHeight; + private int mOriginZoomViewHeight; + private int mDamp; + private float mZoomFactory; + + private TimeInterpolator mInterpolator; + private ValueAnimator mValueAnimator; + private static final int ANIMATION_TIME = 500; + + public PullToZoomScrollView(Context context) { + this(context, null); + } + + public PullToZoomScrollView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PullToZoomScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PullToZoomScrollView); + mZoomViewId = typedArray.getResourceId(R.styleable.PullToZoomScrollView_psv_zoom_view, -1); + mContainerViewId = typedArray.getResourceId(R.styleable.PullToZoomScrollView_psv_header_view, -1); + mDamp = typedArray.getInteger(R.styleable.PullToZoomScrollView_psv_damp, 2);//默认阻尼2 + mMaxZoomHeight = typedArray.getInteger(R.styleable.PullToZoomScrollView_psv_max_over, dpToPx(1500));//默认最大scroll 1500DP + mZoomFactory = typedArray.getFloat(R.styleable.PullToZoomScrollView_psv_zoom_factory, 2.0F); + typedArray.recycle(); + + init(); + } + + private void init() { + mInterpolator = new DecelerateInterpolator(); + } + + public void setScrollListener(OnScrollListener scrollListener) { + mScrollListener = scrollListener; + } + + public interface OnScrollListener { + void onScroll(int headerLayoutHeight, int zoomViewHeight, int scrollY); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + if (mZoomViewId != -1) { + mZoomView = findViewById(mZoomViewId); + } + if (mContainerViewId != -1) { + mContainerView = findViewById(mContainerViewId); + } + getInnerViewHeight(); + } + + private boolean hasInnerView() { + return mContainerView != null && mZoomView != null; + } + + @Override + protected boolean overScrollByCompat(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverScrollX, + int maxOverScrollY, boolean isTouchEvent) { + //overScrollBy如果返回True,那么ScrollView将不会滑动 + return processOverScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent) + || super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); + } + + @SuppressWarnings("unused") + private boolean processOverScrollBy(int deltaX, int deltaY, int scrollX, + int scrollY, int scrollRangeX, int scrollRangeY, + int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { + if (!hasInnerView()) { + return false; + } + if (mContainerView.getHeight() <= mMaxZoomHeight && isTouchEvent) { + if (deltaY < 0) { + int offset = (int) (deltaY * 1.0F / mDamp); + if (mContainerView.getHeight() - offset >= mOriginContainerViewHeight) { + int height = mContainerView + .getHeight() - offset < mMaxZoomHeight ? + mContainerView.getHeight() - offset : mMaxZoomHeight; + setContainerHeight(height); + } + } else { + if (mContainerView.getHeight() > mOriginContainerViewHeight) { + int height = mContainerView.getHeight() - deltaY > mOriginContainerViewHeight ? + mContainerView.getHeight() - deltaY : mOriginContainerViewHeight; + setContainerHeight(height); + return true; + } + } + } + return false; + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (hasInnerView()) { + int action = ev.getAction(); + if (action == MotionEvent.ACTION_DOWN) { + cancelAnim(); + } + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + zoomBackIfNeed(); + } + } + return super.onTouchEvent(ev); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mScrollListener != null) { + if (mOriginContainerViewHeight != 0 && mOriginZoomViewHeight != 0) { + mScrollListener.onScroll(mOriginContainerViewHeight, mOriginZoomViewHeight, getScrollY()); + } + } + } + + private void zoomBackIfNeed() { + cancelAnim(); + if (mOriginContainerViewHeight - 1 < mContainerView.getHeight()) { + mValueAnimator = ValueAnimator.ofInt(mContainerView.getHeight(), mOriginContainerViewHeight); + mValueAnimator.setDuration(ANIMATION_TIME); + mValueAnimator.setInterpolator(mInterpolator); + mValueAnimator.addUpdateListener(mAnimatorUpdateListener); + mValueAnimator.start(); + } + } + + private void cancelAnim() { + if (mValueAnimator != null) { + mValueAnimator.cancel(); + mValueAnimator = null; + } + } + + private ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener = animation -> { + int animatedValue = (int) animation.getAnimatedValue(); + setContainerHeight(animatedValue); + }; + + private void setContainerHeight(int height) { + mContainerView.getLayoutParams().height = height; + if (height >= mOriginContainerViewHeight) { + zoomView(height); + } + mContainerView.setLayoutParams(mContainerView.getLayoutParams()); + } + + private void zoomView(int height) { + int measuredWidth = mZoomView.getMeasuredWidth(); + mZoomView.setPivotX(measuredWidth / 2); + mZoomView.setPivotY(mOriginContainerViewHeight / 3F); + float addOffset = (height - mOriginContainerViewHeight) * mZoomFactory / mOriginContainerViewHeight; + float scale = height * 1.0F / mOriginContainerViewHeight + addOffset; + mZoomView.setScaleX(scale); + mZoomView.setScaleY(scale); + } + + private void getInnerViewHeight() { + post(() -> { + if (mContainerView != null) { + mOriginContainerViewHeight = mContainerView.getHeight(); + } + if (mZoomView != null) { + mOriginZoomViewHeight = mZoomView.getHeight(); + } + }); + } + + public int dpToPx(int dp) { + return (int) (dp * getContext().getResources().getDisplayMetrics().density + 0.5f); + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/ratio/RatioHelper.java b/lib_base/src/main/java/com/android/base/widget/ratio/RatioHelper.java new file mode 100644 index 0000000..c38faf0 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/ratio/RatioHelper.java @@ -0,0 +1,54 @@ +package com.android.base.widget.ratio; + +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; + +import com.android.base.R; + + +class RatioHelper { + + private static final int[] ratioAttrs = {R.attr.layout_ratio}; + private float mRatio; + private View mView; + + RatioHelper(View view) { + mView = view; + } + + void resolveAttr(AttributeSet attrs) { + TypedArray typedArray = mView.getContext().obtainStyledAttributes(attrs, ratioAttrs); + mRatio = typedArray.getFloat(0, 0F); + typedArray.recycle(); + } + + void setRatio(float ratio) { + mRatio = ratio; + } + + int[] measure(int widthMeasureSpec, int heightMeasureSpec) { + + if (mRatio != 0) { + int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); + int heightMode = View.MeasureSpec.getMode(heightMeasureSpec); + + int width = View.MeasureSpec.getSize(widthMeasureSpec) - mView.getPaddingLeft() - mView.getPaddingRight(); + int height = View.MeasureSpec.getSize(heightMeasureSpec) - mView.getPaddingTop() - mView.getPaddingBottom(); + + if (widthMode == View.MeasureSpec.EXACTLY && heightMode != View.MeasureSpec.EXACTLY) { + + height = (int) (width / mRatio + 0.5f); + heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height + mView.getPaddingTop() + mView.getPaddingBottom(), View.MeasureSpec.EXACTLY); + + } else if (widthMode != View.MeasureSpec.EXACTLY && heightMode == View.MeasureSpec.EXACTLY) { + + width = (int) (height * mRatio + 0.5f); + widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(width + mView.getPaddingLeft() + mView.getPaddingRight(), View.MeasureSpec.EXACTLY); + + } + } + return new int[]{widthMeasureSpec, heightMeasureSpec}; + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/ratio/RatioImageView.java b/lib_base/src/main/java/com/android/base/widget/ratio/RatioImageView.java new file mode 100644 index 0000000..911f0fc --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/ratio/RatioImageView.java @@ -0,0 +1,36 @@ +package com.android.base.widget.ratio; + +import android.content.Context; +import android.support.v7.widget.AppCompatImageView; +import android.util.AttributeSet; + + +public class RatioImageView extends AppCompatImageView { + + private RatioHelper mRatioHelper; + + public RatioImageView(Context context) { + this(context, null); + } + + public RatioImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RatioImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mRatioHelper = new RatioHelper(this); + mRatioHelper.resolveAttr(attrs); + } + + public void setRatio(float ratio) { + mRatioHelper.setRatio(ratio); + requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int[] measure = mRatioHelper.measure(widthMeasureSpec, heightMeasureSpec); + super.onMeasure(measure[0], measure[1]); + } +} diff --git a/lib_base/src/main/java/com/android/base/widget/ratio/RatioLayout.java b/lib_base/src/main/java/com/android/base/widget/ratio/RatioLayout.java new file mode 100644 index 0000000..842eef1 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/ratio/RatioLayout.java @@ -0,0 +1,57 @@ +package com.android.base.widget.ratio; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import com.android.base.R; + + +public class RatioLayout extends FrameLayout { + + // 宽和高的比例 + private float ratio = 0.0f; + + public RatioLayout(Context context) { + this(context, null); + } + + public RatioLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RatioLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RatioLayout); + ratio = a.getFloat(R.styleable.RatioLayout_layout_ratio, 0.0f); + a.recycle(); + } + + public void setRatio(float f) { + ratio = f; + requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); + int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom(); + + if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY && ratio != 0.0f) { + + height = (int) (width / ratio + 0.5f); + heightMeasureSpec = MeasureSpec.makeMeasureSpec(height + getPaddingTop() + getPaddingBottom(), MeasureSpec.EXACTLY); + + } else if (widthMode != MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY && ratio != 0.0f) { + + width = (int) (height * ratio + 0.5f); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(width + getPaddingLeft() + getPaddingRight(), MeasureSpec.EXACTLY); + + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/lib_base/src/main/java/com/android/base/widget/ratio/RatioTextView.java b/lib_base/src/main/java/com/android/base/widget/ratio/RatioTextView.java new file mode 100644 index 0000000..f1983c9 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/ratio/RatioTextView.java @@ -0,0 +1,36 @@ +package com.android.base.widget.ratio; + +import android.content.Context; +import android.support.v7.widget.AppCompatTextView; +import android.util.AttributeSet; + +public class RatioTextView extends AppCompatTextView { + + private RatioHelper mRatioHelper; + + public RatioTextView(Context context) { + this(context, null); + } + + public RatioTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RatioTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mRatioHelper = new RatioHelper(this); + mRatioHelper.resolveAttr(attrs); + } + + public void setRatio(float ratio) { + mRatioHelper.setRatio(ratio); + requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int[] measure = mRatioHelper.measure(widthMeasureSpec, heightMeasureSpec); + super.onMeasure(measure[0], measure[1]); + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/ratio/TopDrawableCenterTextView.java b/lib_base/src/main/java/com/android/base/widget/ratio/TopDrawableCenterTextView.java new file mode 100644 index 0000000..4fe0250 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/ratio/TopDrawableCenterTextView.java @@ -0,0 +1,58 @@ +package com.android.base.widget.ratio; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.text.TextPaint; +import android.util.AttributeSet; + +/** + * 使 TopDrawable 居中,不要设置 gravity 属性为 center 或 center_vertical + * + * @author Ztiany + * Email: 1169654504@qq.com + */ +public class TopDrawableCenterTextView extends RatioTextView { + + private Paint.FontMetrics metrics = new Paint.FontMetrics(); + + public TopDrawableCenterTextView(Context context) { + super(context); + } + + public TopDrawableCenterTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TopDrawableCenterTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onDraw(Canvas canvas) { + Drawable[] compoundDrawables = getCompoundDrawables(); + setToCenterPadding(compoundDrawables, canvas); + super.onDraw(canvas); + } + + private void setToCenterPadding(Drawable[] compoundDrawables, Canvas canvas) { + if (compoundDrawables == null) { + return; + } + + Drawable topDrawable = compoundDrawables[1]; + + if (topDrawable == null) { + return; + } + + int compoundDrawablePadding = getCompoundDrawablePadding(); + TextPaint paint = getPaint(); + paint.getFontMetrics(metrics); + float contentHeight = (metrics.bottom - metrics.top + compoundDrawablePadding + topDrawable.getIntrinsicHeight()); + int measuredHeight = getMeasuredHeight(); + canvas.translate(0, measuredHeight / 2 - contentHeight / 2); + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/recyclerview/DividerItemDecoration.java b/lib_base/src/main/java/com/android/base/widget/recyclerview/DividerItemDecoration.java new file mode 100644 index 0000000..c751d1c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/recyclerview/DividerItemDecoration.java @@ -0,0 +1,174 @@ +package com.android.base.widget.recyclerview; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.LinearLayout; + +/** + * 可以指定 SkipDrawStartCount 和 SkipDrawEndCount + */ +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + + public static final int HORIZONTAL = LinearLayout.HORIZONTAL; + public static final int VERTICAL = LinearLayout.VERTICAL; + + private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; + + private Drawable mDivider; + + /** + * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}. + */ + private int mOrientation; + + private final Rect mBounds = new Rect(); + private int mSkipDrawStartCount; + private int mSkipDrawEndCount; + + /** + * Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a + * {@link android.support.v7.widget.LinearLayoutManager}. + * + * @param context Current context, it will be used to access resources. + * @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}. + */ + public DividerItemDecoration(Context context, int orientation) { + this(context, orientation, 0, 0); + } + + public DividerItemDecoration(Context context, int orientation, int skipDrawStartCount, int skipDrawEndCount) { + final TypedArray a = context.obtainStyledAttributes(ATTRS); + mDivider = a.getDrawable(0); + a.recycle(); + if (skipDrawStartCount < 0 || skipDrawEndCount < 0) { + throw new IllegalArgumentException("can not less than 0"); + } + mSkipDrawStartCount = skipDrawStartCount; + mSkipDrawEndCount = skipDrawEndCount; + setOrientation(orientation); + } + + /** + * Sets the orientation for this divider. This should be called if + * {@link RecyclerView.LayoutManager} changes orientation. + * + * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL && orientation != VERTICAL) { + throw new IllegalArgumentException( + "Invalid orientation. It should be either HORIZONTAL or VERTICAL"); + } + mOrientation = orientation; + } + + /** + * Sets the {@link Drawable} for this divider. + * + * @param drawable Drawable that should be used as a divider. + */ + @SuppressWarnings("all") + public void setDrawable(@NonNull Drawable drawable) { + if (drawable == null) { + throw new IllegalArgumentException("Drawable cannot be null."); + } + mDivider = drawable; + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (parent.getLayoutManager() == null) { + return; + } + if (mOrientation == VERTICAL) { + drawVertical(c, parent); + } else { + drawHorizontal(c, parent); + } + } + + @SuppressLint("NewApi") + private void drawVertical(Canvas canvas, RecyclerView parent) { + canvas.save(); + final int left; + final int right; + if (parent.getClipToPadding()) { + left = parent.getPaddingLeft(); + right = parent.getWidth() - parent.getPaddingRight(); + canvas.clipRect(left, parent.getPaddingTop(), right, + parent.getHeight() - parent.getPaddingBottom()); + } else { + left = 0; + right = parent.getWidth(); + } + + final int childCount = parent.getChildCount(); + int itemCount = parent.getLayoutManager().getItemCount(); + int endNotDraw = itemCount - mSkipDrawEndCount; + + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + int childAdapterPosition = parent.getChildAdapterPosition(child); + if (childAdapterPosition < mSkipDrawStartCount || childAdapterPosition >= endNotDraw) { + continue; + } + parent.getDecoratedBoundsWithMargins(child, mBounds); + final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child)); + final int top = bottom - mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(canvas); + } + canvas.restore(); + } + + @SuppressLint("NewApi") + private void drawHorizontal(Canvas canvas, RecyclerView parent) { + canvas.save(); + final int top; + final int bottom; + if (parent.getClipToPadding()) { + top = parent.getPaddingTop(); + bottom = parent.getHeight() - parent.getPaddingBottom(); + canvas.clipRect(parent.getPaddingLeft(), top, + parent.getWidth() - parent.getPaddingRight(), bottom); + } else { + top = 0; + bottom = parent.getHeight(); + } + + final int childCount = parent.getChildCount(); + int itemCount = parent.getLayoutManager().getItemCount(); + int endNotDraw = itemCount - mSkipDrawEndCount; + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + int childAdapterPosition = parent.getChildAdapterPosition(child); + if (childAdapterPosition < mSkipDrawStartCount || childAdapterPosition >= endNotDraw) { + continue; + } + parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds); + final int right = mBounds.right + Math.round(ViewCompat.getTranslationX(child)); + final int left = right - mDivider.getIntrinsicWidth(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(canvas); + } + canvas.restore(); + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + if (mOrientation == VERTICAL) { + outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); + } else { + outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); + } + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/recyclerview/HidingScrollListener.java b/lib_base/src/main/java/com/android/base/widget/recyclerview/HidingScrollListener.java new file mode 100644 index 0000000..6561a6c --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/recyclerview/HidingScrollListener.java @@ -0,0 +1,45 @@ +package com.android.base.widget.recyclerview; + +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; + +/** + * RecyclerView滑动时隐藏toolbar + */ +public abstract class HidingScrollListener extends RecyclerView.OnScrollListener { + + private static final int HIDE_THRESHOLD = 20; + private int scrolledDistance = 0; + private boolean controlsVisible = true; + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + int firstVisibleItem = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); + //show views if first item is first visible position and views are hidden + if (firstVisibleItem == 0) { + if (!controlsVisible) { + onShow(); + controlsVisible = true; + } + } else { + if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) { + onHide(); + controlsVisible = false; + scrolledDistance = 0; + } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) { + onShow(); + controlsVisible = true; + scrolledDistance = 0; + } + } + if ((controlsVisible && dy > 0) || (!controlsVisible && dy < 0)) { // 显示 向上滑动 || 没有显示 向下滑动 + scrolledDistance += dy; + } + } + + public abstract void onHide(); + + public abstract void onShow(); + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/recyclerview/MarginDecoration.java b/lib_base/src/main/java/com/android/base/widget/recyclerview/MarginDecoration.java new file mode 100644 index 0000000..b0c4ea8 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/recyclerview/MarginDecoration.java @@ -0,0 +1,39 @@ +package com.android.base.widget.recyclerview; + +import android.graphics.Rect; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.View; + + +public class MarginDecoration extends RecyclerView.ItemDecoration { + + private int mTop; + private int mLeft; + private int mRight; + private int mBottom; + + public MarginDecoration(int left, int top, int right, int bottom) { + mTop = top; + mBottom = bottom; + mRight = right; + mLeft = left; + } + + public MarginDecoration(int margin) { + mTop = margin; + mBottom = margin; + mRight = margin; + mLeft = margin; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + outRect.top = mTop; + outRect.bottom = mBottom; + outRect.left = mLeft; + outRect.right = mRight; + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/recyclerview/OnRecyclerViewScrollBottomListener.java b/lib_base/src/main/java/com/android/base/widget/recyclerview/OnRecyclerViewScrollBottomListener.java new file mode 100644 index 0000000..0753096 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/recyclerview/OnRecyclerViewScrollBottomListener.java @@ -0,0 +1,99 @@ +package com.android.base.widget.recyclerview; + +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; + +/** + * RecyclerView多布局通用滑动底部监听器 + */ +@SuppressWarnings("all") +public abstract class OnRecyclerViewScrollBottomListener extends RecyclerView.OnScrollListener { + + /** + * layoutManager的类型(枚举) + */ + private int mLayoutManagerType; + + private static final int LINEAR = 1; + private static final int GRID = 2; + private static final int STAGGERED_GRID = 3; + + /** + * 最后一个的位置 + */ + private int[] mLastPositions; + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + + if (mLayoutManagerType == 0) { + if (layoutManager instanceof GridLayoutManager) { + mLayoutManagerType = GRID; + } else if (layoutManager instanceof LinearLayoutManager) { + mLayoutManagerType = LINEAR; + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + mLayoutManagerType = STAGGERED_GRID; + } else { + //do nothing + } + } + + //最后一个可见的item的位置 + int lastVisibleItemPosition; + switch (mLayoutManagerType) { + case LINEAR: + lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); + break; + case GRID: + lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); + break; + case STAGGERED_GRID: + StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager; + if (mLastPositions == null) { + mLastPositions = new int[staggeredGridLayoutManager.getSpanCount()]; + } + staggeredGridLayoutManager.findLastVisibleItemPositions(mLastPositions); + lastVisibleItemPosition = findMax(mLastPositions); + break; + default: { + throw new IllegalStateException("un support layoutManager"); + } + } + + int visibleItemCount = layoutManager.getChildCount(); + int totalItemCount = layoutManager.getItemCount(); + + if ((visibleItemCount > 0 && (lastVisibleItemPosition) >= totalItemCount - 1)) { + onBottom(); + } else { + onLeaveBottom(); + } + } + + private int findMax(int[] lastPositions) { + int max = lastPositions[0]; + for (int value : lastPositions) { + if (value > max) { + max = value; + } + } + return max; + } + + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + } + + + protected abstract void onBottom(); + + protected abstract void onLeaveBottom(); + + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/BannerPagerAdapter.java b/lib_base/src/main/java/com/android/base/widget/viewpager/BannerPagerAdapter.java new file mode 100644 index 0000000..dcd8615 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/BannerPagerAdapter.java @@ -0,0 +1,91 @@ +package com.android.base.widget.viewpager; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewCompat; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.android.base.imageloader.ImageLoaderFactory; + +import java.util.List; + + +class BannerPagerAdapter extends PagerAdapter { + + static ScalableBannerPagerFactory sBannerPagerFactory; + + private final String mTransitionName; + private final Context mContext; + private final List mEntities; + private OnPageClickListener mClickListener; + private boolean mIsLooper; + + BannerPagerAdapter(Context context, List entities, String transitionName) { + mContext = context; + this.mEntities = entities; + mTransitionName = transitionName; + mIsLooper = mEntities.size() > 1; + } + + void setOnBannerClickListener(OnPageClickListener clickListener) { + this.mClickListener = clickListener; + } + + @Override + public int getCount() { + return mEntities.size(); + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return view == object; + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, final int position) { + if (sBannerPagerFactory == null) { + throw new NullPointerException("You need to provide a ScalableBannerPagerFactory!"); + } + + ImageView imageView = sBannerPagerFactory.createBannerPagerView(mContext); + setTransitionName(imageView); + + sBannerPagerFactory.setOnClickListener(imageView, v -> { + if (mClickListener != null) { + mClickListener.onClick(imageView, mIsLooper ? position - 1 : position); + } + }); + + String url = mEntities.get(position); + ImageLoaderFactory.getImageLoader().display(imageView, url); + container.addView(imageView, 0); + return imageView; + } + + private void setTransitionName(ImageView bannerLayout) { + if (!TextUtils.isEmpty(mTransitionName)) { + ViewCompat.setTransitionName(bannerLayout, mTransitionName); + } + } + + @Override + public int getItemPosition(@NonNull Object object) { + return POSITION_NONE; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + container.removeView((View) object); + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + super.setPrimaryItem(container, position, object); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/IPagerNumberView.java b/lib_base/src/main/java/com/android/base/widget/viewpager/IPagerNumberView.java new file mode 100644 index 0000000..af2a24a --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/IPagerNumberView.java @@ -0,0 +1,16 @@ +package com.android.base.widget.viewpager; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-02 12:35 + */ +public interface IPagerNumberView { + + void setViewPager(ZViewPager viewPager); + + void setPageSize(int i); + + void setPageScrolled(int position, float positionOffset); + +} diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/NoScrollViewPager.java b/lib_base/src/main/java/com/android/base/widget/viewpager/NoScrollViewPager.java new file mode 100644 index 0000000..b520c13 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/NoScrollViewPager.java @@ -0,0 +1,37 @@ +package com.android.base.widget.viewpager; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-01-03 15:38 + */ +public class NoScrollViewPager extends ViewPager { + + public NoScrollViewPager(@NonNull Context context) { + super(context); + } + + public NoScrollViewPager(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent ev) { + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return false; + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/OnPageClickListener.java b/lib_base/src/main/java/com/android/base/widget/viewpager/OnPageClickListener.java new file mode 100644 index 0000000..385e228 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/OnPageClickListener.java @@ -0,0 +1,7 @@ +package com.android.base.widget.viewpager; + +import android.view.View; + +public interface OnPageClickListener { + void onClick(View itemView, int position); +} diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/OptimizeBannerPagerAdapter.java b/lib_base/src/main/java/com/android/base/widget/viewpager/OptimizeBannerPagerAdapter.java new file mode 100644 index 0000000..5a46a16 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/OptimizeBannerPagerAdapter.java @@ -0,0 +1,96 @@ +package com.android.base.widget.viewpager; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.AppCompatImageView; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.android.base.imageloader.ImageLoaderFactory; + +import java.util.ArrayList; +import java.util.List; + + +class OptimizeBannerPagerAdapter extends PagerAdapter { + + private final String mTransitionName; + private final Context mContext; + private final List mEntities; + private final List mLayouts = new ArrayList<>(); + private OnPageClickListener mOnPageClickListener; + private boolean mIsLooper; + + OptimizeBannerPagerAdapter(Context context, List entities, String transitionName) { + mContext = context; + mTransitionName = transitionName; + mEntities = entities; + mIsLooper = mEntities.size() > 1; + setLayouts(); + } + + private void setLayouts() { + ImageView view; + mLayouts.clear(); + for (int i = 0; i < mEntities.size(); i++) { + view = new AppCompatImageView(mContext); + mLayouts.add(view); + setTransitionName(view); + } + } + + private void setTransitionName(ImageView bannerLayout) { + if (!TextUtils.isEmpty(mTransitionName)) { + ViewCompat.setTransitionName(bannerLayout, mTransitionName); + } + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return view == object; + } + + @Override + public int getCount() { + return mEntities.size(); + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, final int position) { + ImageView image = mLayouts.get(position); + String url = mEntities.get(position); + image.setOnClickListener(v -> { + if (mOnPageClickListener != null) { + mOnPageClickListener.onClick(image, mIsLooper ? position - 1 : position); + } + }); + ImageLoaderFactory.getImageLoader().display(image, url); + container.addView(image, 0); + return mLayouts.get(position); + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + container.removeView(mLayouts.get(position)); + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + super.setPrimaryItem(container, position, object); + } + + @Override + public int getItemPosition(@NonNull Object object) { + return POSITION_NONE; + } + + void setOnBannerClickListener(OnPageClickListener onBannerClickListener) { + mOnPageClickListener = onBannerClickListener; + } + +} diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/PageNumberView.java b/lib_base/src/main/java/com/android/base/widget/viewpager/PageNumberView.java new file mode 100644 index 0000000..5943dac --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/PageNumberView.java @@ -0,0 +1,111 @@ +package com.android.base.widget.viewpager; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import com.android.base.R; +import com.android.base.utils.android.UnitConverter; + + +public class PageNumberView extends View implements IPagerNumberView { + + private static final String DIVISION = "/"; + + private int mPageSize; + private int mPosition; + + private int mCenterX; + + private Paint mPaint; + private int mTextHeight; + private int mTextBaseLine; + + public PageNumberView(Context context) { + this(context, null); + } + + public PageNumberView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PageNumberView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PageNumberView); + + /*是否可以缩放*/ + int color = typedArray.getColor(R.styleable.PageNumberView_pnv_text_color, Color.BLACK); + /*用于支持5.0的transition动画*/ + int size = typedArray.getDimensionPixelSize(R.styleable.PageNumberView_pnv_text_size, UnitConverter.spToPx(14)); + + typedArray.recycle(); + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setTextSize(size); + mPaint.setColor(color); + + Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt(); + mTextHeight = fontMetricsInt.bottom - fontMetricsInt.top; + mTextBaseLine = mTextHeight - fontMetricsInt.bottom; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + String text = String.valueOf(mPageSize); + float textWidth = mPaint.measureText(text); + float divisionWidth = mPaint.measureText(DIVISION); + setMeasuredDimension((int) (textWidth * 2 + divisionWidth), mTextHeight); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + float divisionHalfSize = mPaint.measureText(DIVISION) / 2; + float left = mCenterX - divisionHalfSize; + float right = mCenterX + divisionHalfSize; + String positionText = String.valueOf(mPosition); + String sizeText = String.valueOf(mPageSize); + canvas.drawText(DIVISION, left, mTextBaseLine, mPaint); + canvas.drawText(positionText, left - mPaint.measureText(positionText), mTextBaseLine, mPaint); + canvas.drawText(sizeText, right, mTextBaseLine, mPaint); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mCenterX = w / 2; + } + + @Override + public void setPageScrolled(int position, @SuppressWarnings("UnusedParameters") float positionOffset) { + mPosition = position; + if (mPageSize > 1) { + if (mPosition == 0) { + mPosition = mPageSize; + } else if (mPosition == mPageSize + 1) { + mPosition = 1; + } + } else { + mPosition = mPageSize; + } + invalidate(); + } + + @Override + public void setViewPager(ZViewPager viewPager) { + //no op + } + + @Override + public void setPageSize(int pageSize) { + mPageSize = pageSize; + mPosition = pageSize;//只有一個的時候 + requestLayout(); + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/PhotoViewPager.java b/lib_base/src/main/java/com/android/base/widget/viewpager/PhotoViewPager.java new file mode 100644 index 0000000..b11a820 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/PhotoViewPager.java @@ -0,0 +1,42 @@ +package com.android.base.widget.viewpager; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import timber.log.Timber; + +public class PhotoViewPager extends ViewPager { + + public PhotoViewPager(Context context) { + super(context); + } + + public PhotoViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + try { + return super.onInterceptTouchEvent(ev); + } catch (Exception e) { + Timber.d("onInterceptTouchEvent() called with: ev = [" + ev + "]"); + } + return false; + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent ev) { + try { + return super.onTouchEvent(ev); + } catch (IllegalArgumentException ex) { + Timber.d("onTouchEvent() called with: ev = [" + ev + "]"); + } + return false; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/ScalableBannerPagerFactory.java b/lib_base/src/main/java/com/android/base/widget/viewpager/ScalableBannerPagerFactory.java new file mode 100644 index 0000000..0ada5ac --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/ScalableBannerPagerFactory.java @@ -0,0 +1,32 @@ +package com.android.base.widget.viewpager; + +import android.content.Context; +import android.view.View; +import android.widget.ImageView; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-07-01 17:43 + */ +public interface ScalableBannerPagerFactory { + + /** + * return a scalable ImageView + */ + ImageView createBannerPagerView(Context context); + + /** + * Set a click listener for image view which is created by {@link #createBannerPagerView(Context)}, like PhotoView: + * + *
    {@code
    +     *         imageView.setOnPhotoTapListener((view, x, y) -> {
    +     *             if (mClickListener != null) {
    +     *                 mClickListener.onClick(imageView, mIsLooper ? position - 1 : position);
    +     *             }
    +     *         });
    +     * }
    + */ + void setOnClickListener(ImageView view, View.OnClickListener onClickListener); + +} diff --git a/lib_base/src/main/java/com/android/base/widget/viewpager/ZViewPager.java b/lib_base/src/main/java/com/android/base/widget/viewpager/ZViewPager.java new file mode 100644 index 0000000..30a2859 --- /dev/null +++ b/lib_base/src/main/java/com/android/base/widget/viewpager/ZViewPager.java @@ -0,0 +1,205 @@ +package com.android.base.widget.viewpager; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.base.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * 支持无限轮播的 ViewPager + * + * @author Ztiany + */ +public class ZViewPager extends FrameLayout { + + private final ViewPager mViewPager; + private IPagerNumberView mPageNumberView; + + private List mImageUrlList = new ArrayList<>(); + + private OnBannerPositionChangedListener mOnBannerPositionChangedListener; + + private boolean mScalable; + private String mTransitionName; + private OnPageClickListener mOnPageClickListener; + + public ZViewPager(Context context) { + this(context, null); + } + + public ZViewPager(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ZViewPager(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ZViewPager); + + /*是否可以缩放*/ + mScalable = typedArray.getBoolean(R.styleable.ZViewPager_zvp_scale, false); + /*用于支持5.0的transition动画*/ + mTransitionName = typedArray.getString(R.styleable.ZViewPager_zvp_item_transition_name); + int pageId = typedArray.getResourceId(R.styleable.ZViewPager_zvp_pager_number_id, -1); + + typedArray.recycle(); + + inflate(context, R.layout.base_widget_banner_view, this); + + mViewPager = getRootView().findViewById(R.id.base_widget_banner_vp); + + if (pageId != -1) { + View pageNumber = findViewById(pageId); + if (pageNumber instanceof IPagerNumberView) { + mPageNumberView = (IPagerNumberView) pageNumber; + mPageNumberView.setViewPager(this); + } + } + } + + public void setOnBannerPositionChangedListener(OnBannerPositionChangedListener onBannerPositionChangedListener) { + mOnBannerPositionChangedListener = onBannerPositionChangedListener; + } + + public void setPageNumberView(IPagerNumberView pageNumberView) { + mPageNumberView = pageNumberView; + } + + public void setImages(List entities) { + if (entities == null || entities.isEmpty()) { + mImageUrlList.clear(); + mViewPager.setAdapter(null); + setPageSize(0); + return; + } + mImageUrlList.clear(); + setPageSize(entities.size()); + if (entities.size() > 1) { + addExtraPage(entities); + showBanner(); + setLooper(); + } else { + mImageUrlList.addAll(entities); + showBanner(); + } + } + + private void setPageSize(int pageSize) { + if (mPageNumberView != null) { + mPageNumberView.setPageSize(pageSize); + } + } + + private void setPageScrolled(int position, float positionOffset) { + if (mPageNumberView != null) { + mPageNumberView.setPageScrolled(position, positionOffset); + } + } + + public void setCurrentPosition(int position) { + if (mImageUrlList.size() > 1) { + if (position <= mImageUrlList.size() - 2) { + position++; + } else { + position = mImageUrlList.size() - 2; + } + } + mViewPager.setCurrentItem(position); + } + + private void setLooper() { + mViewPager.setCurrentItem(1, false); + mViewPager.clearOnPageChangeListeners(); + + mViewPager.addOnPageChangeListener( + new ViewPager.SimpleOnPageChangeListener() { + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (positionOffsetPixels == 0.0) { + setPageScrolled(position, positionOffset); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + super.onPageScrollStateChanged(state); + //(positionOffset为0的时候,并不一定是切换完成,所以动画还在执行,强制再次切换,就会闪屏) + switch (state) { + case ViewPager.SCROLL_STATE_IDLE:// 空闲状态,没有任何滚动正在进行(表明完成滚动) + setViewPagerItemPosition(mViewPager.getCurrentItem()); + break; + case ViewPager.SCROLL_STATE_DRAGGING:// 正在拖动page状态 + break; + case ViewPager.SCROLL_STATE_SETTLING:// 手指已离开屏幕,自动完成剩余的动画效果 + break; + } + } + + @Override + public void onPageSelected(int position) { + if (mOnBannerPositionChangedListener != null) { + if (mImageUrlList.size() > 1) { + if (position == 0) { + position = mImageUrlList.size() - 3; + } else if (position == mImageUrlList.size() - 1) { + position = 0; + } else { + position--; + } + } + mOnBannerPositionChangedListener.onPagePositionChanged(position); + } + } + }); + } + + private void addExtraPage(List entities) { + mImageUrlList.add(entities.get(entities.size() - 1)); + mImageUrlList.addAll(entities); + mImageUrlList.add(entities.get(0)); + } + + private void showBanner() { + PagerAdapter adapter; + if (mScalable) { + BannerPagerAdapter bannerPagerAdapter = new BannerPagerAdapter(getContext(), mImageUrlList, mTransitionName); + bannerPagerAdapter.setOnBannerClickListener(mOnPageClickListener); + adapter = bannerPagerAdapter; + } else { + OptimizeBannerPagerAdapter optimizeBannerPagerAdapter = new OptimizeBannerPagerAdapter(getContext(), mImageUrlList, mTransitionName); + optimizeBannerPagerAdapter.setOnBannerClickListener(mOnPageClickListener); + adapter = optimizeBannerPagerAdapter; + } + mViewPager.setAdapter(adapter); + } + + public void setOnPageClickListener(OnPageClickListener onPageClickListener) { + mOnPageClickListener = onPageClickListener; + } + + private void setViewPagerItemPosition(int position) { + if (position == mImageUrlList.size() - 1) { + mViewPager.setCurrentItem(1, false); + } else if (position == 0) { + mViewPager.setCurrentItem(mImageUrlList.size() - 2, false); + } + } + + public interface OnBannerPositionChangedListener { + void onPagePositionChanged(int position); + } + + public static void setScalableBannerPagerFactory(ScalableBannerPagerFactory scalableBannerPagerFactory) { + BannerPagerAdapter.sBannerPagerFactory = scalableBannerPagerFactory; + } + +} \ No newline at end of file diff --git a/lib_base/src/main/res/drawable-xxhdpi/base_img_error.png b/lib_base/src/main/res/drawable-xxhdpi/base_img_error.png new file mode 100644 index 0000000000000000000000000000000000000000..d79ee1f72c33f2ae2291f8ccf3e5c5eba12cab80 GIT binary patch literal 3872 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 literal 0 HcmV?d00001 diff --git a/lib_base/src/main/res/drawable-xxhdpi/base_img_no_network.png b/lib_base/src/main/res/drawable-xxhdpi/base_img_no_network.png new file mode 100644 index 0000000000000000000000000000000000000000..e3b51e8b2ed24b81fef670d204e2d996b8dd92d7 GIT binary patch literal 17882 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ⅆ9g2 zyO+z78nE{wvaWd-Xr#kKAH@TvXSUI@6asu7lVfhm-7@3Z4r}>d z^Jv7MakDGbdcpy;cqh2dR z$r3?OHNMy@O*Oli+#m(7lbAnbq|dJLJ2u>pP+|WWc2HrQpO0BtJ_K|TZ9hxIh0iQV zZs_sWyU4w5pX9$!_PTS@5((a;DNtbe}p0|WBpSl4+x!NW4; z-huq84HJLhj?R(Z7VgJf^j+!l<#7J%$Dcu0Id^L}+n z#*EkR`ipX*$kQb++RmKNVqqXolCdP}`q+$GJ+Qcrw)89I!D%R6q!R!Z5>=hQ9O?g`WT91y+nN|BXWfJ=Q?{jf^ZG2;0yA2y~ zc_aQS>d7bTZr>e;1XVVTLxTM~o?bnhpBD<1$ykCn&x~(jUoyvQ@z!0-U6(Q(SUz}o z-%cQ^xNsOV2m-6A!&`oFvWp{9BQ#dzI&n`vj&eyH&d)lEQKyE3veO(XntlcTN@MeMe?PZ&ybDmR{8SVpI zN$Pb8_+rh?Yp>qij4T@qo?HE;;X}^^2GU_aBS5A;7i~H_cH3=D<=nE7D{Ehn&0b&4 z=_pNnT+SRyRX}_oUFIEi(v*I_1o1T8vH}C)9VVqLqKc4?L&ve0_tHVlL!Ot3)L zF7@2(Lvp3BA4?8DMmEBu4YP$MTAT$G6ZL~M7x#ajIYQqOi-G9P<`4{4EnTzAzwf3@ zCTuh}{dC6ghT6>wb?tU`s~+x2d5uqtapC3ntTOgDTK8y+|LkTNC33{}8t&P%ym(c@ zaaxP;8B}Y|$aSTV0tu^Kc%y~w9QBi~v&NrN4k*LwAEAjj`7y1PlQUOTYW!v2{lLlRW?*u4{*B*C`#Uu;)bv5}c2R=*9co z?v?LL|0sD8Te&x>CO14pLrO3GiC`Yy>xuoo=SGRA&+InKAXvs8ulphe6u~N-W|T(u RP5$>4cV{oBQx1fr{{TGB>f!(Z literal 0 HcmV?d00001 diff --git a/lib_base/src/main/res/layout/base_fragment_list.xml b/lib_base/src/main/res/layout/base_fragment_list.xml new file mode 100644 index 0000000..347f478 --- /dev/null +++ b/lib_base/src/main/res/layout/base_fragment_list.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/lib_base/src/main/res/layout/base_fragment_refresh_list.xml b/lib_base/src/main/res/layout/base_fragment_refresh_list.xml new file mode 100644 index 0000000..534edcd --- /dev/null +++ b/lib_base/src/main/res/layout/base_fragment_refresh_list.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/lib_base/src/main/res/layout/base_layout_empty.xml b/lib_base/src/main/res/layout/base_layout_empty.xml new file mode 100644 index 0000000..b43a2ff --- /dev/null +++ b/lib_base/src/main/res/layout/base_layout_empty.xml @@ -0,0 +1,43 @@ + + + + + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/layout/base_layout_error.xml b/lib_base/src/main/res/layout/base_layout_error.xml new file mode 100644 index 0000000..a6c6f7d --- /dev/null +++ b/lib_base/src/main/res/layout/base_layout_error.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/layout/base_layout_loading.xml b/lib_base/src/main/res/layout/base_layout_loading.xml new file mode 100644 index 0000000..0f60cb5 --- /dev/null +++ b/lib_base/src/main/res/layout/base_layout_loading.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/layout/base_layout_no_network.xml b/lib_base/src/main/res/layout/base_layout_no_network.xml new file mode 100644 index 0000000..abffc91 --- /dev/null +++ b/lib_base/src/main/res/layout/base_layout_no_network.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/layout/base_layout_server_error.xml b/lib_base/src/main/res/layout/base_layout_server_error.xml new file mode 100644 index 0000000..d4e901b --- /dev/null +++ b/lib_base/src/main/res/layout/base_layout_server_error.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/layout/base_layout_toolbar.xml b/lib_base/src/main/res/layout/base_layout_toolbar.xml new file mode 100644 index 0000000..c4781bc --- /dev/null +++ b/lib_base/src/main/res/layout/base_layout_toolbar.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/layout/base_widget_banner_view.xml b/lib_base/src/main/res/layout/base_widget_banner_view.xml new file mode 100644 index 0000000..47a06e5 --- /dev/null +++ b/lib_base/src/main/res/layout/base_widget_banner_view.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/values-en/base_strings.xml b/lib_base/src/main/res/values-en/base_strings.xml new file mode 100644 index 0000000..2e95ced --- /dev/null +++ b/lib_base/src/main/res/values-en/base_strings.xml @@ -0,0 +1,32 @@ + + + Confirm + Cancel + + + + Failed to load/(ㄒoㄒ)/~~ + Click me to retry + There is no data oh ! + Network is not accessible ! + Server error! + + + %s permission denied + We need you to provide %s permission so that we can serve you better. + Allow %2$s in Settings->Apps->%1$s to use %1$s features. + Settings + + + Calendar + Camera + Contacts + Location + Microphone + Phone information + Body Sensors + SMS + Storage + Accounts + + diff --git a/lib_base/src/main/res/values/base_attrs.xml b/lib_base/src/main/res/values/base_attrs.xml new file mode 100644 index 0000000..2745c3e --- /dev/null +++ b/lib_base/src/main/res/values/base_attrs.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/values/base_color_palette.xml b/lib_base/src/main/res/values/base_color_palette.xml new file mode 100644 index 0000000..dd06dde --- /dev/null +++ b/lib_base/src/main/res/values/base_color_palette.xml @@ -0,0 +1,18 @@ + + + + #0000FF + #FFFF00 + #FFA500 + #FF0000 + #800080 + #ff00ff00 + #FF00FFFF + #FFD700 + #C0C0C0 + #FFFFFF + #000000 + #ff969696 + #00000000 + + \ No newline at end of file diff --git a/lib_base/src/main/res/values/base_ids.xml b/lib_base/src/main/res/values/base_ids.xml new file mode 100644 index 0000000..c2d29c2 --- /dev/null +++ b/lib_base/src/main/res/values/base_ids.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib_base/src/main/res/values/base_strings.xml b/lib_base/src/main/res/values/base_strings.xml new file mode 100644 index 0000000..5960186 --- /dev/null +++ b/lib_base/src/main/res/values/base_strings.xml @@ -0,0 +1,31 @@ + + + 确认 + 取消 + + + 加载失败了/(ㄒoㄒ)/~~ + 点我重试吧 + 还没有相关数据哦! + 网络不可访问! + 服务器开小差了! + + + 去设置 + %s 权限被拒绝 + 我们需要您提供%s权限以使我们能够更好的为您服务。 + 在设置->应用->%1$s->权限管理中开启%2$s权限,以正常使用%1$s功能。 + + + 日历 + 相机 + 通讯录 + 位置信息 + 麦克风 + 获取手机信息 + 身体传感器 + 短信 + 存储空间 + 账户 + + diff --git a/lib_base/src/main/res/values/base_styles.xml b/lib_base/src/main/res/values/base_styles.xml new file mode 100644 index 0000000..0a99ebd --- /dev/null +++ b/lib_base/src/main/res/values/base_styles.xml @@ -0,0 +1,34 @@ + + + + + + + \ No newline at end of file diff --git a/lib_base/src/test/java/com/android/base/rx/RxBusTest.java b/lib_base/src/test/java/com/android/base/rx/RxBusTest.java new file mode 100644 index 0000000..8676648 --- /dev/null +++ b/lib_base/src/test/java/com/android/base/rx/RxBusTest.java @@ -0,0 +1,127 @@ +package com.android.base.rx; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-04-18 16:39 + */ +public class RxBusTest { + + private RxBus mRxBus = RxBus.newInstance(); + + private A mA; + private A mA1; + private B mB; + private B mB1; + private C mC; + private C mC1; + + private static class A { + final String name; + + private A(String name) { + this.name = "A " + name; + } + + @Override + public String toString() { + return "A{" + + "name='" + name + '\'' + + '}'; + } + } + + private static class C extends A { + private C(String name) { + super(name); + } + + @Override + public String toString() { + return "C {" + + "name='" + name + '\'' + + '}'; + } + } + + private static class B { + private final String name; + + private B(String name) { + this.name = "B " + name; + } + + @Override + public String toString() { + return "B{" + + "name='" + name + '\'' + + '}'; + } + } + + + @Before + public void start() { + + mRxBus.toObservable(A.class).subscribe(a -> { + System.out.println(a); + mA = a; + }); + + mRxBus.toObservable("A", A.class) + .subscribe(a -> { + System.out.println(" has id " + a); + mA1 = a; + }); + + mRxBus.toObservable(B.class).subscribe(b -> { + System.out.println(b); + mB = b; + }); + + mRxBus.toObservable("B", B.class) + .subscribe(b -> { + System.out.println(" has id " + b); + mB1 = b; + }); + + mRxBus.toObservable(C.class).subscribe(c -> { + System.out.println(c); + mC = c; + }); + + mRxBus.toObservable("C", C.class) + .subscribe(c -> { + System.out.println(" has id " + c); + mC1 = c; + }); + } + + @Test + public void send() throws Exception { + A a = new A("A01"); + mRxBus.send(a); + Assert.assertSame(a, mA); + Assert.assertNotSame(a, mA1); + + B b = new B("B01"); + mRxBus.send("B", b); + Assert.assertNotSame(b, mB); + Assert.assertSame(b, mB1); + + + C c = new C("C01"); + mRxBus.send("C", c); + Assert.assertNotSame(c, mC); + Assert.assertSame(c, mC1); + + mRxBus.send("A", c); + mRxBus.send(c); + + } + +} \ No newline at end of file diff --git a/lib_base/src/test/java/com/android/base/rx/TestApplication.java b/lib_base/src/test/java/com/android/base/rx/TestApplication.java new file mode 100644 index 0000000..c8dc357 --- /dev/null +++ b/lib_base/src/test/java/com/android/base/rx/TestApplication.java @@ -0,0 +1,28 @@ +package com.android.base.rx; + +import android.app.Application; +import android.content.Context; + +import com.android.base.app.BaseKit; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-28 17:50 + */ +public class TestApplication extends Application { + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + BaseKit.get().onApplicationAttachBaseContext(base); + } + + @Override + public void onCreate() { + super.onCreate(); + BaseKit.get().onApplicationCreate(this); + } + + +} diff --git a/lib_base/src/test/java/com/android/base/rx/TestBaseActivity.java b/lib_base/src/test/java/com/android/base/rx/TestBaseActivity.java new file mode 100644 index 0000000..8fe9203 --- /dev/null +++ b/lib_base/src/test/java/com/android/base/rx/TestBaseActivity.java @@ -0,0 +1,29 @@ +package com.android.base.rx; + +import android.os.Bundle; +import android.support.annotation.Nullable; + +import com.android.base.app.activity.BaseActivity; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-06-22 09:37 + */ +public class TestBaseActivity extends BaseActivity { + + @Override + protected void initialize(@Nullable Bundle savedInstanceState) { + super.initialize(savedInstanceState); + } + + @Override + protected Object layout() { + return 0; + } + + @Override + protected void setupView(@Nullable Bundle savedInstanceState) { + } + +} diff --git a/lib_base/src/test/java/com/android/base/rx/TestListFragment.java b/lib_base/src/test/java/com/android/base/rx/TestListFragment.java new file mode 100644 index 0000000..4244264 --- /dev/null +++ b/lib_base/src/test/java/com/android/base/rx/TestListFragment.java @@ -0,0 +1,26 @@ +package com.android.base.rx; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; + +import com.android.base.adapter.recycler.MultiTypeAdapter; +import com.android.base.app.fragment.BaseListFragment; +import com.android.base.app.ui.AutoPageNumber; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-04-27 15:05 + */ +public class TestListFragment extends BaseListFragment { + + @Override + protected void onViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewPrepared(view, savedInstanceState); + MultiTypeAdapter recyclerAdapter = new MultiTypeAdapter(getContext()); + setupLoadMore(recyclerAdapter, new AutoPageNumber(this,recyclerAdapter)); + } + +} diff --git a/lib_base/src/test/java/com/android/base/rx/TestStateFragment.java b/lib_base/src/test/java/com/android/base/rx/TestStateFragment.java new file mode 100644 index 0000000..0145786 --- /dev/null +++ b/lib_base/src/test/java/com/android/base/rx/TestStateFragment.java @@ -0,0 +1,44 @@ +package com.android.base.rx; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; + +import com.android.base.app.fragment.BaseStateFragment; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-06-22 09:38 + */ +public class TestStateFragment extends BaseStateFragment { + + @Override + public void onAttach(Context context) { + super.onAttach(context); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + protected void onViewPrepared(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewPrepared(view, savedInstanceState); + getStateLayoutConfig(); + } + + @Override + protected void onRefresh() { + super.onRefresh(); + } + + @Override + public void refreshCompleted() { + super.refreshCompleted(); + } + +} diff --git a/lib_cache/.gitignore b/lib_cache/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_cache/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_cache/README.md b/lib_cache/README.md new file mode 100644 index 0000000..00a6f40 --- /dev/null +++ b/lib_cache/README.md @@ -0,0 +1,8 @@ +# 缓存库 + +目前使用的核心缓存库: + +- 微信:MMKV +- DiskLruCache + +对外统一暴露接口: `Storage` \ No newline at end of file diff --git a/lib_cache/build.gradle b/lib_cache/build.gradle new file mode 100644 index 0000000..25af95f --- /dev/null +++ b/lib_cache/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation androidLibraries.androidAnnotations + implementation thirdLibraries.mmkv + implementation thirdLibraries.gson + implementation thirdLibraries.supportOptional + compileOnly thirdLibraries.rxJava + compileOnly kotlinLibraries.kotlinStdlib +} diff --git a/lib_cache/proguard-rules.pro b/lib_cache/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/lib_cache/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/lib_cache/src/main/AndroidManifest.xml b/lib_cache/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6f89825 --- /dev/null +++ b/lib_cache/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/lib_cache/src/main/java/com/android/sdk/cache/CacheEntity.java b/lib_cache/src/main/java/com/android/sdk/cache/CacheEntity.java new file mode 100644 index 0000000..f9f0c1f --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/CacheEntity.java @@ -0,0 +1,15 @@ +package com.android.sdk.cache; + +class CacheEntity { + + String mJsonData;//数据 + long mCacheTime;//有效时间 + long mStoreTime;//存储时间戳 + + CacheEntity(String jsonData, long cacheTime) { + mJsonData = jsonData; + mCacheTime = cacheTime; + mStoreTime = System.currentTimeMillis(); + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/CommonImpl.java b/lib_cache/src/main/java/com/android/sdk/cache/CommonImpl.java new file mode 100644 index 0000000..2c6efe1 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/CommonImpl.java @@ -0,0 +1,79 @@ +package com.android.sdk.cache; + +import android.text.TextUtils; +import android.util.Log; + +import com.github.dmstocking.optional.java.util.Optional; + +import java.lang.reflect.Type; + +import io.reactivex.Flowable; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-01 17:11 + */ +final class CommonImpl { + + static void putEntity(String key, Object entity, long cacheTime, Storage storage) { + if (entity == null) { + storage.remove(key); + return; + } + CacheEntity cacheEntity = new CacheEntity(JsonUtils.toJson(entity), cacheTime); + storage.putString(key, JsonUtils.toJson(cacheEntity)); + } + + private static String getCacheEntity(String key, Storage storage) { + String cacheStr = storage.getString(key, null); + if (TextUtils.isEmpty(cacheStr)) { + return null; + } + + CacheEntity cacheEntity = JsonUtils.fromJson(cacheStr, CacheEntity.class); + + if (cacheEntity == null) { + return null; + } + + if (cacheEntity.mCacheTime == 0) { + return cacheEntity.mJsonData; + } + + if (System.currentTimeMillis() - cacheEntity.mStoreTime < cacheEntity.mCacheTime) { + return cacheEntity.mJsonData; + } else { + storage.remove(key); + } + return null; + } + + static T getEntity(String key, Type clazz, Storage storage) { + String cacheEntity = getCacheEntity(key, storage); + Log.d("cache", "cacheEntity = " + cacheEntity); + if (cacheEntity != null) { + return JsonUtils.fromJson(cacheEntity, clazz); + } + return null; + } + + static Flowable flowableEntity(String key, Type clazz, Storage storage) { + return Flowable.defer(() -> { + T entity = storage.getEntity(key, clazz); + if (entity == null) { + return Flowable.empty(); + } else { + return Flowable.just(entity); + } + }); + } + + static Flowable> flowableOptionalEntity(String key, Type clazz, Storage storage) { + return Flowable.fromCallable(() -> { + T entity = storage.getEntity(key, clazz); + return Optional.ofNullable(entity); + }); + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/DiskLruCache.java b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruCache.java new file mode 100644 index 0000000..96ae866 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruCache.java @@ -0,0 +1,980 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdk.cache; + +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A cache that uses a bounded amount of space on a filesystem. Each cache + * entry has a string key and a fixed number of values. Each key must match + * the regex [a-z0-9_-]{1,120}. Values are byte sequences, + * accessible as streams or files. Each value must be between {@code 0} and + * {@code Integer.MAX_VALUE} bytes in length. + *

    + *

    The cache stores its data in a directory on the filesystem. This + * directory must be exclusive to the cache; the cache may delete or overwrite + * files from its directory. It is an error for multiple processes to use the + * same cache directory at the same time. + *

    + *

    This cache limits the number of bytes that it will store on the + * filesystem. When the number of stored bytes exceeds the limit, the cache will + * remove entries in the background until the limit is satisfied. The limit is + * not strict: the cache may temporarily exceed it while waiting for files to be + * deleted. The limit does not include filesystem overhead or the cache + * journal so space-sensitive applications should set a conservative limit. + *

    + *

    Clients call {@link #edit} to create or update the values of an entry. An + * entry may have only one editor at one time; if a value is not available to be + * edited then {@link #edit} will return null. + *

      + *
    • When an entry is being created it is necessary to + * supply a full set of values; the empty value should be used as a + * placeholder if necessary. + *
    • When an entry is being edited, it is not necessary + * to supply data for every value; values default to their previous + * value. + *
    + * Every {@link #edit} call must be matched by a call to {@link Editor#commit} + * or {@link Editor#abort}. Committing is atomic: a read observes the full set + * of values as they were before or after the commit, but never a mix of values. + *

    + *

    Clients call {@link #get} to read a snapshot of an entry. The read will + * observe the value at the time that {@link #get} was called. Updates and + * removals after the call do not impact ongoing reads. + *

    + *

    This class is tolerant of some I/O errors. If files are missing from the + * filesystem, the corresponding entries will be dropped from the cache. If + * an error occurs while writing a cache value, the edit will fail silently. + * Callers should handle other problems by catching {@code IOException} and + * responding appropriately. + */ +public final class DiskLruCache implements Closeable { + + static final String JOURNAL_FILE = "journal"; + static final String JOURNAL_FILE_TEMP = "journal.tmp"; + static final String JOURNAL_FILE_BACKUP = "journal.bkp"; + static final String MAGIC = "libcore.io.DiskLruCache"; + static final String VERSION_1 = "1"; + static final long ANY_SEQUENCE_NUMBER = -1; + static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}"; + static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN); + private static final String CLEAN = "CLEAN"; + private static final String DIRTY = "DIRTY"; + private static final String REMOVE = "REMOVE"; + private static final String READ = "READ"; + + /* + * This cache uses a journal file named "journal". A typical journal file + * looks like this: + * libcore.io.DiskLruCache + * 1 + * 100 + * 2 + * + * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 + * DIRTY 335c4c6028171cfddfbaae1a9c313c52 + * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 + * REMOVE 335c4c6028171cfddfbaae1a9c313c52 + * DIRTY 1ab96a171faeeee38496d8b330771a7a + * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 + * READ 335c4c6028171cfddfbaae1a9c313c52 + * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 + * + * The first five lines of the journal form its header. They are the + * constant string "libcore.io.DiskLruCache", the disk cache's version, + * the application's version, the value count, and a blank line. + * + * Each of the subsequent lines in the file is a record of the state of a + * cache entry. Each line contains space-separated values: a state, a key, + * and optional state-specific values. + * o DIRTY lines track that an entry is actively being created or updated. + * Every successful DIRTY action should be followed by a CLEAN or REMOVE + * action. DIRTY lines without a matching CLEAN or REMOVE indicate that + * temporary files may need to be deleted. + * o CLEAN lines track a cache entry that has been successfully published + * and may be read. A publish line is followed by the lengths of each of + * its values. + * o READ lines track accesses for LRU. + * o REMOVE lines track entries that have been deleted. + * + * The journal file is appended to as cache operations occur. The journal may + * occasionally be compacted by dropping redundant lines. A temporary file named + * "journal.tmp" will be used during compaction; that file should be deleted if + * it exists when the cache is opened. + */ + + private final File directory; + private final File journalFile; + private final File journalFileTmp; + private final File journalFileBackup; + private final int appVersion; + private long maxSize; + private final int valueCount; + private long size = 0; + private Writer journalWriter; + private final LinkedHashMap lruEntries = + new LinkedHashMap(0, 0.75f, true); + private int redundantOpCount; + + /** + * To differentiate between old and current snapshots, each entry is given + * a sequence number each time an edit is committed. A snapshot is stale if + * its sequence number is not equal to its entry's sequence number. + */ + private long nextSequenceNumber = 0; + + /** + * This cache uses a single background thread to evict entries. + */ + final ThreadPoolExecutor executorService = + new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + private final Callable cleanupCallable = new Callable() { + public Void call() throws Exception { + synchronized (DiskLruCache.this) { + if (journalWriter == null) { + return null; // Closed. + } + trimToSize(); + if (journalRebuildRequired()) { + rebuildJournal(); + redundantOpCount = 0; + } + } + return null; + } + }; + + private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { + this.directory = directory; + this.appVersion = appVersion; + this.journalFile = new File(directory, JOURNAL_FILE); + this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); + this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); + this.valueCount = valueCount; + this.maxSize = maxSize; + } + + /** + * Opens the cache in {@code directory}, creating a cache if none exists + * there. + * + * @param directory a writable directory + * @param valueCount the number of values per cache entry. Must be positive. + * @param maxSize the maximum number of bytes this cache should use to store + * @throws IOException if reading or writing the cache directory fails + */ + public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) + throws IOException { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + if (valueCount <= 0) { + throw new IllegalArgumentException("valueCount <= 0"); + } + + // If a bkp file exists, use it instead. + File backupFile = new File(directory, JOURNAL_FILE_BACKUP); + if (backupFile.exists()) { + File journalFile = new File(directory, JOURNAL_FILE); + // If journal file also exists just delete backup file. + if (journalFile.exists()) { + backupFile.delete(); + } else { + renameTo(backupFile, journalFile, false); + } + } + + // Prefer to pick up where we left off. + DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + if (cache.journalFile.exists()) { + try { + cache.readJournal(); + cache.processJournal(); + return cache; + } catch (IOException journalIsCorrupt) { + System.out + .println("DiskLruCache " + + directory + + " is corrupt: " + + journalIsCorrupt.getMessage() + + ", removing"); + cache.delete(); + } + } + + // Create a new empty cache. + directory.mkdirs(); + cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + cache.rebuildJournal(); + return cache; + } + + private void readJournal() throws IOException { + StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII); + try { + String magic = reader.readLine(); + String version = reader.readLine(); + String appVersionString = reader.readLine(); + String valueCountString = reader.readLine(); + String blank = reader.readLine(); + if (!MAGIC.equals(magic) + || !VERSION_1.equals(version) + || !Integer.toString(appVersion).equals(appVersionString) + || !Integer.toString(valueCount).equals(valueCountString) + || !"".equals(blank)) { + throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " + + valueCountString + ", " + blank + "]"); + } + + int lineCount = 0; + while (true) { + try { + readJournalLine(reader.readLine()); + lineCount++; + } catch (EOFException endOfJournal) { + break; + } + } + redundantOpCount = lineCount - lruEntries.size(); + + // If we ended on a truncated line, rebuild the journal before appending to it. + if (reader.hasUnterminatedLine()) { + rebuildJournal(); + } else { + journalWriter = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(journalFile, true), Util.US_ASCII)); + } + } finally { + Util.closeQuietly(reader); + } + } + + private void readJournalLine(String line) throws IOException { + int firstSpace = line.indexOf(' '); + if (firstSpace == -1) { + throw new IOException("unexpected journal line: " + line); + } + + int keyBegin = firstSpace + 1; + int secondSpace = line.indexOf(' ', keyBegin); + final String key; + if (secondSpace == -1) { + key = line.substring(keyBegin); + if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { + lruEntries.remove(key); + return; + } + } else { + key = line.substring(keyBegin, secondSpace); + } + + Entry entry = lruEntries.get(key); + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } + + if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) { + String[] parts = line.substring(secondSpace + 1).split(" "); + entry.readable = true; + entry.currentEditor = null; + entry.setLengths(parts); + } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) { + entry.currentEditor = new Editor(entry); + } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) { + // This work was already done by calling lruEntries.get(). + } else { + throw new IOException("unexpected journal line: " + line); + } + } + + /** + * Computes the initial size and collects garbage as a part of opening the + * cache. Dirty entries are assumed to be inconsistent and will be deleted. + */ + private void processJournal() throws IOException { + deleteIfExists(journalFileTmp); + for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) { + Entry entry = i.next(); + if (entry.currentEditor == null) { + for (int t = 0; t < valueCount; t++) { + size += entry.lengths[t]; + } + } else { + entry.currentEditor = null; + for (int t = 0; t < valueCount; t++) { + deleteIfExists(entry.getCleanFile(t)); + deleteIfExists(entry.getDirtyFile(t)); + } + i.remove(); + } + } + } + + /** + * Creates a new journal that omits redundant information. This replaces the + * current journal if it exists. + */ + private synchronized void rebuildJournal() throws IOException { + if (journalWriter != null) { + journalWriter.close(); + } + + Writer writer = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII)); + try { + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); + + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + } + } + } finally { + writer.close(); + } + + if (journalFile.exists()) { + renameTo(journalFile, journalFileBackup, true); + } + renameTo(journalFileTmp, journalFile, false); + journalFileBackup.delete(); + + journalWriter = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); + } + + private static void deleteIfExists(File file) throws IOException { + if (file.exists() && !file.delete()) { + throw new IOException(); + } + } + + private static void renameTo(File from, File to, boolean deleteDestination) throws IOException { + if (deleteDestination) { + deleteIfExists(to); + } + if (!from.renameTo(to)) { + throw new IOException(); + } + } + + /** + * Returns a snapshot of the entry named {@code key}, or null if it doesn't + * exist is not currently readable. If a value is returned, it is moved to + * the head of the LRU queue. + */ + public synchronized Snapshot get(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null) { + return null; + } + + if (!entry.readable) { + return null; + } + + // Open all streams eagerly to guarantee that we see a single published + // snapshot. If we opened streams lazily then the streams could come + // from different edits. + InputStream[] ins = new InputStream[valueCount]; + try { + for (int i = 0; i < valueCount; i++) { + ins[i] = new FileInputStream(entry.getCleanFile(i)); + } + } catch (FileNotFoundException e) { + // A file must have been deleted manually! + for (int i = 0; i < valueCount; i++) { + if (ins[i] != null) { + Util.closeQuietly(ins[i]); + } else { + break; + } + } + return null; + } + + redundantOpCount++; + journalWriter.append(READ + ' ' + key + '\n'); + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths); + } + + /** + * Returns an editor for the entry named {@code key}, or null if another + * edit is in progress. + */ + public Editor edit(String key) throws IOException { + return edit(key, ANY_SEQUENCE_NUMBER); + } + + private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null + || entry.sequenceNumber != expectedSequenceNumber)) { + return null; // Snapshot is stale. + } + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } else if (entry.currentEditor != null) { + return null; // Another edit is in progress. + } + + Editor editor = new Editor(entry); + entry.currentEditor = editor; + + // Flush the journal before creating files to prevent file leaks. + journalWriter.write(DIRTY + ' ' + key + '\n'); + journalWriter.flush(); + return editor; + } + + /** + * Returns the directory where this cache stores its data. + */ + public File getDirectory() { + return directory; + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public synchronized long getMaxSize() { + return maxSize; + } + + /** + * Changes the maximum number of bytes the cache can store and queues a job + * to trim the existing store, if necessary. + */ + public synchronized void setMaxSize(long maxSize) { + this.maxSize = maxSize; + executorService.submit(cleanupCallable); + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the max size if a background + * deletion is pending. + */ + public synchronized long size() { + return size; + } + + private synchronized void completeEdit(Editor editor, boolean success) throws IOException { + Entry entry = editor.entry; + if (entry.currentEditor != editor) { + throw new IllegalStateException(); + } + + // If this edit is creating the entry for the first time, every index must have a value. + if (success && !entry.readable) { + for (int i = 0; i < valueCount; i++) { + if (!editor.written[i]) { + editor.abort(); + throw new IllegalStateException("Newly created entry didn't create value for index " + i); + } + if (!entry.getDirtyFile(i).exists()) { + editor.abort(); + return; + } + } + } + + for (int i = 0; i < valueCount; i++) { + File dirty = entry.getDirtyFile(i); + if (success) { + if (dirty.exists()) { + File clean = entry.getCleanFile(i); + dirty.renameTo(clean); + long oldLength = entry.lengths[i]; + long newLength = clean.length(); + entry.lengths[i] = newLength; + size = size - oldLength + newLength; + } + } else { + deleteIfExists(dirty); + } + } + + redundantOpCount++; + entry.currentEditor = null; + if (entry.readable | success) { + entry.readable = true; + journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + if (success) { + entry.sequenceNumber = nextSequenceNumber++; + } + } else { + lruEntries.remove(entry.key); + journalWriter.write(REMOVE + ' ' + entry.key + '\n'); + } + journalWriter.flush(); + + if (size > maxSize || journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + } + + /** + * We only rebuild the journal when it will halve the size of the journal + * and eliminate at least 2000 ops. + */ + private boolean journalRebuildRequired() { + final int redundantOpCompactThreshold = 2000; + return redundantOpCount >= redundantOpCompactThreshold // + && redundantOpCount >= lruEntries.size(); + } + + /** + * Drops the entry for {@code key} if it exists and can be removed. Entries + * actively being edited cannot be removed. + * + * @return true if an entry was removed. + */ + public synchronized boolean remove(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null || entry.currentEditor != null) { + return false; + } + + for (int i = 0; i < valueCount; i++) { + File file = entry.getCleanFile(i); + if (file.exists() && !file.delete()) { + throw new IOException("failed to delete " + file); + } + size -= entry.lengths[i]; + entry.lengths[i] = 0; + } + + redundantOpCount++; + journalWriter.append(REMOVE + ' ' + key + '\n'); + lruEntries.remove(key); + + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return true; + } + + /** + * Returns true if this cache has been closed. + */ + public synchronized boolean isClosed() { + return journalWriter == null; + } + + private void checkNotClosed() { + if (journalWriter == null) { + throw new IllegalStateException("cache is closed"); + } + } + + /** + * Force buffered operations to the filesystem. + */ + public synchronized void flush() throws IOException { + checkNotClosed(); + trimToSize(); + journalWriter.flush(); + } + + /** + * Closes this cache. Stored values will remain on the filesystem. + */ + public synchronized void close() throws IOException { + if (journalWriter == null) { + return; // Already closed. + } + for (Entry entry : new ArrayList(lruEntries.values())) { + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } + } + trimToSize(); + journalWriter.close(); + journalWriter = null; + } + + private void trimToSize() throws IOException { + while (size > maxSize) { + Map.Entry toEvict = lruEntries.entrySet().iterator().next(); + remove(toEvict.getKey()); + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + close(); + Util.deleteContents(directory); + } + + private void validateKey(String key) { + Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); + if (!matcher.matches()) { + throw new IllegalArgumentException("keys must match regex " + + STRING_KEY_PATTERN + ": \"" + key + "\""); + } + } + + private static String inputStreamToString(InputStream in) throws IOException { + return Util.readFully(new InputStreamReader(in, Util.UTF_8)); + } + + /** + * A snapshot of the values for an entry. + */ + public final class Snapshot implements Closeable { + private final String key; + private final long sequenceNumber; + private final InputStream[] ins; + private final long[] lengths; + + private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) { + this.key = key; + this.sequenceNumber = sequenceNumber; + this.ins = ins; + this.lengths = lengths; + } + + /** + * Returns an editor for this snapshot's entry, or null if either the + * entry has changed since this snapshot was created or if another edit + * is in progress. + */ + public Editor edit() throws IOException { + return DiskLruCache.this.edit(key, sequenceNumber); + } + + /** + * Returns the unbuffered stream with the value for {@code index}. + */ + public InputStream getInputStream(int index) { + return ins[index]; + } + + /** + * Returns the string value for {@code index}. + */ + public String getString(int index) throws IOException { + return inputStreamToString(getInputStream(index)); + } + + /** + * Returns the byte length of the value for {@code index}. + */ + public long getLength(int index) { + return lengths[index]; + } + + public void close() { + for (InputStream in : ins) { + Util.closeQuietly(in); + } + } + } + + private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() { + @Override + public void write(int b) throws IOException { + // Eat all writes silently. Nom nom. + } + }; + + /** + * Edits the values for an entry. + */ + public final class Editor { + private final Entry entry; + private final boolean[] written; + private boolean hasErrors; + private boolean committed; + + private Editor(Entry entry) { + this.entry = entry; + this.written = (entry.readable) ? null : new boolean[valueCount]; + } + + /** + * Returns an unbuffered input stream to read the last committed value, + * or null if no value has been committed. + */ + public InputStream newInputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + return null; + } + try { + return new FileInputStream(entry.getCleanFile(index)); + } catch (FileNotFoundException e) { + return null; + } + } + } + + /** + * Returns the last committed value as a string, or null if no value + * has been committed. + */ + public String getString(int index) throws IOException { + InputStream in = newInputStream(index); + return in != null ? inputStreamToString(in) : null; + } + + /** + * Returns a new unbuffered output stream to write the value at + * {@code index}. If the underlying output stream encounters errors + * when writing to the filesystem, this edit will be aborted when + * {@link #commit} is called. The returned output stream does not throw + * IOExceptions. + */ + public OutputStream newOutputStream(int index) throws IOException { + if (index < 0 || index >= valueCount) { + throw new IllegalArgumentException("Expected index " + index + " to " + + "be greater than 0 and less than the maximum value count " + + "of " + valueCount); + } + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + written[index] = true; + } + File dirtyFile = entry.getDirtyFile(index); + FileOutputStream outputStream; + try { + outputStream = new FileOutputStream(dirtyFile); + } catch (FileNotFoundException e) { + // Attempt to recreate the cache directory. + directory.mkdirs(); + try { + outputStream = new FileOutputStream(dirtyFile); + } catch (FileNotFoundException e2) { + // We are unable to recover. Silently eat the writes. + return NULL_OUTPUT_STREAM; + } + } + return new FaultHidingOutputStream(outputStream); + } + } + + /** + * Sets the value at {@code index} to {@code value}. + */ + public void set(int index, String value) throws IOException { + Writer writer = null; + try { + writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8); + writer.write(value); + } finally { + Util.closeQuietly(writer); + } + } + + /** + * Commits this edit so it is visible to readers. This releases the + * edit lock so another edit may be started on the same key. + */ + public void commit() throws IOException { + if (hasErrors) { + completeEdit(this, false); + remove(entry.key); // The previous entry is stale. + } else { + completeEdit(this, true); + } + committed = true; + } + + /** + * Aborts this edit. This releases the edit lock so another edit may be + * started on the same key. + */ + public void abort() throws IOException { + completeEdit(this, false); + } + + public void abortUnlessCommitted() { + if (!committed) { + try { + abort(); + } catch (IOException ignored) { + } + } + } + + private class FaultHidingOutputStream extends FilterOutputStream { + private FaultHidingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int oneByte) { + try { + out.write(oneByte); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override + public void write(byte[] buffer, int offset, int length) { + try { + out.write(buffer, offset, length); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override + public void close() { + try { + out.close(); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override + public void flush() { + try { + out.flush(); + } catch (IOException e) { + hasErrors = true; + } + } + } + } + + private final class Entry { + private final String key; + + /** + * Lengths of this entry's files. + */ + private final long[] lengths; + + /** + * True if this entry has ever been published. + */ + private boolean readable; + + /** + * The ongoing edit or null if this entry is not being edited. + */ + private Editor currentEditor; + + /** + * The sequence number of the most recently committed edit to this entry. + */ + private long sequenceNumber; + + private Entry(String key) { + this.key = key; + this.lengths = new long[valueCount]; + } + + public String getLengths() throws IOException { + StringBuilder result = new StringBuilder(); + for (long size : lengths) { + result.append(' ').append(size); + } + return result.toString(); + } + + /** + * Set lengths using decimal numbers like "10123". + */ + private void setLengths(String[] strings) throws IOException { + if (strings.length != valueCount) { + throw invalidLengths(strings); + } + + try { + for (int i = 0; i < strings.length; i++) { + lengths[i] = Long.parseLong(strings[i]); + } + } catch (NumberFormatException e) { + throw invalidLengths(strings); + } + } + + private IOException invalidLengths(String[] strings) throws IOException { + throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); + } + + public File getCleanFile(int i) { + return new File(directory, key + "." + i); + } + + public File getDirtyFile(int i) { + return new File(directory, key + "." + i + ".tmp"); + } + } +} diff --git a/lib_cache/src/main/java/com/android/sdk/cache/DiskLruCacheHelper.java b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruCacheHelper.java new file mode 100644 index 0000000..497d0d8 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruCacheHelper.java @@ -0,0 +1,426 @@ +package com.android.sdk.cache; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Environment; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Serializable; + +/** + * Reference: https://github.com/hongyangAndroid/base-diskcache + * 创建对象时: + *

    + *     1. 默认从缓存大小为1G
    + *     2. 默认的存储文件夹在SD卡
    + *     3. 如果传入的Context对象为null,则永远使用默认的版本号,否则每次版本升级会删除之前的缓存
    + * 
    + */ +public class DiskLruCacheHelper { + + private static final String TAG = DiskLruCacheHelper.class.getSimpleName(); + + private static final String DIR_NAME = "diskCache"; + private static final int MAX_SIZE = 1000 * 1024 * 1024; + private static final int DEFAULT_APP_VERSION = 1; + private static final int DEFAULT_VALUE_COUNT = 1; + + private DiskLruCache mDiskLruCache; + + public DiskLruCacheHelper(Context context) throws IOException { + mDiskLruCache = generateCache(context, DIR_NAME, MAX_SIZE); + } + + public DiskLruCacheHelper(Context context, String dirName) throws IOException { + mDiskLruCache = generateCache(context, dirName, MAX_SIZE); + } + + public DiskLruCacheHelper(Context context, String dirName, int maxSize) throws IOException { + mDiskLruCache = generateCache(context, dirName, maxSize); + } + + //custom cache dir + public DiskLruCacheHelper(File dir) throws IOException { + mDiskLruCache = generateCache(null, dir, MAX_SIZE); + } + + public DiskLruCacheHelper(Context context, File dir) throws IOException { + mDiskLruCache = generateCache(context, dir, MAX_SIZE); + } + + public DiskLruCacheHelper(Context context, File dir, int maxSize) throws IOException { + mDiskLruCache = generateCache(context, dir, maxSize); + } + + private DiskLruCache generateCache(Context context, File dir, int maxSize) throws IOException { + if (!dir.exists() || !dir.isDirectory()) { + throw new IllegalArgumentException( + dir + " is not a directory or does not exists. "); + } + + int appVersion = context == null ? DEFAULT_APP_VERSION : Utils.getAppVersion(context); + + return DiskLruCache.open( + dir, + appVersion, + DEFAULT_VALUE_COUNT, + maxSize); + } + + private DiskLruCache generateCache(Context context, String dirName, int maxSize) throws IOException { + return DiskLruCache.open( + getDiskCacheDir(context, dirName), + Utils.getAppVersion(context), + DEFAULT_VALUE_COUNT, + maxSize); + } + + // ======================================= + // ============== String 数据 读写 ============= + // ======================================= + + public void put(String key, String value) { + DiskLruCache.Editor edit = null; + BufferedWriter bw = null; + try { + edit = editor(key); + if (edit == null) return; + OutputStream os = edit.newOutputStream(0); + bw = new BufferedWriter(new OutputStreamWriter(os)); + bw.write(value); + edit.commit();//write CLEAN + } catch (IOException e) { + e.printStackTrace(); + try { + //s + edit.abort();//write REMOVE + } catch (IOException e1) { + e1.printStackTrace(); + } + } finally { + try { + if (bw != null) + bw.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public String getAsString(String key) { + InputStream inputStream = null; + inputStream = get(key); + if (inputStream == null) return null; + String str = null; + try { + str = Util.readFully(new InputStreamReader(inputStream, Util.UTF_8)); + } catch (IOException e) { + e.printStackTrace(); + try { + inputStream.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + return str; + } + + + public void put(String key, JSONObject jsonObject) { + put(key, jsonObject.toString()); + } + + public JSONObject getAsJson(String key) { + String val = getAsString(key); + try { + if (val != null) + return new JSONObject(val); + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } + + // ======================================= + // ============ JSONArray 数据 读写 ============= + // ======================================= + + public void put(String key, JSONArray jsonArray) { + put(key, jsonArray.toString()); + } + + public JSONArray getAsJSONArray(String key) { + String JSONString = getAsString(key); + try { + JSONArray obj = new JSONArray(JSONString); + return obj; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + // ======================================= + // ============== byte 数据 读写 ============= + // ======================================= + + /** + * 保存 byte数据 到 缓存中 + * + * @param key 保存的key + * @param value 保存的数据 + */ + public void put(String key, byte[] value) { + OutputStream out = null; + DiskLruCache.Editor editor = null; + try { + editor = editor(key); + if (editor == null) { + return; + } + out = editor.newOutputStream(0); + out.write(value); + out.flush(); + editor.commit();//write CLEAN + } catch (Exception e) { + e.printStackTrace(); + try { + editor.abort();//write REMOVE + } catch (IOException e1) { + e1.printStackTrace(); + } + + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + + public byte[] getAsBytes(String key) { + byte[] res = null; + InputStream is = get(key); + if (is == null) return null; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + byte[] buf = new byte[256]; + int len = 0; + while ((len = is.read(buf)) != -1) { + baos.write(buf, 0, len); + } + res = baos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + } + return res; + } + + + // ======================================= + // ============== 序列化 数据 读写 ============= + // ======================================= + public void put(String key, Serializable value) { + DiskLruCache.Editor editor = editor(key); + ObjectOutputStream oos = null; + if (editor == null) return; + try { + OutputStream os = editor.newOutputStream(0); + oos = new ObjectOutputStream(os); + oos.writeObject(value); + oos.flush(); + editor.commit(); + } catch (IOException e) { + e.printStackTrace(); + try { + editor.abort(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } finally { + try { + if (oos != null) + oos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public T getAsSerializable(String key) { + T t = null; + InputStream is = get(key); + ObjectInputStream ois = null; + if (is == null) return null; + try { + ois = new ObjectInputStream(is); + t = (T) ois.readObject(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (ois != null) + ois.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return t; + } + + // ======================================= + // ============== bitmap 数据 读写 ============= + // ======================================= + public void put(String key, Bitmap bitmap) { + put(key, Utils.bitmap2Bytes(bitmap)); + } + + public Bitmap getAsBitmap(String key) { + byte[] bytes = getAsBytes(key); + if (bytes == null) return null; + return Utils.bytes2Bitmap(bytes); + } + + // ======================================= + // ============= drawable 数据 读写 ============= + // ======================================= + public void put(String key, Drawable value) { + put(key, Utils.drawable2Bitmap(value)); + } + + public Drawable getAsDrawable(String key) { + byte[] bytes = getAsBytes(key); + if (bytes == null) { + return null; + } + return Utils.bitmap2Drawable(Utils.bytes2Bitmap(bytes)); + } + + // ======================================= + // ============= other methods ============= + // ======================================= + public boolean remove(String key) { + try { + key = Utils.hashKeyForDisk(key); + return mDiskLruCache.remove(key); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + public void close() throws IOException { + mDiskLruCache.close(); + } + + public void delete() throws IOException { + mDiskLruCache.delete(); + } + + public void flush() throws IOException { + mDiskLruCache.flush(); + } + + public boolean isClosed() { + return mDiskLruCache.isClosed(); + } + + public long size() { + return mDiskLruCache.size(); + } + + public void setMaxSize(long maxSize) { + mDiskLruCache.setMaxSize(maxSize); + } + + public File getDirectory() { + return mDiskLruCache.getDirectory(); + } + + public long getMaxSize() { + return mDiskLruCache.getMaxSize(); + } + + + // ======================================= + // ===遇到文件比较大的,可以直接通过流读写 ===== + // ======================================= + //basic editor + public DiskLruCache.Editor editor(String key) { + try { + key = Utils.hashKeyForDisk(key); + //wirte DIRTY + DiskLruCache.Editor edit = mDiskLruCache.edit(key); + //edit maybe null :the entry is editing + if (edit == null) { + Log.w(TAG, "the entry spcified key:" + key + " is editing by other . "); + } + return edit; + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + + //basic get + public InputStream get(String key) { + try { + DiskLruCache.Snapshot snapshot = mDiskLruCache.get(Utils.hashKeyForDisk(key)); + if (snapshot == null) //not find entry , or entry.readable = false + { + Log.e(TAG, "not find entry , or entry.readable = false"); + return null; + } + //write READ + return snapshot.getInputStream(0); + + } catch (IOException e) { + e.printStackTrace(); + return null; + } + + } + + + // ======================================= + // ============== 序列化 数据 读写 ============= + // ======================================= + + private File getDiskCacheDir(Context context, String uniqueName) { + String cachePath; + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) + || !Environment.isExternalStorageRemovable()) { + cachePath = context.getExternalCacheDir().getPath(); + } else { + cachePath = context.getCacheDir().getPath(); + } + return new File(cachePath + File.separator + uniqueName); + } + +} + + + diff --git a/lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageFactoryImpl.java b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageFactoryImpl.java new file mode 100644 index 0000000..4e550ae --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageFactoryImpl.java @@ -0,0 +1,42 @@ +package com.android.sdk.cache; + +import android.content.Context; +import android.util.Log; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-09 11:20 + */ +public class DiskLruStorageFactoryImpl implements StorageFactory { + + private static final String TAG = DiskLruStorageFactoryImpl.class.getSimpleName(); + + @Override + public Builder newBuilder(Context context) { + return new DiskLruStorageBuilder(context); + } + + class DiskLruStorageBuilder extends Builder { + + DiskLruStorageBuilder(Context context) { + super(context); + } + + @Override + public Builder enableMultiProcess(boolean multiProcess) { + if (multiProcess) { + Log.d(TAG, "DiskLruStorage was initialized, but do not support multi process"); + } + super.enableMultiProcess(multiProcess); + return this; + } + + @Override + public Storage build() { + return new DiskLruStorageImpl(context, storageId); + } + + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageImpl.java b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageImpl.java new file mode 100644 index 0000000..0b57e0b --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/DiskLruStorageImpl.java @@ -0,0 +1,163 @@ +package com.android.sdk.cache; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import com.github.dmstocking.optional.java.util.Optional; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Type; + +import io.reactivex.Flowable; + +@SuppressWarnings("WeakerAccess,unused") +public class DiskLruStorageImpl implements Storage { + + private DiskLruCacheHelper mDiskLruCacheHelper; + private static final int CACHE_SIZE = 50 * 1024 * 1024;//50M + private final File mDir; + private final int mSize; + + /** + * @param context 上下文 + * @param cachePath 缓存文件 + */ + public DiskLruStorageImpl(@NonNull Context context, @NonNull String cachePath) { + this(context, cachePath, CACHE_SIZE); + } + + /** + * @param context 上下文 + * @param cachePath 缓存文件 + * @param cacheSize 缓存大小,字节数 + */ + public DiskLruStorageImpl(@NonNull Context context, @NonNull String cachePath, int cacheSize) { + mDir = context.getDir(cachePath, Context.MODE_PRIVATE); + mSize = cacheSize; + @SuppressWarnings("unused") + boolean mkdirs = mDir.getParentFile().mkdirs(); + } + + private DiskLruCacheHelper getDiskLruCacheHelper() { + if (mDiskLruCacheHelper == null || mDiskLruCacheHelper.isClosed()) { + try { + mDiskLruCacheHelper = new DiskLruCacheHelper(null, mDir, mSize); + } catch (IOException e) { + e.printStackTrace(); + } + } + return mDiskLruCacheHelper; + } + + @Override + public void putString(String key, String value) { + if (value == null) { + getDiskLruCacheHelper().remove(key); + return; + } + getDiskLruCacheHelper().put(buildKey(key), value); + } + + @Override + public String getString(String key, String defaultValue) { + String result = getDiskLruCacheHelper().getAsString(buildKey(key)); + if (TextUtils.isEmpty(result)) { + result = defaultValue; + } + return result; + } + + @Override + public String getString(String key) { + return getDiskLruCacheHelper().getAsString(buildKey(key)); + } + + @Override + public void putLong(String key, long value) { + getDiskLruCacheHelper().put(buildKey(key), String.valueOf(value)); + } + + @Override + public long getLong(String key, long defaultValue) { + String strLong = getDiskLruCacheHelper().getAsString(buildKey(key)); + if (TextUtils.isEmpty(strLong)) { + return defaultValue; + } + return Long.parseLong(strLong); + } + + @Override + public void putInt(String key, int value) { + getDiskLruCacheHelper().put(buildKey(key), String.valueOf(value)); + } + + @Override + public int getInt(String key, int defaultValue) { + String strInt = getDiskLruCacheHelper().getAsString(buildKey(key)); + if (TextUtils.isEmpty(strInt)) { + return defaultValue; + } + return Integer.parseInt(strInt); + } + + @Override + public void putBoolean(String key, boolean value) { + int bool = value ? 1 : 0; + getDiskLruCacheHelper().put(buildKey(key), String.valueOf(bool)); + } + + @Override + public boolean getBoolean(String key, boolean defaultValue) { + String strInt = getDiskLruCacheHelper().getAsString(buildKey(key)); + if (TextUtils.isEmpty(strInt)) { + return defaultValue; + } + return Integer.parseInt(strInt) == 1; + } + + @Override + public void remove(String key) { + getDiskLruCacheHelper().remove(buildKey(key)); + } + + @Override + public void clearAll() { + try { + getDiskLruCacheHelper().delete(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private String buildKey(String originKey) { + return originKey; + } + + @Override + public void putEntity(String key, Object entity, long cacheTime) { + CommonImpl.putEntity(key, entity, cacheTime, this); + } + + @Override + public void putEntity(String key, Object entity) { + CommonImpl.putEntity(key, entity, 0, this); + } + + @Override + public T getEntity(String key, Type type) { + return CommonImpl.getEntity(key, type, this); + } + + @Override + public Flowable flowable(String key, Type type) { + return CommonImpl.flowableEntity(key, type, this); + } + + @Override + public Flowable> optionalFlowable(String key, Type type) { + return CommonImpl.flowableOptionalEntity(key, type, this); + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/JsonUtils.java b/lib_cache/src/main/java/com/android/sdk/cache/JsonUtils.java new file mode 100644 index 0000000..aeba3db --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/JsonUtils.java @@ -0,0 +1,51 @@ +package com.android.sdk.cache; + +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-01 16:38 + */ +final class JsonUtils { + + private static final String TAG = JsonUtils.class.getSimpleName(); + + private final static Gson GSON = new GsonBuilder() + .excludeFieldsWithModifiers(Modifier.TRANSIENT) + .excludeFieldsWithModifiers(Modifier.STATIC) + .create(); + + static String toJson(Object entity) { + if (entity == null) { + return ""; + } + try { + return GSON.toJson(entity); + } catch (Exception e) { + Log.e(TAG, "JsonSerializer toJson error with: entity = " + entity, e); + } + return ""; + } + + @SuppressWarnings("unchecked") + static T fromJson(String json, Type clazz) { + try { + if (clazz == String.class) { + return (T) json; + } else { + return GSON.fromJson(json, clazz); + } + } catch (Exception e) { + Log.e(TAG, "JsonSerializer fromJson error with: json = " + json + " class = " + clazz, e); + } + return null; + } + +} diff --git a/lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageFactoryImpl.java b/lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageFactoryImpl.java new file mode 100644 index 0000000..28dd871 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageFactoryImpl.java @@ -0,0 +1,30 @@ +package com.android.sdk.cache; + +import android.content.Context; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-01 17:35 + */ +public class MMKVStorageFactoryImpl implements StorageFactory { + + @Override + public Builder newBuilder(Context context) { + return new MMKVStorageBuilder(context); + } + + class MMKVStorageBuilder extends Builder { + + MMKVStorageBuilder(Context context) { + super(context); + } + + @Override + public Storage build() { + return new MMKVStorageImpl(context, storageId, multiProcess); + } + + } + +} diff --git a/lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageImpl.java b/lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageImpl.java new file mode 100644 index 0000000..42520aa --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/MMKVStorageImpl.java @@ -0,0 +1,172 @@ +package com.android.sdk.cache; + +import android.content.Context; +import android.util.Log; + +import com.github.dmstocking.optional.java.util.Optional; +import com.tencent.mmkv.MMKV; + +import java.lang.reflect.Type; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.Flowable; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-01 11:25 + */ +@SuppressWarnings("WeakerAccess,unused") +public class MMKVStorageImpl implements Storage { + + private static final String TAG = MMKVStorageImpl.class.getSimpleName(); + + private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false); + + private final MMKV mMmkv; + + public MMKVStorageImpl(Context context, String mmkvId) { + this(context, mmkvId, false); + } + + public MMKVStorageImpl(Context context, String mmkvId, boolean multiProcess) { + + if (INITIALIZED.compareAndSet(false, true)) { + String rootDir = MMKV.initialize(context.getApplicationContext()); + Log.d(TAG, "MMKV initialized and rootDir is: " + rootDir); + } + + int mode = multiProcess ? MMKV.MULTI_PROCESS_MODE : MMKV.SINGLE_PROCESS_MODE; + mMmkv = MMKV.mmkvWithID(mmkvId, mode); + } + + @Override + public void putString(String key, String value) { + try { + if (value == null) { + remove(key); + return; + } + mMmkv.encode(key, value); + } catch (Error error) { + error.printStackTrace(); + } + } + + @Override + public String getString(String key, String defaultValue) { + try { + return mMmkv.decodeString(key, defaultValue); + } catch (Error error) { + error.printStackTrace(); + } + return defaultValue; + } + + @Override + public String getString(String key) { + try { + return mMmkv.decodeString(key); + } catch (Error error) { + error.printStackTrace(); + } + return null; + } + + @Override + public void putLong(String key, long value) { + try { + mMmkv.encode(key, value); + } catch (Error error) { + error.printStackTrace(); + } + } + + @Override + public long getLong(String key, long defaultValue) { + try { + return mMmkv.decodeLong(key, defaultValue); + } catch (Error error) { + error.printStackTrace(); + } + return defaultValue; + } + + @Override + public void putInt(String key, int value) { + try { + mMmkv.encode(key, value); + } catch (Error error) { + error.printStackTrace(); + } + } + + @Override + public int getInt(String key, int defaultValue) { + try { + return mMmkv.decodeInt(key, defaultValue); + } catch (Error error) { + error.printStackTrace(); + } + return defaultValue; + } + + @Override + public void putBoolean(String key, boolean value) { + try { + mMmkv.encode(key, value); + } catch (Error error) { + error.printStackTrace(); + } + } + + @Override + public boolean getBoolean(String key, boolean defaultValue) { + try { + return mMmkv.decodeBool(key, defaultValue); + } catch (Error error) { + error.printStackTrace(); + } + return defaultValue; + } + + @Override + public void remove(String key) { + try { + mMmkv.removeValueForKey(key); + } catch (Error error) { + error.printStackTrace(); + } + } + + @Override + public void clearAll() { + mMmkv.clear(); + } + + @Override + public void putEntity(String key, Object entity, long cacheTime) { + CommonImpl.putEntity(key, entity, cacheTime, this); + } + + @Override + public void putEntity(String key, Object entity) { + CommonImpl.putEntity(key, entity, 0, this); + } + + @Override + public T getEntity(String key, Type type) { + return CommonImpl.getEntity(key, type, this); + } + + @Override + public Flowable flowable(String key, Type type) { + return CommonImpl.flowableEntity(key, type, this); + } + + @Override + public Flowable> optionalFlowable(String key, Type type) { + return CommonImpl.flowableOptionalEntity(key, type, this); + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/Storage.java b/lib_cache/src/main/java/com/android/sdk/cache/Storage.java new file mode 100644 index 0000000..3c461f0 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/Storage.java @@ -0,0 +1,59 @@ +package com.android.sdk.cache; + +import android.support.annotation.Nullable; + +import com.github.dmstocking.optional.java.util.Optional; + +import java.lang.reflect.Type; + +import io.reactivex.Flowable; + +/** + * 缓存接口 + * + * @author Ztiany + * Date : 2016-10-24 21:59 + */ +public interface Storage { + + void putEntity(String key, @Nullable Object entity, long cacheTime); + + void putEntity(String key, @Nullable Object entity); + + /** + * @param key 缓存的 key + * @param type 缓存实体类型,如果是泛型类型,请使用 {@link TypeFlag}标识 + * @param 缓存实体类型 + * @return 缓存 + */ + @Nullable + T getEntity(String key, Type type); + + Flowable flowable(String key, Type type); + + Flowable> optionalFlowable(String key, Type type); + + void putString(String key, String value); + + String getString(String key, String defaultValue); + + @Nullable + String getString(String key); + + void putLong(String key, long value); + + long getLong(String key, long defaultValue); + + void putInt(String key, int value); + + int getInt(String key, int defaultValue); + + void putBoolean(String key, boolean value); + + boolean getBoolean(String key, boolean defaultValue); + + void remove(String key); + + void clearAll(); + +} diff --git a/lib_cache/src/main/java/com/android/sdk/cache/StorageEx.kt b/lib_cache/src/main/java/com/android/sdk/cache/StorageEx.kt new file mode 100644 index 0000000..168e564 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/StorageEx.kt @@ -0,0 +1,16 @@ +package com.android.sdk.cache + +import com.github.dmstocking.optional.java.util.Optional +import io.reactivex.Flowable + +inline fun Storage.entity(key: String): T? { + return this.getEntity(key, object : TypeFlag() {}.type) +} + +inline fun Storage.flowable(key: String): Flowable { + return this.flowable(key, object : TypeFlag() {}.type) +} + +inline fun Storage.optionalFlowable(key: String): Flowable> { + return this.optionalFlowable(key, object : TypeFlag() {}.type) +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/StorageFactory.java b/lib_cache/src/main/java/com/android/sdk/cache/StorageFactory.java new file mode 100644 index 0000000..dc89f1a --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/StorageFactory.java @@ -0,0 +1,43 @@ +package com.android.sdk.cache; + +import android.content.Context; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-01 17:32 + */ +public interface StorageFactory { + + StorageFactory.Builder newBuilder(Context context); + + abstract class Builder { + + Context context; + String storageId; + boolean multiProcess; + + Builder(Context context) { + this.context = context; + } + + /** + * 是否允许跨进程访问存储 + */ + public Builder enableMultiProcess(boolean multiProcess) { + this.multiProcess = multiProcess; + return this; + } + + /** + * @param storageId 存储标识 + */ + public Builder storageId(String storageId) { + this.storageId = storageId; + return this; + } + + public abstract Storage build(); + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/StrictLineReader.java b/lib_cache/src/main/java/com/android/sdk/cache/StrictLineReader.java new file mode 100644 index 0000000..85f476e --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/StrictLineReader.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdk.cache; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +/** + * Buffers input from an {@link InputStream} for reading lines. + *

    + *

    This class is used for buffered reading of lines. For purposes of this class, a line ends + * with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated + * line at end of input is invalid and will be ignored, the caller may use {@code + * hasUnterminatedLine()} to detect it after catching the {@code EOFException}. + *

    + *

    This class is intended for reading input that strictly consists of lines, such as line-based + * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction + * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different + * end-of-input reporting and a more restrictive definition of a line. + *

    + *

    This class supports only charsets that encode '\r' and '\n' as a single byte with value 13 + * and 10, respectively, and the representation of no other character contains these values. + * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1. + * The default charset is US_ASCII. + */ +class StrictLineReader implements Closeable { + + private static final byte CR = (byte) '\r'; + private static final byte LF = (byte) '\n'; + + private final InputStream in; + private final Charset charset; + + /* + * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end + * and the data in the range [pos, end) is buffered for reading. At end of input, if there is + * an unterminated line, we set end == -1, otherwise end == pos. If the underlying + * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1. + */ + private byte[] buf; + private int pos; + private int end; + + /** + * Constructs a new {@code LineReader} with the specified charset and the default capacity. + * + * @param in the {@code InputStream} to read data from. + * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are + * supported. + * @throws NullPointerException if {@code in} or {@code charset} is null. + * @throws IllegalArgumentException if the specified charset is not supported. + */ + public StrictLineReader(InputStream in, Charset charset) { + this(in, 8192, charset); + } + + /** + * Constructs a new {@code LineReader} with the specified capacity and charset. + * + * @param in the {@code InputStream} to read data from. + * @param capacity the capacity of the buffer. + * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are + * supported. + * @throws NullPointerException if {@code in} or {@code charset} is null. + * @throws IllegalArgumentException if {@code capacity} is negative or zero + * or the specified charset is not supported. + */ + public StrictLineReader(InputStream in, int capacity, Charset charset) { + if (in == null || charset == null) { + throw new NullPointerException(); + } + if (capacity < 0) { + throw new IllegalArgumentException("capacity <= 0"); + } + if (!(charset.equals(Util.US_ASCII))) { + throw new IllegalArgumentException("Unsupported encoding"); + } + + this.in = in; + this.charset = charset; + buf = new byte[capacity]; + } + + /** + * Closes the reader by closing the underlying {@code InputStream} and + * marking this reader as closed. + * + * @throws IOException for errors when closing the underlying {@code InputStream}. + */ + public void close() throws IOException { + synchronized (in) { + if (buf != null) { + buf = null; + in.close(); + } + } + } + + /** + * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"}, + * this end of line marker is not included in the result. + * + * @return the next line from the input. + * @throws IOException for underlying {@code InputStream} errors. + * @throws EOFException for the end of source stream. + */ + public String readLine() throws IOException { + synchronized (in) { + if (buf == null) { + throw new IOException("LineReader is closed"); + } + + // Read more data if we are at the end of the buffered data. + // Though it's an error to read after an exception, we will let {@code fillBuf()} + // throw again if that happens; thus we need to handle end == -1 as well as end == pos. + if (pos >= end) { + fillBuf(); + } + // Try to find LF in the buffered data and return the line if successful. + for (int i = pos; i != end; ++i) { + if (buf[i] == LF) { + int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i; + String res = new String(buf, pos, lineEnd - pos, charset.name()); + pos = i + 1; + return res; + } + } + + // Let's anticipate up to 80 characters on top of those already read. + ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) { + @Override + public String toString() { + int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count; + try { + return new String(buf, 0, length, charset.name()); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); // Since we control the charset this will never happen. + } + } + }; + + while (true) { + out.write(buf, pos, end - pos); + // Mark unterminated line in case fillBuf throws EOFException or IOException. + end = -1; + fillBuf(); + // Try to find LF in the buffered data and return the line if successful. + for (int i = pos; i != end; ++i) { + if (buf[i] == LF) { + if (i != pos) { + out.write(buf, pos, i - pos); + } + pos = i + 1; + return out.toString(); + } + } + } + } + } + + public boolean hasUnterminatedLine() { + return end == -1; + } + + /** + * Reads new input data into the buffer. Call only with pos == end or end == -1, + * depending on the desired outcome if the function throws. + */ + private void fillBuf() throws IOException { + int result = in.read(buf, 0, buf.length); + if (result == -1) { + throw new EOFException(); + } + pos = 0; + end = result; + } +} + diff --git a/lib_cache/src/main/java/com/android/sdk/cache/TypeFlag.java b/lib_cache/src/main/java/com/android/sdk/cache/TypeFlag.java new file mode 100644 index 0000000..9512711 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/TypeFlag.java @@ -0,0 +1,42 @@ +package com.android.sdk.cache; + +import com.google.gson.internal.$Gson$Types; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * copy from gson, 为了不让调用者依赖 gson. + * + * @param 类型标记 + */ +public abstract class TypeFlag { + + private final Class rawType; + private final Type type; + + @SuppressWarnings("unchecked") + protected TypeFlag() { + this.type = getSuperclassTypeParameter(getClass()); + this.rawType = (Class) $Gson$Types.getRawType(type); + } + + @SuppressWarnings("all") + private static Type getSuperclassTypeParameter(Class subclass) { + Type superclass = subclass.getGenericSuperclass(); + if (superclass instanceof Class) { + throw new RuntimeException("Missing type parameter."); + } + ParameterizedType parameterized = (ParameterizedType) superclass; + return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]); + } + + public final Class getRawType() { + return rawType; + } + + public final Type getType() { + return type; + } + +} \ No newline at end of file diff --git a/lib_cache/src/main/java/com/android/sdk/cache/Util.java b/lib_cache/src/main/java/com/android/sdk/cache/Util.java new file mode 100644 index 0000000..b9165d9 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/Util.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdk.cache; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.StringWriter; +import java.nio.charset.Charset; + +/** + * Junk drawer of utility methods. + */ +final class Util { + static final Charset US_ASCII = Charset.forName("US-ASCII"); + static final Charset UTF_8 = Charset.forName("UTF-8"); + + private Util() { + } + + static String readFully(Reader reader) throws IOException { + try { + StringWriter writer = new StringWriter(); + char[] buffer = new char[1024]; + int count; + while ((count = reader.read(buffer)) != -1) { + writer.write(buffer, 0, count); + } + return writer.toString(); + } finally { + reader.close(); + } + } + + /** + * Deletes the contents of {@code dir}. Throws an IOException if any file + * could not be deleted, or if {@code dir} is not a readable directory. + */ + static void deleteContents(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + throw new IOException("not a readable directory: " + dir); + } + for (File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + if (!file.delete()) { + throw new IOException("failed to delete file: " + file); + } + } + } + + static void closeQuietly(/*Auto*/Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + +} diff --git a/lib_cache/src/main/java/com/android/sdk/cache/Utils.java b/lib_cache/src/main/java/com/android/sdk/cache/Utils.java new file mode 100644 index 0000000..3560e13 --- /dev/null +++ b/lib_cache/src/main/java/com/android/sdk/cache/Utils.java @@ -0,0 +1,102 @@ +package com.android.sdk.cache; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.PixelFormat; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +import java.io.ByteArrayOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +final class Utils { + + static int getAppVersion(Context context) { + try { + PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + return info.versionCode; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return 1; + } + + + static String hashKeyForDisk(String key) { + String cacheKey; + try { + final MessageDigest mDigest = MessageDigest.getInstance("MD5"); + mDigest.update(key.getBytes()); + cacheKey = bytesToHexString(mDigest.digest()); + } catch (NoSuchAlgorithmException e) { + cacheKey = String.valueOf(key.hashCode()); + } + return cacheKey; + } + + private static String bytesToHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(0xFF & bytes[i]); + if (hex.length() == 1) { + sb.append('0'); + } + sb.append(hex); + } + return sb.toString(); + } + + static byte[] bitmap2Bytes(Bitmap bm) { + if (bm == null) { + return null; + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bm.compress(Bitmap.CompressFormat.PNG, 100, baos); + return baos.toByteArray(); + } + + static Bitmap bytes2Bitmap(byte[] bytes) { + return BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + } + + + /** + * Drawable → Bitmap + */ + static Bitmap drawable2Bitmap(Drawable drawable) { + if (drawable == null) { + return null; + } + // 取 drawable 的长宽 + int w = drawable.getIntrinsicWidth(); + int h = drawable.getIntrinsicHeight(); + // 取 drawable 的颜色格式 + Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; + // 建立对应 bitmap + Bitmap bitmap = Bitmap.createBitmap(w, h, config); + // 建立对应 bitmap 的画布 + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, w, h); + // 把 drawable 内容画到画布中 + drawable.draw(canvas); + return bitmap; + } + + /* + * Bitmap → Drawable + */ + @SuppressWarnings("deprecation") + static Drawable bitmap2Drawable(Bitmap bm) { + if (bm == null) { + return null; + } + BitmapDrawable bd = new BitmapDrawable(bm); + bd.setTargetDensity(bm.getDensity()); + return new BitmapDrawable(bm); + } +} \ No newline at end of file diff --git a/lib_cache/src/main/res/values/strings.xml b/lib_cache/src/main/res/values/strings.xml new file mode 100644 index 0000000..2018dac --- /dev/null +++ b/lib_cache/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + lib_cache + diff --git a/lib_media_selector/.gitignore b/lib_media_selector/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_media_selector/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_media_selector/README.md b/lib_media_selector/README.md new file mode 100644 index 0000000..6f49fcd --- /dev/null +++ b/lib_media_selector/README.md @@ -0,0 +1,27 @@ +# 多媒体文件选择库 + + 目前基于boxing修改 + +## 1 AndroidN 在 FileProvider 的 xm l配置中加入: + +``` + + +``` + +## 2 So库 + +``` + defaultConfig { + ...... + ndk {//只打包armeabi架构的so库 + abiFilters 'armeabi' + } + } +``` + +## 3 其他备选参考 + +-  [boxing](https://github.com/Bilibili/boxing) +-  [uCrop](https://github.com/Yalantis/uCrop) +-  [ImagePicker](https://github.com/jeasonlzy/ImagePicker) diff --git a/lib_media_selector/build.gradle b/lib_media_selector/build.gradle new file mode 100644 index 0000000..c8e091f --- /dev/null +++ b/lib_media_selector/build.gradle @@ -0,0 +1,57 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + //support + implementation androidLibraries.androidCompatV7 + implementation androidLibraries.androidDesign + + /*imageLoader*/ + compileOnly thirdLibraries.glide + + //other optional + //知乎 Matisse:https://github.com/zhihu/Matisse + //PictureSelector :https://github.com/LuckSiege/PictureSelector + //Album:https://github.com/yanzhenjie/Album + + implementation('com.bilibili:boxing:1.0.4') { + exclude group: 'com.android.support' + } + + implementation('com.bilibili:boxing-impl:1.0.4') { + exclude group: 'com.android.support' + } + + //other optional + //cropper about + // smartCropper:https://github.com/pqpo/SmartCropper + // simpleCropper:https:github.com/igreenwood/SimpleCropView + implementation('com.yalantis:ucrop:2.2.0') { + exclude group: 'com.android.support' + exclude group: 'com.squareup.okio' + exclude group: 'com.squareup.okhttp3' + } +} diff --git a/lib_media_selector/proguard-rules.pro b/lib_media_selector/proguard-rules.pro new file mode 100644 index 0000000..268073a --- /dev/null +++ b/lib_media_selector/proguard-rules.pro @@ -0,0 +1,17 @@ +# 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 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 *; +#} diff --git a/lib_media_selector/src/main/AndroidManifest.xml b/lib_media_selector/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b14b601 --- /dev/null +++ b/lib_media_selector/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + diff --git a/lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingGlideLoader.java b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingGlideLoader.java new file mode 100644 index 0000000..79d1990 --- /dev/null +++ b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingGlideLoader.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2017 Bilibili + * + * 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.android.sdk.mediaselector; + +import android.graphics.Bitmap; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.widget.ImageView; + +import com.bilibili.boxing.loader.IBoxingCallback; +import com.bilibili.boxing.loader.IBoxingMediaLoader; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.Target; + +/** + * use https://github.com/bumptech/glide as media loader. + * + * @author ChenSL + */ +final class BoxingGlideLoader implements IBoxingMediaLoader { + + @Override + public void displayThumbnail(@NonNull ImageView img, @NonNull String absPath, int width, int height) { + String path = "file://" + absPath; + try { + RequestOptions requestOptions = new RequestOptions(); + Glide.with(img.getContext()).load(path).apply(requestOptions).into(img); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + } + + @Override + public void displayRaw(@NonNull final ImageView imageView, @NonNull String absPath, int width, int height, final IBoxingCallback iBoxingCallback) { + String path = "file://" + absPath; + Glide.with(imageView.getContext()) + .asBitmap() + .load(path) + .listener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + if (iBoxingCallback != null) { + iBoxingCallback.onFail(e); + return true; + } + return false; + } + + @Override + public boolean onResourceReady(Bitmap resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + if (resource != null && iBoxingCallback != null) { + imageView.setImageBitmap(resource); + iBoxingCallback.onSuccess(); + return true; + } + return false; + } + }) + .into(imageView); + } + +} diff --git a/lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingUcrop.java b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingUcrop.java new file mode 100644 index 0000000..2754c2d --- /dev/null +++ b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/BoxingUcrop.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2017 Bilibili + * + * 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.android.sdk.mediaselector; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; + +import com.bilibili.boxing.loader.IBoxingCrop; +import com.bilibili.boxing.model.config.BoxingCropOption; +import com.yalantis.ucrop.UCrop; +import com.ztiany.mediaselector.R; + +/** + * use Ucrop(https://github.com/Yalantis/uCrop) as the implement for {@link IBoxingCrop} + * + * @author ChenSL + */ +final class BoxingUcrop implements IBoxingCrop { + + @Override + public void onStartCrop(Context context, Fragment fragment, @NonNull BoxingCropOption cropConfig, + @NonNull String path, int requestCode) { + + Uri uri = new Uri.Builder() + .scheme("file") + .appendPath(path) + .build(); + //参数 + UCrop.Options crop = new UCrop.Options(); + crop.setCompressionFormat(Bitmap.CompressFormat.JPEG); + crop.withMaxResultSize(cropConfig.getMaxWidth(), cropConfig.getMaxHeight()); + crop.withAspectRatio(cropConfig.getAspectRatioX(), cropConfig.getAspectRatioY()); + //颜色 + int color = ContextCompat.getColor(context, R.color.boxing_colorPrimaryDark); + crop.setToolbarColor(color); + crop.setStatusBarColor(color); + //开始裁减 + UCrop.of(uri, cropConfig.getDestination()) + .withOptions(crop) + .start(context, fragment, requestCode); + } + + @Override + public Uri onCropFinish(int resultCode, Intent data) { + if (data == null) { + return null; + } + Throwable throwable = UCrop.getError(data); + if (throwable != null) { + return null; + } + return UCrop.getOutput(data); + } +} diff --git a/lib_media_selector/src/main/java/com/android/sdk/mediaselector/CropOptions.java b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/CropOptions.java new file mode 100644 index 0000000..a6c7f2c --- /dev/null +++ b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/CropOptions.java @@ -0,0 +1,82 @@ +package com.android.sdk.mediaselector; + +import java.io.Serializable; + +/** + * 裁剪配置类 + * Author: JPH + * Date: 2016/7/27 13:19 + */ +public class CropOptions implements Serializable { + + private int aspectX = 1; + private int aspectY = 1; + private int outputX; + private int outputY; + + public CropOptions() { + + } + + int getAspectX() { + return aspectX; + } + + /** + * 裁剪宽度比例 与aspectY组合,如16:9 + * + * @param aspectX + * @return + */ + public CropOptions setAspectX(int aspectX) { + this.aspectX = aspectX; + return this; + } + + int getAspectY() { + return aspectY; + } + + /** + * 高度比例 与aspectX组合,如16:9 + * + * @param aspectY + * @return + */ + public CropOptions setAspectY(int aspectY) { + this.aspectY = aspectY; + return this; + } + + int getOutputX() { + return outputX; + } + + /** + * 输出图片的宽度 + * + * @param outputX + * @return + */ + public CropOptions setOutputX(int outputX) { + this.outputX = outputX; + return this; + } + + int getOutputY() { + return outputY; + } + + /** + * 输入图片的高度 + * + * @param outputY + * @return + */ + public CropOptions setOutputY(int outputY) { + this.outputY = outputY; + return this; + } + + +} diff --git a/lib_media_selector/src/main/java/com/android/sdk/mediaselector/MediaSelector.java b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/MediaSelector.java new file mode 100644 index 0000000..f0fbacb --- /dev/null +++ b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/MediaSelector.java @@ -0,0 +1,159 @@ +package com.android.sdk.mediaselector; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.v4.app.Fragment; +import android.text.TextUtils; + +import com.bilibili.boxing.Boxing; +import com.bilibili.boxing.BoxingCrop; +import com.bilibili.boxing.BoxingMediaLoader; +import com.bilibili.boxing.model.config.BoxingConfig; +import com.bilibili.boxing.model.config.BoxingCropOption; +import com.bilibili.boxing.model.entity.BaseMedia; +import com.bilibili.boxing.utils.BoxingFileHelper; +import com.bilibili.boxing.utils.BoxingLog; +import com.bilibili.boxing_impl.ui.BoxingActivity; +import com.ztiany.mediaselector.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * 基于 Boxing 的多媒体文件选择器 + *

    + *     FileProvider 的 Authorities为 {@code PackageName+ ".file.provider"} 才能正常工作
    + * 
    + */ +public class MediaSelector { + + static { + BoxingMediaLoader.getInstance().init(new BoxingGlideLoader()); + BoxingCrop.getInstance().init(new BoxingUcrop()); + } + + private static final int REQUEST_CODE_SINGLE = 100;//选择单图 + private static final int REQUEST_CODE_MULTI = 200;//选择多图 + + private final Callback mCallback; + private Activity mActivity; + private Fragment mFragment; + + public MediaSelector(Activity contextWrapper, Callback callback) { + mCallback = callback; + mActivity = contextWrapper; + } + + public MediaSelector(Fragment contextWrapper, Callback callback) { + mCallback = callback; + mFragment = contextWrapper; + } + + //Function + public void takeMultiPicture(boolean needCamera, int selectCount) { + BoxingConfig boxingConfig = new BoxingConfig(BoxingConfig.Mode.MULTI_IMG); + if (needCamera) { + boxingConfig.needCamera(R.drawable.ic_boxing_camera); + } + boxingConfig.withMaxCount(selectCount); + start(boxingConfig, REQUEST_CODE_MULTI); + } + + public void takeSinglePicture(boolean needCamera) { + BoxingConfig singleImgConfig = new BoxingConfig(BoxingConfig.Mode.SINGLE_IMG); + if (needCamera) { + singleImgConfig.needCamera(R.drawable.ic_boxing_camera); + } + start(singleImgConfig, REQUEST_CODE_SINGLE); + } + + public void takeSinglePictureWithCrop(boolean needCamera, CropOptions cropOptions) { + //裁减的图片放在App内部缓存目录 + String cachePath = BoxingFileHelper.getCacheDir(getContext()); + if (TextUtils.isEmpty(cachePath)) { + BoxingLog.d("takeSinglePictureWithCrop fail because getCacheDir fail"); + return; + } + + //裁剪信息 + Uri destUri = new Uri.Builder() + .scheme("file") + .appendPath(cachePath) + .appendPath(String.format(Locale.US, "%s.jpg", System.currentTimeMillis())) + .build(); + + BoxingCropOption cropOption = new BoxingCropOption(destUri) + .aspectRatio(cropOptions.getAspectX(), cropOptions.getAspectY()) + .withMaxResultSize(cropOptions.getOutputX(), cropOptions.getOutputY()); + + //获取图片配置 + BoxingConfig singleCropImgConfig = new BoxingConfig(BoxingConfig.Mode.SINGLE_IMG).withCropOption(cropOption); + + if (needCamera) { + singleCropImgConfig.needCamera(R.drawable.ic_boxing_camera); + } + + //去获取图片 + start(singleCropImgConfig, REQUEST_CODE_SINGLE); + } + + private Context getContext() { + if (mFragment != null) { + return mFragment.getContext(); + } else { + return mActivity; + } + } + + private void start(BoxingConfig boxingConfig, int requestCode) { + if (mFragment != null) { + Boxing boxing = Boxing.of(boxingConfig) + .withIntent(mFragment.getContext(), BoxingActivity.class); + boxing.start(mFragment, requestCode); + } else if (mActivity != null) { + Boxing boxing = Boxing.of(boxingConfig) + .withIntent(mActivity, BoxingActivity.class); + boxing.start(mActivity, requestCode); + } + } + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + if (requestCode == REQUEST_CODE_SINGLE) { + processSingle(data); + } else if (requestCode == REQUEST_CODE_MULTI) { + processMultiResult(data); + } + } + } + + private void processSingle(Intent data) { + final ArrayList medias = Boxing.getResult(data); + if (medias != null) { + mCallback.onTakePictureSuccess(medias.get(0).getPath()); + } + } + + private void processMultiResult(Intent data) { + final ArrayList medias = Boxing.getResult(data); + if (medias != null) { + List strings = new ArrayList<>(); + for (BaseMedia media : medias) { + strings.add(media.getPath()); + } + mCallback.onTakeMultiPictureSuccess(strings); + } + } + + public interface Callback { + default void onTakeMultiPictureSuccess(List pictures) { + } + + default void onTakePictureSuccess(String picture) { + } + } + +} diff --git a/lib_media_selector/src/main/java/com/android/sdk/mediaselector/SystemMediaSelector.java b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/SystemMediaSelector.java new file mode 100644 index 0000000..9ac4037 --- /dev/null +++ b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/SystemMediaSelector.java @@ -0,0 +1,294 @@ +package com.android.sdk.mediaselector; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.v4.app.Fragment; +import android.text.TextUtils; +import android.util.Log; + +import java.io.File; + +/** + * 通过系统相册或者系统相机获取照片 + *
    + *     1. 默认的 FileProvider 的 Authorities 为 {@code PackageName+ ".file.provider"}
    + * 
    + * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-08-09 10:50 + */ +public class SystemMediaSelector { + + private static final String TAG = SystemMediaSelector.class.getSimpleName(); + + private static final int REQUEST_CAMERA = 196; + private static final int REQUEST_CROP = 197; + private static final int REQUEST_ALBUM = 198; + private static final int REQUEST_FILE = 199; + + private static final String POSTFIX = ".file.provider"; + private String mAuthority; + + private final MediaSelectorCallback mMediaSelectorCallback; + private Activity mActivity; + private Fragment mFragment; + + private String mSavePhotoPath; + private CropOptions mCropOptions; + private final CropOptions mDefaultOptions = new CropOptions(); + private String mCropTitle; + private boolean mNeedCrop; + + public SystemMediaSelector(Activity activity, MediaSelectorCallback systemMediaSelectorCallback) { + mActivity = activity; + mMediaSelectorCallback = systemMediaSelectorCallback; + init(); + } + + public SystemMediaSelector(Fragment fragment, MediaSelectorCallback systemMediaSelectorCallback) { + mFragment = fragment; + mMediaSelectorCallback = systemMediaSelectorCallback; + init(); + } + + private void init() { + mAuthority = getContext().getPackageName().concat(POSTFIX); + mDefaultOptions.setAspectX(1); + mDefaultOptions.setAspectY(1); + mDefaultOptions.setOutputX(1000); + mDefaultOptions.setOutputY(1000); + } + + private Context getContext() { + if (mFragment != null) { + return mFragment.getContext(); + } else { + return mActivity; + } + } + + private void startActivityForResult(Intent intent, int code) { + if (mFragment != null) { + mFragment.startActivityForResult(intent, code); + } else { + mActivity.startActivityForResult(intent, code); + } + } + /////////////////////////////////////////////////////////////////////////// + // setter + /////////////////////////////////////////////////////////////////////////// + + /** + * 默认的authority 为"包名.fileProvider" + * + * @param authority 指定FileProvider的authority + */ + public void setAuthority(String authority) { + mAuthority = authority; + } + + private CropOptions getCropOptions() { + return mCropOptions == null ? mDefaultOptions : mCropOptions; + } + + /////////////////////////////////////////////////////////////////////////// + // Take method + /////////////////////////////////////////////////////////////////////////// + + public boolean takePhotoFromCamera(String savePath) { + mSavePhotoPath = savePath; + mNeedCrop = false; + return toCamera(); + } + + /** + * 为了保证裁裁剪图片不出问题,务必指定CropOptions中的各个参数(不要为0,比如魅族手机如果指定OutputX和OutputY为0,则只会裁减出一个像素),否则可能出现问题 + */ + public boolean takePhotoFromCameraAndCrop(String savePath, CropOptions cropOptions, String cropTitle) { + mSavePhotoPath = savePath; + mNeedCrop = true; + mCropOptions = cropOptions; + mCropTitle = cropTitle; + return toCamera(); + } + + public boolean takePhotoFormAlbum() { + mNeedCrop = false; + try { + startActivityForResult(Utils.makeAlbumIntent(), REQUEST_ALBUM); + return true; + } catch (Exception e) { + return false; + } + } + + public boolean takePhotoFormAlbumAndCrop(String savePhotoPath, CropOptions cropOptions, String cropTitle) { + mNeedCrop = true; + mSavePhotoPath = savePhotoPath; + mCropOptions = cropOptions; + mCropTitle = cropTitle; + try { + startActivityForResult(Utils.makeAlbumIntent(), REQUEST_ALBUM); + return true; + } catch (Exception e) { + return false; + } + } + + private boolean toCamera() { + if (!Utils.hasCamera(getContext())) { + return false; + } + File targetFile = new File(mSavePhotoPath); + Intent intent = Utils.makeCaptureIntent(getContext(), targetFile, mAuthority); + try { + startActivityForResult(intent, REQUEST_CAMERA); + return true; + } catch (Exception e) { + Log.e(TAG, "takePhotoFromCamera error", e); + } + return false; + } + + private boolean toCrop() { + File targetFile = new File(mSavePhotoPath); + Intent intent = Utils.makeCropIntent(getContext(), targetFile, mAuthority, getCropOptions(), mCropTitle); + try { + startActivityForResult(intent, REQUEST_CROP); + return true; + } catch (Exception e) { + Log.e(TAG, "toCrop error", e); + } + return false; + } + + private boolean toCrop(Uri uri) { + File targetFile = new File(mSavePhotoPath); + Intent intent = Utils.makeCropIntent(getContext(), uri, targetFile, mAuthority, getCropOptions(), mCropTitle); + try { + startActivityForResult(intent, REQUEST_CROP); + return true; + } catch (Exception e) { + Log.e(TAG, "toCrop error", e); + } + return false; + } + + public boolean takeFile() { + return takeFile(null); + } + + public boolean takeFile(String mimeType) { + Intent intent = Utils.makeFilesIntent(mimeType); + try { + startActivityForResult(intent, REQUEST_FILE); + } catch (Exception e) { + Log.e(TAG, "takeFile error", e); + return false; + } + return true; + } + /////////////////////////////////////////////////////////////////////////// + // Process Result + /////////////////////////////////////////////////////////////////////////// + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_CAMERA) { + processCameraResult(resultCode, data); + } else if (requestCode == REQUEST_CROP) { + processCropResult(resultCode, data); + } else if (requestCode == REQUEST_ALBUM) { + processAlbumResult(resultCode, data); + } else if (requestCode == REQUEST_FILE) { + processFileResult(resultCode, data); + } + } + + private void processFileResult(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK && data != null) { + Uri uri = data.getData(); + if (uri == null) { + mMediaSelectorCallback.onTakeFail(); + } else { + String absolutePath = Utils.getAbsolutePath(getContext(), uri); + mMediaSelectorCallback.onTakeSuccess(absolutePath); + } + } else { + mMediaSelectorCallback.onTakeFail(); + } + } + + private void processAlbumResult(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + if (data != null && data.getData() != null) { + Uri uri = data.getData(); + if (mNeedCrop) { + boolean success = toCrop(uri); + if (!success) { + mMediaSelectorCallback.onTakeSuccess(Utils.getAbsolutePath(getContext(), uri)); + } + } else { + mMediaSelectorCallback.onTakeSuccess(Utils.getAbsolutePath(getContext(), uri)); + } + } else { + mMediaSelectorCallback.onTakeFail(); + } + } + } + + private void processCropResult(int resultCode, @SuppressWarnings("unused") Intent data) { + //有时候,系统裁减的结果可能没有保存到我们指定的目录,而是通过data返回了 + if (resultCode == Activity.RESULT_OK) { + if (new File(mSavePhotoPath).exists()) { + mMediaSelectorCallback.onTakeSuccess(mSavePhotoPath); + } else if (data != null && data.getData() != null) { + String realPathFromURI = Utils.getAbsolutePath(getContext(), data.getData()); + if (TextUtils.isEmpty(realPathFromURI)) { + mMediaSelectorCallback.onTakeFail(); + } else { + mMediaSelectorCallback.onTakeSuccess(realPathFromURI); + } + } else { + mMediaSelectorCallback.onTakeFail(); + } + } + } + + private void processCameraResult(int resultCode, @SuppressWarnings("unused") Intent data) { + if (resultCode == Activity.RESULT_OK) { + //检测图片是否被保存下来 + if (!new File(mSavePhotoPath).exists()) { + mMediaSelectorCallback.onTakeFail(); + return; + } + //需要裁减,可以裁减则进行裁减,否则直接返回 + if (mNeedCrop) { + boolean success = toCrop(); + if (!success) { + mMediaSelectorCallback.onTakeSuccess(mSavePhotoPath); + } + } else { + mMediaSelectorCallback.onTakeSuccess(mSavePhotoPath); + } + } + } + + /////////////////////////////////////////////////////////////////////////// + // Interface + /////////////////////////////////////////////////////////////////////////// + + public interface MediaSelectorCallback { + + default void onTakeSuccess(String path) { + } + + default void onTakeFail() { + } + + } + + +} diff --git a/lib_media_selector/src/main/java/com/android/sdk/mediaselector/Utils.java b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/Utils.java new file mode 100644 index 0000000..3490f06 --- /dev/null +++ b/lib_media_selector/src/main/java/com/android/sdk/mediaselector/Utils.java @@ -0,0 +1,368 @@ +package com.android.sdk.mediaselector; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.support.v4.content.FileProvider; +import android.text.TextUtils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; + +/** + * See: + *
    + *      https://stackoverflow.com/questions/20067508/get-real-path-from-uri-android-kitkat-new-storage-access-framework
    + * 
    + * + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2017-08-09 10:54 + */ +final class Utils { + + + private Utils() { + throw new UnsupportedOperationException("Utils"); + } + + /////////////////////////////////////////////////////////////////////////// + // Camera + /////////////////////////////////////////////////////////////////////////// + + /** + * 判断系统中是否存在可以启动的相机应用 + * + * @return 存在返回true,不存在返回false + */ + static boolean hasCamera(Context context) { + PackageManager packageManager = context.getPackageManager(); + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + List list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + return list.size() > 0; + } + + /** + * @param targetFile 源文件,裁剪之后新的图片覆盖此文件 + */ + static Intent makeCaptureIntent(Context context, File targetFile, String authority) { + makeFilePath(targetFile); + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (Build.VERSION.SDK_INT < 24) { + Uri fileUri = Uri.fromFile(targetFile); + intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); + } else { + Uri fileUri = FileProvider.getUriForFile(context, authority, targetFile); + intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + return intent; + } + + /////////////////////////////////////////////////////////////////////////// + // Files + /////////////////////////////////////////////////////////////////////////// + static Intent makeFilesIntent(String mimeType) { + if (TextUtils.isEmpty(mimeType)) { + mimeType = "*/*"; + } + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType(mimeType); + intent.addCategory(Intent.CATEGORY_OPENABLE); + return intent; + } + + /////////////////////////////////////////////////////////////////////////// + // Crop + /////////////////////////////////////////////////////////////////////////// + + /** + * @param targetFile 源文件,裁剪之后新的图片覆盖此文件 + */ + static Intent makeCropIntent(Context context, File targetFile, String authority, CropOptions cropOptions, String title) { + + makeFilePath(targetFile); + Intent intent = new Intent("com.android.camera.action.CROP"); + + Uri fileUri; + if (Build.VERSION.SDK_INT < 24) { + fileUri = Uri.fromFile(targetFile); + } else { + fileUri = FileProvider.getUriForFile(context, authority, targetFile); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + + intent.setDataAndType(fileUri, "image/*"); + intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); + intent.putExtra("aspectX", cropOptions.getAspectX()); + intent.putExtra("aspectY", cropOptions.getAspectY()); + intent.putExtra("outputX", cropOptions.getOutputX()); + intent.putExtra("outputY", cropOptions.getOutputY()); + intent.putExtra("scale", true); + intent.putExtra("return-data", false); + intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); + intent.putExtra("noFaceDetection", true); + intent = Intent.createChooser(intent, title); + return intent; + } + + /** + * @param targetFile 目标文件,裁剪之后新的图片保存到此文件 + * @param src 源文件 + */ + static Intent makeCropIntent(Context context, Uri src, File targetFile, String authority, CropOptions cropOptions, String title) { + + Intent intent = new Intent("com.android.camera.action.CROP"); + intent.setDataAndType(src, "image/*"); + + Uri fileUri; + if (Build.VERSION.SDK_INT < 24) { + fileUri = Uri.fromFile(targetFile); + } else { + fileUri = FileProvider.getUriForFile(context, authority, targetFile); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + + intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); + intent.putExtra("aspectX", cropOptions.getAspectX()); + intent.putExtra("aspectY", cropOptions.getAspectY()); + intent.putExtra("outputX", cropOptions.getOutputX()); + intent.putExtra("outputY", cropOptions.getOutputY()); + intent.putExtra("scale", true); + intent.putExtra("return-data", false); + intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); + intent.putExtra("noFaceDetection", true); + intent = Intent.createChooser(intent, title); + return intent; + } + + /////////////////////////////////////////////////////////////////////////// + // Album + /////////////////////////////////////////////////////////////////////////// + + static Intent makeAlbumIntent() { + return new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + } + + + /////////////////////////////////////////////////////////////////////////// + // 从各种Uri中获取真实的路径 + /////////////////////////////////////////////////////////////////////////// + static String getAbsolutePath(final Context context, final Uri uri) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{split[1]}; + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + return null; + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + Cursor cursor = null; + final String column = MediaStore.Images.Media.DATA; + final String[] projection = {column}; + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(column_index); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /////////////////////////////////////////////////////////////////////////// + // BitmapUtils:如果拍摄的图片存在角度问题,通过下面分发修正 + /////////////////////////////////////////////////////////////////////////// + + /** + * 读取图片的旋转的角度 + * + * @param path 图片绝对路径 + * @return 图片的旋转角度 + */ + @SuppressWarnings("unused") + public static int getBitmapDegree(String path) { + int degree = 0; + try { + // 从指定路径下读取图片,并获取其EXIF信息 + ExifInterface exifInterface = new ExifInterface(path); + // 获取图片的旋转信息 + int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL); + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + degree = 90; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + degree = 180; + break; + case ExifInterface.ORIENTATION_ROTATE_270: + degree = 270; + break; + } + } catch (IOException e) { + e.printStackTrace(); + } + return degree; + } + + /** + * 将图片按照某个角度进行旋转 + * + * @param bm 需要旋转的图片 + * @param degree 旋转角度 + * @return 旋转后的图片 + */ + @SuppressWarnings("unused") + public static Bitmap rotateBitmapByDegree(Bitmap bm, int degree) { + Bitmap returnBm = null; + // 根据旋转角度,生成旋转矩阵 + Matrix matrix = new Matrix(); + matrix.postRotate(degree); + try { + // 将原始图片按照旋转矩阵进行旋转,并得到新的图片 + returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true); + } catch (OutOfMemoryError e) { + e.printStackTrace(); + } + if (returnBm == null) { + returnBm = bm; + } + if (bm != returnBm) { + bm.recycle(); + } + return returnBm; + } + + /////////////////////////////////////////////////////////////////////////// + // FileUtils + /////////////////////////////////////////////////////////////////////////// + + private static boolean makeFilePath(File file) { + if (file == null) { + return false; + } + File parent = file.getParentFile(); + return parent.exists() || parent.mkdirs(); + } + + /////////////////////////////////////////////////////////////////////////// + //如果照片保存的文件目录是由 getExternalFilesDir() 所提供的,那么,媒体扫描器是不能访问这些文件的,因为照片对于你的APP来说是私有的。 + /////////////////////////////////////////////////////////////////////////// + + /** + * 显示图片到相册 + * + * @param photoFile 要保存的图片文件 + */ + public static void displayToGallery(Context context, File photoFile) { + if (photoFile == null || !photoFile.exists()) { + return; + } + String photoPath = photoFile.getAbsolutePath(); + String photoName = photoFile.getName(); + // 其次把文件插入到系统图库 + try { + ContentResolver contentResolver = context.getContentResolver(); + MediaStore.Images.Media.insertImage(contentResolver, photoPath, photoName, null); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + // 最后通知图库更新 + context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + photoPath))); + } +} diff --git a/lib_media_selector/src/main/res/drawable-xxhdpi/ic_boxing_camera.png b/lib_media_selector/src/main/res/drawable-xxhdpi/ic_boxing_camera.png new file mode 100644 index 0000000000000000000000000000000000000000..1a09cce43f7a126b5c9bf04f844dce09827edd4b GIT binary patch literal 786 zcmV+t1MU2YP)0if;dTcr+X&VQ-=>iGCj~=W~!@dru#h4>tn)U2`q>Ou^<-2f>;m>VnOEF z06sObVVj_3jdFZGfF)Z5%>i$k1^K)Jv(yFs2A&Ma33vo%s0x|{8V2PA+yN8R1dRgM z5y%O+0!HEzq z`BOm2aBbHUf_CxhP_2yyfvSyy{C-eIK)^4g({XtNJ*LKLcm1@37|g3=**&d!(BN~N(?3CWWNC<$`Dln?uu0P0HW-tnBRWr<5! zP|bj#8f8IYw^&8PE%pZ(qkfBZ3esB&LAqp9kX{vB=CcCJd{%IoU!x@GFos0&m|Cfk zim=p3;|o=!iq%qt#p*W2YQBN32-EG3A>f{B!>k%X!)%Xw%Qf%Gw_L{r#}qwFn)V9@ zn)ZvDv-8*=`{Xn}_9<$X(=!has}g}bEFkZB7C%`HR=2XmDiy49+nQD@GA}PQu}xaU zI%)tbau+{OWBG8bAqnKyUswp=sZD!PE`c0CrD61MUabo8Eltxp}G|z z*vxWixtyQfZijL)VLR*ci9|02 diff --git a/lib_network/src/main/java/com/android/sdk/net/NetContext.java b/lib_network/src/main/java/com/android/sdk/net/NetContext.java new file mode 100644 index 0000000..472fa10 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/NetContext.java @@ -0,0 +1,119 @@ +package com.android.sdk.net; + +import android.support.annotation.NonNull; + +import com.android.sdk.net.core.ExceptionFactory; +import com.android.sdk.net.provider.ApiHandler; +import com.android.sdk.net.provider.ErrorDataAdapter; +import com.android.sdk.net.provider.ErrorMessage; +import com.android.sdk.net.provider.HttpConfig; +import com.android.sdk.net.provider.NetworkChecker; +import com.android.sdk.net.provider.PostTransformer; +import com.android.sdk.net.service.ServiceFactory; +import com.android.sdk.net.service.ServiceHelper; + +import okhttp3.OkHttpClient; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 11:06 + */ +public class NetContext { + + private static volatile NetContext CONTEXT; + + public static NetContext get() { + if (CONTEXT == null) { + synchronized (NetContext.class) { + if (CONTEXT == null) { + CONTEXT = new NetContext(); + } + } + } + return CONTEXT; + } + + private NetContext() { + mServiceHelper = new ServiceHelper(); + } + + private NetProvider mNetProvider; + private ServiceHelper mServiceHelper; + + public Builder newBuilder() { + return new Builder(); + } + + private void init(@NonNull NetProvider netProvider) { + mNetProvider = netProvider; + } + + public boolean connected() { + return mNetProvider.isConnected(); + } + + public NetProvider netProvider() { + NetProvider retProvider = mNetProvider; + + if (retProvider == null) { + throw new RuntimeException("NetContext has not be initialized"); + } + return retProvider; + } + + public OkHttpClient httpClient() { + return mServiceHelper.getOkHttpClient(netProvider().httpConfig()); + } + + public ServiceFactory serviceFactory() { + return mServiceHelper.getServiceFactory(netProvider().httpConfig()); + } + + public class Builder { + + private NetProviderImpl mNetProvider = new NetProviderImpl(); + + public Builder aipHandler(@NonNull ApiHandler apiHandler) { + mNetProvider.mApiHandler = apiHandler; + return this; + } + + public Builder httpConfig(@NonNull HttpConfig httpConfig) { + mNetProvider.mHttpConfig = httpConfig; + return this; + } + + public Builder errorMessage(@NonNull ErrorMessage errorMessage) { + mNetProvider.mErrorMessage = errorMessage; + return this; + } + + public Builder errorDataAdapter(@NonNull ErrorDataAdapter errorDataAdapter) { + mNetProvider.mErrorDataAdapter = errorDataAdapter; + return this; + } + + public Builder networkChecker(@NonNull NetworkChecker networkChecker) { + mNetProvider.mNetworkChecker = networkChecker; + return this; + } + + public Builder postTransformer(@NonNull PostTransformer postTransformer) { + mNetProvider.mPostTransformer = postTransformer; + return this; + } + + public Builder exceptionFactory(@NonNull ExceptionFactory exceptionFactory) { + mNetProvider.mExceptionFactory = exceptionFactory; + return this; + } + + public void setup() { + mNetProvider.checkRequired(); + NetContext.get().init(mNetProvider); + } + + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/NetProvider.java b/lib_network/src/main/java/com/android/sdk/net/NetProvider.java new file mode 100644 index 0000000..1066565 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/NetProvider.java @@ -0,0 +1,88 @@ +package com.android.sdk.net; + +import android.support.annotation.NonNull; + +import com.android.sdk.net.core.ExceptionFactory; +import com.android.sdk.net.provider.ApiHandler; +import com.android.sdk.net.provider.ErrorDataAdapter; +import com.android.sdk.net.provider.ErrorMessage; +import com.android.sdk.net.provider.HttpConfig; +import com.android.sdk.net.provider.NetworkChecker; +import com.android.sdk.net.provider.PostTransformer; + +public interface NetProvider { + + boolean isConnected(); + + ApiHandler aipHandler(); + + HttpConfig httpConfig(); + + @NonNull + ErrorMessage errorMessage(); + + @NonNull + ErrorDataAdapter errorDataAdapter(); + + PostTransformer postTransformer(); + + ExceptionFactory exceptionFactory(); + +} + +class NetProviderImpl implements NetProvider { + + ExceptionFactory mExceptionFactory; + ApiHandler mApiHandler; + HttpConfig mHttpConfig; + ErrorMessage mErrorMessage; + ErrorDataAdapter mErrorDataAdapter; + NetworkChecker mNetworkChecker; + PostTransformer mPostTransformer; + + @Override + public boolean isConnected() { + return mNetworkChecker.isConnected(); + } + + @NonNull + @Override + public ApiHandler aipHandler() { + return mApiHandler; + } + + @NonNull + @Override + public HttpConfig httpConfig() { + return mHttpConfig; + } + + @NonNull + @Override + public ErrorMessage errorMessage() { + return mErrorMessage; + } + + @NonNull + @Override + public ErrorDataAdapter errorDataAdapter() { + return mErrorDataAdapter; + } + + @Override + public PostTransformer postTransformer() { + return mPostTransformer; + } + + @Override + public ExceptionFactory exceptionFactory() { + return mExceptionFactory; + } + + void checkRequired() { + if (mErrorMessage == null || mErrorDataAdapter == null || mNetworkChecker == null || mHttpConfig == null) { + throw new NullPointerException("缺少必要的参数,必须提供:ErrorMessage、mErrorDataAdapter、mNetworkChecker、HttpConfig。"); + } + } + +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/core/DataExtractor.java b/lib_network/src/main/java/com/android/sdk/net/core/DataExtractor.java new file mode 100644 index 0000000..6127885 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/core/DataExtractor.java @@ -0,0 +1,7 @@ +package com.android.sdk.net.core; + +public interface DataExtractor { + + S getDataFromHttpResult(Result rResult); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/core/ExceptionFactory.java b/lib_network/src/main/java/com/android/sdk/net/core/ExceptionFactory.java new file mode 100644 index 0000000..18ef12b --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/core/ExceptionFactory.java @@ -0,0 +1,13 @@ +package com.android.sdk.net.core; + +/** + * 某些业务调用所产生的异常不是全局通用的,可以传递此接口用于创建特定的异常 + */ +public interface ExceptionFactory { + + /** + * 根据{@link Result}创建特定的业务异常 + */ + Exception create(Result result); + +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/core/HttpResultTransformer.java b/lib_network/src/main/java/com/android/sdk/net/core/HttpResultTransformer.java new file mode 100644 index 0000000..270cf1a --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/core/HttpResultTransformer.java @@ -0,0 +1,106 @@ +package com.android.sdk.net.core; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.android.sdk.net.NetContext; +import com.android.sdk.net.exception.ApiErrorException; +import com.android.sdk.net.exception.NetworkErrorException; +import com.android.sdk.net.exception.ServerErrorException; +import com.android.sdk.net.provider.ApiHandler; +import com.android.sdk.net.provider.PostTransformer; + +import org.reactivestreams.Publisher; + +import io.reactivex.Flowable; +import io.reactivex.FlowableTransformer; +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.ObservableTransformer; + +public class HttpResultTransformer> implements ObservableTransformer, FlowableTransformer { + + private final boolean mRequireNonNullData; + private final DataExtractor mDataExtractor; + @Nullable private final ExceptionFactory mExceptionFactory; + + public HttpResultTransformer(boolean requireNonNullData, @NonNull DataExtractor dataExtractor, @Nullable ExceptionFactory exceptionFactory) { + mRequireNonNullData = requireNonNullData; + mDataExtractor = dataExtractor; + if (exceptionFactory == null) { + exceptionFactory = NetContext.get().netProvider().exceptionFactory(); + } + mExceptionFactory = exceptionFactory; + } + + @Override + public Publisher apply(Flowable upstream) { + Flowable downstreamFlowable = upstream.map(this::processData); + @SuppressWarnings("unchecked") + PostTransformer postTransformer = (PostTransformer) NetContext.get().netProvider().postTransformer(); + if (postTransformer != null) { + return downstreamFlowable.compose(postTransformer); + } else { + return downstreamFlowable; + } + } + + @Override + public ObservableSource apply(Observable upstream) { + Observable downstreamObservable = upstream.map(this::processData); + @SuppressWarnings("unchecked") + PostTransformer postTransformer = (PostTransformer) NetContext.get().netProvider().postTransformer(); + if (postTransformer != null) { + return downstreamObservable.compose(postTransformer); + } else { + return downstreamObservable; + } + } + + private Downstream processData(Result rResult) { + if (rResult == null) { + + if (NetContext.get().connected()) { + throwAs(new ServerErrorException(ServerErrorException.UNKNOW_ERROR));//有连接无数据,服务器错误 + } else { + throw new NetworkErrorException();//无连接网络错误 + } + + } else if (NetContext.get().netProvider().errorDataAdapter().isErrorDataStub(rResult)) { + + throwAs(new ServerErrorException(ServerErrorException.SERVER_DATA_ERROR));//服务器数据格式错误 + + } else if (!rResult.isSuccess()) {//检测响应码是否正确 + ApiHandler apiHandler = NetContext.get().netProvider().aipHandler(); + if (apiHandler != null) { + apiHandler.onApiError(rResult); + } + throwAs(createException(rResult)); + } + + if (mRequireNonNullData) { + if (rResult.getData() == null) {//如果约定必须返回的数据却没有返回数据,则认为是服务器错误 + throwAs(new ServerErrorException(ServerErrorException.UNKNOW_ERROR)); + } + } + + return mDataExtractor.getDataFromHttpResult(rResult); + } + + private Throwable createException(@NonNull Result rResult) { + ExceptionFactory exceptionFactory = mExceptionFactory; + if (exceptionFactory != null) { + Exception exception = exceptionFactory.create(rResult); + if (exception != null) { + return exception; + } + } + return new ApiErrorException(rResult.getCode(), rResult.getMessage()); + } + + @SuppressWarnings("unchecked") + private void throwAs(Throwable throwable) throws E { + throw (E) throwable; + } + +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/core/Result.java b/lib_network/src/main/java/com/android/sdk/net/core/Result.java new file mode 100644 index 0000000..3b08d5b --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/core/Result.java @@ -0,0 +1,31 @@ +package com.android.sdk.net.core; + +/** + * 业务数据模型抽象 + * + * @author Ztiany + * Date : 2018-08-13 + */ +public interface Result { + + /** + * 实际返回类型 + */ + T getData(); + + /** + * 响应码 + */ + int getCode(); + + /** + * 相应消息 + */ + String getMessage(); + + /** + * 请求是否成功 + */ + boolean isSuccess(); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/errorhandler/ErrorMessageFactory.java b/lib_network/src/main/java/com/android/sdk/net/errorhandler/ErrorMessageFactory.java new file mode 100644 index 0000000..9b3aba5 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/errorhandler/ErrorMessageFactory.java @@ -0,0 +1,76 @@ +package com.android.sdk.net.errorhandler; + +import android.text.TextUtils; + +import com.android.sdk.net.NetContext; +import com.android.sdk.net.exception.ApiErrorException; +import com.android.sdk.net.exception.NetworkErrorException; +import com.android.sdk.net.exception.ServerErrorException; +import com.android.sdk.net.provider.ErrorMessage; + +import java.io.IOException; +import java.net.ConnectException; + +import retrofit2.HttpException; +import timber.log.Timber; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 16:11 + */ +public class ErrorMessageFactory { + + public static CharSequence createMessage(Throwable exception) { + ErrorMessage mErrorMessage = NetContext.get().netProvider().errorMessage(); + Timber.d("createMessage with:" + exception); + + CharSequence message = null; + //SocketTimeoutException android NetworkErrorException extends IOException + //1:网络连接错误处理 + if (exception instanceof ConnectException || + exception instanceof IOException + || exception instanceof NetworkErrorException) { + message = mErrorMessage.netErrorMessage(exception); + } + //2:服务器错误处理 + else if (exception instanceof ServerErrorException) { + int errorType = ((ServerErrorException) exception).getErrorType(); + if (errorType == ServerErrorException.SERVER_DATA_ERROR) { + message = mErrorMessage.serverDataErrorMessage(exception); + } else if (errorType == ServerErrorException.UNKNOW_ERROR) { + message = mErrorMessage.serverErrorMessage(exception); + } + } + //3:响应码非200处理 + else if (exception instanceof HttpException) { + int code = ((HttpException) exception).code(); + if (code >= 500/*http 500 表示服务器错误*/) { + message = mErrorMessage.serverErrorMessage(exception); + } else if (code >= 400/*http 400 表示客户端请求出错*/) { + message = mErrorMessage.clientRequestErrorMessage(exception); + } + } else { + //4:api 错误处理 + if (exception instanceof ApiErrorException) { + message = exception.getMessage(); + if (TextUtils.isEmpty(message)) { + message = mErrorMessage.apiErrorMessage((ApiErrorException) exception); + } + } else { + throw new RuntimeException(exception); + } + } + + if (isEmpty(message)) { + message = mErrorMessage.unknowErrorMessage(exception); + } + + return message; + } + + private static boolean isEmpty(CharSequence str) { + return str == null || str.toString().trim().length() == 0; + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/exception/ApiErrorException.java b/lib_network/src/main/java/com/android/sdk/net/exception/ApiErrorException.java new file mode 100644 index 0000000..8b38764 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/exception/ApiErrorException.java @@ -0,0 +1,27 @@ +package com.android.sdk.net.exception; + +/** + * ApiErrorException 表示当调用接口失败 + * + * @author Ztiany + * Date : 2016-10-13 11:39 + */ +public class ApiErrorException extends Exception { + + private final int mCode; + + public ApiErrorException(int code, String message) { + super(message); + mCode = code; + } + + public int getCode() { + return mCode; + } + + @Override + public String getMessage() { + return super.getMessage(); + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/exception/NetworkErrorException.java b/lib_network/src/main/java/com/android/sdk/net/exception/NetworkErrorException.java new file mode 100644 index 0000000..212e0d7 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/exception/NetworkErrorException.java @@ -0,0 +1,10 @@ +package com.android.sdk.net.exception; + +/** + * 网络异常,由于网络原型造成的调用失败,统一封装成成NetworkConnectionException + * + * @author Ztiany + * Date : 2016-05-06 17:23 + */ +public class NetworkErrorException extends RuntimeException { +} diff --git a/lib_network/src/main/java/com/android/sdk/net/exception/ServerErrorException.java b/lib_network/src/main/java/com/android/sdk/net/exception/ServerErrorException.java new file mode 100644 index 0000000..f9eedf0 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/exception/ServerErrorException.java @@ -0,0 +1,36 @@ +package com.android.sdk.net.exception; + +import android.support.annotation.NonNull; + +/** + * 表示服务器异常 + * + * @author Ztiany + * Date : 2016-05-06 17:23 + */ +public class ServerErrorException extends Exception { + + private final int mErrorType; + + public static final int UNKNOW_ERROR = 1; + public static final int SERVER_DATA_ERROR = 2; + + /** + * @param errorType {@link #UNKNOW_ERROR},{@link #SERVER_DATA_ERROR} + */ + public ServerErrorException(int errorType) { + mErrorType = errorType; + } + + public int getErrorType() { + return mErrorType; + } + + @NonNull + @Override + public String toString() { + String string = super.toString(); + return string + (mErrorType == UNKNOW_ERROR ? " 未知错误 " : " Json 格式错误 "); + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/gson/AutoGenTypeAdapterFactory.java b/lib_network/src/main/java/com/android/sdk/net/gson/AutoGenTypeAdapterFactory.java new file mode 100644 index 0000000..bac0ab4 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/gson/AutoGenTypeAdapterFactory.java @@ -0,0 +1,24 @@ +package com.android.sdk.net.gson; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; + +/** + * https://gist.github.com/JakeWharton/0d67d01badcee0ae7bc9 + * https://gist.github.com/Piasy/fa507251da452d36b221 + * 使用AutoGson注解自动映射,比如把需要反序列化的抽象类映射到具体的实现 + */ +public final class AutoGenTypeAdapterFactory implements TypeAdapterFactory { + + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(final Gson gson, final TypeToken type) { + final Class rawType = (Class) type.getRawType(); + final AutoGson annotation = rawType.getAnnotation(AutoGson.class); + // Only deserialize classes decorated with @AutoGson. + return annotation == null ? null : (TypeAdapter) gson.getAdapter(annotation.autoClass()); + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/gson/AutoGson.java b/lib_network/src/main/java/com/android/sdk/net/gson/AutoGson.java new file mode 100644 index 0000000..dc91002 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/gson/AutoGson.java @@ -0,0 +1,29 @@ +package com.android.sdk.net.gson; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * https://gist.github.com/Piasy/fa507251da452d36b221 + *

    + * Marks an AutoValue/AutoParcel-annotated type for proper + * Gson serialization. + *

    + * This annotation is needed because the {@linkplain Retention retention} of + * AutoValue/AutoParcel + * does not allow reflection at runtime. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface AutoGson { + + /** + * A reference to the Auto*-generated class (e.g. AutoValue_MyClass/AutoParcel_MyClass). This + * is necessary to handle obfuscation of the class names. + * + * @return the annotated class's real type. + */ + Class autoClass(); +} diff --git a/lib_network/src/main/java/com/android/sdk/net/gson/ErrorJsonLenientConverterFactory.java b/lib_network/src/main/java/com/android/sdk/net/gson/ErrorJsonLenientConverterFactory.java new file mode 100644 index 0000000..b641b0e --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/gson/ErrorJsonLenientConverterFactory.java @@ -0,0 +1,56 @@ +package com.android.sdk.net.gson; + + +import com.android.sdk.net.NetContext; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Converter; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; +import timber.log.Timber; + +/** + * json 解析容错处理,参考 + * + * @author Ztiany + * Date : 2018-08-13 + */ +public class ErrorJsonLenientConverterFactory extends Converter.Factory { + + private final GsonConverterFactory mGsonConverterFactory; + + public ErrorJsonLenientConverterFactory(GsonConverterFactory gsonConverterFactory) { + mGsonConverterFactory = gsonConverterFactory; + } + + @Override + public Converter requestBodyConverter(Type type, + Annotation[] parameterAnnotations, + Annotation[] methodAnnotations, + Retrofit retrofit) { + return mGsonConverterFactory.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit); + } + + @Override + public Converter responseBodyConverter(Type type, + Annotation[] annotations, + Retrofit retrofit) { + + final Converter delegateConverter = mGsonConverterFactory.responseBodyConverter(type, annotations, retrofit); + + return (Converter) value -> { + try { + return delegateConverter.convert(value); + } catch (Exception e/*防止闪退:JsonSyntaxException、IOException or MalformedJsonException*/) { + Timber.e(e, "Json covert error -->error "); + return NetContext.get().netProvider().errorDataAdapter().createErrorDataStub(type, annotations, retrofit, value);//服务器数据格式错误 + } + }; + + } + +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/gson/GsonUtils.java b/lib_network/src/main/java/com/android/sdk/net/gson/GsonUtils.java new file mode 100644 index 0000000..2a26922 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/gson/GsonUtils.java @@ -0,0 +1,44 @@ +package com.android.sdk.net.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.lang.reflect.Modifier; + +import kotlin.Unit; + +import static com.android.sdk.net.gson.JsonDeserializers.DoubleJsonDeserializer; +import static com.android.sdk.net.gson.JsonDeserializers.FloatJsonDeserializer; +import static com.android.sdk.net.gson.JsonDeserializers.IntegerJsonDeserializer; +import static com.android.sdk.net.gson.JsonDeserializers.StringJsonDeserializer; +import static com.android.sdk.net.gson.JsonDeserializers.UnitJsonDeserializer; +import static com.android.sdk.net.gson.JsonDeserializers.VoidJsonDeserializer; + +/** + * @author Ztiany + * Date: 2018-08-15 + */ +public class GsonUtils { + + private final static Gson GSON = new GsonBuilder() + .excludeFieldsWithModifiers(Modifier.TRANSIENT) + .excludeFieldsWithModifiers(Modifier.STATIC) + /*容错处理*/ + .registerTypeAdapter(int.class, new IntegerJsonDeserializer()) + .registerTypeAdapter(Integer.class, new IntegerJsonDeserializer()) + .registerTypeAdapter(double.class, new DoubleJsonDeserializer()) + .registerTypeAdapter(Double.class, new DoubleJsonDeserializer()) + .registerTypeAdapter(float.class, new FloatJsonDeserializer()) + .registerTypeAdapter(Float.class, new FloatJsonDeserializer()) + .registerTypeAdapter(String.class, new StringJsonDeserializer()) + .registerTypeAdapter(Void.class, new VoidJsonDeserializer()) + .registerTypeAdapter(Unit.class, new UnitJsonDeserializer()) + /*根据注解反序列化抽象类或接口*/ + .registerTypeAdapterFactory(new AutoGenTypeAdapterFactory()) + .create(); + + public static Gson gson() { + return GSON; + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/gson/JsonDeserializers.java b/lib_network/src/main/java/com/android/sdk/net/gson/JsonDeserializers.java new file mode 100644 index 0000000..abfdd21 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/gson/JsonDeserializers.java @@ -0,0 +1,93 @@ +package com.android.sdk.net.gson; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; + +import kotlin.Unit; +import timber.log.Timber; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-13 14:57 + */ +class JsonDeserializers { + + static class DoubleJsonDeserializer implements JsonDeserializer { + + @Override + public Double deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + return json.getAsDouble(); + } catch (Exception e) { + Timber.e(e, "DoubleJsonDeserializer-deserialize-error:" + (json != null ? json.toString() : "")); + return 0D; + } + } + + } + + static class IntegerJsonDeserializer implements JsonDeserializer { + + @Override + public Integer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + return json.getAsInt(); + } catch (Exception e) { + Timber.e(e, "IntegerJsonDeserializer-deserialize-error:" + (json != null ? json.toString() : "")); + return 0; + } + } + + } + + static class StringJsonDeserializer implements JsonDeserializer { + + @Override + public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + return json.getAsString(); + } catch (Exception e) { + Timber.e(e, "StringJsonDeserializer-deserialize-error:" + (json != null ? json.toString() : "")); + return null; + } + } + } + + static class VoidJsonDeserializer implements JsonDeserializer { + + @Override + public Void deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return null; + } + + } + + static class FloatJsonDeserializer implements JsonDeserializer { + + @Override + public Float deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + return json.getAsFloat(); + } catch (Exception e) { + Timber.e(e, "FloatJsonDeserializer-deserialize-error:" + (json != null ? json.toString() : "")); + return 0F; + } + } + + } + + static class UnitJsonDeserializer implements JsonDeserializer { + + @Override + public Unit deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return Unit.INSTANCE; + } + + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/https/HttpsUtils.java b/lib_network/src/main/java/com/android/sdk/net/https/HttpsUtils.java new file mode 100644 index 0000000..911db39 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/https/HttpsUtils.java @@ -0,0 +1,167 @@ +package com.android.sdk.net.https; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + * https://github.com/hongyangAndroid/okhttputils + */ +public class HttpsUtils { + + public static class SSLParams { + public SSLSocketFactory sSLSocketFactory; + public X509TrustManager trustManager; + } + + /** + * 创建SSLParams + * + * @param certificates 本地证书流 + * @param bksFile 用于双向验证,本地bks证书 + * @param password 本地证书密码 + * @return 创建的SSLParams + */ + public static SSLParams getSslSocketFactory(InputStream[] certificates, InputStream bksFile, String password) { + SSLParams sslParams = new SSLParams(); + try { + TrustManager[] trustManagers = prepareTrustManager(certificates); + KeyManager[] keyManagers = prepareKeyManager(bksFile, password); + SSLContext sslContext = SSLContext.getInstance("TLS"); + X509TrustManager trustManager; + if (trustManagers != null) { + trustManager = new SafeTrustManager(chooseTrustManager(trustManagers)); + } else { + trustManager = new UnSafeTrustManager(); + } + sslContext.init(keyManagers, new TrustManager[]{trustManager}, null); + sslParams.sSLSocketFactory = sslContext.getSocketFactory(); + sslParams.trustManager = trustManager; + return sslParams; + } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) { + throw new AssertionError(e); + } + } + + private class UnSafeHostnameVerifier implements HostnameVerifier { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + } + + private static class UnSafeTrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[]{}; + } + } + + private static TrustManager[] prepareTrustManager(InputStream... certificates) { + if (certificates == null || certificates.length <= 0) return null; + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + int index = 0; + for (InputStream certificate : certificates) { + String certificateAlias = Integer.toString(index++); + keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate)); + try { + if (certificate != null) + certificate.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + TrustManagerFactory trustManagerFactory; + trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + return trustManagerFactory.getTrustManagers(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private static KeyManager[] prepareKeyManager(InputStream bksFile, String password) { + try { + if (bksFile == null || password == null) return null; + KeyStore clientKeyStore = KeyStore.getInstance("BKS"); + clientKeyStore.load(bksFile, password.toCharArray()); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(clientKeyStore, password.toCharArray()); + return keyManagerFactory.getKeyManagers(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private static X509TrustManager chooseTrustManager(TrustManager[] trustManagers) { + for (TrustManager trustManager : trustManagers) { + if (trustManager instanceof X509TrustManager) { + return (X509TrustManager) trustManager; + } + } + return null; + } + + + private static class SafeTrustManager implements X509TrustManager { + + private X509TrustManager defaultTrustManager; + private X509TrustManager localTrustManager; + + SafeTrustManager(X509TrustManager localTrustManager) throws NoSuchAlgorithmException, KeyStoreException { + TrustManagerFactory var4 = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + var4.init((KeyStore) null); + defaultTrustManager = chooseTrustManager(var4.getTrustManagers()); + this.localTrustManager = localTrustManager; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + defaultTrustManager.checkServerTrusted(chain, authType); + } catch (CertificateException ce) { + localTrustManager.checkServerTrusted(chain, authType); + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } +} diff --git a/lib_network/src/main/java/com/android/sdk/net/kit/Consumer.java b/lib_network/src/main/java/com/android/sdk/net/kit/Consumer.java new file mode 100644 index 0000000..549cc7b --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/kit/Consumer.java @@ -0,0 +1,14 @@ +package com.android.sdk.net.kit; + +import android.support.annotation.Nullable; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-05-15 19:04 + */ +public interface Consumer { + + void accept(@Nullable T t); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/kit/Extensions.kt b/lib_network/src/main/java/com/android/sdk/net/kit/Extensions.kt new file mode 100644 index 0000000..1e0f0c2 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/kit/Extensions.kt @@ -0,0 +1,10 @@ +package com.android.sdk.net.kit + +import com.android.sdk.net.service.ServiceFactory + +/** + *@author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-04-02 11:09 + */ +inline fun ServiceFactory.create(): T = create(T::class.java) \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/kit/ResultHandlers.java b/lib_network/src/main/java/com/android/sdk/net/kit/ResultHandlers.java new file mode 100644 index 0000000..ff36210 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/kit/ResultHandlers.java @@ -0,0 +1,120 @@ +package com.android.sdk.net.kit; + +import com.android.sdk.net.core.ExceptionFactory; +import com.android.sdk.net.core.HttpResultTransformer; +import com.android.sdk.net.core.Result; +import com.github.dmstocking.optional.java.util.Optional; + +/** + * 用于处理 Retrofit + RxJava2 网络请求返回的结果 + * + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-11-22 17:22 + */ +@SuppressWarnings("unused") +public class ResultHandlers { + + private static class ResultTransformer> extends HttpResultTransformer { + ResultTransformer() { + super(true, Result::getData, null); + } + + ResultTransformer(ExceptionFactory exceptionFactory) { + super(true, Result::getData, exceptionFactory); + } + } + + private static class OptionalResultTransformer> extends HttpResultTransformer, T> { + OptionalResultTransformer() { + super(false, rResult -> Optional.ofNullable(rResult.getData()), null); + } + + OptionalResultTransformer(ExceptionFactory exceptionFactory) { + super(false, rResult -> Optional.ofNullable(rResult.getData()), exceptionFactory); + } + } + + private static class ResultChecker> extends HttpResultTransformer { + @SuppressWarnings("unchecked") + ResultChecker() { + super(false, rResult -> (T) rResult, null); + } + + @SuppressWarnings("unchecked") + ResultChecker(ExceptionFactory exceptionFactory) { + super(false, rResult -> (T) rResult, exceptionFactory); + } + } + + private static final ResultTransformer DATA_TRANSFORMER = new ResultTransformer(); + + private static final OptionalResultTransformer OPTIONAL_TRANSFORMER = new OptionalResultTransformer(); + + private static final ResultChecker RESULT_CHECKER = new ResultChecker(); + + /** + * 返回一个Transformer,用于统一处理网络请求返回的 Observer 数据。对网络异常和请求结果做了通用处理: + *

    +     * 1.  网络无连接抛出 {@link com.android.sdk.net.exception.NetworkErrorException} 由下游处理
    +     * 2. HttpResult==null 抛出 {@link com.android.sdk.net.exception.NetworkErrorException} 由下游处理
    +     * 3. HttpResult.getCode() != SUCCESS 抛出 {@link com.android.sdk.net.exception.ApiErrorException} 由下游处理
    +     * 4. 返回的结果不符合约定的数据模型处理或为 null 抛出 {@link com.android.sdk.net.exception.ServerErrorException} 由下游处理
    +     * 5. 最后把 HttpResult<T> 中的数据 T 提取到下游
    +     * 
    + */ + @SuppressWarnings("unchecked") + private static > HttpResultTransformer _resultExtractor() { + return (HttpResultTransformer) DATA_TRANSFORMER; + } + + public static HttpResultTransformer> resultExtractor() { + return _resultExtractor(); + } + + /** + * 与{@link #resultExtractor()}的行为类型,但是最后把 HttpResult<T> 中的数据 T 用 {@link Optional} 包装后再转发到下游。 + * 适用于 HttpResult.getData() 可以为 Null 的情况 + */ + @SuppressWarnings("unchecked") + private static > HttpResultTransformer, T> _optionalExtractor() { + return (HttpResultTransformer, T>) OPTIONAL_TRANSFORMER; + } + + public static HttpResultTransformer, Result> optionalExtractor() { + return _optionalExtractor(); + } + + /** + * 不提取 HttpResult<T> 中的数据 T,只进行网络异常、空数据异常、错误JSON格式异常处理。 + */ + @SuppressWarnings("unchecked") + private static > HttpResultTransformer _resultChecker() { + return (HttpResultTransformer) RESULT_CHECKER; + } + + public static HttpResultTransformer, Result> resultChecker() { + return _resultChecker(); + } + + private static > HttpResultTransformer _newExtractor(ExceptionFactory exceptionFactory) { + return new ResultTransformer<>(exceptionFactory); + } + + public static HttpResultTransformer> newExtractor(ExceptionFactory exceptionFactory) { + return _newExtractor(exceptionFactory); + } + + private static > HttpResultTransformer, T> _newOptionalExtractor(ExceptionFactory exceptionFactory) { + return new OptionalResultTransformer<>(exceptionFactory); + } + + public static HttpResultTransformer, Result> newOptionalExtractor(ExceptionFactory exceptionFactory) { + return _newOptionalExtractor(exceptionFactory); + } + + private static > HttpResultTransformer newResultChecker(ExceptionFactory exceptionFactory) { + return new ResultChecker<>(exceptionFactory); + } + +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.java b/lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.java new file mode 100644 index 0000000..b8a2b41 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.java @@ -0,0 +1,123 @@ +package com.android.sdk.net.kit; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.android.sdk.net.NetContext; +import com.android.sdk.net.exception.ApiErrorException; +import com.android.sdk.net.exception.NetworkErrorException; +import com.github.dmstocking.optional.java.util.Optional; + +import org.reactivestreams.Publisher; + +import java.io.IOException; +import java.net.ConnectException; + +import io.reactivex.Flowable; +import io.reactivex.flowables.ConnectableFlowable; +import io.reactivex.functions.Function; +import retrofit2.HttpException; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-05-15 19:17 + */ +public class RxResultKit { + + /** + *
    +     * 1. 如果网络不可用,直接返回缓存,如果没有缓存,报错没有网络连接
    +     * 2. 如果存在网络
    +     *      2.1 如果没有缓存,则从网络获取
    +     *      2.1 如果有缓存,则先返回缓存,然后从网络获取
    +     *      2.1 对比缓存与网络数据,如果没有更新,则忽略
    +     *      2.1 如果有更新,则更新缓存,并返回网络数据
    +     * 
    + * + * @param remote 网络数据源 + * @param local 本地数据源 + * @param onNewData 当有更新时,返回新的数据,可以在这里存储 + * @param 数据类型 + * @param selector 比较器,返回当 true 表示两者相等,参数顺序为 (local, remote) + * @return 组合后的Observable + * + */ + public static Flowable> composeMultiSource( + Flowable> remote, + Flowable> local, + Selector selector, + Consumer onNewData) { + + //没有网络 + if (!NetContext.get().connected()) { + return local.flatMap((Function, Publisher>>) tOptional -> { + if (tOptional.isPresent()) { + return Flowable.just(tOptional); + } else { + return Flowable.error(new NetworkErrorException()); + } + }); + } + + //有网络 + ConnectableFlowable> sharedLocal = local.replay(); + sharedLocal.connect(); + + //组合数据 + Flowable> complexRemote = sharedLocal + .flatMap((Function, Publisher>>) localData -> { + //没有缓存 + if (!localData.isPresent()) { + return remote.doOnNext(tOptional -> onNewData.accept(tOptional.orElse(null))); + } + /*有缓存是网络错误,不触发错误,只有在过期时返回新的数据*/ + return remote + .onErrorResumeNext(onErrorResumeFunction(onNewData)) + .filter(remoteData -> selector.test(localData.get(), remoteData.orElse(null))) + .doOnNext(newData -> onNewData.accept(newData.orElse(null))); + }); + + return Flowable.concat(sharedLocal.filter(Optional::isPresent), complexRemote); + } + + public static Flowable> selectLocalOrRemote(Flowable> remote, @Nullable T local, Selector selector, Consumer onNewData) { + //没有网络没有缓存 + if (!NetContext.get().connected() && local == null) { + return Flowable.error(new NetworkErrorException()); + } + //有缓存 + if (local != null) { + return Flowable.concat( + Flowable.just(Optional.of(local)), + /*有缓存是网络错误,不触发错误,只有在过期时返回新的数据*/ + remote.onErrorResumeNext(onErrorResumeFunction(onNewData)) + .filter(tOptional -> selector.test(local, tOptional.orElse(null))) + .doOnNext(tOptional -> onNewData.accept(tOptional.orElse(null)))); + } else { + return remote; + } + } + + @NonNull + private static Function>> onErrorResumeFunction(Consumer onNewData) { + return throwable -> { + if (isNetworkError(throwable)) { + return Flowable.never(); + } else { + if (throwable instanceof ApiErrorException) { + onNewData.accept(null); + } + return Flowable.error(throwable); + } + }; + } + + private static boolean isNetworkError(Throwable exception) { + return exception instanceof ConnectException + || exception instanceof IOException + || exception instanceof HttpException + || exception instanceof NetworkErrorException; + } + +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.kt b/lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.kt new file mode 100644 index 0000000..1184e40 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/kit/RxResultKit.kt @@ -0,0 +1,58 @@ +package com.android.sdk.net.kit + +import com.android.sdk.net.core.Result +import com.github.dmstocking.optional.java.util.Optional +import io.reactivex.Flowable +import io.reactivex.Observable + + +fun , E> Observable.optionalExtractor(): Observable> { + return this.compose(ResultHandlers.optionalExtractor()) +} + +fun , E> Observable.resultExtractor(): Observable { + return this.compose(ResultHandlers.resultExtractor()) +} + +fun > Observable.resultChecker(): Observable> { + return (this.compose(ResultHandlers.resultChecker())) +} + +fun , E> Flowable.optionalExtractor(): Flowable> { + return this.compose(ResultHandlers.optionalExtractor()) +} + +fun , E> Flowable.resultExtractor(): Flowable { + return this.compose(ResultHandlers.resultExtractor()) +} + +fun > Flowable.resultChecker(): Flowable> { + return (this.compose(ResultHandlers.resultChecker())) +} + +/**组合远程数据与本地数据,参考 [RxResultKit.composeMultiSource]*/ +fun composeMultiSource( + remote: Flowable>, + local: Flowable>, + selector: (local: T, remote: T?) -> Boolean, + onNewData: (T?) -> Unit +): Flowable> { + return RxResultKit.composeMultiSource(remote, local, Selector(selector), Consumer { onNewData(it) }) +} + +/**组合远程数据与本地数据,参考 [RxResultKit.composeMultiSource]*/ +fun composeMultiSource( + remote: Flowable>, + local: Flowable>, + onNewData: (T?) -> Unit +): Flowable> { + return RxResultKit.composeMultiSource(remote, local, { t1, t2 -> t1 != t2 }, { onNewData(it) }) +} + +fun selectLocalOrRemote(remote: Flowable>, local: T?, selector: (local: T, remote: T?) -> Boolean, onNewData: (T?) -> Unit): Flowable> { + return RxResultKit.selectLocalOrRemote(remote, local, selector, onNewData) +} + +fun selectLocalOrRemote(remote: Flowable>, local: T?, onNewData: (T?) -> Unit): Flowable> { + return RxResultKit.selectLocalOrRemote(remote, local, { t1, t2 -> t1 != t2 }, { onNewData(it) }) +} \ No newline at end of file diff --git a/lib_network/src/main/java/com/android/sdk/net/kit/Selector.java b/lib_network/src/main/java/com/android/sdk/net/kit/Selector.java new file mode 100644 index 0000000..4b0f089 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/kit/Selector.java @@ -0,0 +1,13 @@ +package com.android.sdk.net.kit; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public interface Selector { + + /** + * returning true means accept remote data + */ + boolean test(@NonNull T local, @Nullable T remote); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/Dispatcher.java b/lib_network/src/main/java/com/android/sdk/net/progress/Dispatcher.java new file mode 100644 index 0000000..079d0c4 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/Dispatcher.java @@ -0,0 +1,18 @@ +package com.android.sdk.net.progress; + + +import android.os.Handler; +import android.os.Looper; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-09-22 16:52 + */ +class Dispatcher { + private static final Handler HANDLER = new Handler(Looper.getMainLooper()); + + static void dispatch(Runnable runnable) { + HANDLER.post(runnable); + } +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/ProgressListener.java b/lib_network/src/main/java/com/android/sdk/net/progress/ProgressListener.java new file mode 100644 index 0000000..c776950 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/ProgressListener.java @@ -0,0 +1,10 @@ +package com.android.sdk.net.progress; + + +public interface ProgressListener { + + void onProgress(long contentLength, long currentBytes, float percent, boolean isFinish); + + void onLoadFail(Exception e); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/ProgressRequestBody.java b/lib_network/src/main/java/com/android/sdk/net/progress/ProgressRequestBody.java new file mode 100644 index 0000000..a4e72db --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/ProgressRequestBody.java @@ -0,0 +1,92 @@ +package com.android.sdk.net.progress; + +import android.os.SystemClock; +import android.support.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.Buffer; +import okio.BufferedSink; +import okio.ForwardingSink; +import okio.Okio; +import okio.Sink; + +class ProgressRequestBody extends RequestBody { + + private final ProgressListener mProgressListener; + private final RequestBody mDelegate; + private final int mRefreshTime; + private BufferedSink mBufferedSink; + + ProgressRequestBody(RequestBody delegate, int refreshTime, ProgressListener progressListener) { + this.mDelegate = delegate; + mProgressListener = progressListener; + this.mRefreshTime = refreshTime; + } + + @Override + public MediaType contentType() { + return mDelegate.contentType(); + } + + @Override + public long contentLength() { + try { + return mDelegate.contentLength(); + } catch (IOException e) { + e.printStackTrace(); + } + return -1; + } + + @Override + public void writeTo(@NonNull BufferedSink sink) throws IOException { + if (mBufferedSink == null) { + mBufferedSink = Okio.buffer(new CountingSink(sink)); + } + try { + mDelegate.writeTo(mBufferedSink); + mBufferedSink.flush(); + } catch (IOException e) { + e.printStackTrace(); + mProgressListener.onLoadFail(e); + throw e; + } + } + + private final class CountingSink extends ForwardingSink { + + private long totalBytesRead = 0L; + private long lastRefreshTime = 0L; //最后一次刷新的时间 + private long mContentLength; + private boolean mIsFinish; + + CountingSink(Sink delegate) { + super(delegate); + } + + @Override + public void write(@NonNull Buffer source, long byteCount) throws IOException { + try { + super.write(source, byteCount); + } catch (IOException e) { + e.printStackTrace(); + mProgressListener.onLoadFail(e); + throw e; + } + if (mContentLength == 0) { //避免重复调用 contentLength() + mContentLength = contentLength(); + } + totalBytesRead += byteCount; + + long curTime = SystemClock.elapsedRealtime(); + mIsFinish = totalBytesRead == mContentLength; + if (curTime - lastRefreshTime >= mRefreshTime || mIsFinish) { + mProgressListener.onProgress(totalBytesRead, mContentLength, totalBytesRead * 1.0F / mContentLength, mIsFinish); + lastRefreshTime = curTime; + } + } + } +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/ProgressResponseBody.java b/lib_network/src/main/java/com/android/sdk/net/progress/ProgressResponseBody.java new file mode 100644 index 0000000..a147d49 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/ProgressResponseBody.java @@ -0,0 +1,79 @@ +package com.android.sdk.net.progress; + +import android.os.SystemClock; +import android.support.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import okio.ForwardingSource; +import okio.Okio; +import okio.Source; + +class ProgressResponseBody extends ResponseBody { + + private final int mRefreshTime; + private final ResponseBody mDelegate; + private final ProgressListener mProgressListener; + private BufferedSource mBufferedSource; + + ProgressResponseBody(ResponseBody responseBody, int refreshTime, ProgressListener progressListener) { + this.mDelegate = responseBody; + mProgressListener = progressListener; + this.mRefreshTime = refreshTime; + } + + @Override + public MediaType contentType() { + return mDelegate.contentType(); + } + + @Override + public long contentLength() { + return mDelegate.contentLength(); + } + + @Override + public BufferedSource source() { + if (mBufferedSource == null) { + mBufferedSource = Okio.buffer(source(mDelegate.source())); + } + return mBufferedSource; + } + + private Source source(Source source) { + return new ForwardingSource(source) { + private long mContentLength; + private long totalBytesRead = 0L; + private long lastRefreshTime = 0L; //最后一次刷新的时间 + + @Override + public long read(@NonNull Buffer sink, long byteCount) throws IOException { + long bytesRead; + try { + bytesRead = super.read(sink, byteCount); + } catch (IOException e) { + e.printStackTrace(); + mProgressListener.onLoadFail(e); + throw e; + } + if (mContentLength == 0) { + mContentLength = contentLength(); + } + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += bytesRead != -1 ? bytesRead : 0; + long curTime = SystemClock.elapsedRealtime(); + + if (curTime - lastRefreshTime >= mRefreshTime || bytesRead == -1 || totalBytesRead == mContentLength) { + boolean finish = bytesRead == -1 && totalBytesRead == mContentLength; + mProgressListener.onProgress(mContentLength, totalBytesRead, totalBytesRead * 1.0F / mContentLength, finish); + lastRefreshTime = curTime; + } + return bytesRead; + } + }; + } +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/RequestProgressInterceptor.java b/lib_network/src/main/java/com/android/sdk/net/progress/RequestProgressInterceptor.java new file mode 100644 index 0000000..d3475cf --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/RequestProgressInterceptor.java @@ -0,0 +1,59 @@ +package com.android.sdk.net.progress; + +import android.support.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-09-20 15:39 + */ +public class RequestProgressInterceptor implements Interceptor { + + private static final int DEFAULT_REFRESH_TIME = 150; + private final UrlProgressListener mInterceptorProgressListener; + private int mRefreshTime = DEFAULT_REFRESH_TIME;//进度刷新时间(单位ms),避免高频率调用 + + public RequestProgressInterceptor(UrlProgressListener interceptorProgressListener) { + mInterceptorProgressListener = interceptorProgressListener; + if (mInterceptorProgressListener == null) { + throw new NullPointerException(); + } + } + + public void setRefreshTime(int refreshTime) { + mRefreshTime = refreshTime; + } + + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + return chain.proceed(wrapRequestBody(chain.request())); + } + + private Request wrapRequestBody(Request request) { + if (request == null || request.body() == null) { + return request; + } + final String key = request.url().toString(); + return request.newBuilder() + .method(request.method(), new ProgressRequestBody(request.body(), mRefreshTime, new ProgressListener() { + + @Override + public void onProgress(long contentLength, long currentBytes, float percent, boolean isFinish) { + Dispatcher.dispatch(() -> mInterceptorProgressListener.onProgress(key, contentLength, currentBytes, percent, isFinish)); + } + + @Override + public void onLoadFail(Exception e) { + Dispatcher.dispatch(() -> mInterceptorProgressListener.onError(key, e)); + } + })) + .build(); + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/ResponseProgressInterceptor.java b/lib_network/src/main/java/com/android/sdk/net/progress/ResponseProgressInterceptor.java new file mode 100644 index 0000000..c369ddf --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/ResponseProgressInterceptor.java @@ -0,0 +1,58 @@ +package com.android.sdk.net.progress; + +import android.support.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.Response; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-09-20 15:39 + */ +public class ResponseProgressInterceptor implements Interceptor { + + private static final int DEFAULT_REFRESH_TIME = 150; + private final UrlProgressListener mInterceptorProgressListener; + private int mRefreshTime = DEFAULT_REFRESH_TIME;//进度刷新时间(单位ms),避免高频率调用 + + public ResponseProgressInterceptor(UrlProgressListener interceptorProgressListener) { + mInterceptorProgressListener = interceptorProgressListener; + if (mInterceptorProgressListener == null) { + throw new NullPointerException(); + } + } + + public void setRefreshTime(int refreshTime) { + mRefreshTime = refreshTime; + } + + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + return wrapResponseBody(chain.proceed(chain.request())); + } + + private Response wrapResponseBody(Response response) { + if (response == null || response.body() == null) { + return response; + } + + final String key = response.request().url().toString(); + return response.newBuilder() + .body(new ProgressResponseBody(response.body(), mRefreshTime, new ProgressListener() { + + @Override + public void onProgress(long contentLength, long currentBytes, float percent, boolean isFinish) { + Dispatcher.dispatch(() -> mInterceptorProgressListener.onProgress(key, contentLength, currentBytes, percent, isFinish)); + } + + @Override + public void onLoadFail(Exception e) { + Dispatcher.dispatch(() -> mInterceptorProgressListener.onError(key, e)); + } + })).build(); + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/progress/UrlProgressListener.java b/lib_network/src/main/java/com/android/sdk/net/progress/UrlProgressListener.java new file mode 100644 index 0000000..4321225 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/progress/UrlProgressListener.java @@ -0,0 +1,8 @@ +package com.android.sdk.net.progress; + +public interface UrlProgressListener { + + void onProgress(String url, long contentLength, long currentBytes, float percent, boolean isFinish); + + void onError(String url, Exception e); +} diff --git a/lib_network/src/main/java/com/android/sdk/net/provider/ApiHandler.java b/lib_network/src/main/java/com/android/sdk/net/provider/ApiHandler.java new file mode 100644 index 0000000..4ba05b6 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/provider/ApiHandler.java @@ -0,0 +1,16 @@ +package com.android.sdk.net.provider; + +import android.support.annotation.NonNull; + +import com.android.sdk.net.core.Result; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 15:37 + */ +public interface ApiHandler { + + void onApiError(@NonNull Result result); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/provider/ErrorDataAdapter.java b/lib_network/src/main/java/com/android/sdk/net/provider/ErrorDataAdapter.java new file mode 100644 index 0000000..faa1f02 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/provider/ErrorDataAdapter.java @@ -0,0 +1,20 @@ +package com.android.sdk.net.provider; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import okhttp3.ResponseBody; +import retrofit2.Retrofit; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 18:00 + */ +public interface ErrorDataAdapter { + + Object createErrorDataStub(Type type, Annotation[] annotations, Retrofit retrofit, ResponseBody value); + + boolean isErrorDataStub(Object object); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/provider/ErrorMessage.java b/lib_network/src/main/java/com/android/sdk/net/provider/ErrorMessage.java new file mode 100644 index 0000000..5b98cec --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/provider/ErrorMessage.java @@ -0,0 +1,42 @@ +package com.android.sdk.net.provider; + +import com.android.sdk.net.exception.ApiErrorException; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 16:01 + */ +public interface ErrorMessage { + + /** + * 网络错误提示消息。 + */ + CharSequence netErrorMessage(Throwable exception); + + /** + * 服务器返回的数据格式异常消息。 + */ + CharSequence serverDataErrorMessage(Throwable exception); + + /** + * 服务器错误,比如 500-600 响应码。 + */ + CharSequence serverErrorMessage(Throwable exception); + + /** + * 客户端请求错误,比如 400-499 响应码 + */ + CharSequence clientRequestErrorMessage(Throwable exception); + + /** + * API 调用错误 + */ + CharSequence apiErrorMessage(ApiErrorException exception); + + /** + * 未知错误 + */ + CharSequence unknowErrorMessage(Throwable exception); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/provider/HttpConfig.java b/lib_network/src/main/java/com/android/sdk/net/provider/HttpConfig.java new file mode 100644 index 0000000..a292d8f --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/provider/HttpConfig.java @@ -0,0 +1,25 @@ +package com.android.sdk.net.provider; + +import io.reactivex.schedulers.Schedulers; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 16:42 + */ +public interface HttpConfig { + + void configHttp(OkHttpClient.Builder builder); + + /** + * default config is {@link retrofit2.converter.gson.GsonConverterFactory}、{@link retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory} with {@link Schedulers#io()} + * + * @return if true, default config do nothing. + */ + boolean configRetrofit(Retrofit.Builder builder); + + String baseUrl(); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/provider/NetworkChecker.java b/lib_network/src/main/java/com/android/sdk/net/provider/NetworkChecker.java new file mode 100644 index 0000000..73ddfa3 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/provider/NetworkChecker.java @@ -0,0 +1,15 @@ +package com.android.sdk.net.provider; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-12-11 18:00 + */ +public interface NetworkChecker { + + /** + * 网络是否连接 + */ + boolean isConnected(); + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/provider/PostTransformer.java b/lib_network/src/main/java/com/android/sdk/net/provider/PostTransformer.java new file mode 100644 index 0000000..3fdf0da --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/provider/PostTransformer.java @@ -0,0 +1,17 @@ +package com.android.sdk.net.provider; + +import com.android.sdk.net.kit.ResultHandlers; + +import io.reactivex.FlowableTransformer; +import io.reactivex.ObservableTransformer; + +/** + * 经过 {@link ResultHandlers} 处理网络结果后,可以添加此接口来添加统一的再处理逻辑,比如 token 实现后的重试。 + * + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-12-21 14:31 + */ +public interface PostTransformer extends ObservableTransformer, FlowableTransformer { + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/service/ServiceFactory.java b/lib_network/src/main/java/com/android/sdk/net/service/ServiceFactory.java new file mode 100644 index 0000000..bea094b --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/service/ServiceFactory.java @@ -0,0 +1,70 @@ +package com.android.sdk.net.service; + +import com.android.sdk.net.gson.ErrorJsonLenientConverterFactory; +import com.android.sdk.net.gson.GsonUtils; +import com.android.sdk.net.progress.RequestProgressInterceptor; +import com.android.sdk.net.progress.ResponseProgressInterceptor; +import com.android.sdk.net.progress.UrlProgressListener; +import com.android.sdk.net.provider.HttpConfig; + +import io.reactivex.schedulers.Schedulers; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-06-07 18:19 + */ +public class ServiceFactory { + + private final OkHttpClient mOkHttpClient; + private final String mBaseUrl; + private final Retrofit mRetrofit; + + ServiceFactory(OkHttpClient okHttpClient, HttpConfig httpConfig) { + mOkHttpClient = okHttpClient; + mBaseUrl = httpConfig.baseUrl(); + + Retrofit.Builder builder = new Retrofit.Builder(); + boolean abort = httpConfig.configRetrofit(builder); + + if (!abort) { + builder.baseUrl(mBaseUrl) + .client(okHttpClient) + .addConverterFactory(new ErrorJsonLenientConverterFactory(GsonConverterFactory.create(GsonUtils.gson()))) + .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())); + } + + mRetrofit = builder.build(); + } + + public T create(Class clazz) { + return mRetrofit.create(clazz); + } + + public T createWithUploadProgress(Class clazz, UrlProgressListener urlProgressListener) { + OkHttpClient okHttpClient = mOkHttpClient + .newBuilder() + .addNetworkInterceptor(new RequestProgressInterceptor(urlProgressListener)) + .build(); + Retrofit newRetrofit = mRetrofit.newBuilder().client(okHttpClient).build(); + return newRetrofit.create(clazz); + } + + public T createWithDownloadProgress(Class clazz, UrlProgressListener urlProgressListener) { + OkHttpClient okHttpClient = mOkHttpClient + .newBuilder() + .addNetworkInterceptor(new ResponseProgressInterceptor(urlProgressListener)) + .build(); + Retrofit newRetrofit = mRetrofit.newBuilder().client(okHttpClient).build(); + return newRetrofit.create(clazz); + } + + public String baseUrl() { + return mBaseUrl; + } + +} diff --git a/lib_network/src/main/java/com/android/sdk/net/service/ServiceHelper.java b/lib_network/src/main/java/com/android/sdk/net/service/ServiceHelper.java new file mode 100644 index 0000000..3fba206 --- /dev/null +++ b/lib_network/src/main/java/com/android/sdk/net/service/ServiceHelper.java @@ -0,0 +1,33 @@ +package com.android.sdk.net.service; + +import com.android.sdk.net.provider.HttpConfig; + +import okhttp3.OkHttpClient; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-08 16:51 + */ +public class ServiceHelper { + + private OkHttpClient mOkHttpClient; + private ServiceFactory mServiceFactory; + + public OkHttpClient getOkHttpClient(HttpConfig httpConfig) { + if (mOkHttpClient == null) { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + httpConfig.configHttp(builder); + mOkHttpClient = builder.build(); + } + return mOkHttpClient; + } + + public ServiceFactory getServiceFactory(HttpConfig httpConfig) { + if (mServiceFactory == null) { + mServiceFactory = new ServiceFactory(getOkHttpClient(httpConfig), httpConfig); + } + return mServiceFactory; + } + +} diff --git a/lib_push/.gitignore b/lib_push/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_push/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_push/README.md b/lib_push/README.md new file mode 100644 index 0000000..be8de78 --- /dev/null +++ b/lib_push/README.md @@ -0,0 +1,3 @@ +# 推送库 + +目前集成的是极光推送,不允许暴露具体的推送 SDK 的 API 到其他模块中,必须采用统一的封装。 \ No newline at end of file diff --git a/lib_push/build.gradle b/lib_push/build.gradle new file mode 100644 index 0000000..304c337 --- /dev/null +++ b/lib_push/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + compileOnly androidLibraries.androidAnnotations + implementation 'cn.jiguang.sdk:jpush:3.1.6' + implementation 'cn.jiguang.sdk:jcore:1.2.5' + implementation thirdLibraries.timber +} diff --git a/lib_push/src/main/AndroidManifest.xml b/lib_push/src/main/AndroidManifest.xml new file mode 100644 index 0000000..43f0d7b --- /dev/null +++ b/lib_push/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib_push/src/main/java/com/android/sdk/push/MessageHandler.java b/lib_push/src/main/java/com/android/sdk/push/MessageHandler.java new file mode 100644 index 0000000..bde64cc --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/MessageHandler.java @@ -0,0 +1,27 @@ +package com.android.sdk.push; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-03-09 11:31 + */ +public interface MessageHandler { + + /** + * 处理透传消息 + * + * @param pushMessage 消息 + */ + void onDirectMessageArrived(PushMessage pushMessage); + + /** + * 通知栏消息被点击 + */ + void handleOnNotificationMessageClicked(PushMessage pushMessage); + + /** + * 通知栏消息到达 + */ + void onNotificationMessageArrived(PushMessage pushMessage); + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/Push.java b/lib_push/src/main/java/com/android/sdk/push/Push.java new file mode 100644 index 0000000..3ae56cb --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/Push.java @@ -0,0 +1,39 @@ +package com.android.sdk.push; + +import android.app.Activity; +import android.support.annotation.NonNull; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-03-09 11:31 + */ +public interface Push { + + void registerPush(@NonNull final PushCallBack pushCallBack); + + void setAlias(String alias); + + void clearAlias(); + + void setTag(String tag); + + void addTag(String tag); + + void deleteTag(String tag); + + void clearTag(); + + void enablePush(); + + void disablePush(); + + void setMessageHandler(MessageHandler messageHandler); + + MessageHandler getMessageHandler(); + + void onActivityCreate(Activity activity); + + void setChannel(String channel); + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/PushCallBack.java b/lib_push/src/main/java/com/android/sdk/push/PushCallBack.java new file mode 100644 index 0000000..be9836f --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/PushCallBack.java @@ -0,0 +1,7 @@ +package com.android.sdk.push; + + +public interface PushCallBack { + void onRegisterPushSuccess(String registrationID); + void onRegisterPushFail(); +} diff --git a/lib_push/src/main/java/com/android/sdk/push/PushContext.java b/lib_push/src/main/java/com/android/sdk/push/PushContext.java new file mode 100644 index 0000000..af9aa72 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/PushContext.java @@ -0,0 +1,53 @@ +package com.android.sdk.push; + +import android.app.Application; +import android.text.TextUtils; + +import com.android.sdk.push.jpush.JPush; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-03-03 16:22 + */ +public class PushContext { + + private static Application mApplication; + private static boolean isDebug; + private static Push sPush; + + private static String APP_ID; + private static String APP_KEY; + + public static void configPush(String appKey, String appId) { + APP_KEY = appKey; + APP_ID = appId; + } + + public static boolean isPushConfigured() { + return !TextUtils.isEmpty(APP_ID) && !TextUtils.isEmpty(APP_KEY); + } + + public static void init(Application application, boolean debug) { + isDebug = debug; + mApplication = application; + initPush(); + } + + private static void initPush() { + sPush = new JPush(mApplication); + } + + public static Push getPush() { + return sPush; + } + + public static boolean isDebug() { + return isDebug; + } + + public static Application getApplication() { + return mApplication; + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/PushMessage.java b/lib_push/src/main/java/com/android/sdk/push/PushMessage.java new file mode 100644 index 0000000..3214be4 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/PushMessage.java @@ -0,0 +1,76 @@ +package com.android.sdk.push; + + +import android.support.annotation.NonNull; + +public class PushMessage { + + private int messageId; + private int notificationId; + private String alertType; + private String content; + private String extra; + private String title; + + public int getNotificationId() { + return notificationId; + } + + public void setNotificationId(int notificationId) { + this.notificationId = notificationId; + } + + public String getAlertType() { + return alertType; + } + + public void setAlertType(String alertType) { + this.alertType = alertType; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getExtra() { + return extra; + } + + public void setExtra(String extra) { + this.extra = extra; + } + + public int getMessageId() { + return messageId; + } + + public void setMessageId(int messageId) { + this.messageId = messageId; + } + + @NonNull + @Override + public String toString() { + return "PushMessage{" + + "messageiId=" + messageId + + ", notificationId=" + notificationId + + ", alertType=" + alertType + + ", content='" + content + '\'' + + ", extra='" + extra + '\'' + + ", title='" + title + '\'' + + '}'; + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/Utils.java b/lib_push/src/main/java/com/android/sdk/push/Utils.java new file mode 100644 index 0000000..f5aa5ec --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/Utils.java @@ -0,0 +1,25 @@ +package com.android.sdk.push; + +import android.content.Context; +import android.content.SharedPreferences; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-02-26 11:36 + */ +public class Utils { + + private static final String PUSH_SP_NAME = "push_sp_name"; + + public static void savePushId(String key, String id) { + SharedPreferences sharedPreferences = PushContext.getApplication().getSharedPreferences(PUSH_SP_NAME, Context.MODE_PRIVATE); + sharedPreferences.edit().putString(key, id).apply(); + } + + public static String getPushId(String key) { + SharedPreferences sharedPreferences = PushContext.getApplication().getSharedPreferences(PUSH_SP_NAME, Context.MODE_PRIVATE); + return sharedPreferences.getString(key, ""); + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/exception/PushUnConfigException.java b/lib_push/src/main/java/com/android/sdk/push/exception/PushUnConfigException.java new file mode 100644 index 0000000..8707836 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/exception/PushUnConfigException.java @@ -0,0 +1,16 @@ +package com.android.sdk.push.exception; + +/** + * @author Ztiany + * Email: 1169654504@qq.com + * Date : 2017-05-18 12:00 + */ +public class PushUnConfigException extends RuntimeException { + + private static final String MESSAGE = "you have not deployed the %s id"; + + public PushUnConfigException(String message) { + super(String.format(MESSAGE, message)); + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/jpush/JPush.java b/lib_push/src/main/java/com/android/sdk/push/jpush/JPush.java new file mode 100644 index 0000000..1cae4a4 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/jpush/JPush.java @@ -0,0 +1,124 @@ +package com.android.sdk.push.jpush; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import com.android.sdk.push.MessageHandler; +import com.android.sdk.push.Push; +import com.android.sdk.push.PushCallBack; +import com.android.sdk.push.PushContext; +import com.android.sdk.push.Utils; + +import cn.jpush.android.api.JPushInterface; +import timber.log.Timber; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-01-26 17:44 + */ +public class JPush implements Push { + + private Context mContext; + private MessageHandler mMessageHandler; + private PushCallBack mPushCallBack; + + static final String JPUSH_ID_KET = "jpush_id_key"; + + public JPush(Application mApplication) { + mContext = mApplication.getApplicationContext(); + } + + @Override + public void registerPush(@NonNull PushCallBack pushCallBack) { + this.mPushCallBack = pushCallBack; + //给 JPushReceiver 设置 JPush + JPushReceiver.sJPush = this; + // 设置开启日志,发布时请关闭日志 + JPushInterface.setDebugMode(PushContext.isDebug()); + // 初始化极光推送服务 + JPushInterface.init(mContext); + String registrationID = getRegistrationID(); + Timber.d("jpush registrationID = " + registrationID); + boolean isRegistrationSuccess = !TextUtils.isEmpty(registrationID); + if (isRegistrationSuccess) { + this.mPushCallBack.onRegisterPushSuccess(registrationID); + } + } + + private String getRegistrationID() { + String registrationID = JPushInterface.getRegistrationID(mContext); + if (TextUtils.isEmpty(registrationID)) { + registrationID = Utils.getPushId(JPUSH_ID_KET); + } + return registrationID; + } + + @Override + public void setAlias(String alias) { + JPushUtils.setAlias(mContext, alias); + } + + @Override + public void clearAlias() { + JPushUtils.clearAlias(mContext); + } + + @Override + public void setTag(String tag) { + JPushUtils.setTag(mContext, tag); + } + + @Override + public void addTag(String tag) { + JPushUtils.deleteTag(mContext, tag); + } + + @Override + public void deleteTag(String tag) { + JPushUtils.setTag(mContext, tag); + } + + @Override + public void clearTag() { + JPushUtils.clearTags(mContext); + } + + @Override + public void enablePush() { + JPushInterface.resumePush(mContext); + } + + @Override + public void disablePush() { + JPushInterface.stopPush(mContext); + } + + @Override + public void setMessageHandler(MessageHandler messageHandler) { + this.mMessageHandler = messageHandler; + } + + @Override + public MessageHandler getMessageHandler() { + return mMessageHandler; + } + + PushCallBack getPushCallBack() { + return mPushCallBack; + } + + @Override + public void setChannel(String channel) { + JPushInterface.setChannel(mContext, channel); + } + + @Override + public void onActivityCreate(Activity activity) { + // no op + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/jpush/JPushReceiver.java b/lib_push/src/main/java/com/android/sdk/push/jpush/JPushReceiver.java new file mode 100644 index 0000000..65bc4cd --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/jpush/JPushReceiver.java @@ -0,0 +1,128 @@ +package com.android.sdk.push.jpush; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; + +import com.android.sdk.push.PushContext; +import com.android.sdk.push.PushMessage; +import com.android.sdk.push.Utils; + +import cn.jpush.android.api.JPushInterface; +import timber.log.Timber; + +import static com.android.sdk.push.jpush.JPush.JPUSH_ID_KET; + +/** + * 极光推送-消息接收广播 + * + * @author Wangwb + * Email: 253123123@qq.com + * Date : 2019-01-28 11:21 + */ +public class JPushReceiver extends BroadcastReceiver { + + static JPush sJPush; + + @Override + public void onReceive(Context context, Intent intent) { + try { + processMessage(intent); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void processMessage(Intent intent) { + Bundle bundle = intent.getExtras(); + + if (PushContext.isDebug()) { + Timber.d("[JPushReceiver] onReceive - " + intent.getAction() + ", extras: " + JPushUtils.printBundle(bundle)); + } + + if (bundle == null) { + return; + } + + if (JPushInterface.ACTION_REGISTRATION_ID.equals(intent.getAction())) { + + processRegisterSuccess(bundle); + + } else if (JPushInterface.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) { + + processMessageReceived(bundle); + + } else if (JPushInterface.ACTION_NOTIFICATION_RECEIVED.equals(intent.getAction())) { + + processNotificationMessageReceived(bundle); + + } else if (JPushInterface.ACTION_NOTIFICATION_OPENED.equals(intent.getAction())) { + + //目前没有配置这个 action,不会接收此类消息 + processNotificationMessageClicked(bundle); + + } else if (JPushInterface.ACTION_CONNECTION_CHANGE.equals(intent.getAction())) { + + Timber.w("[JPushReceiver]" + intent.getAction() + " connected state change to " + intent.getBooleanExtra(JPushInterface.EXTRA_CONNECTION_CHANGE, false)); + + } else { + Timber.d("[JPushReceiver] Unhandled intent - " + intent.getAction()); + } + + } + + private void processNotificationMessageClicked(Bundle bundle) { + Timber.d("[JPushReceiver] 接收到推送下来的通知被点击了"); + PushMessage pushMessage = extractMessage(bundle); + sJPush.getMessageHandler().handleOnNotificationMessageClicked(pushMessage); + } + + // 在这里可以做些统计,或者做些其他工作 + private void processNotificationMessageReceived(Bundle bundle) { + Timber.d("[JPushReceiver] 接收到推送下来的通知"); + PushMessage pushMessage = extractMessage(bundle); + sJPush.getMessageHandler().onNotificationMessageArrived(pushMessage); + } + + @NonNull + private PushMessage extractMessage(Bundle bundle) { + int notificationId = bundle.getInt(JPushInterface.EXTRA_NOTIFICATION_ID); + String alertType = bundle.getString(JPushInterface.EXTRA_ALERT_TYPE); + String alert = bundle.getString(JPushInterface.EXTRA_ALERT); + String title = bundle.getString(JPushInterface.EXTRA_NOTIFICATION_TITLE); + String extras = bundle.getString(JPushInterface.EXTRA_EXTRA); + + PushMessage pushMessage = new PushMessage(); + pushMessage.setTitle(title); + pushMessage.setContent(alert); + pushMessage.setExtra(extras); + pushMessage.setNotificationId(notificationId); + pushMessage.setAlertType(alertType); + return pushMessage; + } + + private void processRegisterSuccess(Bundle bundle) { + String regId = bundle.getString(JPushInterface.EXTRA_REGISTRATION_ID); + Timber.d("[JPushReceiver] JPush 用户注册成功,接收Registration Id : " + regId); + //you can send the Registration Id to your server... + Utils.savePushId(JPUSH_ID_KET, regId); + sJPush.getPushCallBack().onRegisterPushSuccess(regId); + } + + // 自定义消息不会展示在通知栏,完全要开发者写代码去处理 + private void processMessageReceived(Bundle bundle) { + Timber.d("[JPushReceiver] 接收到推送下来的透传消息: " + bundle.getString(JPushInterface.EXTRA_MESSAGE)); + + String message = bundle.getString(JPushInterface.EXTRA_MESSAGE); + String extras = bundle.getString(JPushInterface.EXTRA_EXTRA); + + PushMessage pushMessage = new PushMessage(); + pushMessage.setContent(message); + pushMessage.setExtra(extras); + + sJPush.getMessageHandler().onDirectMessageArrived(pushMessage); + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/jpush/JPushUtils.java b/lib_push/src/main/java/com/android/sdk/push/jpush/JPushUtils.java new file mode 100644 index 0000000..ba5974a --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/jpush/JPushUtils.java @@ -0,0 +1,195 @@ +package com.android.sdk.push.jpush; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import cn.jpush.android.api.JPushInterface; +import timber.log.Timber; + +import static com.android.sdk.push.jpush.TagAliasOperatorHelper.ACTION_ADD; +import static com.android.sdk.push.jpush.TagAliasOperatorHelper.ACTION_CLEAN; +import static com.android.sdk.push.jpush.TagAliasOperatorHelper.ACTION_DELETE; +import static com.android.sdk.push.jpush.TagAliasOperatorHelper.ACTION_SET; + +/** + * @author Wangwb + * Email: 253123123@qq.com + * Date : 2019-01-28 11:29 + */ +class JPushUtils { + + private static final String TAG = "JPushReceiver"; + + /** + * 打印 Bundle Extras + */ + static String printBundle(Bundle bundle) { + if (bundle == null) { + return "null"; + } + + StringBuilder sb = new StringBuilder(); + + for (String key : bundle.keySet()) { + switch (key) { + case JPushInterface.EXTRA_NOTIFICATION_ID: + sb.append("\nkey:").append(key).append(", value:").append(bundle.getInt(key)); + break; + case JPushInterface.EXTRA_CONNECTION_CHANGE: + sb.append("\nkey:").append(key).append(", value:").append(bundle.getBoolean(key)); + break; + case JPushInterface.EXTRA_EXTRA: + if (TextUtils.isEmpty(bundle.getString(JPushInterface.EXTRA_EXTRA))) { + Timber.i(TAG, "This message has no Extra data"); + continue; + } + + try { + JSONObject json = new JSONObject(bundle.getString(JPushInterface.EXTRA_EXTRA)); + Iterator it = json.keys(); + + while (it.hasNext()) { + String myKey = it.next(); + sb.append("\nkey:").append(key).append(", value: [").append(myKey).append(" - ").append(json.optString(myKey)).append("]"); + } + } catch (JSONException e) { + Timber.e(TAG, "Get message extra JSON error!"); + } + break; + default: + sb.append("\nkey:").append(key).append(", value:").append(bundle.get(key)); + break; + } + } + return sb.toString(); + } + + /** + * 获取极光推送AppKey + */ + @SuppressWarnings("unused") + static String appKey(Context context) { + Bundle metaData = null; + String appKey = null; + try { + ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); + if (null != ai) + metaData = ai.metaData; + if (null != metaData) { + appKey = metaData.getString("JPUSH_APPKEY"); + Timber.d("JPush AppKey: " + appKey + ", pkg: " + context.getPackageName()); + if ((null == appKey) || appKey.length() != 24) { + appKey = null; + } + } + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return appKey; + } + + /** + * 校验Tag Alias 只能是数字,英文字母和中文 + */ + @SuppressWarnings("all") + private static boolean isValidTagAndAlias(String s) { + Pattern p = Pattern.compile("^[\u4E00-\u9FA50-9a-zA-Z_!@#$&*+=.|]+$"); + Matcher m = p.matcher(s); + return m.matches(); + } + + static boolean isConnected(Context context) { + ConnectivityManager conn = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + @SuppressLint("MissingPermission") + NetworkInfo info = conn.getActiveNetworkInfo(); + return (info == null || !info.isConnected()); + } + + /** + * 获取设备ID + */ + @SuppressWarnings("unused") + static String getDeviceId(Context context) { + return JPushInterface.getUdid(context); + } + + static void setTag(Context context, String tag) { + if (!JPushUtils.isValidTagAndAlias(tag)) { + Timber.e("Tag Alias 命名不符合规范"); + return; + } + + TagAliasOperatorHelper.TagAliasBean tagAliasBean = createTagAliasBean(tag); + tagAliasBean.action = ACTION_SET; + + TagAliasOperatorHelper.getInstance().handleAction(context, ++TagAliasOperatorHelper.sequence, tagAliasBean); + } + + static void deleteTag(Context context, String tag) { + if (!JPushUtils.isValidTagAndAlias(tag)) { + Timber.e("Tag Alias 命名不符合规范"); + return; + } + + TagAliasOperatorHelper.TagAliasBean tagAliasBean = createTagAliasBean(tag); + tagAliasBean.action = ACTION_DELETE; + + TagAliasOperatorHelper.getInstance().handleAction(context, ++TagAliasOperatorHelper.sequence, tagAliasBean); + } + + static void clearTags(Context context) { + TagAliasOperatorHelper.TagAliasBean tagAliasBean = createTagAliasBean(null); + tagAliasBean.action = ACTION_CLEAN; + + TagAliasOperatorHelper.getInstance().handleAction(context, ++TagAliasOperatorHelper.sequence, tagAliasBean); + } + + @NonNull + private static TagAliasOperatorHelper.TagAliasBean createTagAliasBean(String tag) { + TagAliasOperatorHelper.TagAliasBean tagAliasBean = new TagAliasOperatorHelper.TagAliasBean(); + tagAliasBean.isAliasAction = false; + + if (!TextUtils.isEmpty(tag)) { + LinkedHashSet tags = new LinkedHashSet<>(); + tags.add(tag); + tagAliasBean.tags = tags; + } + + return tagAliasBean; + } + + static void setAlias(Context context, String alias) { + TagAliasOperatorHelper.TagAliasBean tagAliasBean = new TagAliasOperatorHelper.TagAliasBean(); + + tagAliasBean.action = ACTION_ADD; + tagAliasBean.isAliasAction = true; + tagAliasBean.alias = alias; + + TagAliasOperatorHelper.getInstance().handleAction(context, ++TagAliasOperatorHelper.sequence, tagAliasBean); + } + + static void clearAlias(Context context) { + TagAliasOperatorHelper.TagAliasBean tagAliasBean = new TagAliasOperatorHelper.TagAliasBean(); + + tagAliasBean.action = ACTION_DELETE; + tagAliasBean.isAliasAction = true; + + TagAliasOperatorHelper.getInstance().handleAction(context, ++TagAliasOperatorHelper.sequence, tagAliasBean); + } + +} \ No newline at end of file diff --git a/lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasJPushMessageReceiver.java b/lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasJPushMessageReceiver.java new file mode 100644 index 0000000..92b8ea1 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasJPushMessageReceiver.java @@ -0,0 +1,37 @@ +package com.android.sdk.push.jpush; + +import android.content.Context; + +import cn.jpush.android.api.JPushMessage; +import cn.jpush.android.service.JPushMessageReceiver; + +/** + * 自定义 JPush message 接收器,包括操作 tag/alias 的结果返回(仅仅包含 tag/alias 新接口部分) + */ +public class TagAliasJPushMessageReceiver extends JPushMessageReceiver { + + @Override + public void onTagOperatorResult(Context context, JPushMessage jPushMessage) { + TagAliasOperatorHelper.getInstance().onTagOperatorResult(context, jPushMessage); + super.onTagOperatorResult(context, jPushMessage); + } + + @Override + public void onCheckTagOperatorResult(Context context, JPushMessage jPushMessage) { + TagAliasOperatorHelper.getInstance().onCheckTagOperatorResult(context, jPushMessage); + super.onCheckTagOperatorResult(context, jPushMessage); + } + + @Override + public void onAliasOperatorResult(Context context, JPushMessage jPushMessage) { + TagAliasOperatorHelper.getInstance().onAliasOperatorResult(context, jPushMessage); + super.onAliasOperatorResult(context, jPushMessage); + } + + @Override + public void onMobileNumberOperatorResult(Context context, JPushMessage jPushMessage) { + TagAliasOperatorHelper.getInstance().onMobileNumberOperatorResult(context, jPushMessage); + super.onMobileNumberOperatorResult(context, jPushMessage); + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasOperatorHelper.java b/lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasOperatorHelper.java new file mode 100644 index 0000000..9927ca3 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/jpush/TagAliasOperatorHelper.java @@ -0,0 +1,412 @@ +package com.android.sdk.push.jpush; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.support.annotation.NonNull; +import android.util.SparseArray; + +import java.util.Locale; +import java.util.Objects; +import java.util.Set; + +import cn.jpush.android.api.JPushInterface; +import cn.jpush.android.api.JPushMessage; +import timber.log.Timber; + +/** + * 处理 tag alias 相关的逻辑 + */ +class TagAliasOperatorHelper { + + private static final String TAG = "JIGUANG-TagAliasHelper"; + + static int sequence = 1; + + /** + * 增加 + */ + static final int ACTION_ADD = 1; + /** + * 覆盖 + */ + static final int ACTION_SET = 2; + /** + * 删除部分 + */ + static final int ACTION_DELETE = 3; + /** + * 删除所有 + */ + static final int ACTION_CLEAN = 4; + /** + * 查询 + */ + static final int ACTION_GET = 5; + /** + * 检查 + */ + static final int ACTION_CHECK = 6; + + private static final int DELAY_SEND_ACTION = 1; + + private static final int DELAY_SET_MOBILE_NUMBER_ACTION = 2; + + private Context context; + + @SuppressLint("StaticFieldLeak") + private static TagAliasOperatorHelper mInstance; + + private TagAliasOperatorHelper() { + } + + public static TagAliasOperatorHelper getInstance() { + if (mInstance == null) { + synchronized (TagAliasOperatorHelper.class) { + if (mInstance == null) { + mInstance = new TagAliasOperatorHelper(); + } + } + } + return mInstance; + } + + public void init(Context context) { + if (context != null) { + this.context = context.getApplicationContext(); + } + } + + private SparseArray setActionCache = new SparseArray<>(); + + public Object get(int sequence) { + return setActionCache.get(sequence); + } + + public Object remove(int sequence) { + return setActionCache.get(sequence); + } + + private void put(int sequence, Object tagAliasBean) { + setActionCache.put(sequence, tagAliasBean); + } + + @SuppressLint("HandlerLeak") + private Handler delaySendHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case DELAY_SEND_ACTION: + if (msg.obj instanceof TagAliasBean) { + Timber.w(TAG, "on delay time"); + sequence++; + TagAliasBean tagAliasBean = (TagAliasBean) msg.obj; + setActionCache.put(sequence, tagAliasBean); + if (context != null) { + handleAction(context, sequence, tagAliasBean); + } else { + Timber.w(TAG, "#unexcepted - context was null"); + } + } else { + Timber.w(TAG, "#unexcepted - msg obj was incorrect"); + } + break; + case DELAY_SET_MOBILE_NUMBER_ACTION: + if (msg.obj instanceof String) { + Timber.w(TAG, "retry set mobile number"); + sequence++; + String mobileNumber = (String) msg.obj; + setActionCache.put(sequence, mobileNumber); + if (context != null) { + handleAction(context, sequence, mobileNumber); + } else { + Timber.w(TAG, "#unexcepted - context was null"); + } + } else { + Timber.w(TAG, "#unexcepted - msg obj was incorrect"); + } + break; + } + } + }; + + private void handleAction(Context context, int sequence, String mobileNumber) { + put(sequence, mobileNumber); + Timber.d(TAG, "sequence:" + sequence + ",mobileNumber:" + mobileNumber); + JPushInterface.setMobileNumber(context, sequence, mobileNumber); + } + + /** + * 处理设置tag + */ + void handleAction(Context context, int sequence, TagAliasBean tagAliasBean) { + init(context); + if (tagAliasBean == null) { + Timber.w(TAG, "tagAliasBean was null"); + return; + } + put(sequence, tagAliasBean); + if (tagAliasBean.isAliasAction) { + switch (tagAliasBean.action) { + case ACTION_GET: + JPushInterface.getAlias(context, sequence); + break; + case ACTION_DELETE: + JPushInterface.deleteAlias(context, sequence); + break; + case ACTION_SET: + JPushInterface.setAlias(context, sequence, tagAliasBean.alias); + break; + default: + Timber.w(TAG, "unsupport alias action type"); + } + } else { + switch (tagAliasBean.action) { + case ACTION_ADD: + JPushInterface.addTags(context, sequence, tagAliasBean.tags); + break; + case ACTION_SET: + JPushInterface.setTags(context, sequence, tagAliasBean.tags); + break; + case ACTION_DELETE: + JPushInterface.deleteTags(context, sequence, tagAliasBean.tags); + break; + case ACTION_CHECK: + //一次只能check一个tag + String tag = (String) Objects.requireNonNull(tagAliasBean.tags.toArray())[0]; + JPushInterface.checkTagBindState(context, sequence, tag); + break; + case ACTION_GET: + JPushInterface.getAllTags(context, sequence); + break; + case ACTION_CLEAN: + JPushInterface.cleanTags(context, sequence); + break; + default: + Timber.w(TAG, "unsupport tag action type"); + } + } + } + + private boolean RetryActionIfNeeded(int errorCode, TagAliasBean tagAliasBean) { + if (JPushUtils.isConnected(context)) { + Timber.w(TAG, "no network"); + return true; + } + //返回的错误码为6002 超时,6014 服务器繁忙,都建议延迟重试 + if (errorCode == 6002 || errorCode == 6014) { + Timber.d(TAG, "need retry"); + if (tagAliasBean != null) { + Message message = new Message(); + message.what = DELAY_SEND_ACTION; + message.obj = tagAliasBean; + delaySendHandler.sendMessageDelayed(message, 1000 * 60); + String logs = getRetryStr(tagAliasBean.isAliasAction, tagAliasBean.action, errorCode); + Timber.w(TAG, logs); + return false; + } + } + return true; + } + + private boolean RetrySetMObileNumberActionIfNeeded(int errorCode, String mobileNumber) { + if (JPushUtils.isConnected(context)) { + Timber.w(TAG, "no network"); + return false; + } + //返回的错误码为6002 超时,6024 服务器内部错误,建议稍后重试 + if (errorCode == 6002 || errorCode == 6024) { + Timber.d(TAG, "need retry"); + Message message = new Message(); + message.what = DELAY_SET_MOBILE_NUMBER_ACTION; + message.obj = mobileNumber; + delaySendHandler.sendMessageDelayed(message, 1000 * 60); + String str = "Failed to set mobile number due to %s. Try again after 60s."; + str = String.format(Locale.ENGLISH, str, + (errorCode == 6002 ? "timeout" : "server internal error”")); + Timber.w(TAG, str); + return true; + } + return false; + } + + private String getRetryStr(boolean isAliasAction, int actionType, int errorCode) { + String str = "Failed to %s %s due to %s. Try again after 60s."; + str = String.format(Locale.ENGLISH, str, getActionStr(actionType), + (isAliasAction ? "alias" : " tags"), (errorCode == 6002 ? "timeout" : "server too busy")); + return str; + } + + private String getActionStr(int actionType) { + switch (actionType) { + case ACTION_ADD: + return "add"; + case ACTION_SET: + return "set"; + case ACTION_DELETE: + return "delete"; + case ACTION_GET: + return "get"; + case ACTION_CLEAN: + return "clean"; + case ACTION_CHECK: + return "check"; + } + return "unkonw operation"; + } + + void onTagOperatorResult(Context context, JPushMessage jPushMessage) { + int sequence = jPushMessage.getSequence(); + Timber.i(TAG, "action - onTagOperatorResult, sequence:" + sequence + ",tags:" + jPushMessage.getTags()); + if (jPushMessage.getTags() != null) { + Timber.i(TAG, "tags size:" + jPushMessage.getTags().size()); + } + + init(context); + + // 根据sequence从之前操作缓存中获取缓存记录 + TagAliasBean tagAliasBean = (TagAliasBean) setActionCache.get(sequence); + if (tagAliasBean == null) { + Timber.w(TAG, "获取缓存记录失败"); + return; + } + if (jPushMessage.getErrorCode() == 0) { + Timber.i(TAG, "action - modify tag Success,sequence:" + sequence); + setActionCache.remove(sequence); + String logs = getActionStr(tagAliasBean.action) + " tags success"; + Timber.w(TAG, logs); + // 极光推送已经初始化失败 + } else { + String logs = "Failed to " + getActionStr(tagAliasBean.action) + " tags"; + if (jPushMessage.getErrorCode() == 6018) { + //tag数量超过限制,需要先清除一部分再 add + logs += ", tags is exceed limit need to clean"; + } + logs += ", errorCode:" + jPushMessage.getErrorCode(); + Timber.w(TAG, logs); + if (RetryActionIfNeeded(jPushMessage.getErrorCode(), tagAliasBean)) { + Timber.w(TAG, logs); + } + // 极光推送已经初始化成功 + } + } + + void onCheckTagOperatorResult(Context context, JPushMessage jPushMessage) { + int sequence = jPushMessage.getSequence(); + Timber.i(TAG, "action - onCheckTagOperatorResult, sequence:" + + sequence + + ",checktag:" + + jPushMessage.getCheckTag()); + + init(context); + + // 根据sequence从之前操作缓存中获取缓存记录 + TagAliasBean tagAliasBean = (TagAliasBean) setActionCache.get(sequence); + if (tagAliasBean == null) { + Timber.w(TAG, "获取缓存记录失败"); + return; + } + if (jPushMessage.getErrorCode() == 0) { + Timber.i(TAG, "tagBean:" + tagAliasBean); + setActionCache.remove(sequence); + String logs = getActionStr(tagAliasBean.action) + + " tag " + + jPushMessage.getCheckTag() + + " bind state success,state:" + + jPushMessage.getTagCheckStateResult(); + Timber.w(TAG, logs); + } else { + String logs = "Failed to " + + getActionStr(tagAliasBean.action) + + " tags, errorCode:" + + jPushMessage.getErrorCode(); + Timber.w(TAG, logs); + if (RetryActionIfNeeded(jPushMessage.getErrorCode(), tagAliasBean)) { + Timber.w(TAG, logs); + } + } + } + + void onAliasOperatorResult(Context context, JPushMessage jPushMessage) { + int sequence = jPushMessage.getSequence(); + Timber.i(TAG, "action - onAliasOperatorResult, sequence:" + + sequence + + ",alias:" + + jPushMessage.getAlias()); + + init(context); + + // 根据sequence从之前操作缓存中获取缓存记录 + TagAliasBean tagAliasBean = (TagAliasBean) setActionCache.get(sequence); + if (tagAliasBean == null) { + Timber.w(TAG, "获取缓存记录失败"); + return; + } + if (jPushMessage.getErrorCode() == 0) { + Timber.i(TAG, "action - modify alias Success,sequence:" + sequence); + setActionCache.remove(sequence); + String logs = getActionStr(tagAliasBean.action) + " alias success"; + Timber.w(TAG, logs); + } else { + String logs = "Failed to " + + getActionStr(tagAliasBean.action) + + " alias, errorCode:" + + jPushMessage.getErrorCode(); + Timber.w(TAG, logs); + if (RetryActionIfNeeded(jPushMessage.getErrorCode(), tagAliasBean)) { + Timber.w(TAG, logs); + } + } + } + + /** + * 设置手机号码回调 + */ + void onMobileNumberOperatorResult(Context context, JPushMessage jPushMessage) { + int sequence = jPushMessage.getSequence(); + Timber.i(TAG, "action - onMobileNumberOperatorResult, sequence:" + + sequence + + ",mobileNumber:" + + jPushMessage.getMobileNumber()); + + init(context); + + if (jPushMessage.getErrorCode() == 0) { + Timber.i(TAG, "action - set mobile number Success,sequence:" + sequence); + setActionCache.remove(sequence); + } else { + String logs = "Failed to set mobile number, errorCode:" + jPushMessage.getErrorCode(); + Timber.e(TAG, logs); + if (!RetrySetMObileNumberActionIfNeeded(jPushMessage.getErrorCode(), + jPushMessage.getMobileNumber())) { + Timber.w(TAG, logs); + } + } + } + + public static class TagAliasBean { + + int action; + Set tags; + String alias; + boolean isAliasAction; + + @NonNull + @Override + public String toString() { + return "TagAliasBean{" + + "action=" + + action + + ", tags=" + + tags + + ", alias='" + + alias + + '\'' + + ", isAliasAction=" + + isAliasAction + + '}'; + } + } + +} diff --git a/lib_push/src/main/java/com/android/sdk/push/mipush/MiPush.java b/lib_push/src/main/java/com/android/sdk/push/mipush/MiPush.java new file mode 100644 index 0000000..b20ea77 --- /dev/null +++ b/lib_push/src/main/java/com/android/sdk/push/mipush/MiPush.java @@ -0,0 +1,7 @@ +package com.android.sdk.push.mipush; + +import com.android.sdk.push.Push; + + +public abstract class MiPush implements Push { +} diff --git a/lib_qrcode/.gitignore b/lib_qrcode/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_qrcode/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_qrcode/README.md b/lib_qrcode/README.md new file mode 100644 index 0000000..3ee2da5 --- /dev/null +++ b/lib_qrcode/README.md @@ -0,0 +1,3 @@ +## 二维码扫描库 + +修改自 [BGAQRCode-Android](https://github.com/bingoogolapple/BGAQRCode-Android) \ No newline at end of file diff --git a/lib_qrcode/build.gradle b/lib_qrcode/build.gradle new file mode 100644 index 0000000..4163ab4 --- /dev/null +++ b/lib_qrcode/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + lintOptions { + abortOnError false + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + +} + +dependencies { + implementation 'com.google.zxing:core:3.3.3' + api fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + /*implementation uiLibraries.fotoapparat*/ + implementation kotlinLibraries.kotlinStdlib +} \ No newline at end of file diff --git a/lib_qrcode/libs/fotoapparat.aar b/lib_qrcode/libs/fotoapparat.aar new file mode 100644 index 0000000000000000000000000000000000000000..17f988c2ba9db3879a7ebf987047518c80906625 GIT binary patch literal 314592 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

    go!5Ka}L-{Yn`LDMudraOvR9zr7x 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®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

    P6S>Hpa!>81@|`*xI;V z<-QJ~i@9rsb+y2YO@F$;Lr0bVPcg`QzpW%Piqa0uVtAAbQsAqM;%ZLTSn{*+Y*Sre z^LW!Sm79q3&i5&7W9fh!GA^Qv@DfbHP=N8Pn1o>!ke!^@bps+%2qFCNHPJpqP~Q2d ztLktKq1k{(1^JL{stVo=HHV_tll>Z&&@3@lw!ZT+v56NKnGWP|O{o@wm1bv=Q4-;^ z0~o}W71ufE%|wigEo13gJ@$zpPG1}GNFKmgRmtLixZFtc z$ISM;1haG?%EF7MT}tqT5o;p~oj7EF{VA$eb<(-k2L`$`az~WoXx7OLNZ^G=Gz%6s z9^}Ht&Xy)yRFZB+rKg1Lg(pWG;XpiHBX~j2Xrq%-S`sZ~sHetH)=rjw@J4B(E`Dzu zlS1ZQazIDR_eW8*8bc^t!%j*m92`y#qN&1Fk=yuv7Q$~=UpVN!87d6#N=w*Up85&V zVZ5md?JPlBb4TOnQAw8eX7Q8UN}>>`P?qx)*BzjlJ!B%jPa(`TkpkJORhPb5c zS7XW9zIS9a635b%ME;&CcTz`AabwKzvD-q|9%{quNX!@}&fhZqRa&1L)s-sN7tjTVO}F(&Qy zxz+UZ*eB(eUD24+*lm7L!^7-ZPY~pnFOx6G_o2{lG>t5B5czY#yiwA@oI!@0h1Eon z%zRDdz+UO#D`V_SLJZfiErxW5gzV}c-yfQL^0L^X^@|V5NtzM2{f|Z<(IX-<{i8l6 zl)9DOg65lcl(i7&W)fb^l|Vbt+&+$D#w=v(GaZxSXyNV~B)s$PZ-KY`&rCR`1BqsZ zDlyrFUQMpH=`HYnP-87BI54N0vr$JW>#_sw)xS?ttioP~n~-NHKU&yiev9lMeK8wp z$eGjV#iZtN2j|F0`sCtiNxbaHCeY3;(9XHB+z5V)Q9(+<-`^hceELqiy*1C}bIecB zuYT8&f`gAcQ|9AoUF>`KYp3<-#zNvW>OrJ@^gWiVYLHoLY9qU9gV>iv&hlx+edn{! zH*1#Juern`dJzwHe07~fm`q-7OmNWFj>@u_OqrDyK-sn=GF$!fR3{p@RQk= zosaGS_EdBAIS)Ad@rx{&7u5jK4(p5lAAG9+4A894) z|9LI^dvoJIjU@c!{E$nE7^lC%6aI|MShB|3ooyA{0kD_3TOA#q#ykcQwZjdV8m zXzu0{`}y#J^mqd^o;?49xNBR2snHo(0rTwoh4a+;#rehh=Ev(h`8U;hpv)@sBk$&J z7DDP8dm-ji6ZOO-VZZ_N2FhygT?3;3;i&;&nRsKrsDB;o_V-YH4YVwS%nKM>|C^ET zz!hQYPfQ)y+uWm-l#(Pd7^RY)>z^Y6A=_r_iBJdeS2zi)RZOj?UVM+Xv3s;ro3OZ{ z8X&Zknsyqt~fgPiS$!xCPQth&|U_6JlOI^S-I6fAS-%? zaf@z6Y<2}x@L8}K0#)HUfGLgX3Sq;v}VTC~w4!!P)@(GHYtkZ%gZK&30h6EQS+Q$amuVIU|Muo0ne3#4W@3JDT-wD#5f;{ML;MGi_?0MHhT8rglNGpS>- z7_X?Hwa&mUg_%t#C}i^{_kYWj3sqCYdhVNrsO~?TUO`mi2&5J~I&y7a%~O+A>m;L{ zoac??Zr54q*bXz_?r|uUtEl|3j>IH}kptn%1pQ{?7j z*O+}JXEVQa{lzjIo?C^9brHSp#Gw$}@wRq!ozYz*w~SM3CmEXV$&#gm^3I7Ey@5@j*%hhd-Tm?Cc1?XsK7sU*VR1CF(_6R!|(CJNFvr zmo+#hw1g=Fz~dvAiDXOh9}MLu)bz)~k~RE7prBB_2HZQ4J2Q*<6)+w=g=~+XI5Lwn zoIefUAe;4&ef$0AZa#AknZ~C0Gupv;{9FNOt$;WT4iNp-K7LxCddZofdP}Z@{C13x)V6iVDXjIs-BEBU z^v22r`}9fXA5ZRO{(e93vbVSWcZV6RKOSXF-Arx%s}m@WG1JjQz%(}9k58Cw)x(Pmh^L=CXP&Zg}L8*_qJSt4M zIPvRq9qY0jAFv&!BrB4JSWl%#o*0Q}AY-4Td#=6dhpL^!4(_oso4KZ)lLN{nN2O|% zprIC>OE3Pa6sbB(F3=r08uSq)C^e)`TCTHwDl+yCzov3AYw+@`&(^7fJOC7vh`Sl2 zx!8O$)tv$2RT0Xp7V$gTk<|ZCzY8v0bq*hYhOjK*4al7$Y68{t#IK7kP7@(f4T8sf z5J*8cWVj7#CaLm$k-xLYbTRmyBA`s>CS`CbCr*yTX)JRv?uErfCB=FZ zp8h6-R+EjU*M^cAM;A`|F13siyv<@HS8Im>wjNO@jNT0^E;0uL`P<(9==hxSvCWB? z9hW<4GM;oa%-QK?Z-L_7&NXz*PMQGoegX!oggA14I9u>laL+r{Z^`3iT7I^w47KHC z>=%fYJz3$Dqz;ktd|uW|F6n?6#R0}8&}V??OazB<$Eh-RWdd$sbf5m$fSo9s7CHO@Q2`S_oVB0&a-*Bri54`u&ihHaRc0KHyW^ zUxH5OG{{el6#F&M6dR}=hatFB6pl!F?X`;RrRXCfBlKV`$u=>(#NaR0^4=pr(qFKT zDH36LCM{N+6n~12+DKENjqwE7fMe-kgk45px0kgSMPT0NZd32=+Fped6~CtRCG}*t z%@%E!30QMwWN_uk7HKHpmw7AtpflUHfKtI@_-ag7>h?12_S1}*R3mlZ>ERU2$4~CF zHuzE%^^$x$JvmQX>YDBI74GlpzQ6li7WHdgmkErQNSfWRtgpx>KRmYE6&HV1Jv7ye&yIj717iI z1)0-qXhGDJO z83c4?1+UE<@{J*Tlu+nm63KLB$RvYk75i-i>Z7rjAl4J_t%^zsRc)(|)HKUp7N)6a zakgo$?k-@@e+9;#O^Wt6=htjgtasyxf_tOg`36*_d$`has8p1IR^%uM1Qe7}Gx~^{ zVT+d+7E=krE9k|AvT`Mkki@M^oPD2XrYVsTvtV^!kD?DhoaHO%{qO9gTN+E5E9`0` z&#)G3hr3i=#_(@ZEBsf>d@CHcsQuqzyUo0di22+&+z>b4|4C1%=+(1!26ZvQ>6-32(du4d$AM zxJrnh_pKJP1C#CHf7C0xE*k7xe`_~%DLpb2>5xUlcv_!W|NZFjy6o`Ed-ulM@*C0S zRR}kflZ?X-andI&fIF5?1gcR8}bw0wsC_hD%TZ&>-p1>5%bxWC3{M1SCFqp z@8>nb4GtoLxu}Nf@B17@MYp(rR5zh|5RxH&(L7O^`Fc*A%Q>Joc+rHO!hTjhMRYP^_ zK|%E;x{|I#PTMxjH%TO@*~iDh0&i2Z|M3@cp8Kz>pyY4T&dFA&?`RcSPtHgn^J|>f z5QX4iq0a||ugSO8uU^5R)2px*S3FtEGueD@+3vIMNpTig|G4sD4t}%>UXOX=D}8>+-QuV3>3;Y+zacg{V$2ZjJ)a6g*$}I}HQxU^uZ%H;Lpu6{ zg3tXU1^_w!#WJP3EP4jPW{TE3F&;;_O zQ~$waK$ORm@eN!9tA(sD5F?ab;d-qsC(JNf#TP{n?ekK z!BI8kJ5K63ch3&CO2=HlV+dPL&Esne><2{BE;iE+bDT86k6(uzI;@s(c8MdUP$o(+ z>aZe)EWbo3HA8sLn}82Hlq+~ z_BGylLDB5aDzRLw>lFDcR8_)4NC{|aoFT_(WQ@-7sc$`uzj@S%OOpm@`h_+RJ&>Gu zYtD+U3b=MTL6-Cmzibz6kP*F<&u)7ju(U!JF;K(Hs2pWP-rZsLSIXC_JFKLLL!Z&= zT80?%aPMBbbs86J@uT-7f#xrcn)@Y*Db1}%NE$`YmeGMQp}Ayyt4hBr}#p?!xe zM42bQR@MpIvSnzE3)qCvt#aXpw4|m-HfWesyvUhLHzcLTE$-w&JGkNfT69^*S$hqv zO=Xpq`WCIrNU)+iYq!3`Tb={&{V<|g=7$?uR6(XdJ7Fky8O!Qn=R0c(3>Uc z{@Ba*9_Suv7Jd^qP8a`>)5{QSC+rx>vxj1-wKdAKmW-64r{90OU7ciT+I6nKk8{tf znUBHoC@RsCl3c_U_dw5(rW|3%5xjW#4CSCe5@CF#oGOY7GkclPgeukZdNMykNwx$h z!>6ZfHsx3tTF&Se5n`WB8F5WnS?{Y(v*QKL8cVwNwd^9kVv->MCVg7dN9-j`3}bXX5jT30VvHc@wd7OaqZS zFCKuCCLHn$`}k66?VJ~|U=Tco9bv9j3VL)X20dQ);-?LSK3O12hIb4MZ={==(UclCEwyShHIeyz@Y`&1f|K9h|Ks!52omA>ng(BKt=FqK^_KZyz1hbC)tQ7M z7JO%B`?L^S>T2^0ziXAYUf`>!-mX@72E@5$m4vb#2QagPiWccFkh{syLZOPlul^C z!H1NmN?eyTV`IZ=aFHM11-r`KgrkSF@XsQ;m8YO%Yyo#nase$o-z}F6&;tfOy#gN$ zp{YVH&qEs>=*|6ZtZ2`M_OlVJs!Iu6CL*b!=T(h)%Z4OeJH!_>gzw8RQPW6N=a}yG zRG5R^ceUip9@5)RDlnPTjDdE#!%cLa+0t_IGL(zBNeB~cK`avsp*8TlmTm^_Xg7z% zfbN)Bp{`xx925ng2>-m9ruXcw>580B%4PERC331T+Ts6iSoTN@%L)LVrS8*;?cGVC_c&Fi=7y*Uw%2O)ioSk+3&Ti@6 z^)0`FqZB7$z^y|vru3P-GK$uo#UQ{6X@0ES!h>eH!g zPR7btu~lU7RiqL;XrXiHq7@`?E+m;|;+74NK^<*CV5qA%Vr+@8;06v_az?vD0fYVr zM$Hm7ZJ`6)IAw*vDg-Jtab2We0AD~!k8OZz0Bb;6PluL~)KJngB3Y@z)>2q9 zu#WUz4Yiii(#GbSR9xuI&gcQs|3}+9Mpxc#+ul_{MJu*#+eyW)*tS)%D_W`8wr$(C zom6bwZ}ziyJNLG?o%7s%&TaSW`hQ)&K6)Q>%rO(JCiIP}HCQO`qHr}k$zdZ2!yh3IDs>GD6sA-H>@wAP6(OUJr49JbQP4wp_ zT4;7wHY9+-I|A?{nxe@t0KMCD>kcG68aL~;iv#JKO&8r7m$&=38qYEuhorwbJAJ23 zIHnJT*H?{#_s|9#ISJJ0f2O-|Sly|y-c%hy)P82sYEK@TwK+7wEHZvZBl9=z2J;|$4O zG2Hq1lv-T@fh-$2fRhp1)mA}vuXBZdEy?^R`MXld%6p#3V+Ji=zhU&&+qO{FVlO7A z^sYXXUQ$yrj`YF7qeVYoY+$qE3LA-efXzz(nhEr*>tK(umcBSFJtAmnfgS3iXz|xY z%z;(G#Gp*WWgN=2E*TiZp{SnV2h<7|=`|wU4FJaWvPDsf$V3m$H2|F%?jbE>u-?pC zF4L0^7mKwt?-FkV9+WOBe#3s)^=MDOeVR~?-*l42dZj6bTD!e3YDlG|d7R-)V=0sS zaY_&be;N)4e>`NWt)CUXaQ%GaT$rkgd8d3%` zU?%8gcEDOiya2YRI%q+cpDaAu)l5MTgWA4$w^4f@hJuoTrcIq#numa#Ub-%(ykq}7 zG=A-bMd1+y-u&)IXv@^}T%+-Pj`@!{R2jF}7dl&a17a}1!AS*5X}gW$Bj|85aQ}yC z;05gCn7gb+Beg}%#W%X(3|p6{T`dezY#Ys=>o>!F7DX8sze})1TnTVx!Ryvp@^R;L z>MJBDUyUOndmTvsvr-aas3>7xpTHLC(Sk%t|L7xiCmpM*dDElJU2r z;`jMoZHmF))vw#rJ=;7h>%SpbZi^6EXR=RDhx=#!)mE6d_w@ejekbw3`#VI^RVG5@ z8&0`&`k&qCulY3?&8dVEzJ3)5=_F}=+GR-TzwFQOdaMs8H3;3xH>89Cn z@jswAd*4%+NkGDHK{8tbkX4Epl~>ds!aht&XXVV?id{#K zTX=D1dFJ>$6Vza+>ls_jOuJ+F5q4kw;XmuZjt>TgTtC5}2weBLUvrooyhwAn7|*2l zdBOc6su$l?Ro1V_#F0S4&-c*ZOgaGWd)G>Vkr3B5u zVOcp1DLeevB13UXq5Fo05CNu4xp@ImY4!Y_fmMFlepESDbuv{vMk?35wBL7^+@blq z?KQ4h)bM@SH13REBh)`9{mfiSa~$NHOxtqIv`=E0`~8pycE8C<8xe-Me7;*j=-F`L1?{ zE5TNd(9*yPa+oh7!XZGWj=o{zkRaH=<&Dmv6ezL|LPk-71U2NB@=vOmsWU>+u*s(0 zQbE$Xo_z83@CfM2FCf#9TZAR7sDLN-AUH@y4fNW+uoZ>EDPp$JcO%fM_Fe;x9_c{_ zBhu6t59xtcN}hw+BT^hc3gT4RP7uZ*uwzp6r_-XrHsKh(-1O{9!is<&?1ePsV8p?9 z?x6(fX(}7p7wKbf@z4i!o3jgV0emOQpT@kjJ-*jMBNLKI2TXk2$;u*;4s4cjzF>!x z@e}r_gG3z8N%tQ^*l<7`G_rTm4Tac!#kQnxU3K=0tZGy-0_P6cgEC4byW3sCeRARC0ic*7hT*95DxhAfLUN9Ui;x zF%nJD%*c?HDsEP%IZlZ<8Ow9CP++m9u4vp6{qZ;&nyG$*{0PHtR5JjDeSHHZXk*29 z>|)FJbM@7a70g=vUTI)u2YsEbkl1n176uo#33TEYuU;tk_Wdt}DhMbA>(T~~o=|-7 zR7D&_xe+v%2%LvjjVNa>Yj5_ESZV``9a~%d{?)^~Hu!eLOrIM>a@+Pts@_7xPV`Pl zoXb~8fk#5QQ5i$+H1V^;9EG#*!L$rQiX&PGdp|DXN?leH)XFd9+A2*fpR=wNf3ufM)M_uP^HADj<_v) z5L`-ioSnH7is02tPR0#vt;At+J*Vh3&LygkLM7s!KFtTFP)UEr!8;4 z`n`XFteKaW9V~^7xsXNXl}O7{%`srGDZ11jpy?ioiT~-gJUE~T_`?KdueA+9ey!~~ z-seF*u^}))6!9v42m5Ue`wu)02F}tiAz9kx$ty1mv9x~WpeM#KrNsO*x*5VbA_B2r zaCUz9#&Ex|JKJ4DbsJdLKtqT-!G3daT@7#V)MSIafAv0>hAeERW_0Z9mbvBXc!k61 zpk9m52%%m}`&A02lu=s&+V>PsU1uu?`TCFZo~?0*4blU(yEa z(P`AQ^gM#4&2E%H4O_o0*l0_fPi%>%-7*S|c25BG(|6YvwvqB2> zHL1iP{=2;nU&T;Plg2$ky7<*v8?nOB4T2 zp3{~^mPg=q(QL?9p%JN~CHkH78(;Gn!eYJ;K?aK&gP<<}qCIf6#<+F$v|aOVd~-WC zk`DpEJ+h}JYwnOnJL(9HE1FQbT-;&5 za$%7sL}5%KSQ|Hk`Z0#BP+ZWxo!?v1fZpG+3Y}?@yX-w#QnM9maoz1t2DT+nzLfpZ zU1orTD5KvYy%tpb7s!(_>w44J=@}2ev@eh7Mc@5#M17g2!s7-CLz1d z!pR;g=w@}a_Zk!(y{P2QV&_3C!#vbni#4i&>y%+wI$xGb_NeMF= zx?}71$w<8kbUhh|n?6Lj1`}){O%?=L+1f zXpaCRxii z<3YEu+4o<3?dF&R3dlDIksz7@_bg;~gn!zW`l4s}nE3EdLhzt+@E?R_Gf=#W+1<-z zZ*TQ)r4evpovx2e#M6he2sgDZwxP~!hq;7UTiOEUzSfR!qH0-h?n5^LPr%1^2GElF zKx}LX=@@D4rM4UTaG`{Vs2K?vDH%!ZGw{9>J)r*sI1~j!w7bu=m;JY1j_z-O6a56r z?Eg$yc>G@-lfjQ#+I5?+l;D9NKRa`2MB32gg&D%@Nv1H6@f@Kw*JjRRChDRedrYr} z#dcz^x)Jc+fCkEv-w)lVe9BNO{`B!)2jxs0sw#tl^; zXh&6>sy%6C#Ux$uHb}yaAkg{^7MCUV1f&z?1{N5_P~Mx|X)nGwZVfAhVe*^9 z#yQM?99bi(8BKIi4b>0;Q|?-NRWS(#yOU&OmOH*wSViUqG{u$f2lUoulA}ANkC`X+ z^9Z>Z5^rL};M)x!VNm5;{5pqn2>D^Lzebkk_k)-FCl&M^J_!akH#hF@v~F6n@rD ztne^+cSTl%r_P0&%4}!T>;z1M@W75Fv@V9@T2tY17UGTJkfAML?6~Yq6_}g3>u3fJ zHkhLhR`-;Hz`a*;OLG?Jh8HIk9i9a6576s-K}DT8X0jxuXmeI9=|Lz7~4o;s6)$~+1gROQELjbot8nWWq15>vXw;K|`C zH35hi79JPaH!wUX9yC{?C=@kR3{*4ZL)0WRSE{P}zmRK) z$AS6w8J{-(w!y;mcgX!?{msAEs7#O7wnY|00d6jH=u{$iF@C-N3KsL_$xlX0Lo!`( z7NLoU0hU9QbM{H+_dwRkO%tic-y#3gCzO)1#b-RYYhYoLlKZ#72a+8-3p?u@E(hqVr{7Go7)p~=>+@tI@i*)W7E#^IY+ zx85^e$~Fopv4sG=v{wt3Ok;F3D);&4kek-{&g&|N>~&EcKc(6m>xp``M80|0tj^y3 zf=Suwpl9XZE>TeJ{Sj0Yh@35Ay*Rs3y|gbCt%{8aly25oT`sb*VWd%(rVY&wOCx86Ky|XnvL7Wk~1;-=#F$Y7C&iP_<9*-pWp3?6MpDuJ3P$&5nsC z^D5$aYBSMIvyVx~o>v)&KO)yZ=P*5`|GnAWEUO{e%yI`#XqqI&=!K(01Oi6Q0D~WK zlY07=0u}zf)!oW`hmQ8lrw>hYFuED;TtA+0kJWgTMod$9<*|=J3=!Akd9K^bbAVxi zKH6ILhHNn{Wc?TqZaTRyXCKGu#2#HyLvbX3LHQVizB%y-TMm-PDK$|23o zlZjS;ahuaP)ZTu>Yg-Vk{gC7LnWNCtjgEpzuUH@kol?Msa<9!gjI%2**Iata(#_L! z_Em3k@i?Aq>x(J1a{mKdYl4!-oLseW4bi_x~{+Y3dVH# zgZT>P{0c<3e?g%?dP))HFYM!6>zBjS?dv33cxk5}bqyFa_6!WyW(cgTZVtVHW=F}O zu%e7Z>5zX^*=pzs5fso@)7R3M%r3iJ|7%{b)oB>k<&$$Z=>KV(MAFXKRNhJdAKUr= z9SbX^h%A8cZi37P4-#-G?OH?tU1as$6Ie4+lShRm@U_`)3JRd#Yl7}VCU5hKCX2_b zfETxY7+G^F!-gaTqEWxfJo|RHxXQc!aNp zYEJ-dJF>x;o-$Wlrl0&eD&;!VZ zZX7XiO4Of%afXWvCJdRNAo?wz=e+zLZ@}(PC)2721?)f4a%f1FQVb!C*r0Ha%DQ-w zqyIL_=FAp$h25xOR;NXeZoMM~hdc8)B3T3=|nS?W+o2;Z|_S zo99dZI?r&+35|F5YudRl41!D1o)CXUF4P4dPZXv}!%1X`ajfmLt~Af`)-~ z$HbE0*i_+oWDt1UVm!VY4FzCsb`GCmK?1|CWaj+@lbl5!WgW@cv|1!c3?15db|W zrm5f{@rd`Xs=r;>Y`)0HK8RCaT$x;SBy3>esqTR=)pd}bp^jW0qk*VvmGU7cm1S0# zG6rI+ul3!2np1#D;}j($#XrqyFej9dfpCscjxLeR-pgMLNmC=2hsQsyDcav!)Bj}N z_CNTM{tSR}whre1ZU+3mdsGeFx^?`WM z#f3wnlu(jWEe-DCP-=tg8Kjg(g!ZXb2)Y8e;csQPP;95q^rxCnRjdStA?Q(ICsvXSn1^u3yR3!CvlEx zi$hb;3+I@gGlaAr+zs~>m?#(;7#<`IGzJKzZ{GLcO-Mtpe>G50Mlc_`KcN}>TWCc8 z1{%4a#?I!(E~0i03i>vt|GUK`F|@UD)Hkwf`1P zStFhK)2r*=U--N~B6mSIV4Qy&SyvUk(uovXYtjhlVW;|;DQtl7`vRM{eSFV%33KZ? zoAN~x3r}3ecF!0)teb^CVgG(BH=TMiny*7son z#0w@G7G=jjcu=z;+jz}dlE#k=H?+nL&-&t=MmbAfaI+uuGdD!;Gs1bek1GQ-yCwSkO04t0Z6Fz*iRV0XINgQCgXM~TIMalk<6 zxH34{aHl@DC!Vn358t#P0Ig*Ub8x{xLLKd|@dv1wkqw;Oh+}N*17l<6ar4Z4)uqj> z%2a-eNGm8U!U>!i16Pi&I1g@@szaUv#x?pv3I`n)tQ%rzHTo3c(>lG!G@2dD1UIfr zwWywiRPR{(!9d7na?&Ve5jV5SpV-3V7!@qcOM0*`WECIQERuv3ii%Q{MRDH;xrqH-bE}j zM5tGC^_b8ZaYQSC<^?|%`beBpxN%vverv2r0IJ`+L%#t}rG0ehNnO2Sz((OJjf7es z%=`-F^`zZ77`jiyi-4v?tBh^6k|Bw5NxaDqyLy#3)En}qD?oiFWc?v-5}xEAG8@uo zQo)3wQFg}(q{*0m2*-xv;ikCXO(=gD!jhx?J)O4L$5dxKcXo7G2-GBQzt~!B3?(i75EeG#HkPj-;tN+XbSs-LvoIy;8Rrk&?g<1{60oz+(!zO? zM`@?#o7o^v!$V>dH$6&V)QB0$W}&=Dt3_Pkanh(3ooQ+x3W|H2*blqC2R>`=|_YW@px2% z1%@MdZ}>K5+}Q#ls}?Qqg+Y_EO*>l}$L3RbJe~=oZy6|zA|Nrta(3V|sSdsQE^U}5 zQS#BW{|cp{(_ayQt7M>I@vGXwgPw(M&a-J&@ZHLtXauQcz4SWnjxhmYmSw&T=x$*m z)Q-<+h&D~Sg9}*nveWR?j+|v!^ZaUT$}>(OQ>d{rZUA=!N4Mj&-AX)GpacjuD9-3t zZrdX8XUB{QeCsgLtEbuM=^XR7A>xFOdyat!mAQ)8{HbUOp8cM|umJQZ3w&W|EhWI# zp*J`21Qxljg9eF(Nrjq^2noudQmWz}Noa5P(9hxLu3EEn*)>}6+VFImQz&hr1KLp^ z??L3n%z`jei=bIDY6Zxh3_XTf2^2t4mQ_?+KKX!#IeZ&y0drqhbkSf@E*D!MmCdFF z(RFQeFMF3(*Ao*87z$h5w~)1nw6(-M&$4!6bN&9PwS-AQ?2%IeRFL|~i#cBJW0J04 zL&+VIDj=?!X2Jl;D<8|)qrLa!TQE(c0q@5q)KC!z$?jQEpOu<8_@y=X*0g3DE!;Sy zDBGu9i!NpM_x&2y*(HhF#SEn^BHEEv^H0AqLoJShQhrf-VQ$IPADcNvl>_n)@kh&s zTE#pA_1BY4-eDt+Rk54I$&ZBIb6CU4@+2Dr-1Zrq*EC2g2S$Ywg^ZCNMumENw#;+G zb>>Ky8aQVP>dogAYf-CG{Ru5V)o=fs3XP< zLwCQWZ{})VToB3+LR?)4c{;j3H$}+Gh4+noYCxgdx{%}!bNDJ2Ggc@Tvd>>TBL_mf z%)CEE0Qk2eAo)KN0W)(`Gh>H;Lk5)B9W+ExKF$KaH0wzzkeZjwH`E1YwM&#`>RCwo z=jYccawS*0Y3PAs)3qkI!W?V9qh)$c*%zE0hQ(e@^N(Du!2~l}j~@aY?oXfiI(ZMS z2Qxcgzv;ok0=X4DxK}JX8V5mbc!@oytGO=mzzt~n+3R_4l1ekoWcKzu#vhC;>BGv0 zZAB7qpXNz0{H+0Xclp=OgF>=Y)FAy{mEA#SboBU(XJJZ%lI3BlsXt5O2ZEUdxlmAg zxoNW2mi7$KpZYu+>+iu;_wJZ_XHII#5KWsNFOp-$8A$T5PJH#6R5%j$^gR+a=+Jf1 z7`3?SH%QhfHjctkGfkod2Lk=sG4^#Vn1gEp!;BAN+gor#6_<67&@@a*y=|d16O3Bs zhgk<_(5rAw|4@OIYl#CH>r~>+LOdLE#i2LkY+|sJk#D&s_dAKcEMA~rKz(Yba=JH6$z;%k1*aW0%(mWP5LxQY`iBoH?M{zk&_#| z`sfjWX`)$E*n|tdyWMb0ariqWjL4=Wq7qgHr(HsK_LuRoWN_B8gCUq(Mkd)(N&r6^ zD)rn1U`rC8IYr?(a)TygJ!?LVR3hFc)M#reS)C4-;0<7^^Jg5t_aJ{h7o1Gn%*oOs zo^G7Jt&2O~wCNU98+VN`CAG+o6B_>~=Pu`=A8)`Db{_N|`?wDZj z-lGGGPe>hQT*NJmydCs2by{WS&cftU62|zS(jq^MbI}9qx!(Ot&WUVwJ)0!vp=j%La|Yj|h07pAOh#e~3NX!ZH120eq}On7gnF|9sdHj7=U# z;}db?zeQZ;{|fORwl@C;<^RjhpEX3l{74bZE;m1Ga*VYbB#TOE(mrld))L8a!?Gp2 z?>kvGbFunE+vzW+rA7I*V)aR7oMGUP!Dp&J?M)*!GMD2{2Dxa^KtOR9Nxxae#hyZNYQGC#Ykm8TytE4 z48yl|tjCqukqvBk>*6{{l!sF^d}kNev)$!$@n(%@s=8D~>*fvjidbg4HS z0O|p^N7cv<4%3i2gI$FF{ag|m9Yd1gd3c|{HqhY7eh*x2mm$nXzvgM8N5&qj*Jhut=iDG3^4C}G^*PTui;~KOap9SS@kXJd9AV z^}^}!#^1tzzOX}ewPSL;JRG*Tcxs}2*z`BwXrFrh!3~4U^KQPC2YW1BZ03hcGeN8C z4=prb&$A2ppeR@~l)bvdNqhFmaloSX*8kiKQfT5%B6C)85VAzL@f`3e2j6$1*2o4JOb zWz4&<3eB`DrC$o4eV=wpVg+NeZY~73P{Oyf{o&p$)5$Du;)=$&j?6V?n*zy>C^2dS zp0pKYa!_=_+mAE~Xi+KPLk-YkW#ykJ`Np%{J^9cVzt(j2JYG}sysLah{;%KvUI48Bar?#l-iqVJ?(^^MDv`3^ zF9Z1T6WCGUY~JO&y^Rn;sT`Cr1g7ty= zNA+)B-FDO-G#i8Uyd~25`*|URUQD#eXuWTzzXFE)mz(pVzK{6%vSToz@-Njt1eAbl zDb%gqc=ksU>Do5i?8a^Fn=|N|lVq2D=MZcVi5eznxVI3{K`A>e{-q^O&T@s;8!(G7 z@h(a~@^i^)(9Yv~V1#+e%D9cb33p&GKwGnMmY82q|BIT08h7h{zBz$#_sj zbpA2f>~e7#N-%)xQ$T&BYeO0R6`d7~(9mo;3T|U4#tHAUnBWzcYT8)13Abo@(2*~O z)m#qgErOxmOJ2z&NF7BR}rC-(Eu;_uD~ z)S(B*DF;VlD32ziiZzrYI-0*Z&$V}>WlYXE@~;knMG zny5%;X-D)mO6PPqKS*_0@Zo=Zll@X^#x$3A-&jBI>600>_ACmPW9f9b3EM9o5_IR~ z5Lt>loiQ{{s|k54q;IADPT-zLSJtElXjookiyGO)hV?8igG7aYCBt`gZ~Ljx$g8AH zUC-q0)l;sNO0R@C?VE-1F8?m+5Ed#=?QXfXWs@Ov z{5Tz75wzN*o2cGYKw=8l+SNnAS%8*8qZXz0Q`1dCL%BYJ9DGEb`EI7zAx1Z_hOl|0 z!oIqTVRIdI{e3-m8`}g4xHUpez)ff_rteIZKTd;YKhdk+CNwUN4F72=Fr0kwF5HIx zcmea)_MmwV$JW@oSIsl8o5Hb2Jb@4F>o(4k2cT9J>d zfX_olk4{Hb7*_{K&Gvcq+MPQcRov4GsL%3hlr|TfU+cvN#khdj^AiC~Qxt+1?4}U6 zgcX|Dl~mw_g~IcqNw_i^?@U3*7}gWGLss?0>2x&Vdn<}YIkthv-&UsDrejYnuqv&D zC=nMxY{Cd8Yh%GucP`0u43bxpiPVl1MVTevrejGRPU@0oNs6bfeQ9vGYNhssjPUgL zp*EgL-L*A%G<0V~R2IoplCRfZk=r!A17P@P=euEZo~ohIcPcmxT9(P=dX{BI`YTTH|MC35SoH3FUNVE;96%+pW9F;O zMPv=CykzYlp2+=TxCp*Rq>=j)w0k#=h6Jk>?!%W5X+sVByz|IGlxgLQ=@~NFz+imY z+>QRt&EHSp`LC<8Gv{{)YM*=?{H=NTPqtJ1`+WcR^TQA5{|)gS1aX^2mhTbQ^_51@>3+wxxgYkw@n>IUyGD=43zqakIaN?Tr49EWq8^!yogBA@uLxbdX4Y3VUi?ar zf5z+I?o#<)ii846fTz>`#g25xEFxCU7GBxN1+6A3JuJ8jbvMeFJDGC84R zPz6C8r1ydjj+pKk59E;gxH4Fd*Y-PZT{8RQeUbM#6uWjB<``J%gF+GHH3M_iOSBnJqXb;w>sEdaP~k z`tIWD^8Efx4+Tm&tcNbb3=L_P*ync(Pw%d}YZw&7FFDyjrZ^)mldTc{I+G-`QQZD< zf-Ho>atL4YW2TC#5+2>4KEalJZJE4L);J`A@dJGditCxCu6>HP;IgE`D}?|SuRp(= zS?Jddm^uX~o&0E6SH`1)L@aG8e*Qi+O!Em@^p2oQ%OK{p;0kSH8_+?gR-Bc+GTCFS z@P_ApB8~~pH2CQ$Qp51GyS_;}4^=LYsWh+9Tqah9oEO|OhM`laL?*1~q?%oOv?-r* zq&Fw2c^5g(Cs>xb2q#l^(COFdMOBBJAH8?~$Pi^9YYbZb?C-_ZN3Im~%q@pC((!Dt z+$l^Ji3PI{I+6CqKw;3v?aa`p+;u_!ccYESJ_t=cLcTsRP4U1-jy`fiXu=RerhhgG z@tE4WoIeF1;%^1u|Ej?M8{t9oGdz6KZ>{EGk8CH=L?Yv0kL({?mc2Mf2iqj4g(JwA zUlK(XzKD#Wc9FAQo!fTB|Gc0(6*?zeuB7hunH}6s)XBBdzEdwVn@)H-*xZaScYZv5 z*7({bqayK5?``VLi;J6I)Z8c)c5(PimcEDLSB~cVS_c~qMT5_FB70gN#gNK*1!rqv zPy?4j)|09gx2NkrMiVlSd|9?4kG(rnyRG&DP_`C z7>#5Lje87Sqb_0@k~z2&0SZHE43}s^75fmY_Z4#CzobGEJTfXD^><6H7sJDt*4&ZD zRIwx|s1YKfa1nPyD8q2*herLBc~PUFjsnOwOfVdr18N%=LTj)F_A$DA}v{RSq zWUNi>pb}}r48pSQh+Z0EDzvXO>yX-C(KBcttt%zY$uSh>=br~&EsUeLHil=~j~Gy| zpeqU(gIzF0KchnzXY^X%XCByr=IAIb#-A^}mei3avtdZ0J@TTShwIhalq$cg-HfEg zToat=k!c&W+mR+pYU+(r;$|{Z<4Jz~aO)NJb*vctN+s2)bqE!gPd<+$M8q#LPMlnM z%-sgD+VdsEk<@rF9Fe%4{X?8$V>e{hK6hS1Zb#pp00cYS42qWoV+pS~g&E+|3V~Vb zp@+ExGzRFB+BbPHB;%Nhu%(ZX)rv-=h8v$$;zNWh31%i6V^BFvK4jFtI&&0q`rIoT zpg@{@cpOQOF>7(?@B>qyikVYz*6qM`4=vUg;aw$a?YLxSn0n(En_D{zi^fnpC9KIc zZ$6{L_4nh|r76(f{*>E3i`IDrfij83y@o+$Zi1&5?=+gsQqzn=Z23PY*0RG1Meewm zl1zLbC9QHnT+J^vsyP1YPMx?M1`?uBwC)inL38-OIk>%J2^H^n5S6s>fPPcR)zjmO zaPoN;a8u}X-$Tg_`&~ds6UlWY`s|{KNc!$-ixkT?*9@!!- zGUDJ8GxbFqLyfT5aXsNM^NY1oK8rrw4-y4+P2n?ictp_qh}Js`+sHD674ge%EJG*(c$mz+yB?f@?X70rTVXjg28@bf&{oyg2iKH5$S-)Ytkb2 zi<7X5N>R|}m;dPb3ef{dXBIw<^*WOJ2$c0Y6iTEwo}Z^njC><~L)Bh4H34BHLFz^_ zxlgWrc)#B}bugLS{CK}Y|3%`hCw^>Vuv{l&Z75XB!cFL!oM<&G$y4Pjav;luZJ@%e z;@X{x?sE18L&!+E)GU~(8tEalv=6##a7vyDR&y3szyjb;oLt+?I!kvVYEWO3p|-&y z*_7Tpr&?)fk#ECvEmw#Z#yg+0R~0nxz#AHzJU~M`g#_nv-(p+Fp^? z-~*vMp}}zh(}T|=hIFTG=1R+et7xxwm|SFSq}P+y6f57j@4_hTkI*IS5d zRA@NO;g;rB%sT*F;PjkqmnCM=&0uSvde)Tqc*mp0+sXAtj6e(twfZ=E&;0!tTHJ#P z9UwoB(^`>wj6KH?s8VH1C@<^>S)&8&$7P-imFzQi;i&g&DlV#=6OVzTEfRG2T|2xs zLzlLUV>l$Q-}HWf*aSs=D|dVDN~+&j*H7YWa&f9K7E0b4f}w*k9VtH@_Z(;bm~PjMEzyKNh^ww9=6bw{N-LB((HNN#hQF`RY_Xq%=^Dj)V0yph_G z4sLU2%js}0plS~(5&qFM*ShOSl0_vDZx&UA#4UDR?OTKeE4rF9l4L6#{G4mu9wus~ z>^67*+^h@LK>mj4BaI0Y<$8&m2SJ?umMQjsvIK`z|vpgT8DV;3$+MavYBlG53qV&P)S&&ia;pH5Gl)YQ@yt zA$W9XEJh_`kw&|L!oenUCVdt*V^fR%=V&Sv>Mg2JEjumNN0U!x%E;M$tNI8KOrk!j zbT~L6fTCeprgve{ZK3E;ko>lIoh@l=xug8}<2*})fxR4Q@{q}6t4!aHT6-bj&#q49 z!RssDBIAz(wN#!A0_~T~_7*=q*a&H%*RbHn!krD8lb` zvfenZ6}v+M!Px5qmr`lb3u0O)cL}+y36Zff!JvJRg_)-h(d)=EW$C0Hmb82MJ4M(W zBBy=4TH$f?S%XJ>zu^G#(o8Y>@a+mg8_oXSflb$y4%i2}1kKo@w5Jr>2jW{if-G=5 zg2+qVWL23gK$#h*U=%uJs~EjX7T@+29(3d%bY$3j27c$1BmGVz{a||rHBis>qCaHY zzZX=`nzz`*_OKXPqKOtjS(QMYt`AhXq_|(?7@yn(YLT3~iWvwlLEs@B z(5EgK@J_$<8RfRB9)*6BtYwv)Q=D1@*8T|Tg&pwl{CKT&FrCXqGypS)bVO$N?0ZCRB7B7N>Y=-VilLb=-T?Q2g{)0QRq${gS z-S>yDyl-5WvaclwJJ>$1uhZ9^zjjCvZR3!af`9qK^|#mf#Qvs2C>T4~S~>my1~>ns zY5gA`|K{;RO+{o=4DXIA{a&&Je}X^7N;$w-iK-d2B32?Z$u1!pq%PRY&?GtOWVnG|B6TEiRRLW?XLk-)rjh9To{Hu#2nm1G7{CUGUBSc)*vt zS%sQhW|f($!G_eqy}7DP_H?0K=`6={Qf2~7$(^aOVDUCDe>LAI!S)z@Gzn+q3`RP` z*#B5NT1G2_gGBnz<0`09He{N&OB7WKc#2Hxei=5%0aB|$hq{`f7uh$s5HK#df&xM0 zBykq;6f(G2831A*OMsm)L52fW{%*<^Dpkv{tJA78j~Dg*Q;G4D3Hb9SxC2ixiDW5; zNN*WtyepO8eLLwgo%+H(6^lNaHmVqSOK7R6{hJE!L>YDxGRJ0?Xe)`Ec>xP!6@n0Y zg!!Rx$$4-lHaxG*WGri$*1BlToHPM z4nwzD_>knRbk~y2rphp$5fhtg%N7Zrv$lD(;{o)FjEQ#dD#<9N02I zh0s*iuhFFDk4AN6{5n(R$7qGosSRP*2_-t{nI~3C7`|$rtv;=C9^_%pJ0_l_KXz0z zr)nE5T0j(E4$j{iztA@JxhUAIW>$wtY-CEzvbB$TSEC*BM#rCZhEZen3{{ydKfSJx zz0R==59gr%T0MC&oVdI-UY|XpR;pmN4}@~N#0r4sgK=Z2E;>P>Ed&R$XAabj7itud z=LN3(h_PDrbk{mUq-4Z6lH(GjjD}vHdUsvG31Jy_5mi5tSa&`pH!eJ$d0H0Imgtsa zxYiL-iZ-=?prW(`_CB3!_sPFOdvA3_S^m)hC}P+W-u+rE@U4Y8NpmwEQD@tw;SyQb zn4&I73u?VaFdI*9VAQ2W50fO|jCr`-eopH^yrgxjH9FC$7i^g_bCJROiHL9050%!W zSkGH)WptKul;Rk*tgf!*@lwAEV+Joh$~`YRoy2Giovt~?nkz>u!+#GrXHY+1FmE;E zq;o+Ww47Jp=N(yTkWa30Tx2!NW52AoBY?|OyJ6s-to&X<%I3RMSZCansY&q!)Ivuj z4%^uy;rkS=%0Ya|t8|6K$#eiV z5U*p*vxdDPybTT}6l9et=gg8$U1+f!+;AfnWNw0Nj+)$AIM`1FXK&F)xG1o>nU{447l*CtH%cWynv zDPQL3YmpWfKh)-{r<&viYb*6M=GOF4K>C!ym9>G?-kKdqd@pF$|$ zhv#opwv~xVV;l4Ei$b=OO;>A%P1Qn^y6g_eP0c?aZo0FNW}7o{h7xf&Cg!Ya-2J%b zKhX^qG&M9 zvu)FL?Dzd*CiZi_v%~VG-Yw)DXAU(R*>3sp0bS;LFh;8#8aA{+?CIyRp%b*XAMLkB zchM65r`u0{JlGNa)N)an|LH{Wf3D^KY9ajhr98{5SQz#zpxci&kBZ=bNx7bW?WhrZ z@x~MTO6r6o*G_r*gqgJ|mo&Y{vd=7xWu|Sc9TOKvI@9jzL*2#y$Jtv2MY?s}nm`qv zP`JChyA*}HySqc;?v}Va6i~Rkd*SZx?(P)Yd++~rM@O9V?fANH@;>8@^^7^!Tyu;a zkF6`&kGG?xE$}D<;q8bMGBy0EndYYZyi0POK9<{TWtnvjogo^JB4hJ%61T}ntw~PM z*~Mv|B)ZV{>v++gW>MBM6(&m21-7VmGYWQRoc0Xm10DFn2^hr*>v%WGGK(lEMT_7ez5JKEur9oQfW^q?iOsC1WXX|$8w*K8W%ZCFVPt@ z3shP1{AMP08_tkLh?5&$jNm-Lh)cC~#~K54YQ3B!%gjx3!g5il*h14M#+?Wd5e2NK zC$X2gqoF6rgvL%aTHE@}c=P%mePa%rR)LDsKHx8wV|;b|o8?GzvJq+t5J7A5B&(08 z^wa3m5cK?H+D$ol>TUfsVWF!@O}}ff5OtP5o^zRztm3YskWQ0;ypMZ<%8uA?}Qi#i<$H`260chu6?4mKB~tgZci+PTRHV#=A3 zv&B3;wga~H_|P9tgALIkO2p=96z`XWFV=)YeriZR#`$lM8D)rNUJ^ee_%b zsLj$%<`_{8x*WQR;1q6{7^$0r7n3h5=0&DiO3ZfS8Fj&ra83)u$%|$L9#EWV!R;P{ zliD$3vi&DoZffq4vrV>W#q=942s|fMgIeCC-vFq0M7UFoKN$XE`CM^>j3Y0n;w^eB zqSn`En}}~Vq;GhHe2K~tYOTL>B~WW6gD6JF1Dcxjr`eeVo>BNA;Ds3-w6=LmcBxrR zCgGxJH%8rScw3^Eod7YAv=m!Lzsi8;Ks@#Kg)*%R7}pX35}DlD+!)B5u8(r<+{<)7 z`L4s(0eT!`8ey&}o+?#p^ycZPujR^`bCP7#*8B1y@Na-Qh6u^oqjXclNow5OZ43X|Wt}?DllzRm+v0mZ3OeKlrSc zNCrNlu5YDdbDmkZp&zlS+qCy3Fy&kiaQHOeQLCBA2>nQ}GkVa_!S(6siAn+{pCE>3 zzNLL>pE`mQpmBqmi}&kLT{vu7k4R!1l0H|I~crw8uZsnu@OCHxDd_62u%+3g=E2 zN3Nt$z32`*4h{}{IxwazM!NP59G&?}$r&lcU&WvjF$WWkwY<%mQ9gU$w+@D7#7t=# zuQd|gkln7yk07U)>!j-2xFUa}-?IKZSRrRCaG3b)j&Y8D<%{Mv*=O$lx?=>+newZp zp%S7S$&%c6epO#nX*ac-Y^{}XB7Tr(cUbayG6m=RB{{gRK?&jhsh6+yiGwh zXU$}<>Wq4`H<@1*5ie9B)8~MDEqUBVP&|RyGMUyZ^I#W4xb}XFcnLawuoF!Q%OtI> zRSEy8)FtNWMk`kDA$8yZ%utsUQ2k0*#4DpRW&sC9FwMkk>_v+vupcq^wLy$kOuJ8r zMP$gzSwzC(?VJpaZI%!Nt1Blbx_D}8R!dFVI(-D=>Mf!6CLI8NE4C_u;~=aZU1=rO z8)l9zSyAN^rtd9lF#;Y)4iu*cn%tj+ZfpvWD*3;P`{q~1{z*^x(Tl$ zN$J=!3Z}lKLG%>!EZO=f^(LE~ueU$_Q9=5B>}rhF>Dc3yGp_>&swHsJ<5mRR32#v8c6mm^eKWRMFQsQ(x`HuITSm|^Dkx4s7 zVuoEJc}6+noy-2J0l z@?u?9NJzFY`|&5gllkR%)}M{^*81-EKXZ&=6pH3873m|gbIgA(8Jb%X8&q}axr>tN z*sx!lP&ekC#T(}x+316qD~@Q3@~y=ji}hr!ph^K%ly@Ckba&};8)g$!+!>b3BLftv zlZS5ZS(Gk78*(S-1WXt9Oby+v^ehM1z7s!JhQ2ZT%Xe^-XDsgwMNV3pz5xyKj=w6= zw%PILc5T4>Rc}HrQ@@cen41%FFrgqM$_rMW$K0yaWr^I(E>6H6$dIQ4+_|vmtm+$R z_;p-HTq7absFYl_yj+d#V5m0eX=_S;(w@#SH`xYz2wN_tX+i0LLA9YQL39$>W{h=~ z5EqjbXkX9H+J(^%3!uUJEeYDRD5u2|Lo8)HAdZ^rObTWF%HJlfo1fg059QEQ>l3rn zf?IuP;psS8_ng>a#>*mBVxu`R{ZkW4UWN!uRHVm41RH9V@=#XSAXkHhf#sQ95fyXQ zph6R4iD7A-@&|JiG}4a_)q^jd*rvPVnJ0Bd*1!cFT;mQP^V_kU=eRg+r&<$}mysHdIL$fgS_v&*R0C;C<3@C?-%W zc1%MpCkn^Vw75%Y83=XZ^nT{Ot?_Z@*cY080;=tSjhL~#DIXgM-6R8jx1l6I=%YyA zw(=8DPVr@I2A`KSzD3_h9pL=)S3l2M#zfqx-!fPR^2KsQFB2he1>v{3*lOeyjj9>D zC$#%tKhiC-wAAqR4iSrMlo~H{OGm!uax$RnA_ux3zo4(u6l2$U$qPs9r=n3eJS7ds zA%*c@@DHRcmu$&IAmS#G(Z6Qg%LD zh7MOh9!#~ zleXIAlsje>jc)ctqWRUKWmLj&A^l-rE%<5G+=ZNseB;zePrhnOH9QWYdckz`#vkQP z*vq`>Z=+M#Bmn{&)Mf9VKEsr6FtMcQQ1SIfs8F%5rno-0*%{6a>E=)$y95J?_uyYQ z{a5d>cLUqSv~LS*-k|1C9_{zg>!vn8kS~#Wb0k_m-`BSK_`O=vVS|(He>Zwwa=aqW zhzGl1m@~y{JLf2x)cT7CQnbj|iaep|Am$+N?`ibd&Pp+)7QcGx0Mi>fcj5ZC92hkQ zbnA>ukOU;Yd#90%dEB~0k}&+B)TG>il(38c;m<-1FO_pX%bYI83=M1ZD|- z%*iczvPm7-E-xC6DW z(?cwmiz5Ndmd_Pax&Mu+^4aL(?$~nWtG_EK?U~lcLjZfbz>^kTU0aKo6K!W>k1P6g zw+8b+eW{n0rJc@SC_ngDdj-D#8t0Bq&VsInmNte)Hl}3E|FZq|zed`fT?P`&Kv)AU zDO67X4HM=V&7Ctma)#Sx#^NxYHUyY6GlU?qh^dZ|hkJ9QG`cVYY(fn^R$4N~sA)Br+$yr-Iu- z=IPKb)foVeCgdz2N;~F!ml8+AByN6N-vY}g&X%rXJV!ZMR93wbBpM$wFR!jR=QXG1 z%U|31jYFbeq{DOXKXdEtu@5)TzefUu#man_kpWK_nZ+Gbo&M#dX}k zn{kg!_00QmQ++*D!mZe1Lz|^cm0{8>`%)nweIAcZ_}vespY3*mA%i5a2NH>W&*hDJ z4|%yVB1N-Hnlel>e<*veyoK%7(St3H`w{wj<94epA1gBVUS@l+HV3>s(3nSMK&9u=V{s{rITc?K`dQm2W>=RhIAx4-Ay() z{Wh!hwM{?%d5fBh zki4Mq1q>|bU!SV{A7UN;tE2oM53;R1svweYQ)idb4P0PYgdMuHqMUb6DhUWFc@Qof z31v=x%{3t8jG=YSdY98Tvik=rf*7js+i@(*q%OSG4`Q;3@$>U>R^F-g`=KeuEwJ?- zWssK=Z~3Z4SK|Ob5Kq4)&#>W$&IRGKkh(%@inHF9L08XB)Q|;o@Vcz6G@3TKeg?>` zguO$3$)mbi#_92As(s-Hd(`}>Tee%}$jL#(Dx8dm2QyP{HZ?2Y6PE!p(s2JXj+zWR zL8Zeqybzc}Hnn>mv5B_({jspQ zI-etwG3|YfQK9g{0Ub(9Z`{_HCpo~*u@9DZ(Q3$&QiICEqSn({glZRHlc+Nx3A)*^ zrNxz|Jm0u5v<{o7FIV$)TZ@S|;-DQkG`?H8x=K*v0csnL;)Aj+s~SAlW&`AdWL&%4 zK_U&{fKETRNhY_bs+f-t3c*6BSAe{Y61BA)r|57Y(B3V?i-oRbz0|XfCRqbU%Vo!f zKb^d_s0Egw(`e8l&q0WEI-;I#%(Sf**e7~4>5QPl7mrn%v1=gYYha=Kfv_rloY zcevMyXT-^Kk_YU*tNd;1grF>>pF<(`RVKRqFg@RicyTwP^S+7lMX?aNrV26YfI8fx04~H`Gw<{fJ9-!RnE3v)QMOrn19ARW`!n;e z?a#k;{{M$b+<&%#P__Qs$lZ66!Xy<*T7r6MRqdB8ge~SZkOW;hj@|MH%FHPAQ4L;i z!URKod-zNA)-L0;!ypUK9+U6AICqzMeE^YV=%mTyh0o;OW;Xwyqb-3Ch?*!}&`?S_ zVN1YRY;5b$mkrD&rY}o#j`j!_6oq309)=C#r9HdIU>H$wHMDZ z<}YI|O#=bf!Nw8ZjORpkFGnqfLVv=VTY*l+Rk$L(Pd17gB@4H-f9wrf{OAQ}b^&gB zSJI+^#D?6q&BRy)CSE#rm-vd=f7TaBLE{TBs$|#%rmy|J;d#d#ru+vEa5zemRPK?6 zRp*{A?-F0V2%4#WTyg<_^Y*A|HR0TKZQb+6|Vym=@s&^^+ zT0E%62QfnC(Sn)Y!azHQIY#$CRmf%ebJiqxFa{H_2Dthr@NlXr3G;fwGC#M@x`F~PTvz*F07nvCx|n7HE( zlRNFvSbD*m!UFSbF;DvRytC`&65zC zO6ZDbYfCQ~KAxt+TR7t6B$`Y?xe#tTN5w^Jx72iM#%N$>)wyiwXDa&=KmaSIl;?o9 z(8h{Cv2;CM_o)&b;r1m_WwtWuWOxo+7@eN;Qz>D3owcJ^s8pK^t^p@?;i zW&8KihzyV8OJyFAup?lA4u&5){!+&wOmx)GpGM9PDJ=;y*R1?R)$;}F#3a(S876m{ zkE`1rl)L4yhe-_=HN1p;gLKOl2%tA%DNfVi8dyUq*rvr9;-OfFZ9TC1`V{7sxfniu zhgC-02CB}lH_UJ|AuE6>5VpHApn$VT`c;5pkzP8!PJP&ZL$|FMj0FXcTsyv3bt6f* zXEX#;8aA93-hz5}lj{DtG+nmS=k)5H1|YwoOu}ST{6I)Xv>+NK2HEv`pl;3L4Pic` zFj^#&1x_x&dW+`^zQ!Ie4z-72Ka;&3C>~#aXRiq?c6_$M93P_jz7=wsf4r9-%T7qT zQ}RdR{hzy+t@Xo1_^)7K=>Pg;?B6^O|M$_+zp+PtO%2Oe4#fzy4n;=`sJjMki+apd z^O`VIiv~(%V@jxX@lHcLM3TY7U6CvE*=s(>w8?15lg&^NxD=Q5irxExWhq*CyuNwF zcXe?yBJla~htuEmoQ1DR=lwT=j<5GPKf!TVO$c^Nwq7)bFLheu24?R!vs5%DP>^FE zg3Z`W?5gaBBg78(`r?vy47Ns9Jw0MDC7$xWmmzfgg1`_x`lxuvgi9u`%LWljmw9Bb zcV&wHNa&ZoAnQMhaRx1~X8Ldm0Ld}Sc7&d5rnNm!r>n5p@uMmNW(##vmAJf&{|7zX z2M#ziv()|9hO%Tqay?G$m!E-fL1Ekho98jFVC_+k-cdGFrPOtCwmJ>r$LRb}0o1 zc8^A{LOLK27UVwXIGdI*BEO3$O!60cKq0qlR57=B7wR@pjSPVkm@(H_bt^jRHrZTp zWS%FbOtzBp;OM%Pzj)wcfg9&HZ;9fFt&A$;9Yqmq_#C?m*Zzh&ZTAt`+W}M|*YrRR z_bfKmQ+nf>!>+CBh^0uDXz}5qYb0){5FND=OBImKB~5+=HHgiyOl@OFaE$bqoQofB zK-u)k9+gH6dWFJLT`T^n>S7$delIjNU|YDXNwIWMjdp-t3qyt%BGwtzlLDe^>I;ME zip|i8gP|kQ$k1ai{XUYp0RqpezLACi?~D>NdrE{UNZFi{Z6khdd}YMM=%;PX3GV5N zBxs^kODNK|W?$s zYh|3)g*0oehhjAe1<*`4{Ue(?`;7GjW%&^GVn5kFO;jf`vr+77VY;;ci1^L^5DGqviX=L=obz)R1^wZ6dZXAG5IT=#rHSN8LbB1iixipk35NdjgMf(`UZ_)zrTqRH zVRi)L0*Fi^cDKF$h7J1o9j#wT!9=npMkX|z2*biwZCQ+!BQVg8g4Wl}wt1Om&$*vc zj=WBJX{$vkJf&O$I>zaWoCH~J#>dAI#ld!VhMrLUQVdLx_+^#2dQN@Vb~C=`@DF+% zF5asga$_B8je}K0Oy)^0_jPv3zrWBols)gSf5)H@mV66YzDuS0jFEz7LpnejdfMp- zC$x~;w)PG>YLG$}JUZp_EtvWBUhI5UuiKB{P4IC`;#2VX&pUX@bi!)5ztDs7uju)2>mHz;vxOzl{6Ditc@4NGhQu#DQzSwg zUP*C7p4t?XA>oR`1I&~zY;i%yB`#|dC5NGUYsllc)tU$XP=a_lv^&9Upql;)tAOQm zE{>j6Xh9hh{R@0}BYmab9Apmqe0x1*1Up+cYawlXlA+8H@S7yfcCUpIjXNH;HWho* zRZ`(NS-E6u?$ zcUS> zdvz0xn#hiKWO1CPIT^nutD6>L211~kZy&P%yx#8ca^Kdfx2e-Dw0*ob7|}DZwpp1> zG_Ty3b{+AwsyxVU^ZVqnJ+O81$}%jPOjhd#JWv&}{OGVf-TRy#06%bLallHUT{wI# zPJ=pi@-{4JF)KRtMb0s*{jn$v+GW1k5ERN|caEj#oSz5(#>lYMV59=dabIBdG7 zg6nUR8Cz2lEHR!9=4mbVy0`68BC!hQi-F9=WSyNX4b~uKwz2AA>x>k`KW*S|m;5x$ zCSkv2K<>@!tf}*ad6NJ6IwAA*aJ*Vd9ibpw@V30LH8sr~%1{xlg)iKgn4Dt~r3&fB zxUP7v0g*%Le(9MaGOO4pQ7;tg3+Al#e0N0#ieNf;)iUGt$QAax0At;=S`evdtJ7gPyNZN&J@u5c)|5wdyX)it#RZ{vA%it0XlIW!6?t` zj7_TYMAh$7;0rIx#*bSY0JIN42N|I8i^fcD;kMqC_3>nn`nCyp$|+Z^xkU~Loi9%1XOH+~vraT@r9RrCJ`u6NKRN%rPRKT8+7J~pp+ainX7F6G} zhgHg%WkL!tGJ|QgS57W;#*gP??=7joIuSPi(4wWENo5Vaagb-Bo?0S*-+7!_=3ZhL zR&szGvy)Ho4N`k5EArjP~fTWjk08_r95nP zBb4s^ka_stDO*b8XgOk+p*BWbV-pn58TA=UXQ`1(6{|F0-+a`OEpVgqs9M1+5?d+9 zUgCZl*m%(#uDW+f(Tu=2g-c-AqQOz1Va;CNdnv)7qN8VBar-{mRc$m+?xxLgv*ft} zo+V8yo3t~UpW+-{L^aa1gXPTgIYl++a(#MoW00*hN2@kg6;9rFCt%;;K~z@xvWu!d zZKIvw&p4W4>9LY`dZ{&^Jd=>rtH;#32g8zZ{JhE+^kt3>ORx-XW2ZKalUnb=_|Dnx zA^zTY6(Ip6WUWu(6*fHOzL&m4e*9F$QPMT~RDe{}m=-;hW6agg)9QK#S;aD52KxXB zWa1leiEWq6Ga%u@l?rANAi2X+mj8TVMShE^x;FPc<0r`=F1K{dA!Q=Y$rQ^5OYnJd zYTNT-+-2CL@JHX>Dw0~%bzgv5iBG24mm*Rovbh<2bS}C#zQc7s-kwZ^pJ>9T>UIt zW=>3S@OY&d zaMXlEwd3>I8vEz+G6=Ux<{72kPZ*TvXJYDYi!1}h3gaG{Nm;zn`%)B*3- z$=Gi{-{%)?FwM6RLlw?B(>dmae9{G_tcFU}h6H1+69oAhLT+#-C{ulYwK?>KW!Y-m zt2rewd;oZ#^P{tIs-cOE_n+U+G;Q&l0^em)%f&`!Wc%a=B?(abYs)C#xi&!AHl#8= ztR$Z}il}E=caeTglS}O(qHUtQiK}U)Ow1O|p(bDzp|OIWSYxqDmNvId5NWU2EeXh# zVOo;x++$C-`r$c(NF+Kb<)wA9&D_d0FOhHHB_6P&mKew>b_jGGz)0&)ih?b$)Nblq zuvFukzy^un&WCM0(xx-SHe+vEmC94rSEy4_+lCr?RV2SKDx@3GZxLN8u^;oDACoW4 z1pXQ*eQa?6cT)46;2fV4qnGd!ALP4a{;O;v)>(|sQk}Es?D0}bxoiJLt}Hv*Re(Em z2#z0^DO+9Tq>N0rBZ$TC9@|qqHmgHP4yENc6W4|Z)UgS@|Un<8;I43`8LC+IgfPTLZ_V>4x z+-9+*h{F=zvV0|-tsMvG&cG;6UAA=$;cE~9hwE=h!ig)BwCc}=NXtJ9@GG4Ai9Vyr zsPXWE48f&O%3EjAUM1fK1SmqD4@&S5#NHLR`eH2gRU%$Ou_Vvir^s2bVWIRp6?hW5 zPA1P?L_O;rIm*87Zd}sX6$j1H>8T0bfHPBiz3j9(3AiyR4<*?@#-N1jM-K+lc?P^V zFAYP|e*4m)-nM61SSbZ0jJ!}l4vr);;F)){ zkV_rc#y~%gO#^x+`J&j0ieRZ~smJnhnoox?_{>{w^6ejCY zQzbPOhwoB{bw4!W_=ZcI6ZzviTEbNTZv`N*@X0vVIw%ds?L!{X(6%E7(4*<@SpOX8uHFKEr*em4?A}E`Z@Z-1n20I`g z0mu7^NQOXXYY}_vhUfPgC0KP11XXy5boQ6msGuD)L?D}keQRk@_YMURS$4gLpRwJ7 zCF2ccZi{Hlsbhm|h-#2|E-pV9H)*E@vqx|tl1a?RxWO1PcA<7^7jYpX^aJ7HXPE{| zM*ei^@lP4_U(C%&z@&{_gk+ zhZ=Bp{KI~+$J^@dUzmBxlEsluh~6d_V>#WY_VdMi<}_UHC~I{F#Vn!yfHUCtCzz>X zrP;AA)40Aj0rP!`RpK38yrdCR-4cM)8ENoHnxa-uY4TEFar-F`*V~QpVs+4p-ar6)em?ZH^^M!)38zgn_}h zXg%CW4rkma+w)JDzl9slzjI0PFPWuA{r^8?1UlI{{{3s|Z0Y)6g)$VXNXxJNK>ApO zm1D{c{t*x?u6-ON9x3@tUZJpzMrj_cK&aK83Rj~SYDjETRtxx+lI?kp{GdP_^ZxCf zR&^+xlmsTEEWypp_3+iqZIIye?fo6v9}fc?qrm*ib9E0Idi`istrvN1_3nawhi2+q z!F(OX!;Th~qMZvMidgfI9mu;*zfwNH5@Hm_1W;?yKA42$=<7G}Evvb*6mN5FRjYL? zzidj$Epob8&OSyms34UbJ^;IY4N4YqfU<-px$;2%Na9wM5@zcY}z=4GoDtzd& z*iP}>q^3oA!clZ;98Gz}`r}aJ9Z*&j*zGvUtcf=O(c;vEti8UA8RFaOnYrpBbhLD1 z3HHqSRZpgI%nuK%Fs{Y%$HRI=uGz26e(8nN3_ zBW<$)eOKjGDd_QT)=H8_-8{Iipi0`K?QTYcN)J^(ZMv~vFV60n(eQ497*Kf@<3c$2 zOp-^ZehN77vp3_+ZLu>?^jJvai85?&$3wES@D1Z_L%n-Ehp^e8F02zaC?>2!ywW}Y zV`dh??~A>cuq#uOwV`XZLzDHdYjp^G)njyeaUVZ6wN-X5WJPXUl>G*oXVy0+_6RPI zZom>V4d9g1Hbqi<{d=Q&`(ExO4Al%Za)opCd(_fi|G7~6`f)@w4FLw`@UPQ)^8fuH zxj0+eFbdjR3R{>OTmR=XBdgE@ocn?H)11;JgCbg*nk{6O#K9lFsFQ?D2;7iewy@|l zfS}B!3>f zdP8kA%R*HgOVrL1)$H#r?k*fL^5WnSLQK9M-srKDuxM^I5sx%l8l*M=R$C`k5z5&J z6^)yuZCUg0-=q-RWD?0kzWC;_xLs?=e}id{^eFEDI6%k0Ol$76j~ZQ@e!uu`UH!+ENKhqDl)IR?6a`8poxwkSF1}A6w zy1PYZ+0@x*`&--4a`2MJq+m^R!<9kK87uI(uF)4(K+LXH*yhcY)5vDmEQ)@;?JM^B zAcr=TY*1Ivq}@Nc;QQ%c6T!j2;2{3(vorsP&;EBDqoJee|Gn`qUz zt@T?RwxP@Tn=JI=p8W=#C8&}h_-M+!+ShBUu7juL$%>*Pi;RPn2`(`vD+4TJAJ`=4 z0<1h);w76<;-vJ(-L&5Y0bJir!%XO;EuCekr=~oM-YoG_+r)BE{~|Gh;T3#rz)UW- zCGW)CK*JL8V2!uYD_}{k=}y%^1Fpi!D`r%=GJ|Y+{PziXZ|#;5Urn4Asay17^r3t$*k-3uB;**d|8aL16zqiEsCm`fHRVo@HD9RiL!w17H)Xn zsUG3@rO$o~+xsd!WxGouKP7gLhviG~C6mlg$CxEIa(07}jiT4YiZ+DJ)&1;=Gy2+G z3{Y;_Mc9K}J3~jdSq+7Uv0L=j!{doKzWA!GA@^&OUE=A+SG5?*OtD{PocIj9r$spI z&3o+q5tVP^ZJF(Po0bd$$$?O-g{4?mGeB3#*A|6>3=$RYPCurqhyuA_*g5=sKMcz! z$`PuO0iD}7TAg9aADZ66sE}*xpWh_a7DRO0wBmV4Gu8ESi+iZV7 zxc6saP*@qD52i~B+`?>eXn39NmGu@y(zgrv(-N|#jddM=qxi^tMtUUt#C$}$Lwc~6 z+5ArqUJ*Om&KEGSAOD(zNBbXuB4g>~ENlmKHFf-Z73E(lFtz`6>x)!3fn~-O# zr8H(PJUbifuaOZb3){c}qolGP3{OYaUehjFs(-McNO+$cQ{6Q$o+?58^KIiuOtNGX!1dE2w*>>{;3e>v5l`X89;%3Qm?yTDINwkvAnE zDd^`bV(usZ%mkyd;LMC8QOzY5$d2r{1cM1%bDul(DG-D&lQ$?| zYO)#o_Qn-@3H4Jnt*3J3xarGA^3CfF4g*YAc3I!nR7mi5kR0VveJqEE5Hqd3h5t^5 zSSf<(1uLQ{mtYZk-F;k?1!xQJiI}d6?>HbrRu;4|Y#%c?-oJT-98U#!J^~{)m&#gYnmV zmUZ{6+``?w=?R?!XBxXWI2!Im)I;(fig&CbS!~X@U!-(2tq|SqB$tA9v)TFa067CC zrf$jN3OYtY#nYdqT<>I;Y`@@@0kR8_G=>=g4xsu{ztSHxN~u!5&{i8o_I*|wu;P6l zrgt@68nazvhK&;WT9Y?XG^_Dr4zugSPJC01!r>I5EqS=8Qm)ZJ6ZF2W#cM-(D$+Hj zMpLQ5w*tGuii>_^#R#MH%#^Z~i4(A;c%CYStT=TVi0p-s5V+)CA$ItntGIx?(u-tz z>LWyXCuvPJI#Duy{fhj>iT+uMwW!VzjS|dy1n?G^ZkYpSomI;eY2LbWZjm|Z`tYV= zo=nRdsCDqAIP0*73t<=Gb63lwr7M0QF`lsrL?!xFZ-U^8nhb+n)r&(*i(u%1-0>KP z>=aEa_keho$-ck2La$U?Yn29I^OBtugj@GdI1fK0Yudq6JQRVX_eKS)c{#6~X&S_Sh>kRVg#~)u&w=HeexuxZh5p{N|ElcId1#ri*|vwU8((7Y zE%66TRPqYKN1%wB*<3kP-Ub6cXEr%gPCj-k(b`%qyUr_eJrxoEN1~%26%G&RLEv4K zY)8YQbRErBvN*tWY&oJyQb<}_TGgwSkzBkz|I5sdyGo2+8{a%WZVN%IUVMI^dfwNe z-CDzd?oHWg)mFQ_^KRYvPhOZhzvS~Z4oCxBvfs6!p`_KX#7&9YO?Z^_&0VYJyAI`rOq%C}6Y8es?Ea+dC8R`Cmf+#yXS^~{Q-JSn# z=>JVZD%IAMQPr_NvykdCWq)HS0IZ_pq+n9xG-#p9AQ{Qob*xGvR%d1FwtLM|FM$f@ z(Rz9>l=qE{k26iIdZ#j>H@V;5Q+bqMvnD`saax2KHj}#&9bWco!(n&&eH78XJz#M%)yKMvfQO3FIJ9SHAQH!YlG9d zrELl7c&>dWFh{2*s)DHfZDq@?Vr`et+rFbjt2=jnUj%}lvZ?Ku?uW`vRL9V~@*r^) z4**W=twqa_%4#mO)MssmV;s%jxy_fI&}B(!MpPmq+Dg-EnUS*qb``iB={Y)Ubd;(a zYBIWu37T2(jq`crME%ps9pz|Cn8W9FLTH*}=tE3MQ|#gFdo#sJ-iVVU%zj$~zesZm^j z;!gGTKzt3OO7PbA6sl!6}sZPfn0ZPcb+n6+adS!<)}_EIY5$+bkYRyB!U zJ;ypgKVI77$* zjWRN04Fv53#GRmN(n+vQFYfMG5$j95DGX|YG>7k+W->X$yGH6tGSXOCxlD0wB9|{w zz75m!g+F83AjoJ{TekFh=`uI3Sux%qfMsjJyELjwxuz2{I-&PY z-gR7c6mmdke`ROE2F>)M&8E!ziiG#JNk=EAaJ205-NPk0qRt3~Z_`;;2Jxag<*xGV z;J7gYSZ+#QZdaBfl~8;NDZDW6-R6bP7ANVEZ-kLdOT$^w-qu=xM&lBTO;S~I_BuaJ zb@zMh46gGHj%+v*=4)8+O5O&u=Uwa*rbH}W8+5}9UPUa`7Z?<1#X4+HH>Af`k>}sa zc%CuYg_K+w%RM(bVmKK-F{skQ#3$67-ht1EC{BG-Orvo(VjWZ07=7Q;_NTdWSH(## zu=OdnBZwwqnX$jiOZPuvDaSfcqegut=`l~c*&ggmpCibb z@r3p@8R;n>j-SEH7{KG^jz%6wt-}A(z_9%V4l1;YSpFxT78wun2GzDOk13*amc8#K z6>yuP)8ofW*r<5VaS^Q>m@Zjs=KWU#B%58NE|O=bytB|F@&p)|7V_5BB+uf% z0;0OZ9^f}epA*u`;tv%!4!@3XT-R(-zC&dv}m0b2t0yUVO5&I?ekv>^Zn!jrOEK zTk_+&Sl;1ET|(fBwwS~FqQy6-1EM`HCrc|%tD)L?A)P~sgs@Ujy&%_uW;NpR;lmuo zi>IyQCAiGkenmz~s&HuNbuN9rC5gx9(8ka6*_pycPD-ar z`@;F;(|~75$_igq$43^qtI%Ll*}|0O8e%~jL-dGRu=GN$PGew$PNiZ4s=fyCg5wQi zl|G4n+8c>33vI3&ez97a5u9wVBulm^JX=&jw6t?eG9*bgdWH`p2X z+KX}LP4CX}qop-of1W1T$Kd9==b{>GKt_;sW! zf*vNK`(>S)z`k8^Y;Vak779eq17L#!zrPmPBNr+nD$|03yf3E9)+$tGG3~f!F43$jnOiZ0j zo&I5ABb9aSQH9X?>;EvUH?=L5H8iSeYaNGcd!ENLGZ%KRD9o~LcLSLEgG__R0N9&P z+#gVcfa_^O(~fpwOaGGLy&oCej}yP0&3}BnJm7zaeFe1p-&GgJb}a6O;rStlOn%^P zV1%iQ&7n(EK}U33-+p-%A!vMiyl=9=hNcE#(2t4z3f}2W&NA>&tQac@H&w^ZeZyP1 z-X`zW6Dv#F#4M`vK(@@5h-F)Yh&yAooOm(x7>nV^1|L^Q8E?yxG-5oqBzGnbhcnbD zewj=Rxm68HG8C-(nfsJ1P}EU?qP`xs`dQ5cXSM!qv__dvXK6F#`<+f9DG*!qgV)d_ z9#1aiKN*oAI?VFD1vISMoYopdp>cIU<#f%Kmgi_=$MjSo#%k?NMX0%HC3D*EiD(e{ zX-K9Szx6DSbmq4VFTJ!1+2IG98My$H!68)3$GxpVb#HEgs=g8eo6r2zMs07xL>S0L zw(7NY%g!_GG{Z*1)EwH8X`(41e)PlmRUdv|dVqF)18@69&{;Xi1252JWFWRPYP zGuU&q?yOiz)tX~?icskhv+wedcV52muo8YXPFZ$sq{p`|p06p$4{~a;t$aySx2?@x zD^l?4qH795l>?6TBby#+CPdB20OFTt`jau7FhIFBmch*}rq0E7mFn)Iz0N!Pd3xTXh znHzT*S*6r9kftxzCvnimek&OnIp+^YB%>Cg zl-o8PJ2W(1fC!-Xz)e*pz`7RoPdLXTAL#8G3aY7Rd2i)`%x*^wycgRr1a=sx>Q`TGGvfIK#HD15qv&ykcsi6~^7DGQ9d#9O3Ho!a`4s*d0ajgaP&^zeB zF-ZduGHfQ&BG81srpOu~s{1!`7XNC{cmFC1{hxq{<-Y?FeM5p#A$S-Sm5OC} zjm9M=dl8ffRWjIdxcn^39QEqDnVVKmS6rXy&adrdLQ}r_btj~lvn+1s3HMI!mPamTp>BSYt#?f?ORZG)~(wU~%269-k6tG<`^s=2kb3&pU+ss|pR%WoQ886AD zK>@M3GBN1@L%V{unL1%(6PZ^SwwUH1ABOdgK~mx*8lapyGarmmXA0VuEuC0ht7}!R zU!ZYSoHvorfu80f31Jqs7Oqx8?p2`hhU+Lzbg_a22~>~R);4aUGm~MX25u&>2cIV8 z<#a5&eGzqFpd`f$ps9}%3c`*9Fo$WcItLmJDQHZ=kh|GPPiuXUlr<5@R4KdQkCPKq zCtsJ#MC~9NGFYF82d5n6VVhwy7nkRu>V%sT>NT_;GVjSfjy7X3P%PF`I$%BSV0TlA zq&!VNvu!AKLY-MEbw%LATYxN zR0{_Qej;p>^F3zs>AAQwCYR(al70MrKik44Y(MiCey08vKfM1vew!NQ|H7p& zQu#kv!sf4s)JhUd3K-Fy%PS<(R@nPO#tiXQ>Nf;X-y^Mw6wtuh!WzE)7pWCwLSYxc z-pLmnc^VIkqr!?`uXpZT^>IE}y%g;9fn*AX1bLR`FaDhZYHXM9OT8gZ+d*!mS7_P| z^Gbp_(MB9$N1(i*4aL4?x=BB;x9r?_V-GNF(1z=Q z6!og%jVA;i2zs@y>!Lo(D~GLq3f#R8?b3+2(`}ITEWrs*g(5iMb06XuSU3wa)9KV5 zb2m3gN;684pF90TQLjhH2mDUdwM|zNlMmWQu;UK$tb7LmA)Kdx4M|E|IkcwjCRSrC z5#I<8-gZSc;rU+z2ZKfQQY)V~H(WiqC;P{y>FFH{=Y?b_%~BPm?y~2KGg{kal&Xk2d_3d=cN$Ft8o1EyW-WPEw z*~UVwwsj*{(30;YQgDjLZ2V$tRcjJe#g#gaPMk?mPpw_v)Q&IY=U8eiA^)A_x2kGJxqlCWNKgQ(?>xe|w%|3HXSQ z>rL42;4dsCE#vkLths|VuKHF|{%W}lgQS!+2sp6ZSFTGI2W$~T58{8AW%0k4w2PCcl%1uE zXTcugKTp@l8@HSBM9;mSQeYH3LM-;2Lbxv-t-+*XBwAK* zoSOAx{PF%_lkxKn{twV+P$4e2o!}(K-u^bkx|y^et+`K|WQTo)hsW!Lx|AGgFS^IE z0ImQPAsE{62%rYP)ZZQ?{fl<>q{A zT7|1fg=fdQvx+wpy>7*;b=awaA$a6QhWM4QNGl9p40OC`E|Z_V-7}0cD;YaJY%gyd zW4MP6255E)Zgd-u2aoi1EIMRmsbLA8J$Dvd`127PS z9}wgSa*lF*{orbZtq(jqRW`eV+F%pfFglv_WNie9J1h)9DUu-vhU;0}k|QK;isb(z|@W;!i`%N{w-`Y-r9%daq*Dq;^?)bJR<#7}TUk`wnUa zMz-KGh&z6{<5VT|mK@u`=|;B{$ZbmprCquM$>$Q?kQMsf$b5)-Nu1z*4dGwb^)lQg zL{ULtq={#3S9B4-RR;G-%ftj>QEp>by+bQVu&JUIE&Hy!c9rS zajgPmitUTzYv>Fn29WJoWGR-@gejp+tKJdfPvs_cU=MBSzb+yrZbp&hHfY1GN;3w@ zt8k*nUVRZ@I~E{8mD`RPpah6~_($b3np=GU#PG6>vT$eFID4I#Ik z`L~U+1ube{g>Ru2GhEclXC=NSrxdOj!(0K|778~yKble${9N*K`*H{M4 zr?5td@LD5vs-!{N6{YgV>w8+L)6s3t0IB6f=!~Z*-%!$(io`&iO0aL|)5%v7;lr0>>xPmzH)F_GI1cL#5#c>dg?^hCeO6gi`p4c-r zb(*XjU~z1@s1_1T(#`tQ7krwyKn}rI0NV5Eqyxp~Cv?#db~+O*OOFg0I{dl@Nwb@J zZ;w}QhIdeT7xu|j$O5{Q0r;-jT1@7(M}~*&xZK!T&-iCl=(28y{T_qBk0@=5&>)&h z^9O|5pOZkN6<_~;vO6k_lpftU0t!c@@&XxU`z>5sC@T0i`x4iE5g%Y(wm=%7GdC0H zrpCEP)>+sndbql$swI*gTzFxRLknHZOF(`z=46jRH;_zKZAdimj-7}d&Rk^7_w@fg zT;7SGy!mUIBmY;qKiPlVdH(5kTI1OmZ4l!_t*P-wi$mC8_iM;kaGO*j8z=TCDSSL* ze_1LL3Q)tbmey+czOK8usfgD!+p#YvzvK+9rHGdZI0u65&J2g;>}%)D>r>^Y@GNJ2 z`E1--0hFcL=0wNiZ~Ki7&wQ+p*E?1)uPb}lbXQgu!<8Z)-g2!n{M6aT=b@iG`I;wG zuBMWxTB@X$sa`BB)s&R%yeuZuuPI(Dva2j*WH!{*yCG&|>@xKxbCOr9E7ZKU(<;2o zhv zZE$HOHbh9MIH`p6I*P1XU{XG+u3Xe@%WXC6_3`1s|9xy}j51G3gEPD=HI z^s1a1qU?ZKHSTIUr=bR}uteVn!{R_Z8dh8yQ>Hn3<79}-%C_89Z*l-5t9}PVdEnuooeOP9j)X9i z#7Jy|VaYBvaNr&tuAZVkblZo3^{W?e^s6JOol;0;`taf~od@iqi%XK|G2+p^F#>qT zfi}5K8PgTfKKxDV0!Mw>@MmI^tk`Pq7?q%2aI)C zJWt%A5^o`ffyMs$0&`MF4aIcMA9BAWBC3k+yc_onM>FQ>US0f1a+#YhRfN-B^IB{3 z??7Cj_9qZ_KA!^j+7oJK%v1&3cy5Bjj>Qk~taGZdKRRujEVK3H;d83{2`TB6Lzd}F zKj`&}x9rwPh2&&o&9#&QTrzgJ3xUYJN(n6u^M$Q?0>t%az>`+UWwy02QLj#awfx>M!4_J4l5)TiW;V0CjhxfTLJfFTmKJMZN4X%r>NODo`#H^B9U+(zy(?mPDc1m>n zdN4XFPq>xD?0w_BOrGiq!=ky>>bc(+DTt8vKl=5Atly0^dtB4wH2PEm5iPgMf=sJp zqqi1xhm69QS&newE1Gi)jJkgwlG)Hj&)_^^ zn;+l)m=oR}es>f+a15h-qqO*>lPO^76m6O}nHg>bk~>vSy<+$zo<89|HfH!{RfOP5 z6WU_HiHVZ1b5Sr{B2 zL$1>GP;i4^!gU5YOUN9N>W7u?u`_XB)z_{@M*>*fL(*cDZOrMCD!_*Gd}x2SUT~BJ z;W{tXK_d_{-+da4bmg-h`~zr>IX+dU=wsC!h21ld&c%3pY5uWKr}tLNCDyyK=Ru$GiW!zuLy> zHPlMUy5u>9K=R7CopKttz^o!mq2$seL$O`ZA9WH zu%Nl`5}N!6YOZ09tm3ti>ds<#92P1K{_Vh;u_ZOoR3&2mP}pUW4gb&l*PR`dh}4E( z5~?zX#p{Y6QqqUi)o_3a}T*P|kUK9J2z z@fXqOcpwKbCcAOY>nTXO;obt7eqE{m)#`&6e%vcI>p@9|_KTkf%4sjwbi>akc%!@Q zmO0Ph$R*M9?BiJ?jsv%u+Fy6>Q?KJKAaQ5iUw{3}Q`J^uaxLkaD{LmWE^vZZ9qZCj zCvIv)o`$haa#!vxD9aSFEYln^Q0lBiP*%Qo}U2%Y=>yG~+qQuxZeFd&}i_8(2yww`=*=PC5M!`E^G}lPyPdJHx z!4?f%q?c+Qr_gV!>GT4<1ZG{aZO*{=s#B@tvvav89EQVt#KR3|4d*jBvq4Nl`tBc1DR<|d1++cM ztCpkylMq7E9i)*I(r^`zw$WFn$gPjFW34~h-|)7LXND%oGq~qmAs-`kyWdF~Hd+JS z8FzeTp8kLffO~x!O!%aHDuk-}00=2zFMWH+?c|eA_@Bu9V{WLv<;A`_;v*Vi-Fug~ z`lAj(QywXQOHlEN_lkOYFahI4=-RjYHGRDUHUqk(N%kyNU$+_>my zJ>8wEse%(}(kkVkiV(f2ZV*e`brv@#qC_wNcU&4*D8}b26fsl?Nq$hFvCr|2})8f>^IXJ-XcSu38E4o|?b9b0P?$)B(a);P@cHi%o7nD?Po??7}Au z9+tcd{7?YbpB?R9WO(S??2i!KA)J%zmv}*C1V`;yuny3lgUIcU*dIWCs^+JLuak7k_=zBFmJWzrn{32g_aY10e#DeX-mRd8`3GHw6aUV6NPvW6B$7 z*}l{_YcjwpY+1S%Hr6NRx?CfsVZVtzn)KV*vR_hTdQT>>9-yCzszv^lfQez`w$Nl9hvXb8-a6B0Bhel1JXdBz5Q9)F+axK zx0h1s-1TgVpTUxK&aAe?FwDM=*-T!0EQ^kaK4;T2KD2K-%?I&=HvhBWl~G>^{JwyJ zas2D8FU9}Gg8!41q`I{--XX>hJz`5k6b=roudod4PGj?RKVt@3qC&yNn}`sRVR8}l z3Lr5HbnDuCmRh+jWmYIGXT0N^EYtPYj*H3Sa|<#pQ++JVUrGh@{qm9z*ApSIL---x z9y`n@xi7gVTRTPtKW>Y=z>c~vA+!LrO$Mz=O>Je%X?WVKcX+jdNwc=LVSB3T{>UYI z3kwYzYOGks@)L}wmQJG(QKpUQY8#nzi|ZYgGLHW9O$laZHlssNMP!e4w)^VPO77=m zm)z^~=SwXY8p|El8V83kz=FmK0Z(UDLJRpy4Z8&mjY*0eLY|FQ`L z4Gpb=f5@Lw@AitVke^1&IK6Xq;HI|c4GNLQG*LS(-^nV|6XesPVy7V)8rKO`g16QL zFoft&R3H=>6&~~ok3NIM0(|6lZ>e-CuZ^*jQ7awF$N4Hh6d6r z+&j30TbqT7b27S5Xk!d(h+K)SM~g34z%91IH5G$oXA{Hoa6v_4xQFZwBkUZ8u|`AV zJUQt(ljL$#wDTZ5)U8KsqB#9YEn?Q6m@o(sPWEPbC^>0*t*i4C#u0aiD$-hCc?AlY zDYFg=OdY1hejjnqU9fZaf>~G}D_~HD!LdL@sI@p^EZietUXqLd5mjx2%ufDQL5|rz zh@~+!L+*A%M2^BTt`5rkL?~3ns@h=2g@d>7+L#j&N52w=rPR`o^48+OFhW zh}E=*OLtvx16#i8eMQzTAQaa+!@jU~o8DUaRhfyXFyg2uDhP@`a{-^y>kkgvdx!s| zl!b2K-%M`KTnc1R1Xj5vR@;)Uc{#a=dp@ zTORci5!~C2)x0*Oq^g&S3E9;$^!9hxdGjtL!dQX_m;wJZT~>z)ijTBjwel72orx+Y zZ#55p>bG37XWFY{2F zT1Mk9UT$QMExTHN`*~Pe`{H{;5v_`QEcv6`=l)z*^+O!0=t7Z0CD*0n?d*cC!ye(-*`lpNO(n;9SQ0FUt3nu_jrXYP1d>Uh@x7SrmQ76ZW|K z6d(OXPVSKPwFgEV7CNDq>$Sio{RiR`iJT|L;B;7tx$C#$Yh>Hc02eghxC{>2EpQ6Q z$oezts_Y*0AH$rk3UP2zS>KeWFz@V=(X-mScFIRsw3N3#*@m6hqUAEabNl%xP5@o( zU3>zbKvH$`anUv@+D|tzgAoi}j?>1mM@UIh#|LAWP_H6!7v=C?;aAzQDW&j1b*|eA zO`52SrvY)D9}+qO)h-+~+AS}!&Z3c6;@sw5Ny!(Bd^l{? zhEnGe9^{f4tPL@*>1)RMs4_@(R?1UDp~_t#6I6#wVaH?|#Ro0#`KPeUT!Wjr3YFI2 zv8o3gU@H7Iew!kS*&1S5nM;@4Cn~h72%k&CHin{Fwf^<-Ez#^y3thPt9nVXW!SmGW z5PCgbJIAE&{Nt-<@}*PMN1r0A`Dn7d@2v5?+ViF0wPLBac#AeK z{QIfiRon{*ddTv?ruP?dz?Y6F`x!JT-*CSES=xuy5a=SLUQH+@t&lC-mAIGc{N%Q1 z1$(efvo3q&GEUfj-j;~wj)`!hEBiUB6y==9HArDs(!?GY)c~fxVjI`O{CaD6dtVLd zGQlqiaT3y&;8g7t-dshQ9NA7Tl~<9(7T9o+$F9|J4E{ibK8ehTKE}mS24e8er13B z%^BgENCGzg+VioI{)bBQzbS;csi}#Pp|Q1yrIWFZ>AxHY|3xI+(H1e*)vIF%jbjiv zk`TJPrIA^PY^ad%BqO?oh<=6Z;s!Ul&_$ki-$-WSEXOp#N=c;`0ACo0tDEi;*?b|= zU{KTO7zG}*{e%~-x*m}V%3q^Qp3f8ewVEzcogXv2FSuVbyblRKU!Ro0be~kV6j`&s zjkbIKR+zF~2g;=n$kuH#)#ZDc)lD|Aa_{n~!UcX0w=+HONCi%$EIalBA68!yVi&6S z!mGf$zE3sK^-?hWhDb@98RTc>hd`khYHTx=JGR>(VA1ifoL3*QVznptKK$ZS-Jd!% zdDm78Iic4OnUTv26*=(w>Yjn6afy9VQ>iUC!FpZrooE6U0ry1Zsm2b+L{y}JPraHr zA`Mp>52C5!I4VHgUqQT?MO^L=VKS|aaD5ns=yAmENmF@lW5u#8k zW9sMoTUU>y=D^t64!G{M3cK3I>job0LWq8}j;GxUH71$hTq=Wd&|=@;sfWg!>2uZ7@12FE@K=l zAZ~iMz-;Dg++obN^x@SAL+1Fa}4W*O)6>73B}L|G~UgSU^MA7uZmS(kkcLipt5MNGh&T z7Ti(uunDnh3CkxE#CZecLkN<+VDregNv!h-;&gH87v=~?so99fB30DFpY-DSg3gz9 zBZPXnW;VT2YQ!d&$}KrY5c-BqNs4>0y!v!~BewJ5q+egzmv@x1lqIfQF|cxzd6$K; zNBKH497o4JnJ0*79|y)kmPMTEDO2nbqjFq7VG)D^O(;855YiVL+4n{a(`RxLP&|)g z7Y{pYm7wmd$a&Zun8_8o#zY`zaaT~{fA7_aGE-|27wh9pu-2i~p|0*=EtHA1=GyOQ z&xG5=bDL_wWnpNjZvYqQW|e!LokY~H-vW}ljAqbvj9+sE2Mg(gN0HK!BiCY{ zU4j^?5jOiDRXToR?TbgS)V?^G8FC4hnAejvLkN)j{xvicF;^!zQ!)FZnK4-7C4mT& z4uABm>}sa+@z;~L5r7~yX!z7C;Rzfzw}Txk3f`Hbc|pJL8w$&U8XREd3BbG z@3_FF-^0laF{+yHwphG#bZ3npfhrDqDZ-?lZPxFV{Z-N^epEGFVHEvv`{*tqgjW*( zr0?}hXf`m)h>GQFT{t_a#Z6)hjU~3Rff>48=DzA&bR_pqowVqdTP@oKmvc!&;?j|G zY>sPvwVF|s=GD@q+efXgvA8sIl$U~pF}Pl$s`RQWMyl(m^Y8A$^eouA@JP%1&C(!r zo6i03mlR@dNH+}3NzKKeYIF_5+?&i8%JZ=1o`HS!^7VL8=$9K!InIJx9DaAbW}v4y zV3hDLSTsbi_LnO5R#_Mwh9!;W$!pFQ87rysuX!C#%BqbjE#)n@5hwou90i3bx(=0( z{!kXMp%eVZDIUH{$5Dy zYw-z)J2B>+wI`yaZKY^gbH94SGq3~s#_=2w@u=+gJd#&%3Adv?7~wr5x2lffFZn^1 z_^Q#aYt(u|Quc^8w(Xn(% z8U8Da%$3+j+-unVSC-5AEOVqYAlC&@Ed^3&^Its9OxPfrxOCj-=)xA(OJvK9!d#I& zPDD3UA1w$gR3AKNPbWl<|2r9jP{e)+_wr z;r9)zLq-9;@X)VJ|6@U!EGS@-Pao^%F-yn-av;^d$|u>V5cY9s9)6 zdKfQ!-ic4jG=qMnkFK3!8#1?Dr74?2Ic!SlN5~(N7PY?P^;+7sU2|n5G-~fHhqf1A z-!gaWMZW(2brSZj&wq|jX=PMmD|e#KyTY@oChpfM@5Uah#=FYvPO!GPUP>WX@)Ri7 z@qO`!%)rM5_#5@x#_Q|XjZ|+h&LF`Z{;;Q6NEm3p0eyy?`EE66KUasbZfit4E|2wY zJj4xmhq-Ot%>uKi%}u>xf?8zPUN9iZpq4t$r&({CM#D54(D^HdVX* zi%wnqYh3t0vMYOILmLHqOFNhU-;d{?6g)~mdo*TbeBSivDvM=1>Vn+wv&Y1zH@TR%yn-l+P{nsWVRgz65gH`9HyuA{T{yS{vf ze}MfaIWE3p`NOdr!JRICC(iZmCncp8%_3STHkWPLcNhq?EXO1Vb~C~q*oAThk{a8f zc4E;Pp4>po0h(6be&U>W$3Ev=oW0|}SZm#6WUR=Ds)(vL^L_Bn?kS>h zqMvlx%h93uT_HTaw+g4Y5PIS9KHenySVP zoznG`ZY&C;X(lEy*rS>B`J1!y6w^JsBSsVLXY6BkpB{5@e?0>1^((CuJHu?2b{eY^ z*rC1nlcf<~nutDjnv+W=ObGC|DYP9PvLsVY+Tg}8BWfX~bD4MKj4g3M`DJlmmUo=& z?~?Q&Lmv)qK5-hl=kzE)rQI#tzb~D$ZIU181p%E(cs}%_u(DBR*=oOS4C2oN5A!tZ zTh?FhiYqnJ(Xj;BO34>IjIahUl?vb21k#b&MTsH(n2x5ie8ENH1YV-F0Arid07DTxO zxjgP-FfwlXqVL^*1H{_6zpF44 zRIq3_1z~GspSjp$%UL^yojjjOh|B+2mPR-i3*!$;S8S1={h=qQq%BIZMY8RzexHN= z_rB4VQaTo1dq&K6vn3MHcf-T1w<){E?2z`d4kFDUt;vbknb#W!I<*fXrPvLbKS(Xj z!6IqbTz#mdIhzzlg^}gPhL>MUx3{~|DqfrJ!$mp88Rnf<1-6;UFpki3cY5p+>%1Tt z-;My;lMQ35TCi)q+*hPZhywBl`pPQZx^mnWOcE>a+`ERbpR!;2DtNXbr7YKj4MwihS;=G;ugDEhc{?+3mZK_1SYhCEGB6w7lI)T;KR;Q7fO_ z7F|m^uilec-LBGha-IU*M&oS(HEr`3Hf5xq3vS7THMkiuuA>?(2HnIWR{g^TiRs~& zggw1Vgn=*26u$S$=W*T8tfGubfZEvlTkId*fI+KuR(Bts<5iI%wx#E4NxNRwDUuR7 zwn(f_wx%I>UUYEf@)^^{n;nb~hggA8N(VKz%v%WM4pJA;@tD(Fufj6Md;KXbJ$%#Y zm#&a0e2!V-=48Uu1X>WEkd8aiX(nPdLf1hMN+u$k|fJE@11o z3e#VG2xMsfpaYrq?hgC3)f)9aAq}`0Cg{!~jwsYnb%RbS*j4WZj)fVoyk*G+L6q1T z*Y@4Pvc_Rt=)B}8ky*$d#!vIxKqf@2XAHsd3#0E>f!I8!b5SyB90XERmhEu3>_6h~ zsa&I~BkXFZ1^){@xxBOY#J6fES=?{06^dt$)#9xboyUzQ&%$E^v z@hPuE*>#y6#aZPO{7biMi)bUAUZmOY54b?K;WW__+r%2ZcX0AUR75rm3hhP`>v}B1 z@ngPctC){HR|ks)*Uw623MA%Hp`bJcruKkHo5KVpqVVlTr%v=NPB+(VXPr{XdPaU- zqjrGyM%bDny_sl5V#W+0BWMuQh_pp12B|2qz3oVLe$%`k{pU?ApDW1DkJ4bqIdM-p z^8?IsLH>(R56cK3Qoy+Isy$3-N$L+U!Ac-7RP}BJ$=x7>pZENQ|Huii7)7L^kUV>( zKX=``)s17;gzQTP+sn7jn}^2G%|nsV6V#|4w|##^89vIL*e)RQgzYt5>~zN+-^6^g zGLXG>$R&hfc&Q(?MlnBt20p99ZMD?#5hs4TA4C7stZoU>u%(Ix%lMQk%X#u7MaVzu zjj+%*;l0$FzQvvc<@5b+Q-YWV*}_kXnKsgM^cDZ*TJ=eAX~dp<8=}ZRsMr)nbaIw6 z=1WwywG&qr(sK0*m3P7dy<-22OGwZeA5uRoWMSUgb~n#&cA;xBN=f#HtnL{ThQwK( zaZg@>rUZ>IDua>1F-#O`$ED16GdDe5BgPpl?Oh5k1=&EI!uc zQVS2u(ZVr9+;Re{Qqt9FQM)mHoN>&E=;=qtedPr9xb0s)s|Kl3pC?N0@?gaBP=$l6 zIW^_VkXwfw9Kq_0jQSH*Ugr!rZ0I>`c)>#zo7XSYPUtlF-3;b&cQEOri zHlYBf$rFoEg)&H^a2*fJH`!Ei6e~4nmF^BfvCsdHnsG4HqJDP9BT@zy z2Z8ZX9#1b9$nHT_YPyN?vuSQ*-h4q$n~21q5;nen-hU&Yzp^2Sq#D>xaxUD7tUard$$ayiPS5ab)CjaQ;a4hl-X`N|PFHFRUSvI6*oW8U3&~UY5kVGc zM9CO@i?;paXhWP9Yb@0pD1o#7f3LQ5YHnt71FOwd2d9+3r$Na>39Rz&LKG|AQj4j0 zqYC5yxOH3&R?j%AEyzvnGWmesZ%!bLZgMuq^n@q0gcJRv2{5GQQ}$U^SAi83CNSWY z=8PC`8eRE1`8Bj!JmPv!M+@1D_#;^W&PH3f5*%_66LYEnzpekCnOENwH*fTr!ceap z(GfRYkw5k5qB^zjF-@gQs^|{zIl{xXY_n`OOx*QoSZ<=x5Nc8}5QWT_8JE_$`|F5{_{|vE9=80;hgXCk9x=+8gSrj&A|7@ToI40UBd;B7`^XmWt=A-bFngJ z-y0ydOr_Hpj3$A##P}2za>ePi0jYnit0DuLPi?Avy+U)1X?;#Sc&1hpMi~DqTH7t1(Z0QoGSn#N<|>@ z$iLcEWMkGH-*Mot2cS=<+c!@-%WvKf3d3!%WT0mF?DNHOD0wBX_L1Sbc( zJ!;kDNf|Xxs1t{a9_Q-lE52b9(3+TZ!qHdN~t2}_rp}_v`vZqwlf#^%` zKj@lU15A83(`gqMI6P68{WXTUmBxrg?7MZq!`u5e=mLT8%J>~UjHM6mBDpkO1@v8Z z)_d4-rDhw|Edag&=$OtvRG1C$%5ysl#m;;fYH_p7U|RN{>y4lUnx<}+&38{ulHnbOv=r_>gT+t*L<0BFDYfypu?C|sd76aM+XQRtH^(fyUBW+b z9PzZhe@f<#LJr=NW6br92_C@US)#Rty2PI!2C|CH*V08ln=YD|c%+*e12dbOjF*3A zbP(T@Z#!YfRR^zNX3gb3*)g?GhrIr)fSBE>XBw0g3q&S=M|TRKCgwRnR8(baA*YWa z>JJ(0C=3W0sjwJ&v>#8lw@pEBHus#R;%2r|pTlWW>o01FrtBy|n4}*A3-P=pXe`T( zh|-yc9bXrdUDIy!k{*{ixP$L}vafBPJ6}E~5@i=>W)@cVPYSH)jAq^(?z@pe8Pq2R z@p?fkShqc3u0q_uji7AARHje8*+D^62w!C8@R<|o&MvA;^LUnhI%IrGO8{h$Vx>_o zDL6;6{?w29@IRYj%Un!_V|8|@{4M*&8~UI8cKXnLgEdT|?US;LK$Y5VZdRVQrsL+B z*wsKe8 zX!kcSKO2~HY`hlAaxq_|h4pw~IJH42N(`TDLRT<8;(7c~kl6XkG8;_K4Cq3V^t3@F z?kImuCoD61>p6)FT?#8bP68=HXPBUjV+~k?u~B{JX`D=&)kkqQS;kj+3zr{;ikbh8IMb{tugvCx=ROvqWAHzx6j+oQ<#C)s%YS z*7vISUUpo1rbi+|8gdDqx+9q|g0|s>>KGQ7BWO>62i5?DrpLH>!in^8reqU>cKy9F zbriff8Rm-gW8L-L)IZ&dE8icpMTHSS(u-vr__g-5oY2Z5e6I?&tj#TIeM};Sh-`6u zY9O!Oa$gDfs5%m%srCGA>RV28DP%LEv^48PqYdc#;J?6RCapLrTt9{5DCgfdi17w> z>d0oI$Ow;1TYaAdXN|eqshfn)X>2o(JxU1>)!Ek2lQ3Nu8$ zZX;I$!);fKSmW?;O{uaBPu~C?F_SiN31qqFYk9mAr6{Ym?d4icg$&Gw6G2#U8d)@# z@Vwf{D~LL#YE~#o;2z$Eb>ME%4zwLQJ{8tmSdnCPu0xPjYD#oGV}XA1FvzY2kkKcw zM6L&)H9Sk6!vNJ$Qx4q`2+Jd*`vHn2O~#&U0}nu`qd}hVIyiCmj>*hUn#-;yH%7d; zoRoyfz0!$}-9J8UmCNUI^FX~Tv+g4qB&pTlI%}h5$4Aq>(cPHI`=~UE949435QCBm z9S7I$WZ{m$&hbaQ_M}1QY>?iBGwM^dc?OZ0qqJT%K7#R|0ph)ddb|PR`DcMv0W%|G zxTk(<=nTB?%ca(OMdmKTTEbuvQbJT@4X4_z`n#Pg^Gr{B?(DVk^cZ=|*8$Mrkt-wHYrC3;_U1zQW&9cS z8vz;hLD)e@bzOe*Xu3CVG>m3gQqer_PX1a4*ar{?!?<*j?NkdJOZZ+d+LM{kIe`(+ z;i;89u|*&b6C6B5y}jgy-}>!E5#=Mk2t{AzG8JFyoNYBOqAkw6AA4x+_M#=>Jvm|- z%>t|66_>I*`4`=gw%cNg@zTNy*#4by{4b(k8?b)+K#u5JpzlMjL1s64s%o* z^4zDo7&Je{mC$WS7YNuU%3v?sMD^`&jIoQG#Y?Zx(F3=6&$MSoe?@A#AJ)Y10787q zia5mc8y#%@Kz6M@TGlo~S-`2U!IIU-F~cwGyRKGExpVAx&%hV&`SDFt^xU^S?{P76 zxuf|;5xUl)EVC;Pe-wpXCufZH4G(wh7FVolN$T(v?4)Ge;ui9HvT|kL0-+!U)4Z;t zT!pc!G^<5gOSl?kXNg|6sS=dBBb8FmTuXq_l6nUQ>*20m<#-jtFnX$0X!{Z#w+)Hg zLu*vw%0a7kdT@J6IpWI?5woL$&~r^)l#TWd-oG=C#W10AY;(f@5B9yom^3{vy9{}z zyhZBeQkajBRXVQg7}My7+r=r{_|Vu})3uI$=G7H-5>sj$-4jv64hUBhZq#OR$+@ph zL|di9OXTC=Cl3lone{JLb}gJNlp|^vN>!2F8RJAYfa%yPswK7IH}efi4N=bu2X>R{z8^2N*;CS+?u?z1uI+m?Op=x z_2MS(!}UF97I|C!k}NYbwHe@rma(|wvJvS< zpG2paDrc=c%)$}IcLUUH$35Gsk7)V++-ps_1{s=6o$TlJbX*F~U1yQY+a|!${)j-r zr?ie|IA+hT`%I7lo=EbeBaAY;CIiy5CK8D3VPxyZU?TZkECJg;74lcTZ{$X|hKcI& z@HRSZtt%P%2h*!)>Mu*Y*?Hcj?q{MU4Gsw|*Wgs&=bjOkLc7jsfjGOPKZNZvk!ADg z*E`HiIu(-5C(&X7gAUJA=dj8GGwkDNMgZ;9ahLR!e@Dn2Y zw$mBDu1*)%QF_eG!rQNY-_pReww>PSDem)LgIclN(@(l-yV(8ClF_<&I&@sM;#HTo z@-=RyzHpcpSwM7sN}DRerOmCD?P22o-n*y@eT*Ldv~-Jx{d|$?5Fsvr&vm8sskz^h z?clFuTsP64$+~4B;B$V-rks+W$wg&vktUq8#aZeeP_USGk+1&zF)GPYOY$XBTD8#Vg4%_Sz(@5n zRA5(s90}-x^t@sg)nY@_rxth`zag2>CBL%H(&dxN>*lFreXIO8{Nd1Wg=X45X}ur5 zM&NrBYLF+f%P^M?J#5KkG%c+Jn^{5z^UGF#Ww~~xo_k+z{!;(8BQs~MlwhfiQd)x# zFr^rgC;IjmTASUzJnYY+rP;s?t(>kT|AEV7|oA0-*)qAU0>Fdy9@%&qy z8rmvrP3NMieCx4rP)M-T>)V+{m|?E>UYgwSUkTcI-BL{^&U5Pl$Flj z{Q2fH#Bp*NW_%p`n~U6=hhN1%B1x)LDUPM3OkB6v%D!9-^@5Uow_@jOinq1dlNGeu z;oHgd#&yWc+9)+swElgu1X zle(3+RPnS~1wc7ltlx{-Tu(wXCvFw7vetOJb=DnZwaRGQ*L0_rpIb$ibw9?EYra9Y zZ0RHqmwNjqD%z_>dJ2xR?A2IeJIhEWZp4d~`gmFlIpxI8Q)FmWF=wrPrdM-Oz2+5< z=3^vKFvEHMGijd)=WBC$T#Qcq&7jB6;OWp6-rAXINpQq(llA0U&JeopIn6qiw2h0I zkfBgUrtquWmNQVRG#AI>o0kFJ2pcf|01(c+(xV<+DymQ^sN+HY@HN8+GiF{oj*by zs>lQQI@Cm$6N~8>WWsmQkGE~!^3zgT)T^~D`jcNKLy0MPoCJKx#vh%!6E6%u?q*by zzwnDssl0tB1h&~jKQ~0qHuUq+QIcnoH8hPAxb^I=tphb}Y#XPG^Yx$pk#Z}N^VV*p z5wUMG?NH}qqOvIdzH2y-X}C|gkT?AUe?YB!pKcx3pL z4wq~_r=E>W+Q>i%z(v9LzygNa-(HaE8Y@_iat8*6RJqO&<|Pe>f$4CWjJ0|JgLBFp%g7pyQn-or5-y#h8kTAF;%IZep_;69D%G}FIw z&%gvraQif2SR}cr38Ej?i9PAc#qA(L{Y_xgEm| zRe!Ik4CF^tqDozsX1Z__7~mJb3i?h)W$T{OcSTwj!0;h0@3@YhJFuFjuh#=<)JMT>I?< zd2XfPx`Nn2^zcP&8RHUvl7v-td@?@$enqIc<#oMgBGE_*CRd^jJC4H% zlTA+noev$C`#hA~){6+691Q89(yVaUFPrsBz>VY8`G}I;FD|)F=Yu&00Q6FW>#!Bx zS9wv#vqn`;(z&;E>)nOVdsgt#TBd~~28NbhxXbU2!jpAmxtWG_ z9NT=4E@$yBuXyK|&NmW)*ax%3&x^r}p!6i{R${}&EKfFr)z3`;q@-oxkBQwTBDry<(jBh6x8)7$% zbVG|cd=v_A0~_!5FcUP#qPKYbu<~!E8BJ8fgFvQ#q`78K|9HQTWqg68*wu81vl%!? za@UkBP25@Zne&;=ir!gg^=*oS)95-$HpB>rT1t|~drf*vIlJmWO=HJV?Qrx>x4yw` zj@MO1%TnO4w16w2z4kZbWhtf?j;Jqu9?)@OQefUSY&8OtyN3N}42}=x`jSSm3 z01Hcv+)S*V3Eu5S!y(`?b^f{U4Ynj*oR;f{3d2&@*zl~-$6ckp#kG}G-p>S&Y^P?r z+d&`+=Hx?D8p^tSmXz_m;!tQb-1L*m6=vDlBW54b1`yL#?Ni3SGI7)2)Jd!JNMemGFaK~C~Agq2=VvGyJ}`&9E93f`?!x#@9ED$jY@@gp{$$HBC_DO9=( zId(AR)ir&49TOpsMqlZJhDP3PaWOTH&-YPXZpn5R2Md+%n5otHmHe?hbJ6tgMz5;* z6aCJ5c@mFs=cwTK!1a5*-(k9$F+6IERkvByG)(_qhgMIq60P{lGdHZNleS)Fo-+4s zxjSk;pNCPWdDKLuT+9ftwY2Ns^E$f=in6afuqgfKM9Zto%rlUN?K<|U=`p|)p(ioe zG&{Yr9-Eu%W9L*!ezg;+J?wc}gGYyfE^A@W;NBAVz;G0wy+r&Q{~H2mUHN`jzj=^^YHeK2>;{`;ybRJSUJg8`FEX%Ae=^>7pGC2XQ3 zk&K&Q+nwm@TPr}jA-+Ql&Q)*MrqRKRm0pq`kZQY6zsWhp8d*3YitE_(p4QZpl3&$F0k2LpD3Kquq?0fcM+cQl@yT zZu!XKY)*gsIhrMLIz`X)`*eE~1$AuPaO9~~ql{Xb=jv>d!7e60eSgxPAsvF!$LImx zVdu~r@6iYz-N4UUHXHs}do2T=O+=3sB;2nM_1E80MoH0nb)X@NI8lS1KTD6$nwI%R zFPi8rAGo>PP?T$@%1-i`bVv= z#jT?h{GIs@B%fku5XnVnf6+BrH!xaOp*bjicW38!Pt{9n?LXm9FgP6j4!!>61zpJm zr@vUDdeB&H@eOl+t@G6qsBiXdyx84TuEnrBpflwQ)0B#z2s?Y!-Tk-U&2DZ}`YOUm zb?OZ-ICY&kX7#d@$$bEr>GtKF;H z2QYH1UzdH1BKe(kd6G$Sb8(VM5VI4`Gb8mw?a_SgOI+Y5H_smP#Ase(Ij`RnquFZ+ z2_;T$uOZde;m&!Ov2Bg~xP_}4UjRJ~th z)ZiRT&A$IwEat|-f1Y@(0l9h8qc34IDn-$qbzS9s{G^r%*Tj(Tr3NcY-f^8_K;%BP z*65ZX+uVfxtyhrJ9)I2Cyy%!o*4$u~t6IHy`%99(yP5qo!BKWjWLP(pV^h87Q%mwX z#ii$8b3<1eaB$ZgTg6|EdE4jv$aE?z# z_XU}Rg}#s8Gw?Hm8yW0VXFqT4^L1H4Lnetd0B*DnDm=d4zN zbx5N41sQXg)3S#`g4E!sO*)j}H*lxNjT08Eycus2hGZe~D4gewCy>8To?*y5{s_iA zuoIqJ#n@UBszk7oxhT*eQ48i%&)DruR~|{;Q7C;y14P-(hk4U%93HwD1>*))5|Fv> zBpk|ABT%mrP@A&{WuEcb2@pgKgt}F^r=p>^0Ve*j1U(G_HNPce(4$1#*0~-EZ39T(Yq ze}(GBa0J!O8)`jNB>sRadE3JY_rSvz?P@TL;Q}Ak?lm8)QPEc~n1)5;!c;2Yz(XK= z7JKpX;V$-}@;VzKdoH@M*2&Vy=27jXOKkqOV1=2&lprROSPt2Umn=>YUZt19%_m(V zXjpm>lrn`T)NB@!NwDKzPUuauqVDKu^Oq|7#((i#Y%Dbx}FHL&E`I!gY9CD={G0Qbj#7OPOGj8z(GF0>b<;rctqbco-gk6Wf{ zY1EPA*@Q-gybu9 zXf_DV7_Tj`T?|Y^7^@Ms4~Or`NHC8k$Hq|??(7S6(Hr`_GocsZ5mMl$SA=LDOZ21H znJ~pfAI3CmbUKb=uUBXj_KVJ_FMADgsO|#>IJ?dJ-I}vZIT6g!cS<2n8pVt-Z4>H# zb6W~5XUK&AVUpGrQ@kExI+YLHJ(N9-D%$T)=oH!4U>jO*#zg3oWlu3+P+0F#jAUOD zNpCs@5;3lQ>^7gzzZ47icG4V;S(c-_*ymge+9048sIsA#o?ssgqR%O!ir);5HIU#h zXi9?t@%JIvKn0n?lz?GH2^~;v>EImb)B`I|itz9=0}KR2x&;(4!6=Y&9bWS6wT6&= zUApUpQD5s7uq4Yb9d~|6PTp`ok`9Gv2NPa@QQ{ZQ{gdntbT7?K#NDF=y2j=|GLF!J4~JIBih;AB*qwiLgFFA&WL}>Bf)H4h7MK+eXVj&F z9g!d6)1pJteW)Vwi36}lfQGO$(iWD6l%dJtG7rrGrJrE{S}+$;4{=MNG>A60SqMWB zietuB@g;t9P6*ZrUWBSvdOHJ71j{8&ZH^8!-Z3*}jPGZps0qeO3F|Y*-Q>$b#@Q(1 z4|8X(Hyb#Gzh?zh%id-@jK@5Wvu6okfj`-g#E;ZPLxQq^+G8Mt+g1%(r3pd*s-9Yg z^l=gogo*dP?l?F>q&1G3fI#2=)^#WnY*oy@%AQW-$cdbg#Jk*_L_WrZ#d%ah2oLQG zh2de%?|%I zWlRyY1FcfBmWeC{Mxle$=X8U!lJupevjS(J1#HR$8zNzY7R5#sCRh(^vhvEkH|mpS zv1$l;oB%`@g19MP3@ee(g>sWbm=|m%1vVnc9EjeYmdUlFNJ+1 zKUTBdW=x~qdC+ndm}ntP?J5oyy+dEHv&J9{zUSr)2T_R>E{*)ymg%c;kXcaPlS^EQ z3Bx>5eKzGAfery2BpM4*h($|=tz=7L2$d8GO!t971(-fd6BzP2+qL(TRH0ph`~1&Z z2v7DogQ;WGs6u22@c?L|urP{;Ygw*cFX$qLG8=wk-Mj-t?onAJ8Ln7M509H464yTUyxQ@^;aWUm42urY&F^)G^?BhC; zB=VVPzMNF2EJNyB9?H05`#yqu))L+8gp2A5#T25Eys|$=1t)-OOal(jsN4OI2@D_& z2>hg{axA`9n(Xk+fyoZ;;tBb13A!1a<)qOUz;20)#YY+{WD~}dZw`!aUF5ed8L`Y+ zkBApr*bg^SnvOF~^d$pJ=?5wU%uDd43XIz)93)O*Kqn#QA?F4}849nBOL8=}odg_L~eUN5ea`wi}$cgpmO zcynTN9T819l;D!vtR=b&3F&0jR4CaPh&$efuDK!Gm@??`{#G-blQEnZMYN`fMISI3 z5OZ?vsrzULtW}2OQO~MDsj z5et+n{O%$1BM3#VFyp(oUCahpzp@Vp2!W;gG4*5-FjDT0fDPgA0sACL}!pCSz$1oepy&)$k5aQM5R{*opi$vZ{gXTSp86pjqdms$Bj?R$YlIN`fhN-aQFMS` zup?UG2~3`l98^9_Y7<5jF*<&t9>IdSv|x!cf1`k2YuIl_XZxWFQ%O^#Xum*+P7Rz0 zM4>q(T4TZ@GBM#CpMeCFMovP_R;zLmN$MIGm={EuJNI;2c!^IcwOVOU6Kv0~tNYZ%ES=}ZJ} zb1v@%OysXIU<3;a8gyUDv>N`41@Ra*Ev}hDR4(1!wQg9qCqjsXU-S&zW(z|o^!P`P zDVymGv5@nb?sIdkQp*ab(`eXR1Ry6Uv*GKYK%71gjSH5rpuKmCUI_)>5bdeXLz^dz z8Rn#-5la!~)_MyVA#1GjC!r^4}L!MNDAJ!m$`Z^+EoMS&&&2 zpJYnV?ur91IEaQel6oRb2#3=Es(e*GvFfi)-t9NuTiLl`)u^F_#jEy)Bh0p@pg)7w zAgqOJ#N3AW5quzbXmp4tjv>#HHnUw5E=u)ifq??uCJn;TeKAW?Ec(VM3!x)>e$n~H zNJ}OwI5{2g8KB%tmst{oo47qo1uq^&2}~u>^{UxXJmA@NAPoRevc|FMiC4J7%;M1i zJINmQ4$loD+5#bjAEOHQ3InB~n}`9a6@1b-76s<{IESigF*;Z zqs?Qr&c<>JNywx)SY0OX;Jt?#j?q}22-yB7Nx^7CC7X^Q9 zLaj&#sxqkygxAKK(o2-HCgAyn)o1T9)7Of+Sh(8%?$ueukHR#9*Dk3;XOc1Le1nPSrHiDgtZVwSKj5IzGVM-XI z&XZZ{QDu*Y*3gxPKzaw=f5Y1qPy=={E?{)B(e_Ek7(x>xE_#>1Xh0sRvt1Y@I_)-R zST$dX1SfizIF#B%9%1jmR8nxSM@KClKbasB(splSO254dj44!qGN-B!8@d~pm|-m` zazlq4J5aCdePIFRjC}Jn1C!q(22ap;8`x1GVuZ2MB4$A_BMlDf)P&?AeUbU+2sw=yE$E z6jbz{f!Xh7d&6>HEyd2Tj!*y?w+J&LhO>ag@2ofDrAs16r1aNB(1mnV2NJ`ZY=zn1 z;ovnoEl;vQbUjFXN3xz@7-WGv97kEyb)ejwQfOV?YU49J%sEK?f(Rz%TzdiOZI@09 zUkr>k}@s6DR-uG1tOge&C5Np#TP_S@Pp{%k15;Zq+4KgkbPKbj~86!Q;Qr zEVY&pW;M@WW-V-a_?X5ZyW!LO3Ex8292}{sjZy1>$6(<9@SrD$4TJ?FDP^;D2E=9m z=4ahanF#|kCFfM5F-ouBMB-XZAobhw<2>cy?hAF-7#)F1|F_E&J%m5bknHD{3q461 zMU=JiSmvT0%h$I(Ffk=))EW5$psLT2q5GA{nXD6dB(j^5H zmo2MGHTs6q_&cVxygiNx)ZSIdUNy=A$&Uluv7M**az$}W+JN|Kx$G!)8&(hbu{B+F zs4t4-x#~j|*eKdAZKFXq=hip5SMr;?-#VPIUg5Mz^nJM!q;Q>gY5QsgENIU+s{fv< z25)u~Sa*mj3hAMOqE*e+Z_FUzt&116qiN>HM4s5FywZPJi%&nc`&wLg9c`JF{#Uc5 z^@OUTm3kRw+`CXWOT^i&Or5#y|I~ygf!JVHrD0#DC0r??nue@_P+>L}%T7>|jeARo z+m^0#oDx_!03`1~o3+zI;uv!Dm*w(iRvGCppf`?!9gR6&Na%-;PCJFs$kG`U*9L_) z7Fn!Av;8fQ0;@!A3-Iq$&Qsrbnd?C4;s4YvH=qh)DCd%NDi5;xCv=D-&KS%MQGMWD z3`M^Qa(xwCSG64K;dV3tc=Pdj}q(c#0j;blw*6EueN zBFBZV^1Z|9)ZlxBlOFz;W-$v=UjDnB{Rwm_>hJIRP$6|~*aFbh4I^9t(ceul#W4hh z5zqZu|JU0Dl6rO}N?=d`MLqIF=O`Fo^7k{4)+P=92=hD=38|k8Zy|YX6%^7ATXk42 zq91)RQ~^b7prf_mTdxZmcY~*WCQP?wn@*iB3+Q5vCXfv~cS&qOp>iLjgh?lY5Y^t7 z7rc$eZSPST0;AsiaABYWaRVo~h{AjfOd@|E`IJEie_9?d^SD#mU%rb;SSe*OaKVmd zsDcIg>lvK%u?>r11S;siD2Nb9HpmaeLrUIgqdU{gF0=VT5~?jiuyRI`S@eszSdDw~ z*!Ymys&wz@&@f9-d6yF*LnMVETh!s%fB0<5Q5^dqaxOzgtT*E|%PvatL4jhk(8&`{ zcd(p*RIWBIdcb>)!De{|nI?w8eop!~aHbIOzj$+-2UHGz&aFUigKk-KX*XY@0=^)4 zp1_^;cpxHzz)t}nE465T)=wPZwZ(BvsX|rkhT@=(&Y?){$mTR(C2*71b!d6r=YV%= z1-~Zix_a()CUieh=%XUk5{7n_UtljC;Ik|jSR*|}f z4R99i(rG?ua^lU~6KM58V_L5jh>?~j3{xO8@(8_*>4E5|chp2&x%Ju+Q=S6L9^!L$ z5yi%#_oE3a@4W1~fhr98tYB-c$D2ae2pTEeexjn^SEA#Vg6>oSIW^Tv;^gpESq$-D zoepqwM{u`A;-qWdL3BPX6i74?;%4^s92<^%pflPBlUM?46pv7NI4B@qYY&zLD2!R7 z%|rUXLf|>`$`um+RD56|3uX`oJ()j{)W*Lxq40l>v7?v!+0qy;4E38^6EX+y#{Mz< zk}j1)oGjB}wpMlA@{bPMzzoSx6>Zk@*#XL-Y`Elty&47!VAIrpkKz%$WUr+Q9#0TY zoXNnP@uzXCqD!_T;HVwoJ!#nPFSiynAd<8UQ7Nbs0G_za#Hep7$_@O|rw>Cga=^pZ zV+U*}iDNdti06O4_tsh$;I*9NgWyG?iCvrV;9}(jUi?^c3KEB6=CMHwC`z7y1i>gi z$enfn-G!EeBP|Hp&`0D3hZUx+rjLQ(7S4g}LjQO=xPdzGL%08MaDroa;4E8IeZrIM znRXFwAx{0x&7x~%ciq|7s_Zl-M8HfuW^iSGI5(dZL?zw_z z*oYFm5xSyeC!fC|1qn&H3w!2VK+uA5FZ=;$7P^BC`nsmUXLxF;!K#PFU|{kP5uafG zzFI&=9-XtuGxag(M=U~?g@PluVB`a<&=$u?B`*u42ql{v;W5$|PD2_~{Gou-fg3l< zVe+BNyK{6N_{W!X+H2F(76L|PE;SDsuCa8#dOK1KL;FKItFzc#!(W_LUTgL6?{dtS ze_em^SLmEjV!nZBEY~Vt&g_yIyPZEs`i#XrtJWVWtUKHT9s*Xsfj&S+<&kZXuVz^3 z>6v_H`E|Y_6=XocV1R(2pn!mYgnk|%Advrl0|9~kd>#J)7(jvka}50djWM*furYM9 zbNpYyzv3?0fq{X5Abzsp{?A}jJ7Z^nzJ;yrA4hsO8|$frI=KNxq>yK6Z^#KA2N<}Y zsWS~sLqoq?$?D&|Pr>)a1$MESqSR&4<3IuH7w=9z0_S&!9SxR&xY&2WdZguQCo&b- zJ@if#l?^mqojEgieg(v5A`sn1mKDPPh;2+|DXpB45^nja#<0xw;f)#hvvX!t{@x}b ztB-mM_O>8&u^L>ejimXX4$@#RgXx^1cU9ic5kAzgwrqO%WO&RFgO%~|G&;tg=S*lKc$2E&!w|-cKUy% zAZr&>~;GM)% zn72wTTob~>l^d=wK;@u9=nRa%t=*^-xa^5#M^p5pj!18w;~aH1Hvzji zS!W%g)BV>N%j>Qe>Kvch7Snv>H@m?M&kMe+!b{Q>>U2(jIN{^hd%H33t@O{VT;JW8|=J9vxwlysFI(^Y`xl{1lX0JP| zzevqT4L(pRh9zZKGEFH;EKf|zNySK}I^3pnCj&J+24Yw;g)2r&rm238e1{YS_L#qv zKT);qO1B~JjqL&poR;V$Zv0il`S#!u%d_Wv(!$QnH{EbH?URIROq<}0Ua8)Ge$Mrq z=HwgP|Lr#+_~a(3K9iLhMpeoOr%qBk=V`rLONy`d!j7V;^K7CYwX@Y8*Xqm8$`vXI zX3YAX$93qz*>m-W^Ygn_@1M!?f7!8y$U`r!^mq?cXp8L04f)54>hos?F)OD6J@OW} zXW$7Zwa*vinbz;VRqRI9*1L>(L)JSbh+LYc*s%8a^ewR&bMF2=Y3_gT-e(mtubagI z1uxGjXh>|cvk~2M>TyZy?F(i`*0P>gicak1GuybGD@Jpp`3-{IdG*(AJFL#R{ov>IpCfXo-elc5ai&J;m`C68 zf8Z%^xPe{*l}TlZIi*;NCd-T`%j1Aba)DSBRVhRVsF-o>w&ZFs5MZc|7ufBFu=QyeuS1^%PKj2(Snd2I+%>C5<44V_r&8erAd<(15hm;u27}1bCww@W1(s zWGGPS79eH?>4AbJjlY4cAia``5_ByFE4u7Y9%Nw~ zFDtPKT_Z>#EVF{@9S9I$cH$q1zR0lk4(6yr1NifYz8n>ZqMa%l= zCZQM62$Mb@fjArP2#87We1~olNGB`+P`zVt3}O;I>7Z*xFS`(qh(*^*z&q##2f`$+ z<7kc`!8?eA0E*9_P?Mku9NjbMIT+ytl@kyrKs6%rGrAEVEwG>j`FaQ7v + + + + + + diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/CameraUtils.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/CameraUtils.java new file mode 100644 index 0000000..58ea035 --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/CameraUtils.java @@ -0,0 +1,131 @@ +package com.android.sdk.qrcode; + +import android.content.Context; +import android.util.DisplayMetrics; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import io.fotoapparat.parameter.Resolution; + +@SuppressWarnings("unused") +final class CameraUtils { + + private static final double MAX_ASPECT_DISTORTION = 0.15;//最大比例偏差 + private static final int MIN_PREVIEW_PIXELS = 480 * 800;//小于此预览尺寸直接移除 + + private static final class SizeComparator implements Comparator { + @Override + public int compare(Resolution lhs, Resolution rhs) { + return -(rhs.width * rhs.height - lhs.width * lhs.height); + } + } + + static Resolution findBestPictureSize(Context context, Collection collection) { + if (collection.isEmpty()) { + return null; + } + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + Resolution bestPictureResolution = findBestPictureResolution(new int[]{displayMetrics.heightPixels, displayMetrics.widthPixels}, new ArrayList<>(collection)); + Debug.log("bestPictureResolution:" + bestPictureResolution); + return bestPictureResolution; + } + + static Resolution findBestPreviewSize(Context context, Collection collection) { + if (collection.isEmpty()) { + return null; + } + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + Resolution bestPreviewResolution = findBestPreviewResolution(new int[]{displayMetrics.heightPixels, displayMetrics.widthPixels}, new ArrayList<>(collection)); + Debug.log("bestPreviewResolution:" + bestPreviewResolution); + return bestPreviewResolution; + } + + /** + * 找到最好的适配分辨率 + */ + private static Resolution findBestPictureResolution(int[] screenSize, List rawSupportedPicResolutions) { + // 排序 + List sortedSupportedPicResolutions = new ArrayList<>(rawSupportedPicResolutions); + //递增排序,重要 + Collections.sort(sortedSupportedPicResolutions, new SizeComparator()); + // 移除不符合条件的分辨率——高:宽 + float screenAspectRatio = 1.0F * screenSize[0] / screenSize[1]; + Debug.log("screenSize:" + Arrays.toString(screenSize)); + Debug.log("screenAspectRatio:" + screenAspectRatio); + Iterator it = sortedSupportedPicResolutions.iterator(); + + while (it.hasNext()) { + Resolution size = it.next(); + int width = size.width; + int height = size.height; + // 在camera分辨率与屏幕分辨率宽高比不相等的情况下,找出差距最小的一组分辨率 + // 由于camera的分辨率是width>height,我们设置的portrait模式中,width height; + int maybeFlippedWidth = isCandidatePortrait ? height : width; + int maybeFlippedHeight = isCandidatePortrait ? width : height; + float aspectRatio = 1.0F * maybeFlippedHeight / maybeFlippedWidth; + Debug.log("maybeFlippedWidth:" + maybeFlippedWidth + " maybeFlippedHeight:" + maybeFlippedHeight); + Debug.log("aspectRatio:" + aspectRatio); + float distortion = Math.abs(aspectRatio - screenAspectRatio); + if (distortion > MAX_ASPECT_DISTORTION) {//移除不满足比例的分辨率 + it.remove(); + } + } + if (sortedSupportedPicResolutions.isEmpty()) { + return rawSupportedPicResolutions.get(rawSupportedPicResolutions.size() - 1); + } + // 如果没有找到合适的,并且还有候选的像素,对于照片,则取其中最大比例的 + return sortedSupportedPicResolutions.get(sortedSupportedPicResolutions.size() - 1); + + } + + private static Resolution findBestPreviewResolution(int[] screenSize, List rawSupportedSizes) { + // 按照分辨率从大到小排序 + List supportedPreviewResolutions = new ArrayList<>(rawSupportedSizes); + Collections.sort(supportedPreviewResolutions, new SizeComparator()); + // 移除不符合条件的分辨率——高:宽 + double screenAspectRatio = 1.0F * screenSize[0] / screenSize[1]; + + Iterator it = supportedPreviewResolutions.iterator(); + Resolution size; + while (it.hasNext()) { + size = it.next(); + int width = size.width; + int height = size.height; + // 移除低于下限的分辨率 + if (width * height < MIN_PREVIEW_PIXELS) { + it.remove(); + continue; + } + // 在camera分辨率与屏幕分辨率宽高比不相等的情况下,找出差距最小的一组分辨率 + // 由于camera的分辨率是width>height,我们设置的portrait模式中,width height; + int maybeFlippedWidth = isCandidatePortrait ? height : width; + int maybeFlippedHeight = isCandidatePortrait ? width : height; + float aspectRatio = 1.0F * maybeFlippedHeight / maybeFlippedWidth; + double distortion = Math.abs(aspectRatio - screenAspectRatio); + if (distortion > MAX_ASPECT_DISTORTION) {//移除不符合比例的分辨率 + it.remove(); + continue; + } + // 找到与屏幕分辨率完全匹配的预览界面分辨率直接返回 + if (maybeFlippedWidth == screenSize[0] && maybeFlippedHeight == screenSize[1]) { + return size; + } + } + if (supportedPreviewResolutions.isEmpty()) { + return rawSupportedSizes.get(rawSupportedSizes.size() - 1); + } + // 如果没有找到最合适的 + return supportedPreviewResolutions.get(supportedPreviewResolutions.size() - 1); + } + +} diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/Debug.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/Debug.java new file mode 100644 index 0000000..ef829c9 --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/Debug.java @@ -0,0 +1,25 @@ +package com.android.sdk.qrcode; + +import android.util.Log; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2019-04-02 12:55 + */ +public class Debug { + + private static final String TAG = "QRCODE"; + private static boolean debug = BuildConfig.DEBUG; + + public static void setDebug(boolean debug) { + Debug.debug = debug; + } + + public static void log(String msg) { + if (debug && msg != null) { + Log.d(TAG, msg); + } + } + +} diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/ProcessDataTask.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/ProcessDataTask.java new file mode 100644 index 0000000..47532e9 --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/ProcessDataTask.java @@ -0,0 +1,66 @@ +package com.android.sdk.qrcode; + +import android.os.AsyncTask; + +import io.fotoapparat.parameter.Resolution; + + +class ProcessDataTask extends AsyncTask { + + private final Resolution mSize; + private byte[] mData; + private Delegate mDelegate; + + ProcessDataTask(byte[] data, Resolution size, @SuppressWarnings("unused") int rotation, Delegate delegate) { + mData = data; + mSize = size; + mDelegate = delegate; + } + + ProcessDataTask perform() { + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + return this; + } + + void cancelTask() { + if (getStatus() != Status.FINISHED) { + cancel(true); + } + } + + @Override + protected void onCancelled() { + super.onCancelled(); + mDelegate = null; + } + + //https://stackoverflow.com/questions/16252791/zxing-camera-in-portrait-mode-on-android + @Override + protected String doInBackground(Void... params) { + int width = mSize.width; + int height = mSize.height; + byte[] rotatedData = new byte[mData.length]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + rotatedData[x * height + height - y - 1] = mData[x + y * width]; + } + } + int tmp = width; + width = height; + height = tmp; + try { + if (mDelegate == null) { + return null; + } + return mDelegate.processData(rotatedData, width, height); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public interface Delegate { + String processData(byte[] data, int width, int height); + } + +} diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/QRCodeView.kt b/lib_qrcode/src/main/java/com/android/sdk/qrcode/QRCodeView.kt new file mode 100644 index 0000000..249e49d --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/QRCodeView.kt @@ -0,0 +1,281 @@ +package com.android.sdk.qrcode + +import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import io.fotoapparat.Fotoapparat +import io.fotoapparat.configuration.CameraConfiguration +import io.fotoapparat.configuration.UpdateConfiguration +import io.fotoapparat.log.logcat +import io.fotoapparat.preview.Frame +import io.fotoapparat.selector.* +import io.fotoapparat.view.CameraView + +abstract class QRCodeView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr), ProcessDataTask.Delegate { + + private lateinit var mCameraView: CameraView + private lateinit var mFotoapparat: Fotoapparat + + private lateinit var mScanBoxView: ScanBoxView + protected var mDelegate: Delegate? = null + + private var mProcessDataTask: ProcessDataTask? = null + protected var mSpotAble = false + + private val framingRect = Rect() + private var framingRectInPreview: Rect? = null + + init { + initView(context, attrs) + } + + private fun initView(context: Context, attrs: AttributeSet?) { + mCameraView = CameraView(getContext()) + mScanBoxView = ScanBoxView(getContext()) + mScanBoxView.initCustomAttrs(context, attrs) + addView(mCameraView) + addView(mScanBoxView) + try { + mFotoapparat = createFotoapparat() + } catch (e: Exception) { + e.printStackTrace() + mDelegate?.onScanQRCodeOpenCameraError(e) + } + } + + private fun createFotoapparat(): Fotoapparat { + val configuration = CameraConfiguration( + previewResolution = firstAvailable( + wideRatio(highestResolution()), + standardRatio(highestResolution()) + ), + previewFpsRange = highestFps(), + flashMode = off(), + focusMode = firstAvailable( + continuousFocusPicture(), + autoFocus() + ), + frameProcessor = this::processFrame + ) + + return Fotoapparat( + context = this@QRCodeView.context, + view = mCameraView, + logger = logcat(), + lensPosition = back(), + cameraConfiguration = configuration, + cameraErrorCallback = { + mDelegate?.onScanQRCodeOpenCameraError(it) + } + ) + } + + private fun processFrame(frame: Frame) { + val processDataTask = mProcessDataTask + if (mSpotAble && (processDataTask == null || processDataTask.isCancelled)) { + + mProcessDataTask = object : ProcessDataTask(frame.image, frame.size, frame.rotation, this) { + override fun onPostExecute(result: String?) { + + if (mSpotAble) { + if (!result.isNullOrEmpty()) { + try { + mDelegate?.onScanQRCodeSuccess(result) + stopSpot() + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + cancelProcessDataTask() + } + }.perform() + } + } + + /** + * 设置扫描二维码的代理 + * + * @param delegate 扫描二维码的代理 + */ + fun setDelegate(delegate: Delegate) { + mDelegate = delegate + } + + /** + * 显示扫描框 + */ + fun showScanRect() { + mScanBoxView.visibility = View.VISIBLE + } + + /** + * 隐藏扫描框 + */ + fun hiddenScanRect() { + mScanBoxView.visibility = View.GONE + } + + /** + * 打开后置摄像头开始预览,但是并未开始识别 + */ + fun startCamera() { + try { + mFotoapparat.start() + } catch (throwable: Throwable) { + throwable.printStackTrace() + } + + } + + /** + * 关闭摄像头预览,并且隐藏扫描框 + */ + fun stopCamera() { + stopSpotAndHiddenRect() + try { + mFotoapparat.stop() + } catch (throwable: Throwable) { + throwable.printStackTrace() + } + } + + /** + * 延100开始识别 + */ + fun startSpot() { + postDelayed({ + mSpotAble = true + startCamera() + }, 100) + } + + /** + * 停止识别 + */ + fun stopSpot() { + cancelProcessDataTask() + mSpotAble = false + } + + /** + * 停止识别,并且隐藏扫描框 + */ + fun stopSpotAndHiddenRect() { + stopSpot() + hiddenScanRect() + } + + /** + * 显示扫描框,并且延迟1.5秒后开始识别 + */ + fun startSpotAndShowRect() { + startSpot() + showScanRect() + } + + /** + * 当前是否为条码扫描样式 + * + * @return + */ + val isScanBarcodeStyle: Boolean + get() = mScanBoxView.isBarcode + + /** + * 打开闪光灯 + */ + fun openFlashlight() { + try { + mFotoapparat.updateConfiguration(UpdateConfiguration(flashMode = firstAvailable(torch(), off()))) + } catch (e: Exception) { + e.printStackTrace() + } + + } + + /** + * 关闭散光灯 + */ + fun closeFlashlight() { + try { + mFotoapparat.updateConfiguration(UpdateConfiguration(flashMode = firstAvailable(off()))) + } catch (e: Exception) { + e.printStackTrace() + } + + } + + /** + * 销毁二维码扫描控件 + */ + fun onDestroy() { + mDelegate = null + } + + /** + * 取消数据处理任务 + */ + protected fun cancelProcessDataTask() { + mProcessDataTask?.cancelTask() + mProcessDataTask = null + } + + /** + * 切换成扫描条码样式 + */ + fun changeToScanBarcodeStyle() { + mScanBoxView.isBarcode = true + } + + /** + * 切换成扫描二维码样式 + */ + fun changeToScanQRCodeStyle() { + mScanBoxView.isBarcode = false + } + + fun setDebug(debug: Boolean) { + Debug.setDebug(debug) + } + + protected fun getFramingRectInPreview(previewWidth: Int, previewHeight: Int): Rect? { + if (!mScanBoxView.getScanBoxAreaRect(framingRect)) { + return null + } + if (framingRectInPreview == null) { + val rect = Rect(framingRect) + val cameraResolution = Point(previewWidth, previewHeight) + val screenResolution = Utils.getScreenResolution(context) + val x = cameraResolution.x * 1.0f / screenResolution.x + val y = cameraResolution.y * 1.0f / screenResolution.y + rect.left = (rect.left * x).toInt() + rect.right = (rect.right * x).toInt() + rect.top = (rect.top * y).toInt() + rect.bottom = (rect.bottom * y).toInt() + framingRectInPreview = rect + } + return framingRectInPreview + } + + interface Delegate { + + /** + * 处理扫描结果 + */ + fun onScanQRCodeSuccess(result: String) + + /** + * 处理打开相机出错 + */ + fun onScanQRCodeOpenCameraError(error: java.lang.Exception) + + } + +} \ No newline at end of file diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/ScanBoxView.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/ScanBoxView.java new file mode 100644 index 0000000..e15e080 --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/ScanBoxView.java @@ -0,0 +1,568 @@ +package com.android.sdk.qrcode; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; + + +public class ScanBoxView extends View { + + private int mMoveStepDistance; + private int mAnimDelayTime; + + private Rect mFramingRect; + private float mScanLineTop; + private float mScanLineLeft; + private Paint mPaint; + private TextPaint mTipPaint; + + private int mMaskColor; + private int mCornerColor; + private int mCornerLength; + private int mCornerSize; + private int mRectWidth; + private int mRectHeight; + private int mBarcodeRectHeight; + private int mTopOffset; + private int mScanLineSize; + private int mScanLineColor; + private int mScanLineMargin; + private boolean mIsShowDefaultScanLineDrawable; + private Drawable mCustomScanLineDrawable; + private Bitmap mScanLineBitmap; + private int mBorderSize; + private int mBorderColor; + private int mAnimTime; + private boolean mIsCenterVertical; + private int mToolbarHeight; + private boolean mIsBarcode; + private String mQRCodeTipText; + private String mBarCodeTipText; + private String mTipText; + private int mTipTextSize; + private int mTipTextColor; + private boolean mIsTipTextBelowRect; + private int mTipTextMargin; + private boolean mIsShowTipTextAsSingleLine; + private int mTipBackgroundColor; + private boolean mIsShowTipBackground; + private boolean mIsScanLineReverse; + private boolean mIsShowDefaultGridScanLineDrawable; + private Drawable mCustomGridScanLineDrawable; + private Bitmap mGridScanLineBitmap; + private float mGridScanLineBottom; + private float mGridScanLineRight; + + private Bitmap mOriginQRCodeScanLineBitmap; + private Bitmap mOriginBarCodeScanLineBitmap; + private Bitmap mOriginQRCodeGridScanLineBitmap; + private Bitmap mOriginBarCodeGridScanLineBitmap; + + + private float mHalfCornerSize; + private StaticLayout mTipTextSl; + private int mTipBackgroundRadius; + + private boolean mIsOnlyDecodeScanBoxArea; + + public ScanBoxView(Context context) { + super(context); + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mMaskColor = Color.parseColor("#33FFFFFF"); + mCornerColor = Color.WHITE; + mCornerLength = Utils.dp2px(context, 20); + mCornerSize = Utils.dp2px(context, 3); + mScanLineSize = Utils.dp2px(context, 1); + mScanLineColor = Color.WHITE; + mTopOffset = Utils.dp2px(context, 90); + mRectWidth = Utils.dp2px(context, 200); + mBarcodeRectHeight = Utils.dp2px(context, 140); + mScanLineMargin = 0; + mIsShowDefaultScanLineDrawable = false; + mCustomScanLineDrawable = null; + mScanLineBitmap = null; + mBorderSize = Utils.dp2px(context, 1); + mBorderColor = Color.WHITE; + mAnimTime = 1000; + mIsCenterVertical = false; + mToolbarHeight = 0; + mIsBarcode = false; + mMoveStepDistance = Utils.dp2px(context, 2); + mTipText = null; + mTipTextSize = Utils.sp2px(context, 14); + mTipTextColor = Color.WHITE; + mIsTipTextBelowRect = false; + mTipTextMargin = Utils.dp2px(context, 20); + mIsShowTipTextAsSingleLine = false; + mTipBackgroundColor = Color.parseColor("#22000000"); + mIsShowTipBackground = false; + mIsScanLineReverse = false; + mIsShowDefaultGridScanLineDrawable = false; + + mTipPaint = new TextPaint(); + mTipPaint.setAntiAlias(true); + + mTipBackgroundRadius = Utils.dp2px(context, 4); + + mIsOnlyDecodeScanBoxArea = false; + } + + public void initCustomAttrs(Context context, AttributeSet attrs) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.QRCodeView); + final int count = typedArray.getIndexCount(); + for (int i = 0; i < count; i++) { + initCustomAttr(typedArray.getIndex(i), typedArray); + } + typedArray.recycle(); + + afterInitCustomAttrs(); + } + + private void initCustomAttr(int attr, TypedArray typedArray) { + if (attr == R.styleable.QRCodeView_qrcv_topOffset) { + mTopOffset = typedArray.getDimensionPixelSize(attr, mTopOffset); + } else if (attr == R.styleable.QRCodeView_qrcv_cornerSize) { + mCornerSize = typedArray.getDimensionPixelSize(attr, mCornerSize); + } else if (attr == R.styleable.QRCodeView_qrcv_cornerLength) { + mCornerLength = typedArray.getDimensionPixelSize(attr, mCornerLength); + } else if (attr == R.styleable.QRCodeView_qrcv_scanLineSize) { + mScanLineSize = typedArray.getDimensionPixelSize(attr, mScanLineSize); + } else if (attr == R.styleable.QRCodeView_qrcv_rectWidth) { + mRectWidth = typedArray.getDimensionPixelSize(attr, mRectWidth); + } else if (attr == R.styleable.QRCodeView_qrcv_rectWidth_percent) { + float percent = typedArray.getFloat(attr, 0.4F); + Point screenResolution = Utils.getScreenResolution(getContext()); + mRectWidth = (int) (Math.min(screenResolution.x, screenResolution.y) * percent); + } else if (attr == R.styleable.QRCodeView_qrcv_maskColor) { + mMaskColor = typedArray.getColor(attr, mMaskColor); + } else if (attr == R.styleable.QRCodeView_qrcv_cornerColor) { + mCornerColor = typedArray.getColor(attr, mCornerColor); + } else if (attr == R.styleable.QRCodeView_qrcv_scanLineColor) { + mScanLineColor = typedArray.getColor(attr, mScanLineColor); + } else if (attr == R.styleable.QRCodeView_qrcv_scanLineMargin) { + mScanLineMargin = typedArray.getDimensionPixelSize(attr, mScanLineMargin); + } else if (attr == R.styleable.QRCodeView_qrcv_isShowDefaultScanLineDrawable) { + mIsShowDefaultScanLineDrawable = typedArray.getBoolean(attr, mIsShowDefaultScanLineDrawable); + } else if (attr == R.styleable.QRCodeView_qrcv_customScanLineDrawable) { + mCustomScanLineDrawable = typedArray.getDrawable(attr); + } else if (attr == R.styleable.QRCodeView_qrcv_borderSize) { + mBorderSize = typedArray.getDimensionPixelSize(attr, mBorderSize); + } else if (attr == R.styleable.QRCodeView_qrcv_borderColor) { + mBorderColor = typedArray.getColor(attr, mBorderColor); + } else if (attr == R.styleable.QRCodeView_qrcv_animTime) { + mAnimTime = typedArray.getInteger(attr, mAnimTime); + } else if (attr == R.styleable.QRCodeView_qrcv_isCenterVertical) { + mIsCenterVertical = typedArray.getBoolean(attr, mIsCenterVertical); + } else if (attr == R.styleable.QRCodeView_qrcv_toolbarHeight) { + mToolbarHeight = typedArray.getDimensionPixelSize(attr, mToolbarHeight); + } else if (attr == R.styleable.QRCodeView_qrcv_barcodeRectHeight) { + mBarcodeRectHeight = typedArray.getDimensionPixelSize(attr, mBarcodeRectHeight); + } else if (attr == R.styleable.QRCodeView_qrcv_isBarcode) { + mIsBarcode = typedArray.getBoolean(attr, mIsBarcode); + } else if (attr == R.styleable.QRCodeView_qrcv_barCodeTipText) { + mBarCodeTipText = typedArray.getString(attr); + } else if (attr == R.styleable.QRCodeView_qrcv_qrCodeTipText) { + mQRCodeTipText = typedArray.getString(attr); + } else if (attr == R.styleable.QRCodeView_qrcv_tipTextSize) { + mTipTextSize = typedArray.getDimensionPixelSize(attr, mTipTextSize); + } else if (attr == R.styleable.QRCodeView_qrcv_tipTextColor) { + mTipTextColor = typedArray.getColor(attr, mTipTextColor); + } else if (attr == R.styleable.QRCodeView_qrcv_isTipTextBelowRect) { + mIsTipTextBelowRect = typedArray.getBoolean(attr, mIsTipTextBelowRect); + } else if (attr == R.styleable.QRCodeView_qrcv_tipTextMargin) { + mTipTextMargin = typedArray.getDimensionPixelSize(attr, mTipTextMargin); + } else if (attr == R.styleable.QRCodeView_qrcv_isShowTipTextAsSingleLine) { + mIsShowTipTextAsSingleLine = typedArray.getBoolean(attr, mIsShowTipTextAsSingleLine); + } else if (attr == R.styleable.QRCodeView_qrcv_isShowTipBackground) { + mIsShowTipBackground = typedArray.getBoolean(attr, mIsShowTipBackground); + } else if (attr == R.styleable.QRCodeView_qrcv_tipBackgroundColor) { + mTipBackgroundColor = typedArray.getColor(attr, mTipBackgroundColor); + } else if (attr == R.styleable.QRCodeView_qrcv_isScanLineReverse) { + mIsScanLineReverse = typedArray.getBoolean(attr, mIsScanLineReverse); + } else if (attr == R.styleable.QRCodeView_qrcv_isShowDefaultGridScanLineDrawable) { + mIsShowDefaultGridScanLineDrawable = typedArray.getBoolean(attr, mIsShowDefaultGridScanLineDrawable); + } else if (attr == R.styleable.QRCodeView_qrcv_customGridScanLineDrawable) { + mCustomGridScanLineDrawable = typedArray.getDrawable(attr); + } else if (attr == R.styleable.QRCodeView_qrcv_isOnlyDecodeScanBoxArea) { + mIsOnlyDecodeScanBoxArea = typedArray.getBoolean(attr, mIsOnlyDecodeScanBoxArea); + } + } + + private void afterInitCustomAttrs() { + if (mCustomGridScanLineDrawable != null) { + mOriginQRCodeGridScanLineBitmap = ((BitmapDrawable) mCustomGridScanLineDrawable).getBitmap(); + } + if (mOriginQRCodeGridScanLineBitmap == null) { + mOriginQRCodeGridScanLineBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.qrcode_default_grid_scan_line); + mOriginQRCodeGridScanLineBitmap = Utils.makeTintBitmap(mOriginQRCodeGridScanLineBitmap, mScanLineColor); + } + + mOriginBarCodeGridScanLineBitmap = Utils.adjustPhotoRotation(mOriginQRCodeGridScanLineBitmap, 90); + mOriginBarCodeGridScanLineBitmap = Utils.adjustPhotoRotation(mOriginBarCodeGridScanLineBitmap, 90); + mOriginBarCodeGridScanLineBitmap = Utils.adjustPhotoRotation(mOriginBarCodeGridScanLineBitmap, 90); + + if (mCustomScanLineDrawable != null) { + mOriginQRCodeScanLineBitmap = ((BitmapDrawable) mCustomScanLineDrawable).getBitmap(); + } + + if (mOriginQRCodeScanLineBitmap == null) { + mOriginQRCodeScanLineBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.qrcode_default_scan_line); + mOriginQRCodeScanLineBitmap = Utils.makeTintBitmap(mOriginQRCodeScanLineBitmap, mScanLineColor); + } + + mOriginBarCodeScanLineBitmap = Utils.adjustPhotoRotation(mOriginQRCodeScanLineBitmap, 90); + + mTopOffset += mToolbarHeight; + + mHalfCornerSize = 1.0f * mCornerSize / 2; + + mTipPaint.setTextSize(mTipTextSize); + mTipPaint.setColor(mTipTextColor); + + setIsBarcode(mIsBarcode); + } + + @Override + public void onDraw(Canvas canvas) { + if (mFramingRect == null) { + return; + } + + // 画遮罩层 + drawMask(canvas); + + // 画边框线 + drawBorderLine(canvas); + + // 画四个直角的线 + drawCornerLine(canvas); + + // 画扫描线 + drawScanLine(canvas); + + // 画提示文本 + drawTipText(canvas); + + // 移动扫描线的位置 + moveScanLine(); + + } + + /** + * 画遮罩层 + * + * @param canvas + */ + private void drawMask(Canvas canvas) { + int width = canvas.getWidth(); + int height = canvas.getHeight(); + + if (mMaskColor != Color.TRANSPARENT) { + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(mMaskColor); + canvas.drawRect(0, 0, width, mFramingRect.top, mPaint); + canvas.drawRect(0, mFramingRect.top, mFramingRect.left, mFramingRect.bottom + 1, mPaint); + canvas.drawRect(mFramingRect.right + 1, mFramingRect.top, width, mFramingRect.bottom + 1, mPaint); + canvas.drawRect(0, mFramingRect.bottom + 1, width, height, mPaint); + } + } + + /** + * 画边框线 + * + * @param canvas + */ + private void drawBorderLine(Canvas canvas) { + if (mBorderSize > 0) { + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setColor(mBorderColor); + mPaint.setStrokeWidth(mBorderSize); + canvas.drawRect(mFramingRect, mPaint); + } + } + + /** + * 画四个直角的线 + * + * @param canvas + */ + private void drawCornerLine(Canvas canvas) { + if (mHalfCornerSize > 0) { + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setColor(mCornerColor); + mPaint.setStrokeWidth(mCornerSize); + canvas.drawLine(mFramingRect.left - mHalfCornerSize, mFramingRect.top, mFramingRect.left - mHalfCornerSize + mCornerLength, mFramingRect.top, mPaint); + canvas.drawLine(mFramingRect.left, mFramingRect.top - mHalfCornerSize, mFramingRect.left, mFramingRect.top - mHalfCornerSize + mCornerLength, mPaint); + canvas.drawLine(mFramingRect.right + mHalfCornerSize, mFramingRect.top, mFramingRect.right + mHalfCornerSize - mCornerLength, mFramingRect.top, mPaint); + canvas.drawLine(mFramingRect.right, mFramingRect.top - mHalfCornerSize, mFramingRect.right, mFramingRect.top - mHalfCornerSize + mCornerLength, mPaint); + + canvas.drawLine(mFramingRect.left - mHalfCornerSize, mFramingRect.bottom, mFramingRect.left - mHalfCornerSize + mCornerLength, mFramingRect.bottom, mPaint); + canvas.drawLine(mFramingRect.left, mFramingRect.bottom + mHalfCornerSize, mFramingRect.left, mFramingRect.bottom + mHalfCornerSize - mCornerLength, mPaint); + canvas.drawLine(mFramingRect.right + mHalfCornerSize, mFramingRect.bottom, mFramingRect.right + mHalfCornerSize - mCornerLength, mFramingRect.bottom, mPaint); + canvas.drawLine(mFramingRect.right, mFramingRect.bottom + mHalfCornerSize, mFramingRect.right, mFramingRect.bottom + mHalfCornerSize - mCornerLength, mPaint); + } + } + + /** + * 画扫描线 + * + * @param canvas + */ + private void drawScanLine(Canvas canvas) { + if (mIsBarcode) { + if (mGridScanLineBitmap != null) { + RectF dstGridRectF = new RectF(mFramingRect.left + mHalfCornerSize + 0.5f, mFramingRect.top + mHalfCornerSize + mScanLineMargin, mGridScanLineRight, mFramingRect.bottom - mHalfCornerSize - mScanLineMargin); + + Rect srcGridRect = new Rect((int) (mGridScanLineBitmap.getWidth() - dstGridRectF.width()), 0, mGridScanLineBitmap.getWidth(), mGridScanLineBitmap.getHeight()); + + if (srcGridRect.left < 0) { + srcGridRect.left = 0; + dstGridRectF.left = dstGridRectF.right - srcGridRect.width(); + } + + canvas.drawBitmap(mGridScanLineBitmap, srcGridRect, dstGridRectF, mPaint); + } else if (mScanLineBitmap != null) { + RectF lineRect = new RectF(mScanLineLeft, mFramingRect.top + mHalfCornerSize + mScanLineMargin, mScanLineLeft + mScanLineBitmap.getWidth(), mFramingRect.bottom - mHalfCornerSize - mScanLineMargin); + canvas.drawBitmap(mScanLineBitmap, null, lineRect, mPaint); + } else { + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(mScanLineColor); + canvas.drawRect(mScanLineLeft, mFramingRect.top + mHalfCornerSize + mScanLineMargin, mScanLineLeft + mScanLineSize, mFramingRect.bottom - mHalfCornerSize - mScanLineMargin, mPaint); + } + } else { + if (mGridScanLineBitmap != null) { + RectF dstGridRectF = new RectF(mFramingRect.left + mHalfCornerSize + mScanLineMargin, mFramingRect.top + mHalfCornerSize + 0.5f, mFramingRect.right - mHalfCornerSize - mScanLineMargin, mGridScanLineBottom); + + Rect srcRect = new Rect(0, (int) (mGridScanLineBitmap.getHeight() - dstGridRectF.height()), mGridScanLineBitmap.getWidth(), mGridScanLineBitmap.getHeight()); + + if (srcRect.top < 0) { + srcRect.top = 0; + dstGridRectF.top = dstGridRectF.bottom - srcRect.height(); + } + + canvas.drawBitmap(mGridScanLineBitmap, srcRect, dstGridRectF, mPaint); + } else if (mScanLineBitmap != null) { + RectF lineRect = new RectF(mFramingRect.left + mHalfCornerSize + mScanLineMargin, mScanLineTop, mFramingRect.right - mHalfCornerSize - mScanLineMargin, mScanLineTop + mScanLineBitmap.getHeight()); + canvas.drawBitmap(mScanLineBitmap, null, lineRect, mPaint); + } else { + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(mScanLineColor); + canvas.drawRect(mFramingRect.left + mHalfCornerSize + mScanLineMargin, mScanLineTop, mFramingRect.right - mHalfCornerSize - mScanLineMargin, mScanLineTop + mScanLineSize, mPaint); + } + } + } + + /** + * 画提示文本 + * + * @param canvas + */ + private void drawTipText(Canvas canvas) { + if (TextUtils.isEmpty(mTipText) || mTipTextSl == null) { + return; + } + + if (mIsTipTextBelowRect) { + if (mIsShowTipBackground) { + mPaint.setColor(mTipBackgroundColor); + mPaint.setStyle(Paint.Style.FILL); + if (mIsShowTipTextAsSingleLine) { + Rect tipRect = new Rect(); + mTipPaint.getTextBounds(mTipText, 0, mTipText.length(), tipRect); + float left = (canvas.getWidth() - tipRect.width()) / 2 - mTipBackgroundRadius; + canvas.drawRoundRect(new RectF(left, mFramingRect.bottom + mTipTextMargin - mTipBackgroundRadius, left + tipRect.width() + 2 * mTipBackgroundRadius, mFramingRect.bottom + mTipTextMargin + mTipTextSl.getHeight() + mTipBackgroundRadius), mTipBackgroundRadius, mTipBackgroundRadius, mPaint); + } else { + canvas.drawRoundRect(new RectF(mFramingRect.left, mFramingRect.bottom + mTipTextMargin - mTipBackgroundRadius, mFramingRect.right, mFramingRect.bottom + mTipTextMargin + mTipTextSl.getHeight() + mTipBackgroundRadius), mTipBackgroundRadius, mTipBackgroundRadius, mPaint); + } + } + + canvas.save(); + if (mIsShowTipTextAsSingleLine) { + canvas.translate(0, mFramingRect.bottom + mTipTextMargin); + } else { + canvas.translate(mFramingRect.left + mTipBackgroundRadius, mFramingRect.bottom + mTipTextMargin); + } + mTipTextSl.draw(canvas); + canvas.restore(); + } else { + if (mIsShowTipBackground) { + mPaint.setColor(mTipBackgroundColor); + mPaint.setStyle(Paint.Style.FILL); + + if (mIsShowTipTextAsSingleLine) { + Rect tipRect = new Rect(); + mTipPaint.getTextBounds(mTipText, 0, mTipText.length(), tipRect); + float left = (canvas.getWidth() - tipRect.width()) / 2 - mTipBackgroundRadius; + canvas.drawRoundRect(new RectF(left, mFramingRect.top - mTipTextMargin - mTipTextSl.getHeight() - mTipBackgroundRadius, left + tipRect.width() + 2 * mTipBackgroundRadius, mFramingRect.top - mTipTextMargin + mTipBackgroundRadius), mTipBackgroundRadius, mTipBackgroundRadius, mPaint); + } else { + canvas.drawRoundRect(new RectF(mFramingRect.left, mFramingRect.top - mTipTextMargin - mTipTextSl.getHeight() - mTipBackgroundRadius, mFramingRect.right, mFramingRect.top - mTipTextMargin + mTipBackgroundRadius), mTipBackgroundRadius, mTipBackgroundRadius, mPaint); + } + } + + canvas.save(); + if (mIsShowTipTextAsSingleLine) { + canvas.translate(0, mFramingRect.top - mTipTextMargin - mTipTextSl.getHeight()); + } else { + canvas.translate(mFramingRect.left + mTipBackgroundRadius, mFramingRect.top - mTipTextMargin - mTipTextSl.getHeight()); + } + mTipTextSl.draw(canvas); + canvas.restore(); + } + } + + /** + * 移动扫描线的位置 + */ + private void moveScanLine() { + if (mIsBarcode) { + if (mGridScanLineBitmap == null) { + // 处理非网格扫描图片的情况 + mScanLineLeft += mMoveStepDistance; + int scanLineSize = mScanLineSize; + if (mScanLineBitmap != null) { + scanLineSize = mScanLineBitmap.getWidth(); + } + + if (mIsScanLineReverse) { + if (mScanLineLeft + scanLineSize > mFramingRect.right - mHalfCornerSize || mScanLineLeft < mFramingRect.left + mHalfCornerSize) { + mMoveStepDistance = -mMoveStepDistance; + } + } else { + if (mScanLineLeft + scanLineSize > mFramingRect.right - mHalfCornerSize) { + mScanLineLeft = mFramingRect.left + mHalfCornerSize + 0.5f; + } + } + } else { + // 处理网格扫描图片的情况 + mGridScanLineRight += mMoveStepDistance; + if (mGridScanLineRight > mFramingRect.right - mHalfCornerSize) { + mGridScanLineRight = mFramingRect.left + mHalfCornerSize + 0.5f; + } + } + } else { + if (mGridScanLineBitmap == null) { + // 处理非网格扫描图片的情况 + mScanLineTop += mMoveStepDistance; + int scanLineSize = mScanLineSize; + if (mScanLineBitmap != null) { + scanLineSize = mScanLineBitmap.getHeight(); + } + + if (mIsScanLineReverse) { + if (mScanLineTop + scanLineSize > mFramingRect.bottom - mHalfCornerSize || mScanLineTop < mFramingRect.top + mHalfCornerSize) { + mMoveStepDistance = -mMoveStepDistance; + } + } else { + if (mScanLineTop + scanLineSize > mFramingRect.bottom - mHalfCornerSize) { + mScanLineTop = mFramingRect.top + mHalfCornerSize + 0.5f; + } + } + } else { + // 处理网格扫描图片的情况 + mGridScanLineBottom += mMoveStepDistance; + if (mGridScanLineBottom > mFramingRect.bottom - mHalfCornerSize) { + mGridScanLineBottom = mFramingRect.top + mHalfCornerSize + 0.5f; + } + } + + } + postInvalidateDelayed(mAnimDelayTime, mFramingRect.left, mFramingRect.top, mFramingRect.right, mFramingRect.bottom); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + calFramingRect(); + } + + private void calFramingRect() { + Point screenResolution = Utils.getScreenResolution(getContext()); + int leftOffset = (screenResolution.x - mRectWidth) / 2; + mFramingRect = new Rect(leftOffset, mTopOffset, leftOffset + mRectWidth, mTopOffset + mRectHeight); + + if (mIsBarcode) { + mGridScanLineRight = mScanLineLeft = mFramingRect.left + mHalfCornerSize + 0.5f; + } else { + mGridScanLineBottom = mScanLineTop = mFramingRect.top + mHalfCornerSize + 0.5f; + } + } + + public boolean getScanBoxAreaRect(Rect rect) { + if (mIsOnlyDecodeScanBoxArea) { + rect.set(mFramingRect); + return true; + } else { + return false; + } + } + + public void setIsBarcode(boolean isBarcode) { + mIsBarcode = isBarcode; + + if (mCustomGridScanLineDrawable != null || mIsShowDefaultGridScanLineDrawable) { + if (mIsBarcode) { + mGridScanLineBitmap = mOriginBarCodeGridScanLineBitmap; + } else { + mGridScanLineBitmap = mOriginQRCodeGridScanLineBitmap; + } + } else if (mCustomScanLineDrawable != null || mIsShowDefaultScanLineDrawable) { + if (mIsBarcode) { + mScanLineBitmap = mOriginBarCodeScanLineBitmap; + } else { + mScanLineBitmap = mOriginQRCodeScanLineBitmap; + } + } + + if (mIsBarcode) { + mTipText = mBarCodeTipText; + mRectHeight = mBarcodeRectHeight; + mAnimDelayTime = (int) ((1.0f * mAnimTime * mMoveStepDistance) / mRectWidth); + } else { + mTipText = mQRCodeTipText; + mRectHeight = mRectWidth; + mAnimDelayTime = (int) ((1.0f * mAnimTime * mMoveStepDistance) / mRectHeight); + } + + if (!TextUtils.isEmpty(mTipText)) { + if (mIsShowTipTextAsSingleLine) { + mTipTextSl = new StaticLayout(mTipText, mTipPaint, Utils.getScreenResolution(getContext()).x, Layout.Alignment.ALIGN_CENTER, 1.0f, 0, true); + } else { + mTipTextSl = new StaticLayout(mTipText, mTipPaint, mRectWidth - 2 * mTipBackgroundRadius, Layout.Alignment.ALIGN_CENTER, 1.0f, 0, true); + } + } + + if (mIsCenterVertical) { + int screenHeight = Utils.getScreenResolution(getContext()).y; + if (mToolbarHeight == 0) { + mTopOffset = (screenHeight - mRectHeight) / 2; + } else { + mTopOffset = (screenHeight - mRectHeight) / 2 + mToolbarHeight / 2; + } + } + + calFramingRect(); + + postInvalidate(); + } + + public boolean getIsBarcode() { + return mIsBarcode; + } + +} \ No newline at end of file diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/Utils.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/Utils.java new file mode 100644 index 0000000..95fe2a4 --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/Utils.java @@ -0,0 +1,84 @@ +package com.android.sdk.qrcode; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.util.TypedValue; +import android.view.Display; +import android.view.WindowManager; + +final class Utils { + + private Utils() { + } + + @SuppressLint("ObsoleteSdkInt") + static Point getScreenResolution(Context context) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Point screenResolution = new Point(); + if (android.os.Build.VERSION.SDK_INT >= 13) { + display.getSize(screenResolution); + } else { + screenResolution.set(display.getWidth(), display.getHeight()); + } + return screenResolution; + } + + static int dp2px(Context context, @SuppressWarnings("all") float dpValue) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.getResources().getDisplayMetrics()); + } + + static int sp2px(Context context, @SuppressWarnings("all") float spValue) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, context.getResources().getDisplayMetrics()); + } + + + static Bitmap adjustPhotoRotation(Bitmap inputBitmap, @SuppressWarnings("all") int orientationDegree) { + + if (inputBitmap == null) { + return null; + } + + Matrix matrix = new Matrix(); + matrix.setRotate(orientationDegree, (float) inputBitmap.getWidth() / 2, (float) inputBitmap.getHeight() / 2); + float outputX, outputY; + if (orientationDegree == 90) { + outputX = inputBitmap.getHeight(); + outputY = 0; + } else { + outputX = inputBitmap.getHeight(); + outputY = inputBitmap.getWidth(); + } + + final float[] values = new float[9]; + matrix.getValues(values); + float x1 = values[Matrix.MTRANS_X]; + float y1 = values[Matrix.MTRANS_Y]; + matrix.postTranslate(outputX - x1, outputY - y1); + Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap.getHeight(), inputBitmap.getWidth(), Bitmap.Config.ARGB_8888); + Paint paint = new Paint(); + Canvas canvas = new Canvas(outputBitmap); + canvas.drawBitmap(inputBitmap, matrix, paint); + return outputBitmap; + } + + static Bitmap makeTintBitmap(Bitmap inputBitmap, int tintColor) { + if (inputBitmap == null) { + return null; + } + Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap.getWidth(), inputBitmap.getHeight(), inputBitmap.getConfig()); + Canvas canvas = new Canvas(outputBitmap); + Paint paint = new Paint(); + paint.setColorFilter(new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(inputBitmap, 0, 0, paint); + return outputBitmap; + } + +} \ No newline at end of file diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeDecoder.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeDecoder.java new file mode 100644 index 0000000..f7ea79b --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeDecoder.java @@ -0,0 +1,104 @@ +package com.android.sdk.qrcode.zxing; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.RGBLuminanceSource; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("unused,WeakerAccess") +public class QRCodeDecoder { + + static final Map HINTS = new EnumMap<>(DecodeHintType.class); + + static { + List allFormats = new ArrayList<>(); + allFormats.add(BarcodeFormat.AZTEC); + allFormats.add(BarcodeFormat.CODABAR); + allFormats.add(BarcodeFormat.CODE_39); + allFormats.add(BarcodeFormat.CODE_93); + allFormats.add(BarcodeFormat.CODE_128); + allFormats.add(BarcodeFormat.DATA_MATRIX); + allFormats.add(BarcodeFormat.EAN_8); + allFormats.add(BarcodeFormat.EAN_13); + allFormats.add(BarcodeFormat.ITF); + allFormats.add(BarcodeFormat.MAXICODE); + allFormats.add(BarcodeFormat.PDF_417); + allFormats.add(BarcodeFormat.QR_CODE); + allFormats.add(BarcodeFormat.RSS_14); + allFormats.add(BarcodeFormat.RSS_EXPANDED); + allFormats.add(BarcodeFormat.UPC_A); + allFormats.add(BarcodeFormat.UPC_E); + allFormats.add(BarcodeFormat.UPC_EAN_EXTENSION); + + HINTS.put(DecodeHintType.POSSIBLE_FORMATS, allFormats); + HINTS.put(DecodeHintType.CHARACTER_SET, "utf-8"); + } + + private QRCodeDecoder() { + } + + /** + * 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。 + * + * @param picturePath 要解析的二维码图片本地路径 + * @return 返回二维码图片里的内容 或 null + */ + public static String syncDecodeQRCode(String picturePath) { + return syncDecodeQRCode(getDecodeAbleBitmap(picturePath)); + } + + /** + * 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。 + * + * @param bitmap 要解析的二维码图片 + * @return 返回二维码图片里的内容 或 null + */ + public static String syncDecodeQRCode(Bitmap bitmap) { + try { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int[] pixels = new int[width * height]; + bitmap.getPixels(pixels, 0, width, 0, 0, width, height); + RGBLuminanceSource source = new RGBLuminanceSource(width, height, pixels); + Result result = new MultiFormatReader().decode(new BinaryBitmap(new HybridBinarizer(source)), HINTS); + return result.getText(); + } catch (Exception e) { + return null; + } + } + + /** + * 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR + * + * @param picturePath 本地图片文件路径 + * @return Bitmap + */ + private static Bitmap getDecodeAbleBitmap(String picturePath) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(picturePath, options); + int sampleSize = options.outHeight / 400; + if (sampleSize <= 0) { + sampleSize = 1; + } + options.inSampleSize = sampleSize; + options.inJustDecodeBounds = false; + return BitmapFactory.decodeFile(picturePath, options); + } catch (Exception e) { + return null; + } + } + +} \ No newline at end of file diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeEncoder.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeEncoder.java new file mode 100644 index 0000000..1033ba5 --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/QRCodeEncoder.java @@ -0,0 +1,122 @@ +package com.android.sdk.qrcode.zxing; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +import java.util.EnumMap; +import java.util.Map; + +/** + * 创建二维码图片 + */ +@SuppressWarnings("unused,WeakerAccess") +public class QRCodeEncoder { + + private static final Map HINTS = new EnumMap<>(EncodeHintType.class); + + static { + HINTS.put(EncodeHintType.CHARACTER_SET, "utf-8"); + HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); + HINTS.put(EncodeHintType.MARGIN, 0); + } + + private QRCodeEncoder() { + } + + /** + * 同步创建黑色前景色、白色背景色的二维码图片。该方法是耗时操作,请在子线程中调用。 + * + * @param content 要生成的二维码图片内容 + * @param size 图片宽高,单位为px + */ + public static Bitmap syncEncodeQRCode(String content, int size) { + return syncEncodeQRCode(content, size, Color.BLACK, Color.WHITE, null); + } + + /** + * 同步创建指定前景色、白色背景色的二维码图片。该方法是耗时操作,请在子线程中调用。 + * + * @param content 要生成的二维码图片内容 + * @param size 图片宽高,单位为px + * @param foregroundColor 二维码图片的前景色 + */ + public static Bitmap syncEncodeQRCode(String content, int size, int foregroundColor) { + return syncEncodeQRCode(content, size, foregroundColor, Color.WHITE, null); + } + + /** + * 同步创建指定前景色、白色背景色、带logo的二维码图片。该方法是耗时操作,请在子线程中调用。 + * + * @param content 要生成的二维码图片内容 + * @param size 图片宽高,单位为px + * @param foregroundColor 二维码图片的前景色 + * @param logo 二维码图片的logo + */ + public static Bitmap syncEncodeQRCode(String content, int size, int foregroundColor, Bitmap logo) { + return syncEncodeQRCode(content, size, foregroundColor, Color.WHITE, logo); + } + + /** + * 同步创建指定前景色、指定背景色、带logo的二维码图片。该方法是耗时操作,请在子线程中调用。 + * + * @param content 要生成的二维码图片内容 + * @param size 图片宽高,单位为px + * @param foregroundColor 二维码图片的前景色 + * @param backgroundColor 二维码图片的背景色 + * @param logo 二维码图片的logo + */ + public static Bitmap syncEncodeQRCode(String content, int size, int foregroundColor, int backgroundColor, Bitmap logo) { + try { + BitMatrix matrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, size, size, HINTS); + int[] pixels = new int[size * size]; + for (int y = 0; y < size; y++) { + for (int x = 0; x < size; x++) { + if (matrix.get(x, y)) { + pixels[y * size + x] = foregroundColor; + } else { + pixels[y * size + x] = backgroundColor; + } + } + } + Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixels, 0, size, 0, 0, size, size); + return addLogoToQRCode(bitmap, logo); + } catch (Exception e) { + return null; + } + } + + /** + * 添加logo到二维码图片上 + */ + private static Bitmap addLogoToQRCode(Bitmap src, Bitmap logo) { + if (src == null || logo == null) { + return src; + } + + int srcWidth = src.getWidth(); + int srcHeight = src.getHeight(); + int logoWidth = logo.getWidth(); + int logoHeight = logo.getHeight(); + + float scaleFactor = srcWidth * 1.0f / 5 / logoWidth; + Bitmap bitmap = Bitmap.createBitmap(srcWidth, srcHeight, Bitmap.Config.ARGB_8888); + try { + Canvas canvas = new Canvas(bitmap); + canvas.drawBitmap(src, 0, 0, null); + canvas.scale(scaleFactor, scaleFactor, srcWidth / 2, srcHeight / 2); + canvas.drawBitmap(logo, (srcWidth - logoWidth) / 2, (srcHeight - logoHeight) / 2, null); + } catch (Exception e) { + bitmap = null; + } + return bitmap; + } + +} \ No newline at end of file diff --git a/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/ZXingView.java b/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/ZXingView.java new file mode 100644 index 0000000..254c02d --- /dev/null +++ b/lib_qrcode/src/main/java/com/android/sdk/qrcode/zxing/ZXingView.java @@ -0,0 +1,61 @@ +package com.android.sdk.qrcode.zxing; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; + +import com.android.sdk.qrcode.QRCodeView; +import com.android.sdk.qrcode.Debug; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; + +public class ZXingView extends QRCodeView { + + private MultiFormatReader mMultiFormatReader; + + public ZXingView(Context context, AttributeSet attributeSet) { + this(context, attributeSet, 0); + } + + public ZXingView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initMultiFormatReader(); + } + + private void initMultiFormatReader() { + mMultiFormatReader = new MultiFormatReader(); + mMultiFormatReader.setHints(QRCodeDecoder.HINTS); + } + + @Override + public String processData(byte[] data, int width, int height) { + String result = null; + Result rawResult = null; + + try { + PlanarYUVLuminanceSource source; + Rect rect = getFramingRectInPreview(width, height); + Debug.log("rect:" + rect); + Debug.log("data width * height:" + width + "*" + height); + if (rect != null) { + source = new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top, rect.width(), rect.height(), false); + } else { + source = new PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false); + } + rawResult = mMultiFormatReader.decodeWithState(new BinaryBitmap(new HybridBinarizer(source))); + Debug.log("rawResult:" + rawResult); + } catch (Exception e) { + e.printStackTrace(); + } finally { + mMultiFormatReader.reset(); + } + if (rawResult != null) { + result = rawResult.getText(); + } + return result; + } + +} \ No newline at end of file diff --git a/lib_qrcode/src/main/res/mipmap-xxhdpi/qrcode_default_grid_scan_line.png b/lib_qrcode/src/main/res/mipmap-xxhdpi/qrcode_default_grid_scan_line.png new file mode 100644 index 0000000000000000000000000000000000000000..8b642a36258fc9666485e9f8d75cc5f19ca9585c GIT binary patch literal 6033 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 j)_R8Jh~9>5Q;X{A1!iD{Qh7Epmw>Y>vO{{5_ zAQ{k3qXX2)03h+n)yds<8vuUhCxQPx0qhdlXy^LtcaSa@cS(SDOG?Yg?NQjfPig<5 z!(dhQUm!=df73g5{CAj{uQ9#ej&7!6}Op3b~eHVK>BWG#u!s*J*`y|Sv{L-f8A>P!d+UlqO zc-pfpnQ#E<5dVIg9DoRzy^o5T!wT@qyw?~eVOZ|^?0i|!I4jGKp={zp+x9l|^Hj-2EeQ6^YSN zhUrhI+~?gPnL(3O5Cy4TrLTBBJ?E~%(MeYKJHV1WdXP}KfB_riXhm*yb{=gPb&D(c zLZb^`@m}5^t!iOixd#vBm=nclR$E^cPUM(*#-pO>VF@;-ryGpNu zT0ccjM47Lh3V-OB9@a)JxD_-W#*0mEaC^b`Suh~6eCzgt8oUo&2i8^_rAEirg6l0u zxb8QjOB)N zi0Fe_ipoa_3=0pDJqR~g9E)!aiYggtk_xRyZTK^Xkj?4&dw8xyx+;2GQU{%#`@Y + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib_social/.gitignore b/lib_social/.gitignore new file mode 100644 index 0000000..c06fb85 --- /dev/null +++ b/lib_social/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +*.iml +.idea/ +.gradle +/local.properties +.DS_Store +/build +/captures +*.apk +*.ap_ +*.dex +*.class +bin/ +gen/ +local.properties \ No newline at end of file diff --git a/lib_social/README.md b/lib_social/README.md new file mode 100644 index 0000000..9d75756 --- /dev/null +++ b/lib_social/README.md @@ -0,0 +1,58 @@ +# 说明 + +## 微信登录 + +微信登录需要在`包名.wxapi` 包中添加一个名为 `WXEntryActivity` 的 Activity,并继承该 module 提供的 `AbsWXEntryActivity`。 + +```java +/** + * 微信分享、登录回调 + * + * @author Ztiany + */ +@SuppressWarnings("all") +public class WXEntryActivity extends AbsWXEntryActivity { + + +} +``` + +manifest 配置参考 + +```xml + + +``` + +## 微信支付 + +微信支付需要在`包名.wxapi` 包中添加一个名为 `WXPayEntryActivity` 的 Activity,并继承该 module 提供的 `AbsWeChatPayEntryActivity`。 + +```java +/** + * 微信分享、登录回调 + * + * @author Ztiany + */ +@SuppressWarnings("all") +public class WXPayEntryActivity extends AbsWeChatPayEntryActivity { + +} +``` + +manifest 配置参考 + +```xml + + +``` + +## 支付宝支付 + +- sdk 版本:alipaySdk-15.6.2-20190416165100-noUtdid \ No newline at end of file diff --git a/lib_social/build.gradle b/lib_social/build.gradle new file mode 100644 index 0000000..d2905cd --- /dev/null +++ b/lib_social/build.gradle @@ -0,0 +1,57 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + } + } + + lintOptions { + abortOnError false + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + +} + +dependencies { + api fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + implementation androidLibraries.lifecycleExtensions + implementation androidLibraries.androidCompatV4 + implementation androidLibraries.androidCompatV7 + implementation androidLibraries.androidAnnotations + implementation androidLibraries.lifecycle + implementation androidLibraries.lifecycleJava8 + implementation androidLibraries.lifecycleExtensions + implementation thirdLibraries.rxJava + implementation thirdLibraries.retrofit + implementation thirdLibraries.retrofitConverterGson + implementation thirdLibraries.okHttp + implementation thirdLibraries.gson + implementation thirdLibraries.retrofitRxJava2CallAdapter + implementation thirdLibraries.timber + api 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:5.1.6' +} diff --git a/lib_social/libs/alipaySdk.aar b/lib_social/libs/alipaySdk.aar new file mode 100644 index 0000000000000000000000000000000000000000..b092a8cfc6ff4a2ad482b0843a8c5ac84cfaa7a2 GIT binary patch literal 264696 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 z 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

    ke2SIPWG}-7 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 z z=^unJON=S)C!M^F(JyIuiIHArXvtpjxE&qT$#^>=_8^4Ml#D2Y8o{JOR z#qwv8t|ZUsZVZ8Kx*rfn*@aGyK@=9xv7-&Lcei-5^I>BGdC+2?FoMUIZBYA_>5rZzMc-nRns`Ft8~Q$)Pyn&xPQD?_3*JccsuL49^W z&e!+ALAs5T>kcc=8``2WZo;;r%P|Ip2LOW>=bVlXofhv1W<+o`4?(I}OhdKuwKsSoN-s{wcPaM7PJ* zu$_*A+aiyk)&z7dwBktaK@GoB&q5r#1?cdtR8XMlIE6AT$UPCg*5Y~Y)Lpl}4 zP6_0=2|^2yxRk3x2ZMtUdt8wP0P>p9=xX%#0BUkuFMDT8*-Q#I5gV$Q%t7fCv|_Sr zO@i0d&jR1G=f@h~Gv|Zp%i3Nj&U<%1R`cnMjGYW;ZU2uUbAOO}+`RS#XOra^!j8L0 z+#Z($O*=iO19eV@j=KWfyiu*>2C{?5=qeUJ%7e`4oe2*~3H4z@(@xq0e(alxm!jy* z=r~iK%fLudI7$9lrZZvATqCaOTY6xU0TtFyV^laG+yFHr$3Z>j6`bxZT5yTHcSA}u z0^gZ9W%t;vEo2VySiG?mt0}gEcS8)N{XioetAUGSYgCa7ao;T-7(DnYM(=`w%}86? zDT)hgv)v$eEVCSw9CNl|y7Db58um1+PuQL(5EIL<_f`}_a6}fyZ|W9D(GUIvi-F}W zXb%9ZsAjA_qa3U%adM z(j9cM*%eDj-PRXz&Q+8j$O=kis4B%6rc(A$m`j5@6d%yya1$O{1lj`h9x;1p^r9;} zVQsiOj833IY|1=1GUmv`uJ_4pxa9`X@i@HZ9!NQ}ERtd^xuv2s64vICs>ZDI=Kt+L zo+Y~jBl4>yf(zOB%f?eGpx2wpxD?)rr^%nrFJ6vK-XkikY zhjF22d+!#1I#s4AU{;%7vtV@3V~q-JHGWaGO!UavtXwQrg-RFZXv(QvjT&vzcui^{ zaX-a$OvjhXQse$iWt0kIiX?&0%_V5G0`-^LG#&Pt(s*_b`#pe!BcaTMH*4wwjo6fT zfu_7PNP%_<7J^hUA(X5c4g00|hEgyv8aJ91onN!Ct%W6XEKA%(jM0(5bWBKP`gmb@ z^Mx(J8e>RcNMfAQ!I(CN#%%$$Snpi)xv~PQ-^v8Q`KT6(43z%6bM`y+$OO5pR zVbA3A_`QHEyNzKv2H<+Tw0ru#!Whp=Pc>-Ed)|cb;{^6ExnF3uzp4%R@O!%ARJs!< zprQQE*omKN4qka$4tY4%rQC%XQ=^pyKvfcCsC!kd2ncZKv!-07$;|1MnQxK!XYn6w zlZftRWivD1u*d<49hueVx zh3=^e9I=x?F-@3HCM8jBF#~_);6heV1y^7jLq2wllRxM2I;L(t@=44YtCw@I$47+I z*uy=58$7?uS_h2F2=LoZSeII-jT&<+XA+3bBIc>nzS?m|d;>;(1pHhF<#-9rd@^6z zYaz}O?-(kqiy@Vh5*5CpK`y%V6{b(GJ?I-GO>7H~=|ea>RC0YJRJYltWiARBOr|3c zKJJU%4)+(u53`eaWuxv2v#7~uom&yAHdDqO7LL;4^(V)-xzwI_pIT1A1= z*g4_Rp7=r!?ZOp+WuMAA(-9tB`ifCt4O{hDTl=xp^0WH|aGYO}P_pFTpdLeS9FPpQ zwA9m}WHOQ2ZUVi2rL?6{uE4vP&4CzK3asrbq1V)GoEBinH+06>DFG8bLZe07jTj_S zMbD+&MS0fxisCVwV1&af>NK8!DOF0kF0j628v)tfq8Z5&MGQmYQ8Qrw=~dy0f98DX zXw^~Z$H_SJ8LO2pA34Va)Pnf*leDLq}7)ywx~X$Srf{MR3Wdi*IHfKVcF0Q zFQ&aQt|MrDRsFb3QPgY!&)``0lK#T=whwX_?vZS&myHsa#7B&?&o&lq5ee~<^YyaO zdC4osondsv9YSBNrEAcgFvToc3}5siaw&U3;SF}@dzR-An#Q`QH;7)MMUI)Gwk_ZtHh5|9VUX27rDmfoXD?R%( zTn~>|W;6ed9n$ZMAj}@iEJ6a-^~3g-$R^vc%- zrXLZt(86ynd7BEN{la|D{%CL2Rw-7wZ(Wtpni=#Vi3;mdGVgDbF!BFB+UgFbMlJv`z(3wg+nN1KA}uslUUop} z58%ohFEyIT%6t#)U*ibGEb<|&$%_s6yL48F=AjLzjB~0C{XR*PItZ~ila?==+fN2o0mHk1k=PDZU(hH6r-zPYl z#L8n2<}n9fsz*N<=I4)6R&*ARMBX^!!WRK;K1Op%b*R0VNMJp> z53V)oWtay6jq>aWBT*lM+nla>kie7QU-pbMNddh1gJ1C9A}sXZM_2{m3~&*(x3{(g z{6D~b+%OOqyPLfr=sc5V%QvhEIUQOciw10L*+{n}ys;yGeDP0i>uQ1m`D&IPXKuFr z*Jgz@E|pc)RZT7;JwAmWMz%k)Ef{18xv;*S4@M@jm|p4CLZ56Y(GasRM~&VR9KzqkB51G(Rj9+0hA0u|Dz;Chi3Vld!FEDCBw`bv>1u^Mwn z=B74@xFZxqZ&TD-4eopS_Ls`}s?Da*`5)B${uWo-|B9???Du&@j*qQ2s#tK>a}ez;{TQ)q0_N`s4}AsX<+kb{&-+Sed-U*dvBpRFm~ zdHs#NbM zkUhWt%)TZJgxR-eJmv_?jvFNBMRR91Gg$HbSeT@G%WK0_jIz7GO5H=g(pl7h&`1B< ze(3-73;erDT0>hER}$$%L#kS98LxlefyfPxO9W*bi*m5Vh7ymWPk0v#Gy$Jd+j6De zV=YCKHYl5y_}LfJ?{KKjO`U{DP1#dc-A~j{$WJ`m4w@~;T{r&%h*5xXml>>0+cc=1=eg6kJvpv(QG5Feu3oIxxGnQ}mo4j&_NFNLc|ppjI+Unf;TuInR~5mrq|NMj0AVVF_;UQo2P6qBBj zN6MZnFgcn{4Kl_ku`ksTzi!MSJ~?5}5?hPq*g;w%_UrcHvTl0f9fv`}9l|i6GYT5} zW)FZ(cze3iMt7jAmvV=1ka))rhb_po6o-ZR1q4tKD-7~m6kl}Vyt*>IlD#6jJ=REm z7<|#bys_4Z#<#LUH}iZ}$Z$ib=lr|@bjbsR5gH1}!;oeS2ZU_(!jV?EpiX<(n^hXm zRaRS&s^TUuD*GP0cbtsPYnjdiO)o2=n#k-nheM-+)xsh)(6AM|N`tk26)tMMTG^A{ zOvf&b$9EuA4z60E?s7d=&uN{Wc|GVB;?#r0`G96@5B*MX&wA8u322d3hCOUhA?sje zG38AfcNuO;{#isikcI2C-`7Z(oYOKpz5XY&XkD@y9c>FO?~;`FRuTGTIl!{nxOmgZ zt{=c>q20b93={mX*_D-jN(I zDoPh7vh1SjPEC9C`dDqHBH+`0u!=x?OqEbf)nmoWS}7Zqy$D?e?VX6dG1>kKv6O?N zB%LWtdr+wn87_7ngUz~lkChxLL%Lwkr3TfKRJ3V7g@3it)y2n|Hos77qzh{0rm@VS z?znJvxpfk4!%C3lC^ZfNjq{13T ziESntuDBd`<+_2NnzfnmVhR^>@~BIyJj*bAPUw}=jN~tqGxw_)bgwPFOt?4h*L_F% z0cNkDr#ak{0>Z7cP>ihuGGCW>gJ1;<6-F4waVY{A#sYe}Hy|W%_?CtSHG!OOJd%p~ zI-o~qU#5D7PsH32L9)%lC;eMoVZ*G5-MI8J&|iEY?{?cLW26AW zN!C-Nv8u|W!xOyvRdvuW})t&kx1?Z7%xY*BdmAcGLyn?3#5JX{u z0={+3S)J@--D0I>ZQrcwfuGwQLu~{XdJ+pP2!jq@M$@g2ws?T7?#`F zgV;yfIkFaG2fKrvVf`N+=`;~}dyk#D#qYQAHs8eghumzaP;MbQJzeC*KmkUntwajo zAsY#Nu{)*GeryHUEJ_EInY0&d&19b5K27v-XkAe0)GsKfdD45o5&z<*+kW;L;9tH# z{q60}f9mo6EgG$^|Bq|zElx5yEDAPx+$~=D8$6I;)YM$?lZa0*Zj|uIf5S_ zXXanbZ*wryx;(a`qC*P7b^Cv#2*lqOgv62g(Cq<36v=%4ac#{>zM1Ht+kZm9)w44T zfoNdLHNr$a5W}3&>4=?!1@H*mLkCg=qu?B|QE~zQaCid{Kv|Yf4};QrbAXG91~87F z(7-s|jHot@^m!Kmi9^sA4JSD5c0gqSka8gnfCjFhrAx7t!@!zX zV?YoQnFhwrjy45jLd1qipwOw<4L&01S+venAW zX&ufs)Wp+6;=-x4ob=$*C_{mSqxASlk~T_Sr*{H;^ESq#)8Ms zzU=#@n3|djmwQp#0LR}BJUNSw9B(oM ze$k7-77N=(I{WgEKB|TWVOctr{t}ytHaa4D>mhBEfzp(W$6<7(CwFpso6q`X549-{ zc^P}D;2QLN?MpQ?D^X6EmW&zb7u}7Ph)VJN)+*l-n1^>odybOdbr3k!jWZ{7vKxm} zc67C8Q|PCbcF>i&%cB6BO3jmXtM#Vxfa`44W>N@)kL;SWs-LN)syKBy_HtW|(-$L_ zJgyyQa@PTq=GdMrdEa8YjYVxwc8*u zyn+R(zbwJzs-W%>Iu3`L9ug>AjxD(ULi7EyX)5`AX!RxlS$SHxhsvt%v19=u+PJ3* z7%T+9+4rF$)esN(P^cOp{KAAdyqUV;J?#qn=umQ-m7K#D}v;q30F}WegB@wL%z8^^7l)M8|3KxQ9p|Bb814h<{g>#$1kuTj9Q5~tigMInZ z{kQGh|EX8`S8=RMO;>e63F$-OM=?FfyiV^vIt0BXGl_u`Qc4zDRJnM4cZsdezypWv zdAR)tJfZKMy1C8xXtZE2;Zd6|jFyzS2Mg;jx95#1ceiQotQ~*94^XDi5L{=w&V+ip zy^0W5XhyQm=ZW~I};+2DFi139M6_&X?fY=Y7B5x4dVnCE_MwcpFfwW2T@x>nuTB zlSaK~FHLuz%DfQ2erR8)EZHD+O6L9Dp*wR7t8;LWdSXhiU0HBft!EABPkTES_?ZXy z%z?wx9Tm$>^oABH!B%+;7Vz0I+Fg^=KDAN7TUWMBQ{(=Z3^_rZN#b!^l;2krJwErtA0E3nZWWfAnH=j)Rr0vj zI>L>-vM=d{mXTYgq*nOmiw^MoR>hFoIpqbK@NUbqWsn_Kr<%cA>{M1tm3`0nZ!aFk z&+3Dc=VRh6^ObY!FpQI-{aE7}na9Mht_f-1`~Np(+JuO-xpYxp75x_xwG7 z!0&7TZ+Q2q&v!yUS=nWCV**fKf}-e`-X)j5G0$h7D}i!A>KrDEat|phSTdGzjs8M7 z@`-ioesbzQa5BO*?_r+?`Dfi^Nb0Sz>Q5d){I}{BRR8r17&-jY`2T$toK3C&&*AWr z3}_)#P1FGM$%Y9O^nL6&8U6$$Fpsb2HRupc!!n|}XNh!J(9rT2A=@QS-8Y5&iv()c zy0+GjynIpEv$H?w>`UYi?$o&_qN9;`d$8$5UCHH(j2M)?B~NWGTNG3iweK zst)=f>yu2QGE?mXbvWkbW`sC zCh;TMB?mR2*#&?aFznia)S=(ZkV^teNyhchx4%f({|xJvhJ*hDN*Et z?V0^XUn0P{Z`;Am`@f>dg(XGXvwqO@!P!j*AY%6zy#?MeLYs+j9_T}^WeqiJ>t#)m znhj`T^#I-y?(CtD9Y*lnY!Q4^gY;f-4sfGym_GVn5J3`{gNT_}usI}B!JXH}^|k*Nn4 zNa-D5L7Yy`$MO^Iy@UH|(gIG-R}Jnr_F_(f*8gmaHEaw_+cVMYEpIVmoi?X4rZYlg zmBFILs=~6yvc@)NU1pWR#>K+L21LUQQw*C8b4Sxh+rkkRMLH6j@E(u=`^y1(HUs(| zsqnh>!$3{r68O4)&|%PFkzo`GW)o}^ZW9O-d=vhn>^&1TX3*$z_&T#9Tbukpj_+=j z!@v-iw$-QzzHTq&rcVU-`pEP7`@I22Pa6FVpzEvlklw}7lT;{^pFVW}~lhz9=)zd%_Q^w@4j#XtaVu0|>0S z#}Tf{5#MyMB zTBT}jKNzymL&_ynl!RDy}0t+^i=+{dd zVfqqvb;fIsepI~QOZHcyXlzyeuu+a$*bHJc4}2bW<$(pO_8PZ*XwB))(h#t}y`Bdt z(`9}MaB&OQe^sG)Ymca0NIyMs;b~|QRma_$68GxNJBItpBG5~bW2X7FzTUPC>~wQ; z0K49(T>pmC*pRW+hZ_|PmmYh(+|P32XgILd>&zjnY#mDVE1~XC?bMh&XYs-u8%Zk# z=axlrJBriSdBkrfUryq&fB32%_wowzwx;>0PALotN=2R(aS3fw4kOg0e?OIxbyQbn zLQ~=jp25@WaaSPZj9+gNK2(2gcudO^j320ioW;}7krtEAOe2w>qT&DOxn0Uy^}bxj zpBrWP0W1vd7(UEx?+BKvdb#sDmoMUsPk9d+Ks@*Wj)gTUqL((W2Tr?;QK!nxH#Et8 z)8E3x<9Xx9VxQT-QB8rAk!$Yb;SAbE5I?6HQc}Z2M`Lh9M{AG7I4Q1_O}y_H(KHfP zsMJYM#8f7wRHqlIPX!+kcEt|zWS5s^%xDrC`E&RfOuXu14mxF;v?GqG<}AoE^H9}| z<%yR>?MuqWmr5jFPrd>K6O6;sE zkcd&iozs6L4fo^LllB&xIupH&zf29&sFao*%gaUi#bZuYA|)zHmL2;efH#Jm=1fav zwV@#8?TiXyUb#|M(o&K~MmQu?luCRNUnV)|Y!hhMi(6h7?R2X2Hig3jdkT^WG=R4^ z#PI@Io{(q#J*zN{Yfy-D{Ux)HJGzWAXlmzzgw!<2FPY(Z*)Ujz04_9$xjo2qQDzvu zI{+0-#(B0*ZH2~*p`w<&amnZ%l=Pc%2|1RkrxLv{6_vya_4#9OrIRx?=)H^QJAc}3|@4oO(x?9P+{zG9Fj6?k{MuDNBC_yO=sna3cH0< zq1g|ekh{%I38^hhgj`3XY;9{AH-=yAF#$J>@=I>-tud*k>1yMnLl1u09eMl$(VPyCCjYVRHMb+&3!;xd_LaJwy;kp|ilu?m)5u!KLq44=66C-A{@5C*Gz_A_hcobW-? z#+~n|nJ1A8#!&h$KqC+o1Bni~Xmrq@{20rfHTai* zaDlWna&Au_-=cPGU`|mF))0+PLwVrh>CP)MTOesvP1{na>Jbe-VY9$9r$8-5cnk2R z=^!dYZNZkwI&g@EM+|Rl7z}4nC8i6fykb}shpiX~(P}o^YO|`3p-9+Ux1?_g0XFK{!ht*O z3dfc%1<~3eMpVkCe#XS+d@xrYe{9Jb-}tMR>s)g&Uun<%HNnvZ>Nr_@u82f1?&+qB zWO<`T-JBVXR|%}yD_dI-!nfcHo;?zH1}U2C8A^`~NhxsXI+Enejf}`_1^9ma`c1ug z5ORd|+Y0^qRr55SJ&5gJ-un)82#)YAZUGiE#fgaHDSR%<`9>J3W2W6=i^6&8j;n=z zTY%Rb>gN>o{E^G=aghV-nnsCrg}5wwj#?+oypvl^4$gSXt)dud=2_1rg)O}meM47F zX})J6CpnHo+F<<+oy0*L90l&~N<-I7%!6JGl z6b}R9dgZi1c1O7!Z z_+GIL=Fs76mEbFe7ps!*I#7b}6M8~GsUtt+jc>2vketsgU0(dMcFchjfzhj)-`A{b zW^+MuEq`K!5O@0)4q*44zmi?=U^Ut^Y<3E4HAW{)OUdNIS^Yrl8jUHKpz;DSg} zUEe}|#ec1bNi7Ys`)Yc-6M-V!noGc&U4Q>;Psn{kJu%Ugzf&#Qw}skybR!4mIE-o%gq2YbOWmXCThkzDUQ0waToz1$p9ULbta=8P_C)|b&-nbAjMkXUiskvK0LC--!b;@2Nj{~2jh zAA5`g?fk%ksc}Yr>YbVGtz&%6Y+Bi$CMBt6PvcC${Biom{f{KdSevfk2JXujal-$P zNtD^Y>tQMXQqaCmZChiAe~7w?OBG3?GqhmL*T5_HDsMtT7ZKY(-eMoRGUL>-r@7aB z0n4TprpAMzM8JEI?)AWh5|ILzC76lc;p2}xnP^3n{Gw6c?-*RvH+R@J<`{Lr5mw&c z*8~f6W*BAyOhrebmUrx?H=9jovL==9R)=|JKRe)SR%<`hhfEOYw4L^iKp?tTyRV3( zI!5$avgM^dVGr1tG-6+l@>8Q^*hNY1#YRS|10BavPYp=JzsJT$T#t9hZz*7$i(#Wb zI_W>ichcrYIXt=pDH)x}O^mgE=gk`wpdz2TXbuH(X!Ooo92GZwFICm&BbvIc(gDife~}j3rh9G5s$x?-EN1ku$MGaqDYQsFD{Patv?~wYwb~r~uZ+7!<3eRa1L*@{b(D%C02)cgo){WyE!M)B zR45-lP0vOy2vx7lR%H!C!jMce5t>gVFJJUqfIoJ2#=1g=wE4Ik??TwQOPnbbgTipm zGV6y%hxs$F_tWLvT%P~?J!G$X=dICpSaEz1*JVL8j(;h1VC%0TUl7oH-VqTQRh&sZG7)%nkoMYS69xSq- z9`aluGr|Ghu~_y{aX5njl7mt709+U^-tlxk?zh)F^lQg(XRW6L>&+vFJkjIg&chOZdZLisJwfEt8VeE7-1&$j$nznR~9+#5Hitnra`jAX65RxVr%#Hl6ffJ6P* zy;!DGr|ROp_7d6Ww!{bRO}*-Y97jE;PO#DOq3gu#(btHQmphi>9m4nS&@C&PraNPv zS~52}-K}DN{Us+jTh+|kwFS6bZPF?ha2)wjG+=j8Efbxm!Opy#=uYu55oC#y(j%Q` zNrXU>Ci>3;OEk5GZ6mC4tgP>eD=_1+)o2_sY|SAhW|5_4H(G1(p6V3m6O`Gpkbspm zu@)(jJwHW`2?dqjSQ~O_meg5}vOb?w~@xQL3b?Vtg%iOmFabpTT@oV%?F0cLYFAmrj zJPL|`>22B1{Nx|fDPon^)o~xD{w^%I(9xtsX4ASDWK7e<1(|2tX1NQ>=aO}sm$p6c z!iiKWbwM%Kre0}Se#q6dV0)X-(J5y{kjPoQGlySU_)HmYgwk>Q-4`QGWbaGW{(#!U zsD^lt+K`YBVy>wvA^qt9lzwRrl<{N&gMalfVM)}y?0xBkFUKX6@GDp@in4em7onh# zW@S*qVhAOkXN(kWqKZYY>X2lKX@rHB1N=%{gr=VHvWR7C!j*h)QsgJakBqQvx($*r zauy$^J#L|Z^k7s2g_YJQVLPl_H71_DEm6sPk8G8K$!SIe579jq5oya_T?sDi6k{>& z?9E`tF-*OoT?RA6*#wv-HHSNNukkjrxt{0=W^;$rFUFOT&IK?63Pa;BE|@t5EDV?Z zlKV*}6f(>>+{u0=jG-r^^AMX6gtsW<*#){@@VLd2OtI4Qzi(j#ldi=X4HhDQE4dcy z#!)tld`gAdk!R$4(Jilt8tx7F8pN+N@bG-&X&$5kkmoe5Jf-C(>cs%J$bLIQKT}5_ z%W*qF!p1(R@Hl3+5DvuR^)->2mxCNAB);JO!!e0Dd~*>6|MJE0Z;NdIkz@LIPl2ku z{elpZZ`NSL0gja5yi$Ish^1}W5E=pz`*{gvv6Pfj;QYqA!FWrp>KcN|r}~*D8HuC# zz9g|3J}f$#SpDJJNlpf*z1zr+zt1n2UGCg7t(As?g9rr0kp&zqvlL4zR{O+?EGnaQ zG3^2vvt-N6*~Ky-H!Na({v|NzKB6X$aBaaMii!wyfeZ}z z!)B9(r=R=h?%V4YwO}0&Tbj?#M^j=?VVxBfFbt@%N|hH=FnzOx8tX_sP*;Oqm`(h1 z3IOAVO&psbrFw4ATlgXu2o66{hRw^%BtlxFZoLTL)TWjAr{*X;(_n63Nf_tWofr+9 zY8@{4LEdpOA{yuM3l~dhj=!YRi00C6j>;~=ll;()ms_Zew1_-xa_DGNZ8L}MYoB-u zHzVusegB5f?hM_#h!p)=o>Rng9&8s}{aPUj@&f%2TY`|;B!Bu7g`!@bOZU`s|O z07hXY)qlNGHMJ&_wzG6m0XVzbxX4*Ly8!F}PXBiW0s%Kj1-F0b5ZJjv5=cN&!sq>8 z_m&22z7m_EU=?sqc9%Oti&T&SGic*cj#%@bsV`jlhw4o*qhCM?VI%d#~e47|0 zicir>Eze6zPpiGjOHRpzL)#w>8~@JC!o*z9^v|WJj9|3kL%j%Op#dzDLL6Vgq~J-x zaB(UBA_;0>ILM=a&`JK=CM(YW5I=D{H+g$gfT+ElndQF};`QU@75o3uvsMsFQ>gSN z#pR{Shh=K58;ToVol-k(HCN({n@k z1+ndWn@}*D6;3MfYPY`+fQk^S%O#v-xV1SPGF&vS{wP-edx(uZ`zflk+SFi(Wuw0J zoe4ognd@QTuH(cLd_^v|%kWm>G*cT5A!t_6T{uo<^^zZve#gVYZLk1FZ|B zmcI$UuAu{{d#OKQ`@}kM;X~TRY!-#;e)Bf7g&8Z*Ao%$Ym>-=h;6eXD9Pqa=bN`Rb zl=)xeOl`yNj}PQe3gNJDM=2;I70PzS6@X#RO4UUck?)w#ZOqNd$-%ChUa=<>1S>x%4HO5Hg-nHq0@H-oV^x|V zjY+~$u;kMJQc!L5Jphe@Et{Gs3RT?HVR?|QlzlS2e7_SI38ba4(PL={n4qvqXK}YF=^`OQl*T}8D%2?jKep{%o3Q|WW_uC{Y zt~qDzk6ua*+(6{l1Bym!b=GYhI#&u#|58_}6KeX+%9Ncgxi zv!Nc5*F~{*G$9LKHuA{g57+k2kd8l`1JN$^cpWgZU;A{GNH+x|hmo)G&TCMIm(}0G z3fL~o8G~K#+8PV+I8yvGG?Iwo@$I zrOs_^>osuhth{vxdd?eQ+P^P^qU%RY10q$)E$KAh0h;9KZ4PFm&X(H<^GA!&&R^xVb1uaG*FVOE z^|z|Cvj3rhsmUv-*;@nbL~Seqb}s*~jxa@m7p7kbE&GJ>q7DgG(Xe1CC^fZ@WcRDu z<;qL)k}K(2%e5A*!nHn? zS4Md793cv(&ELLHMCVzZtU1Y8h;yvWQk|?K109`G$4{f$rCqrxb!}H2vrE=@Hm}K^ zY0Q4)POXeZ*jljZDyF~rFaB;WdEe?t7t?FqiMH^$!}z|G)UX|8gMziA}TSg(j*d(kH$2>J$Z$L;{Hpgt928Sia7ML@d08NAQO$20zZztDwnhspWq_TobZPzX|PJ3}zZI;r+}p?73ol=~G) z3^48@!!^Y_DfV5F?ZZ7(2X#rdG45i+-Nid8_mjh!k#C}2WCksfzeT+aN0h7%)rT4N zM$kMkZw@#x4v4aBu1-6&Mzd^=)yEjsMLUI<(ixWrB#N-z z2Ks_tZX+dybDEwXy3ix978LVh*4t23oE=vh(N|STvoDNw#~EC8Si5mGl4sc)g7bD6 zkz>ykH+c}H$|UL`J}~hYx-;Z=s&$lJaMLMWps&o8y|n0gd$qI%!LrRv1ZyY$qTF_v z5(l@@i5HDLJQRWZY&Eleo-SXhJr!nm6NjLe9$k+$UuQD3? zf_NHD*=|>{G7Q%%l_j^bw{o;{G}~EN?#wPPlL%R061fhc;Gml}2Fx==Rlq&zGpsIe zml|>{Vn)x#%q2xuf$}xNQuX4?pj=5)tyO2DEBWU5S?bRA{Y0bbEK|yz_OoTGIQ;=P zbe`!*tkNI(B`h)4+7aZtaExW7fpnSo(p+wcL;(zvWt%FtGJK7Tja0!ViYY%*v!&=%egDiDBOi3PKam7~m^aWZ6-G7gP|be3*J_Y8Q`r1wmP^09wQMOEu|IIghJ zv;XTodc7mU?__~uvs0f-{JDKozW4%#1J@H79{V!Y-^Vq})AjAe_)IYogU({n3TOyW zRJdx7yV1w)b6m-j=Q1qA_==>FBQ~Eb&h?5@rp;&-8n>%X_S$Jlr;&r?C|G@SR*NDE z-D-4OkyUZ^0pw}o-awX$0Uu`=EuUZG8?~lkr|U&pwQZ1R=tX4(9I8!H3jC)@GLzL5wv?{gvuE^K9KZ#<`i9L1VwW!I4Sju?jTvd zAJEkTU>{s}$ZWFau%0IPpW4;0V2|d?xX_<=w?_+FIK&gW(?%(u`<7B&A5q;q7FOAE zTUwz!0~x)DGERNd9}?8>ApY`wx~vh9JD%0KBF^c+q+8e3chysYP%r;xUzi+|a^{nF z3QIQW1vDK7>Nf2`pZvLldnnaY7*4Nj$OR+&hf&DZ;7+&fpb54fdcK&H1lU!SGY1d7 zk37c?c(5gexW@I|lcQli-E-voP7_GR>0R=;sl&2ao|y6Qy7f61kSvNhv(%rKCZxb} z6hacalxAeNqUtJc8U$xwGMf@YDQxP#DzLd6`(%pv4t(?MjpK$kLW zS!kfzN|i+kIP*lWu&YQEkr>KsDo@4uL=OOBgp989Gpq+c|5`MM79_M0`X?&-BmAAG z7yTbmufKxiKO^J6WWALCF`eKxJDM1f^! zvzwO)hhs9A$@|mcA$Xg zf5uY&7Z27~L={HkUr4nR)TpQ`R1zAD+@R+Q3a21{oyAgVlnM$N9656%hn=ul*kZre z^tt0?YY57GvD%L#*|yAzR|;=1O5`>-yPoVazdoGKTOjQD!Z#EJSIX`HugFGctuvicc`J-u5(Lu&h|PErMf(=sy*e);e% z9&0db$AdE!qrD%=-S%tb8}x8o!2;D!_?4B2#-4<8G(i(LL-O&Q-OL)fltcGTsluXR zNZKdLV5~T(nA}1#jQR7G^X8tvwvoV@_G0|8ysN*xE8zMcS>L~GPyNCfRUPe9|Nal@ zqrVcNR_^@*@zX+^I)8PMW{{@AvZ|yBGLJKhU4QlZ`f{Ma?|$bti7@95wS3mt+*HEM zbXaiwZc`pEa9M+O6Y<3LhWj_~$!6EdOpo6?d@qCpcHLNd!d-5(HA^LnK)j>sa2bn0 z(%p|}MpGZ^0tzF8{-6+XXhNA6seX89S`-a(pX~nsgS3ANvuxSAM&U}^nrYj%ZQHhO z&$R7ITUBY>HY;sAt1>I|)LQS}@Be=n-#Ys|`zGea7#Cwk^ytxBkKS4*k_m~W1qc}= zhKQVFLqIue^fOVQS5VP#g=u2ojbw(P#FjFLxmx1|r}y(_q8}y`IYP6f^}lg3j^%`; zLS#)FA>LRx;bXB@nDT@wI%R>tTRwzmU)R`fvFP^{I(u$gXQuDr`kZF<19!S={Y%*= z;2Zsn!;P5@4?GrKr^?EBJ&e|ZHBjCalzs>zt|Hib2NoNRu13sQbv&4k~1!DvL_hnJJhIdN!DKsHt1ginVTYzoOfL!%m` zy`hU7`{~j%+OyVXek$x;UfQrY#I$kIfxf}vs3n2!M%Mc_>e0Mo#`P_xn+C6E+x$RK z+dys{99+UvKYxWjrNt7EyogV1;G7K^+OZf%tlAut9Rra)c!Z}IT%%j(DR~=hkG+W?M zn*@YHYwTB>H;`8Mswz1h^ax_}jbLo!;thfBCiO`covn`j<^gvRK3_>ry?F^>;YlDb zaIZXJko~%T9;~0~b}Cf3!0pmwWJ!ON-R?U35xSH3Ec)b}7I-4Lr|QNZu10RxC`2R& z?Ae21M}ZyVk_J0slCJ?%8ls9M%Z406Bbz_{%Ip;01Zq+OHX4&NhOzFCN6+8i$AP%K zyhdT*twxu0Mld8}90h5#XJLcKC6XxjAn3N_|hIyx-b z-BnmXJ&3KK$yXDOaVUsUYtruFXG}-kP{Q((l8ZGd3cd z!}a`yQTz#zT-i|X=>njb5p#w}?^KRv`7vTP+oA;{Vcx!4DHL8IqmEakp)X%%^(uMc z_?I_;lXpX@0Q>UA_8&#^|AQ>F`M%;G%?ZpLkV{!qrNxp_v7hhzIYs8P2C?KJY3=TVL$UY;a_sCT@{7b z1?LCH5zx8md9ArL#`TF?5sCDD0WuM6#U2%-x6?`N*JYoXxb+(ihx!5^UhAI=qR*Nc z-3zN}Ap`UgF+}Gc5mL~p#YY+n($I)q`O{>H%TrjE+q0|fm=~8_1pJU?=0tBi$Lx7D zw8PQvt>z)=Ouk9BraNo7s@GmcjWJa_nEEYQOG~%{NO*L>;GtO|J5-qPkmDr z*Bt$mo@^aC-KDolqqa$2w80LA3L_ELt5UB7Hl$K*+pcANzyd7=LAvQjeP9q{-d#7N zf5nv?49mgmVQ66L0r$E-k$IAp#j~fv%+1!b-%h}eVc_R8ZxG5m`j8274haj%HxdDq zB(x+fVsiilk`o7520P&iS;@#wy(9-P1rm*zY?4ET#|XAi5Zhab;1X z<exa7#KU`&K&!G@&@cpe@fCAGb+o2k9B(qvXZaR+|!ID5f-H zH`U3u9(SdaT_IZ(9bJ@E{d@`11!WLd2*`RR^VCZy*Li}ML|oR1ceJ7Do>oo?a$uCS zInR#ho=IhI-hO%10ZzJe&tSTd79QnaFFzUbjyNDaM{e^DUUl$&7KA^%;Lr4CPnbu` zY|T}vImxiQskW9ufS2J7b7n|ef%0?#S8Q zOToi?V9E2#-(@%oDK%Jiv$@MFsP{o-II6Q?T%s3?VQ;H}WryI81r6Bh+T80=&mQ~= zHv9^eKDllnSV-LMC3bJgQs`1tTfVG;ebElbSWVx8JY1&cPr9ngo5{DC+_KaAO!t0n z7g9>I%GqFAFO5}(W-(G@VsGSssM*LpcR-)=TfD9EWIUv&W6qs5R#wc0UO9=5ms`m~ zTUD-;ELpY)J%Mr4=dG6|eobqvHqLa&M%I@E5#d>rhP&f7NVAs2#MgEq%iC~cqm*{+ z!*??YB5_O>Czpl0EV#8*XKFrIvULlFkX8_*wuzOnQsV{ zou`cC>+c&NeH8a}ZU=dB+E|IexrNF$g~QP6f(m>zj<}sW%c^6eG}L9s-SrJq$dLDK z)MwCrH1XAIIUIiOuv?G0@>o~AYZ=Th7e6q(A$Vs#kDW*%Oe@sm^;$uY+7qSR<%tWI zF71ivn1Zwc)m zJ^fyM_czrK#a^Xd;ZG#!{f|1{|B;XS*HuTUn!f6y5IVoCtg46-6;Z%f(2%A=0LqbQ zVtup@h%qD-q+zE0w^So(7VD~e)pxAh5OGVt$9}>iuZ$w5#CY?m!S+ti*?eCYPs_lM zU%&ACm^AjKMj8R5$sy7Zv*>cvnW~A(w|f0uAvGbZu$XFM^W`M*yfip-)O&Sd4j42J z)pJg=A{k*H5=+IcD9n#iZIpXGKxMuKCJ_ArQJ6tg2{t)XsliyhX_C@Chy@&jiFsPS zm0QR^s7BQCizrW)&SCsfXN!4e9U3$xlSXShV-1F(9gHDY8P)TQuE*{#GE zxwPvLvSY_B5qGOqhn0(_#6Rs2Mx{$NL|TAZ-9yXJuBxWRCMhkZP^$3rv9>VTnqt^Z zH3!gws=LEQR4hz0bIM1%fR0b=bYkrbq$HSY?}5e&oA|;XbkGXp1b={>SO4>Cet)+< zrzMt{awho>;&b$huPDG9*uz>P7$Xrb+fSps3;A0nP@ZqFMtYvj>>#q$R+X`6Ai0p9 zF(i?JtsuYO#X&N2=(56v9$QCI^M}LRwPZf668UiH@stlo)Huq^lcg* z#gF0y{EfHINg_An{~#XW|CV%P{_oa6wMnvnB{;%ompG(tmpE1=mnCoso0PGKO7}m-ygthB)9QAnXi)ytS8bWCz=3h9Tl=iXKel}{=ENg_Hbe3aATp|Cpybn$*P#a~ zU%rs4$N%BwxzKs`m~G4p0IyXR#QTBpZi_tz7yP8As*-BhZT1m4l>+}{JzqIoAgY+H zDQp{w4@)ZBel$i6>NDvCo3z=uYT`-4New3x>lWNYcBRUvQ)DC@YC6!&dlNDG#y1 z0K!TZA?Y%TICl--!SJ*0K)a|$x7lQ%irt!6{bxL8uRA*_vlbn9di@}rFv?~7+P zwj*tIKt-@3Pq`O4#%C5*kxvD7lW?*FoRIlnY(l0&L(|)~+M~Y~cA z8!o!Ba&k5J*(S|-I9;?!**f$1s)ftvJ5A&DOGiQId8;5Q$$eJP-97ZMQib7$;qEX1 z!n_`@Ztk}q_5-;s1B-4s7`q_Dvz3YT!ZuZcf#3uFyTkyR6!HR z3y+4QL?!y#hfXC{Uaq_>CEMgpu9mp`6Rag27Tw7*F$>-G8p`k8b`xdYZ*emV`a74a`9d@m_~jE_t7j)^PAsejBCAkrE=+6`@%KKAvOEg9GOWw!t{&u8-zwC~%|7^9{e`N6fN89aRg}1ed z|I+fYf0$pXu&-%mo)0bOpo#HP^SPr4f$GnRu*aELX`!}9|5cCv2QLct5t7dgp(7oi z4)8g86fnU*{CxTV*+qZDShBy|SgS4yV+RguS=upi*iscAe}!4?N7O;wfKS+C1#JQ+ zn!9wXmpE4*Z_GwKlIK}n+{+SbU+l(cyLfm1d>Z&&>&m}ut=QKl1=$#+%NDJq=3gbX zOC&>~689uWUz#nS+(R%L!{w8C;noh=n$%iQG!YWtKJT5bdn@LWaTdv;UWAmF(Sh|( z^J3KY@&4rvsjbsRVu#Xt<{^E9Y0l^;%I-Ey8yjmOL0g>Hh)VE{&%|?Tf>q0#qSEr# zdJa6Ui?MvSeE2SaFqx2ss{ISj5`SY5T3hmdel(jEWh z!%H-}kl22qP443`Mn?3N7_Jm0?;8N41(t zuj>8K5b{6K8=q#PdPB`_UdRjV3cFQXvGAxnOe@)xOlArbBXQ{-o`XzV?H->6TkHWa ztSLT%iF|AI4%(AkELz5k$XIyPG|QJ}FFb4&)ul}`QL3~RT0<4$cs=-LZRx%{(U*8y z`Ul+ivyh`zYS%o-&GH)M7uop9oGlv&g|?k0+tfvu^xegEM=G^-hGK>X8aGyWy!r+5 z6oEXFOxY*v+*6A!9@5Wnqe|JHS!Gp9UBMgh4^9miZ#H|c^}AfBa3iLZ&D^bCZ4z>mve7J^)F%$-i8tmn-<%1TtxK52Pm;Lqkjr!;3jZOB%%-1prgv8? zmd3QmbaI>QJ&ZHuy2GaK0Ub-wUhUYP@}Bg=_F_X);SqNjk&G=1ZbR6-?gS}%t{Bzo(>qqE{P(|)l1(8TyLBv7RK!Z-;!;Xo6?P0P;B~nUO!2RkM{Sj1u3;O4* z1$(C&{k}+q550iRy6EolVqkmqU9qgZH{kOX<|~#n3O@;j*i?)bawCn2{7^?E0>~n$ z1^5(fCK@tY1J!OykU30Rq)g(ioCpRFOg^d`8eayo5SDNvksNZ8Qe0w~4j=I;k(cmS z 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�)8W8ts&Z5FTE+vN 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ⅅ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 + + + + + + + + + + + diff --git a/lib_social/src/main/java/com/android/sdk/social/ali/AliPayExecutor.java b/lib_social/src/main/java/com/android/sdk/social/ali/AliPayExecutor.java new file mode 100644 index 0000000..83b8184 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/ali/AliPayExecutor.java @@ -0,0 +1,132 @@ +package com.android.sdk.social.ali; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.text.TextUtils; + +import com.alipay.sdk.app.PayTask; + +import org.reactivestreams.Subscriber; + +import java.util.Map; + +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.functions.Consumer; + +/** + * 支付宝支付执行器 + *
    + *     支付宝SDK版本:2016.01.20
    + * 
    + * + * @author Ztiany + */ +public class AliPayExecutor { + + private static final int PAY_RESULT_SUCCESS = 1; + private static final int PAY_RESULT_CANCEL = 2; + private static final int PAY_RESULT_FAIL = 3; + private static final int PAY_RESULT_WAIT_CONFIRM = 4; + + public static Observable doAliPay(final Activity activity, final String sign) { + return Observable.create( + (ObservableEmitter subscriber) -> { + if (subscriber.isDisposed()) { + return; + } + try { + PayTask payTask = new PayTask(activity); + Map pay = payTask.payV2(sign, false);//不要出现丑陋AliPay对话框-_-! + subscriber.onNext(new AliPayResult(pay)); + subscriber.onComplete(); + } catch (Exception e) { + subscriber.onError(e); + } + }); + } + + /** + * 检测是否安装支付宝 + */ + public static boolean isAliPayInstalled(Context context) { + Uri uri = Uri.parse("alipays://platformapi/startApp"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + ComponentName componentName = intent.resolveActivity(context.getPackageManager()); + return componentName != null; + } + + /** + * 同步返回的结果必须放置到服务端进行验证(验证的规则请看https://doc.open.alipay.com/doc2/ + * detail.htm?spm=0.0.0.0.xdvAU6&treeId=59&articleId=103665& + * docType=1) 建议商户依赖异步通知 + */ + private static Integer parseResult(AliPayResult payResult) { + //String resultInfo = payResult.getResult();// 同步返回需要验证的信息 + String resultStatus = payResult.getResultStatus(); + // 判断resultStatus 为“9000”则代表支付成功,具体状态码代表含义可参考接口文档 + if (TextUtils.equals(resultStatus, "9000")) { + return PAY_RESULT_SUCCESS; + } else { + // 判断resultStatus 为非"9000"则代表可能支付失败 + // "8000"代表支付结果因为支付渠道原因或者系统原因还在等待支付结果确认,最终交易是否成功以服务端异步通知为准(小概率状态) + if (TextUtils.equals(resultStatus, "8000")) { + //Toast.makeText(PayDemoActivity.this, "支付结果确认中", Toast.LENGTH_SHORT).show(); + return PAY_RESULT_WAIT_CONFIRM; + } else if (TextUtils.equals(resultStatus, "6001")) { + return PAY_RESULT_CANCEL; + } else { + // 其他值就可以判断为支付失败,或者系统返回的错误 + return PAY_RESULT_FAIL; + } + } + } + + public static class PayConsumer implements Consumer { + PayResultCallback payResultCallback; + + public PayConsumer(PayResultCallback payResultCallback) { + this.payResultCallback = payResultCallback; + } + + @Override + public void accept(AliPayResult aliPayResult) { + int result = parseResult(aliPayResult); + if (result == PAY_RESULT_CANCEL) { + payResultCallback.onPayCancel(); + } else if (result == PAY_RESULT_FAIL) { + payResultCallback.onPayFail(aliPayResult.getMemo()); + } else if (result == PAY_RESULT_SUCCESS) { + payResultCallback.onPaySuccess(); + } else if (result == PAY_RESULT_WAIT_CONFIRM) { + payResultCallback.onPayNeedConfirmResult(); + } + } + } + + @SuppressWarnings("unused") + public abstract static class PaySubscriber implements PayResultCallback, Subscriber { + + @Override + public void onComplete() { + } + + @Override + public final void onNext(AliPayResult aliPayResult) { + int result = parseResult(aliPayResult); + if (result == PAY_RESULT_CANCEL) { + onPayCancel(); + } else if (result == PAY_RESULT_FAIL) { + onPayFail(aliPayResult.getMemo()); + } else if (result == PAY_RESULT_SUCCESS) { + onPaySuccess(); + } else if (result == PAY_RESULT_WAIT_CONFIRM) { + onPayNeedConfirmResult(); + } + } + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/ali/AliPayResult.java b/lib_social/src/main/java/com/android/sdk/social/ali/AliPayResult.java new file mode 100644 index 0000000..416e546 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/ali/AliPayResult.java @@ -0,0 +1,85 @@ +package com.android.sdk.social.ali; + +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import java.util.Map; + +@SuppressWarnings("unused,WeakerAccess") +public class AliPayResult { + + private String resultStatus; + private String result; + private String memo; + + AliPayResult(String rawResult) { + + if (TextUtils.isEmpty(rawResult)) { + return; + } + + String[] resultParams = rawResult.split(";"); + for (String resultParam : resultParams) { + if (resultParam.startsWith("resultStatus")) { + resultStatus = gatValue(resultParam, "resultStatus"); + } + if (resultParam.startsWith("result")) { + result = gatValue(resultParam, "result"); + } + if (resultParam.startsWith("memo")) { + memo = gatValue(resultParam, "memo"); + } + } + } + + AliPayResult(Map rawResult) { + + if (rawResult == null) { + return; + } + for (String key : rawResult.keySet()) { + if (TextUtils.equals(key, "resultStatus")) { + resultStatus = rawResult.get(key); + } else if (TextUtils.equals(key, "result")) { + result = rawResult.get(key); + } else if (TextUtils.equals(key, "memo")) { + memo = rawResult.get(key); + } + } + } + + @NonNull + @Override + public String toString() { + return "resultStatus={" + resultStatus + "};memo={" + memo + + "};result={" + result + "}"; + } + + private String gatValue(String content, String key) { + String prefix = key + "={"; + return content.substring(content.indexOf(prefix) + prefix.length(), + content.lastIndexOf("}")); + } + + /** + * @return the resultStatus + */ + String getResultStatus() { + return resultStatus; + } + + /** + * @return the memo + */ + public String getMemo() { + return memo; + } + + /** + * @return the result + */ + public String getResult() { + return result; + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/ali/PayResultCallback.java b/lib_social/src/main/java/com/android/sdk/social/ali/PayResultCallback.java new file mode 100644 index 0000000..d9bdf0f --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/ali/PayResultCallback.java @@ -0,0 +1,14 @@ +package com.android.sdk.social.ali; + + +public interface PayResultCallback { + + void onPayCancel(); + + void onPayFail(String errStr); + + void onPaySuccess(); + + void onPayNeedConfirmResult(); + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/common/Status.java b/lib_social/src/main/java/com/android/sdk/social/common/Status.java new file mode 100644 index 0000000..735e2a3 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/common/Status.java @@ -0,0 +1,97 @@ +package com.android.sdk.social.common; + +import android.support.annotation.NonNull; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-07 17:42 + */ +@SuppressWarnings("WeakerAccess,unused") +public class Status { + + public static final int STATE_SUCCESS = 0; + public static final int STATE_FAILED = 1; + public static final int STATE_CANCEL = 2; + public static final int STATE_REQUESTING = 3; + + private final T result; + private final Throwable t; + private final int status; + + private Status(T result, Throwable t, int status) { + this.result = result; + this.t = t; + this.status = status; + } + + public int getStatus() { + return status; + } + + public boolean isRequesting() { + return status == STATE_REQUESTING; + } + + public boolean isError() { + return status == STATE_FAILED; + } + + public boolean isSuccess() { + return status == STATE_SUCCESS; + } + + public boolean isCancel() { + return status == STATE_CANCEL; + } + + public boolean hasData() { + return result != null; + } + + public T getResult() { + return result; + } + + public T getResultOrElse(T whenNull) { + if (result == null) { + return whenNull; + } + return result; + } + + public Throwable getError() { + return t; + } + + public static Status success(T t) { + return new Status<>(t, null, STATE_SUCCESS); + } + + public static Status success() { + return new Status<>(null, null, STATE_SUCCESS); + } + + public static Status error(Throwable throwable) { + return new Status<>(null, throwable, STATE_FAILED); + } + + public static Status loading() { + return new Status<>(null, null, STATE_REQUESTING); + } + + public static Status cancel() { + return new Status<>(null, null, STATE_CANCEL); + } + + @NonNull + @Override + public String toString() { + return "Status{" + + "result=" + result + + ", t=" + t + + ", status=" + status + + '}'; + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/common/Utils.java b/lib_social/src/main/java/com/android/sdk/social/common/Utils.java new file mode 100644 index 0000000..f2d7305 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/common/Utils.java @@ -0,0 +1,17 @@ +package com.android.sdk.social.common; + +import android.text.TextUtils; + + +public class Utils { + + private Utils() { + } + + public static void requestNotNull(String str, String errorMsg) { + if (TextUtils.isEmpty(str)) { + throw new NullPointerException(errorMsg); + } + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXEntryActivity.java b/lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXEntryActivity.java new file mode 100644 index 0000000..7448883 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXEntryActivity.java @@ -0,0 +1,58 @@ +package com.android.sdk.social.wechat; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import com.tencent.mm.opensdk.modelbase.BaseReq; +import com.tencent.mm.opensdk.modelbase.BaseResp; +import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler; + +import timber.log.Timber; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-07 13:51 + */ +public class AbsWXEntryActivity extends Activity implements IWXAPIEventHandler { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Timber.d("onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + // 如果分享的时候,该界面没有开启,那么微信开始这个 Activity 时会调用 onCreate,所以这里要处理微信的返回结果。 + // 注意:第三方开发者如果使用透明界面来实现 WXEntryActivity,则需要判断 handleIntent 的返回值。 + // 如果返回值为false,则说明入参不合法未被 SDK 处理,应finish当前透明界面,避免外部通过传递非法参数的 Intent 导致停留在透明界面,引起用户的疑惑。 + boolean result = WeChatManager.handleIntent(getIntent(), this); + Timber.w("onCreate handleIntent result = " + result); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + Timber.d("onNewIntent() called with: intent = [" + intent + "]"); + setIntent(intent); + boolean result = WeChatManager.handleIntent(intent, this); + Timber.w("onCreate onNewIntent result = " + result); + } + + /** + * 微信发送请求到第三方应用时,会回调到该方法 + */ + @Override + public void onReq(BaseReq baseReq) { + Timber.d("onReq() called with: baseReq = [" + baseReq + "]"); + } + + /** + * 第三方应用发送到微信的请求处理后的响应结果,会回调到该方法 + */ + @Override + public void onResp(BaseResp baseResp) { + Timber.d("onResp() called with: baseResp = [" + baseResp + "]"); + WeChatManager.handleOnWxEntryResp(baseResp); + this.finish(); + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXPayEntryActivity.java b/lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXPayEntryActivity.java new file mode 100644 index 0000000..5364fe1 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/AbsWXPayEntryActivity.java @@ -0,0 +1,41 @@ +package com.android.sdk.social.wechat; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import com.tencent.mm.opensdk.modelbase.BaseReq; +import com.tencent.mm.opensdk.modelbase.BaseResp; +import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler; + +public abstract class AbsWXPayEntryActivity extends Activity implements IWXAPIEventHandler { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + WeChatManager.handleIntent(getIntent(), this); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + WeChatManager.handleIntent(getIntent(), this); + } + + @Override + public void onReq(BaseReq baseReq) { + } + + @Override + public void onResp(BaseResp baseResp) { + WeChatManager.handleOnWxEntryResp(baseResp); + this.finish(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/AuthResult.java b/lib_social/src/main/java/com/android/sdk/social/wechat/AuthResult.java new file mode 100644 index 0000000..d01883d --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/AuthResult.java @@ -0,0 +1,32 @@ +package com.android.sdk.social.wechat; + +import android.support.annotation.NonNull; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-07 15:17 + */ +@SuppressWarnings("unused") +public class AuthResult { + + private int errcode; + private String errmsg; + + int getErrcode() { + return errcode; + } + + String getErrmsg() { + return errmsg; + } + + @NonNull + @Override + public String toString() { + return "AuthResult{" + + "errcode=" + errcode + + ", errmsg='" + errmsg + '\'' + + '}'; + } +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/PayInfo.java b/lib_social/src/main/java/com/android/sdk/social/wechat/PayInfo.java new file mode 100644 index 0000000..54b93c9 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/PayInfo.java @@ -0,0 +1,86 @@ +package com.android.sdk.social.wechat; + + +import android.support.annotation.NonNull; + +@SuppressWarnings("unused,WeakerAccess") +public class PayInfo { + + private String mAppId; + private String mPartnerId; + private String mPrepayId; + private String mPackage; + private String mNonceStr; + private String mTimestamp; + private String mSign; + + public String getAppId() { + return mAppId; + } + + public void setAppId(String appId) { + mAppId = appId; + } + + public String getPartnerId() { + return mPartnerId; + } + + public void setPartnerId(String partnerId) { + mPartnerId = partnerId; + } + + public String getPrepayId() { + return mPrepayId; + } + + public void setPrepayId(String prepayId) { + mPrepayId = prepayId; + } + + public String getPackage() { + return mPackage; + } + + public void setPackage(String aPackage) { + mPackage = aPackage; + } + + public String getNonceStr() { + return mNonceStr; + } + + public void setNonceStr(String nonceStr) { + mNonceStr = nonceStr; + } + + public String getTimestamp() { + return mTimestamp; + } + + public void setTimestamp(String timestamp) { + mTimestamp = timestamp; + } + + public String getSign() { + return mSign; + } + + public void setSign(String sign) { + mSign = sign; + } + + @NonNull + @Override + public String toString() { + return "PayInfo{" + + "mAppId='" + mAppId + '\'' + + ", mPartnerId='" + mPartnerId + '\'' + + ", mPrepayId='" + mPrepayId + '\'' + + ", mPackage='" + mPackage + '\'' + + ", mNonceStr='" + mNonceStr + '\'' + + ", mTimestamp='" + mTimestamp + '\'' + + ", mSign='" + mSign + '\'' + + '}'; + } +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/SingleLiveData.java b/lib_social/src/main/java/com/android/sdk/social/wechat/SingleLiveData.java new file mode 100644 index 0000000..3f03d46 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/SingleLiveData.java @@ -0,0 +1,42 @@ +package com.android.sdk.social.wechat; + +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.MediatorLiveData; +import android.arch.lifecycle.Observer; +import android.support.annotation.NonNull; + +class SingleLiveData extends MediatorLiveData { + + private int mVersion = 0; + + @Override + public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) { + + final int observerVersion = mVersion; + + super.observe(owner, t -> { + if (observerVersion < mVersion) { + observer.onChanged(t); + } + }); + } + + @Override + public void observeForever(@NonNull Observer observer) { + + final int observerVersion = mVersion; + + super.observeForever(t -> { + if (observerVersion < mVersion) { + observer.onChanged(t); + } + }); + } + + @Override + public void setValue(T value) { + mVersion++; + super.setValue(value); + } + +} \ No newline at end of file diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/WXApiFactory.java b/lib_social/src/main/java/com/android/sdk/social/wechat/WXApiFactory.java new file mode 100644 index 0000000..1693efc --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/WXApiFactory.java @@ -0,0 +1,45 @@ +package com.android.sdk.social.wechat; + +import io.reactivex.Observable; +import io.reactivex.schedulers.Schedulers; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.http.GET; +import retrofit2.http.Query; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-07 14:50 + */ +class WXApiFactory { + + static ServiceApi createWXApi() { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.weixin.qq.com/sns/") + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())) + .build(); + + return retrofit.create(ServiceApi.class); + } + + interface ServiceApi { + + //获取openid、accessToken值用于后期操作 + @GET("oauth2/access_token") + Observable getAccessToken(@Query("appid") String appId, @Query("secret") String appSecret, @Query("code") String code, @Query("grant_type") String granType); + + //检验授权凭证(access_token)是否有效 + @GET("auth") + Observable validateToken(@Query("access_token") String access_token, @Query("openid") String openid); + + //获取用户个人信息 + @GET("userinfo") + Observable getWeChatUser(@Query("access_token") String access_token, @Query("openid") String openid); + + } + + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/WXToken.java b/lib_social/src/main/java/com/android/sdk/social/wechat/WXToken.java new file mode 100644 index 0000000..5d7141d --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/WXToken.java @@ -0,0 +1,40 @@ +package com.android.sdk.social.wechat; + +import android.support.annotation.NonNull; + +@SuppressWarnings("unused") +class WXToken extends AuthResult { + + private String access_token; + private String expires_in; + private String refresh_token; + private String openid; + private String scope; + private String unionid; + + String getAccess_token() { + return access_token; + } + + public String getRefresh_token() { + return refresh_token; + } + + String getOpenid() { + return openid; + } + + @NonNull + @Override + public String toString() { + return "WXToken{" + + "access_token='" + access_token + '\'' + + ", expires_in='" + expires_in + '\'' + + ", refresh_token='" + refresh_token + '\'' + + ", openid='" + openid + '\'' + + ", scope='" + scope + '\'' + + ", unionid='" + unionid + '\'' + + '}'; + } + +} \ No newline at end of file diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/WXUser.java b/lib_social/src/main/java/com/android/sdk/social/wechat/WXUser.java new file mode 100644 index 0000000..f637e2b --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/WXUser.java @@ -0,0 +1,92 @@ +package com.android.sdk.social.wechat; + +import android.support.annotation.NonNull; + +import java.util.List; + +@SuppressWarnings("unused") +public class WXUser extends AuthResult { + + /** + * 普通用户的标识,对当前开发者帐号唯一 + */ + private String openid; + + private String nickname; + + /** + * 普通用户性别,1为男性,2为女性 + */ + private int sex; + + private String province; + private String city; + private String country; + + /** + * 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空 + */ + private String headimgurl; + + /** + * 开发者最好保存unionID信息,以便以后在不同应用之间进行用户信息互通。 + */ + private String unionid; + + private List privilege; + + public String getOpenid() { + return openid; + } + + public String getNickname() { + return nickname; + } + + public int getSex() { + return sex; + } + + public String getProvince() { + return province; + } + + public String getCity() { + return city; + } + + public String getCountry() { + return country; + } + + public String getHeadimgurl() { + return headimgurl; + } + + public String getUnionid() { + return unionid; + } + + public List getPrivilege() { + return privilege; + } + + @NonNull + @Override + public String toString() { + return "WXUser{" + + "openid='" + openid + '\'' + + ", nickname='" + nickname + '\'' + + ", sex=" + sex + + ", province='" + province + '\'' + + ", city='" + city + '\'' + + ", country='" + country + '\'' + + ", headimgurl='" + headimgurl + '\'' + + ", unionid='" + unionid + '\'' + + ", privilege=" + privilege + + ", errcode=" + getErrcode() + + ", errmsg=" + getErrmsg() + + '}'; + } + +} \ No newline at end of file diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginException.java b/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginException.java new file mode 100644 index 0000000..7c6a8c2 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginException.java @@ -0,0 +1,56 @@ +package com.android.sdk.social.wechat; + +import android.support.annotation.NonNull; + +import com.tencent.mm.opensdk.modelbase.BaseResp; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-07 17:35 + */ +class WeChatLoginException extends Exception { + + private int mErrorCode; + private String mErrMsg; + + WeChatLoginException(int errorCode, String errMsg) { + mErrorCode = errorCode; + mErrMsg = errMsg; + } + + @Override + public String getMessage() { + return getMessageFormBaseResp(); + } + + @NonNull + @Override + public String toString() { + return getMessageFormBaseResp() + "---" + super.toString(); + } + + private String getMessageFormBaseResp() { + String message = "未知错误"; + + switch (mErrorCode) { + case BaseResp.ErrCode.ERR_USER_CANCEL: + message = "发送取消"; + break; + case BaseResp.ErrCode.ERR_SENT_FAILED: + message = "发送失败"; + break; + case BaseResp.ErrCode.ERR_AUTH_DENIED: + message = "发送被拒绝"; + break; + case BaseResp.ErrCode.ERR_UNSUPPORT: + message = "不支持错误"; + break; + case BaseResp.ErrCode.ERR_COMM: + message = "一般错误"; + break; + } + return message + " errMsg = " + mErrMsg; + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginImpl.java b/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginImpl.java new file mode 100644 index 0000000..e1654a3 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatLoginImpl.java @@ -0,0 +1,24 @@ +package com.android.sdk.social.wechat; + + +import com.tencent.mm.opensdk.modelmsg.SendAuth; + +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.functions.Function; + +/** + * @author Ztiany + * Email: ztiany3@gmail.com + * Date : 2018-11-07 14:36 + */ +class WeChatLoginImpl { + + static Observable doWeChatLogin(SendAuth.Resp resp) { + final WXApiFactory.ServiceApi serviceApi = WXApiFactory.createWXApi(); + return serviceApi + .getAccessToken(WeChatManager.getAppId(), WeChatManager.getAppSecret(), resp.code, "authorization_code"/*固定参数*/) + .flatMap((Function>) wxToken -> serviceApi.getWeChatUser(wxToken.getAccess_token(), wxToken.getOpenid())); + } + +} diff --git a/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatManager.java b/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatManager.java new file mode 100644 index 0000000..598ce14 --- /dev/null +++ b/lib_social/src/main/java/com/android/sdk/social/wechat/WeChatManager.java @@ -0,0 +1,291 @@ +package com.android.sdk.social.wechat; + +import android.annotation.SuppressLint; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import com.android.sdk.social.common.Status; +import com.android.sdk.social.common.Utils; +import com.tencent.mm.opensdk.constants.ConstantsAPI; +import com.tencent.mm.opensdk.modelbase.BaseResp; +import com.tencent.mm.opensdk.modelbiz.WXLaunchMiniProgram; +import com.tencent.mm.opensdk.modelmsg.SendAuth; +import com.tencent.mm.opensdk.modelpay.PayReq; +import com.tencent.mm.opensdk.openapi.IWXAPI; +import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler; +import com.tencent.mm.opensdk.openapi.WXAPIFactory; + +import timber.log.Timber; + + +@SuppressWarnings("unused") +public class WeChatManager { + + private static WeChatManager sWeChatManager; + private final IWXAPI mWxApi; + + private static String sAppId; + private static String sAppSecret; + + public static synchronized void initWeChatSDK(Context context, String appId, String appSecret) { + if (sWeChatManager != null) { + throw new UnsupportedOperationException("WeChatManager has already been initialized"); + } + sAppId = appId; + sAppSecret = appSecret; + sWeChatManager = new WeChatManager(context); + } + + private static synchronized void destroy() { + sWeChatManager.currentState = null; + sWeChatManager.mWxApi.unregisterApp(); + sWeChatManager.mWxApi.detach(); + sWeChatManager = null; + } + + static String getAppId() { + Utils.requestNotNull(sAppId, "weChat app id"); + return sAppId; + } + + static String getAppSecret() { + Utils.requestNotNull(sAppSecret, "weChat appSecret"); + return sAppSecret; + } + + private WeChatManager(Context context) { + mWxApi = WXAPIFactory.createWXAPI(context.getApplicationContext(), getAppId(), false); + mWxApi.registerApp(getAppId()); + sWeChatManager = this; + } + + public static synchronized WeChatManager getInstance() { + if (sWeChatManager == null) { + throw new UnsupportedOperationException("WeChatManager has not been initialized"); + } + return sWeChatManager; + } + + /////////////////////////////////////////////////////////////////////////// + // 通用 + /////////////////////////////////////////////////////////////////////////// + + static boolean handleIntent(Intent intent, IWXAPIEventHandler iwxapiEventHandler) { + WeChatManager weChatManager = sWeChatManager; + if (weChatManager != null) { + return weChatManager.mWxApi.handleIntent(intent, iwxapiEventHandler); + } else { + Timber.w("WeChatManager handleIntent called, but WeChatManager has not been initialized"); + } + return false; + } + + @SuppressWarnings("unused") + public boolean isInstalledWeChat() { + return mWxApi.isWXAppInstalled(); + } + + static void handleOnWxEntryResp(BaseResp baseResp) { + Timber.d("baseResp.type = " + baseResp.getType()); + if (ConstantsAPI.COMMAND_SENDAUTH == baseResp.getType()) { + handAuthResp(baseResp); + } else if (ConstantsAPI.COMMAND_PAY_BY_WX == baseResp.getType()) { + handleOnWxEntryPayResp(baseResp); + } else if (ConstantsAPI.COMMAND_LAUNCH_WX_MINIPROGRAM == baseResp.getType()) { + handleMiniProgramResp(baseResp); + } + } + + /////////////////////////////////////////////////////////////////////////// + // 小程序 + /////////////////////////////////////////////////////////////////////////// + + /** + * 跳转小程序 + * + * @param userName 小程序原始id + * @param path 拉起小程序页面的可带参路径,不填默认拉起小程序首页 + */ + public void navToMinProgram(String userName, String path) { + WXLaunchMiniProgram.Req req = new WXLaunchMiniProgram.Req(); + req.userName = userName; + req.path = path; + // 可选打开开发版,体验版和正式版 + req.miniprogramType = WXLaunchMiniProgram.Req.MINIPTOGRAM_TYPE_RELEASE; + mWxApi.sendReq(req); + } + + private static void handleMiniProgramResp(BaseResp baseResp) { + WXLaunchMiniProgram.Resp launchMiniProResp = (WXLaunchMiniProgram.Resp) baseResp; + //对应小程序组件