Repository: AriaLyy/KeepassA Branch: master Commit: 02a1fb2dd22e Files: 669 Total size: 2.1 MB Directory structure: gitextract_bx47hwcv/ ├── .cz-config.js ├── .gitignore ├── KeepassA_privacy_policy.html ├── LICENSE ├── README.md ├── VersionManager/ │ ├── build.gradle │ └── src/ │ ├── functionalTest/ │ │ └── java/ │ │ └── com/ │ │ └── alg/ │ │ └── plugin/ │ │ └── version/ │ │ └── VersionManagerPluginFunctionalTest.java │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── alg/ │ │ └── plugin/ │ │ └── version/ │ │ └── VersionManagerPlugin.java │ └── test/ │ └── java/ │ └── com/ │ └── alg/ │ └── plugin/ │ └── version/ │ └── VersionManagerPluginTest.java ├── app/ │ ├── .gitignore │ ├── AndResGuard.gradle │ ├── build.gradle │ ├── channel │ ├── firebase.gradle │ ├── multidex-config.txt │ ├── proguard-rules.pro │ ├── schemas/ │ │ └── com.lyy.keepassa.dao.AppDatabase/ │ │ ├── 1.json │ │ ├── 2.json │ │ ├── 3.json │ │ └── 4.json │ ├── src/ │ │ ├── androidTest/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── lyy/ │ │ │ └── keepassa/ │ │ │ ├── AutoFillTest.kt │ │ │ ├── ComposeKeeTrayTotpTest.kt │ │ │ ├── ComposeKeepassTest.kt │ │ │ ├── KeepassDbTest.kt │ │ │ ├── UrlTest.kt │ │ │ └── UtilTest.kt │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── assets/ │ │ │ │ ├── fingerprint_anim.json │ │ │ │ ├── headAnim.json │ │ │ │ ├── loadingAnimation.json │ │ │ │ ├── lockedAnim.json │ │ │ │ └── version_log/ │ │ │ │ ├── version_log_de_rDE.md │ │ │ │ ├── version_log_en.md │ │ │ │ ├── version_log_ru_rRU.md │ │ │ │ ├── version_log_zh_CN.md │ │ │ │ └── version_log_zh_TW.md │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── lyy/ │ │ │ │ └── keepassa/ │ │ │ │ ├── base/ │ │ │ │ │ ├── AnimState.kt │ │ │ │ │ ├── BaseActivity.kt │ │ │ │ │ ├── BaseApp.java │ │ │ │ │ ├── BaseBottomSheetDialogFragment.kt │ │ │ │ │ ├── BaseDialog.kt │ │ │ │ │ ├── BaseFragment.kt │ │ │ │ │ ├── BaseModule.kt │ │ │ │ │ ├── BaseService.kt │ │ │ │ │ ├── Constance.kt │ │ │ │ │ ├── DbMigration.kt │ │ │ │ │ ├── KeyConstance.kt │ │ │ │ │ ├── OnDialogDismissListener.kt │ │ │ │ │ └── ViewBindingAdapter.kt │ │ │ │ ├── common/ │ │ │ │ │ ├── PassType.kt │ │ │ │ │ └── SortType.kt │ │ │ │ ├── dao/ │ │ │ │ │ ├── AppDatabase.kt │ │ │ │ │ ├── CloudServiceInfoDao.kt │ │ │ │ │ ├── DbRecordDao.kt │ │ │ │ │ ├── EntryRecordDao.kt │ │ │ │ │ ├── QuickUnlockDao.kt │ │ │ │ │ └── SearchDao.kt │ │ │ │ ├── entity/ │ │ │ │ │ ├── AutoFillParam.kt │ │ │ │ │ ├── CloudServiceInfo.kt │ │ │ │ │ ├── CommonState.kt │ │ │ │ │ ├── DbHistoryRecord.kt │ │ │ │ │ ├── EntryRecord.kt │ │ │ │ │ ├── IOtpBean.kt │ │ │ │ │ ├── QuickUnLockRecord.kt │ │ │ │ │ ├── SearchRecord.kt │ │ │ │ │ ├── SimpleItemEntity.kt │ │ │ │ │ ├── TagBean.kt │ │ │ │ │ └── TotpType.kt │ │ │ │ ├── event/ │ │ │ │ │ ├── AttrFileEvent.kt │ │ │ │ │ ├── AttrStrEvent.kt │ │ │ │ │ ├── ChangeDbEvent.kt │ │ │ │ │ ├── CheckEnvEvent.kt │ │ │ │ │ ├── CloudFileSelectedEvent.kt │ │ │ │ │ ├── CollectionEvent.kt │ │ │ │ │ ├── DbHistoryEvent.kt │ │ │ │ │ ├── DbPathEvent.kt │ │ │ │ │ ├── DelAttrFileEvent.kt │ │ │ │ │ ├── DelAttrStrEvent.kt │ │ │ │ │ ├── EditorEvent.kt │ │ │ │ │ ├── FillInfoEvent.kt │ │ │ │ │ ├── KeyPathEvent.kt │ │ │ │ │ ├── ModifyDbNameEvent.kt │ │ │ │ │ ├── ModifyPassEvent.kt │ │ │ │ │ ├── MoveEvent.kt │ │ │ │ │ ├── MsgDialogEvent.kt │ │ │ │ │ ├── MultiChoiceEvent.kt │ │ │ │ │ ├── ShowTOTPEvent.kt │ │ │ │ │ ├── StateChangeEvent.kt │ │ │ │ │ ├── TimeEvent.kt │ │ │ │ │ └── WebDavLoginEvent.kt │ │ │ │ ├── ondrive/ │ │ │ │ │ ├── DriveItem.kt │ │ │ │ │ ├── MsalApi.kt │ │ │ │ │ ├── MsalResponse.kt │ │ │ │ │ ├── MsalSourceItem.kt │ │ │ │ │ └── MsalUploadSession.kt │ │ │ │ ├── receiver/ │ │ │ │ │ └── ScreenLockReceiver.kt │ │ │ │ ├── router/ │ │ │ │ │ ├── ActivityRouter.kt │ │ │ │ │ ├── ContentInterceptor.kt │ │ │ │ │ ├── DeeplinkActivity.kt │ │ │ │ │ ├── DialogRouter.kt │ │ │ │ │ ├── FragmentRouter.kt │ │ │ │ │ └── ServiceRouter.kt │ │ │ │ ├── service/ │ │ │ │ │ ├── DbOpenNotificationService.kt │ │ │ │ │ ├── autofill/ │ │ │ │ │ │ ├── AutoFillClickReceiver.kt │ │ │ │ │ │ ├── AutoFillHelper.kt │ │ │ │ │ │ ├── AutoFillService.kt │ │ │ │ │ │ ├── PackageVerifier.kt │ │ │ │ │ │ ├── StructureParser.kt │ │ │ │ │ │ ├── W3cHints.kt │ │ │ │ │ │ ├── datasource/ │ │ │ │ │ │ │ └── KDBAutoFillRepository.kt │ │ │ │ │ │ └── model/ │ │ │ │ │ │ ├── AutoFillFieldMetadata.kt │ │ │ │ │ │ ├── AutoFillFieldMetadataCollection.kt │ │ │ │ │ │ └── FilledAutoFillField.kt │ │ │ │ │ ├── feat/ │ │ │ │ │ │ ├── IFeature.kt │ │ │ │ │ │ ├── KdbHandlerService.kt │ │ │ │ │ │ ├── KdbOpenService.kt │ │ │ │ │ │ ├── KpaSdkService.kt │ │ │ │ │ │ ├── RoomFeature.kt │ │ │ │ │ │ └── XLogFeature.kt │ │ │ │ │ ├── input/ │ │ │ │ │ │ ├── CandidatesAdapter.kt │ │ │ │ │ │ ├── EntryOtherInfoAdapter.kt │ │ │ │ │ │ ├── EntryOtherInfoDialog.kt │ │ │ │ │ │ └── InputIMEService.kt │ │ │ │ │ └── play/ │ │ │ │ │ └── PlayServiceUtil.kt │ │ │ │ ├── util/ │ │ │ │ │ ├── AutoLockDbUtil.kt │ │ │ │ │ ├── BarUtil.kt │ │ │ │ │ ├── ClipboardUtil.kt │ │ │ │ │ ├── CommonKVStorage.kt │ │ │ │ │ ├── EncryptUtil.kt │ │ │ │ │ ├── EventBusHelper.kt │ │ │ │ │ ├── Extensions.kt │ │ │ │ │ ├── FingerprintUtil.kt │ │ │ │ │ ├── HitUtil.kt │ │ │ │ │ ├── IconUtil.kt │ │ │ │ │ ├── ImageExtensions.kt │ │ │ │ │ ├── KLog.kt │ │ │ │ │ ├── KVStorage.kt │ │ │ │ │ ├── KdbUtil.kt │ │ │ │ │ ├── KeepassAUtil.kt │ │ │ │ │ ├── KpaExtensions.kt │ │ │ │ │ ├── KpaListEntryExtensions.kt │ │ │ │ │ ├── KpaListGroupExtensions.kt │ │ │ │ │ ├── KpaUtil.kt │ │ │ │ │ ├── LanguageUtil.kt │ │ │ │ │ ├── NotificationUtil.kt │ │ │ │ │ ├── PasswordBuildUtil.kt │ │ │ │ │ ├── PermissionPageManagement.java │ │ │ │ │ ├── PermissionsUtil.kt │ │ │ │ │ ├── PlayUtil.kt │ │ │ │ │ ├── QuickUnLockUtil.java │ │ │ │ │ ├── RealPathUtil.java │ │ │ │ │ ├── VibratorUtil.kt │ │ │ │ │ ├── cloud/ │ │ │ │ │ │ ├── CloudFileInfo.kt │ │ │ │ │ │ ├── CloudUtilFactory.kt │ │ │ │ │ │ ├── DbSynUtil.kt │ │ │ │ │ │ ├── DropboxContentHasher.java │ │ │ │ │ │ ├── DropboxUtil.kt │ │ │ │ │ │ ├── ICloudUtil.kt │ │ │ │ │ │ ├── OneDriveUtil.kt │ │ │ │ │ │ ├── PwDataMap.kt │ │ │ │ │ │ ├── SynStateCode.kt │ │ │ │ │ │ ├── WebDavUtil.kt │ │ │ │ │ │ └── interceptor/ │ │ │ │ │ │ ├── DbMergeDelegate.kt │ │ │ │ │ │ ├── DbSyncCheckInterceptor.kt │ │ │ │ │ │ ├── DbSyncCompareInterceptor.kt │ │ │ │ │ │ ├── DbSyncRequest.kt │ │ │ │ │ │ ├── DbSyncResponse.kt │ │ │ │ │ │ ├── DbSyncUploadInterceptor.kt │ │ │ │ │ │ └── IDbSyncInterceptor.kt │ │ │ │ │ └── totp/ │ │ │ │ │ ├── Base32String.java │ │ │ │ │ ├── ComposeKeeOtp.kt │ │ │ │ │ ├── ComposeKeeOtp2.kt │ │ │ │ │ ├── ComposeKeeTrayTotp.kt │ │ │ │ │ ├── ComposeKeepass.kt │ │ │ │ │ ├── ComposeKeepassxc.kt │ │ │ │ │ ├── IOtpCompose.kt │ │ │ │ │ ├── OtpEnum.kt │ │ │ │ │ ├── OtpUtil.kt │ │ │ │ │ ├── SecretHexType.kt │ │ │ │ │ └── TokenCalculator.java │ │ │ │ ├── view/ │ │ │ │ │ ├── ChoseDirModule.kt │ │ │ │ │ ├── KpaCaptureManager.java │ │ │ │ │ ├── MarkDownEditorActivity.kt │ │ │ │ │ ├── QrCodeScannerActivity.kt │ │ │ │ │ ├── SimpleAdapter.kt │ │ │ │ │ ├── SimpleEntryAdapter.kt │ │ │ │ │ ├── StorageType.kt │ │ │ │ │ ├── UpgradeLogDialog.kt │ │ │ │ │ ├── collection/ │ │ │ │ │ │ ├── CollectionActivity.kt │ │ │ │ │ │ └── CollectionModule.kt │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── CreateCustomStrDialog.kt │ │ │ │ │ │ ├── CreateDbActivity.kt │ │ │ │ │ │ ├── CreateDbFirstFragment.kt │ │ │ │ │ │ ├── CreateDbModule.kt │ │ │ │ │ │ ├── CreateDbSecondFragment.kt │ │ │ │ │ │ ├── CreateGroupDialog.kt │ │ │ │ │ │ ├── CreatePassKeyDialog.kt │ │ │ │ │ │ ├── GeneratePassActivity.kt │ │ │ │ │ │ ├── PathTypeDialog.kt │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── AFSAuthFlow.kt │ │ │ │ │ │ │ ├── DropboxAuthFlow.kt │ │ │ │ │ │ │ ├── IAuthFlow.kt │ │ │ │ │ │ │ ├── OneDriveAuthFlow.kt │ │ │ │ │ │ │ └── WebDavAuthFlow.kt │ │ │ │ │ │ └── entry/ │ │ │ │ │ │ ├── CardListHelper.kt │ │ │ │ │ │ ├── CreateEntryActivity.kt │ │ │ │ │ │ ├── CreateEntryHandler.kt │ │ │ │ │ │ ├── CreateEntryModule.kt │ │ │ │ │ │ ├── CreateEnum.kt │ │ │ │ │ │ ├── CreateFileCard.kt │ │ │ │ │ │ ├── CreateStrCard.kt │ │ │ │ │ │ ├── ICreateHandler.kt │ │ │ │ │ │ └── ModifyEntryHandler.kt │ │ │ │ │ ├── detail/ │ │ │ │ │ │ ├── AppIconAdapter.kt │ │ │ │ │ │ ├── AppIconLayoutManager.kt │ │ │ │ │ │ ├── EntryDetailActivityNew.kt │ │ │ │ │ │ ├── EntryDetailModule.kt │ │ │ │ │ │ ├── GroupDetailActivity.kt │ │ │ │ │ │ ├── GroupDetailModule.kt │ │ │ │ │ │ └── card/ │ │ │ │ │ │ ├── EntryBaseInfoCard.kt │ │ │ │ │ │ ├── EntryFileCard.kt │ │ │ │ │ │ ├── EntryNoteCard.kt │ │ │ │ │ │ ├── EntryStrCard.kt │ │ │ │ │ │ └── EntryTagCard.kt │ │ │ │ │ ├── dialog/ │ │ │ │ │ │ ├── AddMoreDialog.kt │ │ │ │ │ │ ├── ChooseTagDialog.kt │ │ │ │ │ │ ├── CloudFileListModule.kt │ │ │ │ │ │ ├── CloudFileSelectDialog.kt │ │ │ │ │ │ ├── CreateTagDialog.kt │ │ │ │ │ │ ├── DialogBtnClicker.kt │ │ │ │ │ │ ├── DonateDialog.kt │ │ │ │ │ │ ├── ImgViewerDialog.kt │ │ │ │ │ │ ├── LoadingDialog.java │ │ │ │ │ │ ├── ModifyGroupDialog.kt │ │ │ │ │ │ ├── ModifyPassDialog.kt │ │ │ │ │ │ ├── MsgDialog.kt │ │ │ │ │ │ ├── OnMsgBtClickListener.kt │ │ │ │ │ │ ├── PlayDonateDialog.kt │ │ │ │ │ │ ├── PlayDonateModule.kt │ │ │ │ │ │ ├── TimeChangeDialog.kt │ │ │ │ │ │ ├── TipsDialog.kt │ │ │ │ │ │ ├── WebDavLoginModule.kt │ │ │ │ │ │ ├── otp/ │ │ │ │ │ │ │ ├── CreateOtpDialog.kt │ │ │ │ │ │ │ ├── CreateOtpModule.kt │ │ │ │ │ │ │ ├── TotpDisplayDialog.kt │ │ │ │ │ │ │ └── modify/ │ │ │ │ │ │ │ ├── IOtpModifyHandler.kt │ │ │ │ │ │ │ ├── ModifyOtpDialog.kt │ │ │ │ │ │ │ ├── OtpKeeTrayHandler.kt │ │ │ │ │ │ │ ├── OtpKeeTraySteamHandler.kt │ │ │ │ │ │ │ ├── OtpKeepOtherHandler.kt │ │ │ │ │ │ │ ├── OtpKeepassHandler.kt │ │ │ │ │ │ │ └── OtpKeepassXcHandler.kt │ │ │ │ │ │ └── webdav/ │ │ │ │ │ │ ├── DefaultLoginAdapter.kt │ │ │ │ │ │ ├── IWebDavLoginAdapter.kt │ │ │ │ │ │ ├── NextcloudLoginAdapter.kt │ │ │ │ │ │ ├── OtherLoginAdapter.kt │ │ │ │ │ │ └── WebDavLoginDialogNew.kt │ │ │ │ │ ├── dir/ │ │ │ │ │ │ ├── ChooseGroupActivity.kt │ │ │ │ │ │ └── DirFragment.kt │ │ │ │ │ ├── fingerprint/ │ │ │ │ │ │ ├── FingerprintActivity.kt │ │ │ │ │ │ ├── FingerprintCloseFragment.kt │ │ │ │ │ │ ├── FingerprintDescFragment.kt │ │ │ │ │ │ ├── FingerprintModule.kt │ │ │ │ │ │ └── KeyStoreUtil.kt │ │ │ │ │ ├── icon/ │ │ │ │ │ │ ├── IconAdapter.kt │ │ │ │ │ │ ├── IconBottomSheetDialog.kt │ │ │ │ │ │ └── IconModule.kt │ │ │ │ │ ├── launcher/ │ │ │ │ │ │ ├── ChangeDbFragment.kt │ │ │ │ │ │ ├── DelHistoryPopMenu.kt │ │ │ │ │ │ ├── IAutoFillFinishDelegate.kt │ │ │ │ │ │ ├── IOpenDbDelegate.kt │ │ │ │ │ │ ├── LauncherActivity.kt │ │ │ │ │ │ ├── LauncherModule.kt │ │ │ │ │ │ ├── OpenAFSDelegate.kt │ │ │ │ │ │ ├── OpenDbFragment.kt │ │ │ │ │ │ ├── OpenDbHistoryActivity.kt │ │ │ │ │ │ ├── OpenDbHistoryModule.kt │ │ │ │ │ │ ├── OpenDropBoxDelegate.kt │ │ │ │ │ │ ├── OpenOneDriveDelegate.kt │ │ │ │ │ │ ├── OpenWebDavDelegate.kt │ │ │ │ │ │ ├── SaveEntityDelegate.kt │ │ │ │ │ │ └── SearchEntityDelegate.kt │ │ │ │ │ ├── main/ │ │ │ │ │ │ ├── EntryListFragment.kt │ │ │ │ │ │ ├── EntryListModule.kt │ │ │ │ │ │ ├── HomeFragment.kt │ │ │ │ │ │ ├── HomeModule.kt │ │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ │ ├── MainModule.kt │ │ │ │ │ │ ├── MainSettingActivity.kt │ │ │ │ │ │ ├── QuickUnlockActivity.kt │ │ │ │ │ │ ├── SearchSuggestionProvider.kt │ │ │ │ │ │ └── chain/ │ │ │ │ │ │ ├── DevBirthdayChain.kt │ │ │ │ │ │ ├── DialogChain.kt │ │ │ │ │ │ ├── DonateChain.kt │ │ │ │ │ │ ├── IMainDialogInterceptor.kt │ │ │ │ │ │ ├── MainDialogResponse.kt │ │ │ │ │ │ ├── PermissionsChain.kt │ │ │ │ │ │ ├── TipChain.kt │ │ │ │ │ │ └── VersionLogChain.kt │ │ │ │ │ ├── menu/ │ │ │ │ │ │ ├── EntryDetailFilePopMenu.kt │ │ │ │ │ │ ├── EntryDetailStrPopMenu.kt │ │ │ │ │ │ ├── EntryPopMenu.kt │ │ │ │ │ │ ├── GroupPopMenu.kt │ │ │ │ │ │ └── IPopMenu.kt │ │ │ │ │ ├── search/ │ │ │ │ │ │ ├── AutoFillEntrySearchActivity.kt │ │ │ │ │ │ ├── CommonSearchActivity.kt │ │ │ │ │ │ ├── DelListener.kt │ │ │ │ │ │ ├── SearchAdapter.kt │ │ │ │ │ │ ├── SearchDialog.kt │ │ │ │ │ │ └── SearchModule.kt │ │ │ │ │ └── setting/ │ │ │ │ │ ├── AppSettingFragment.kt │ │ │ │ │ ├── DBSettingFragment.kt │ │ │ │ │ ├── SettingActivity.kt │ │ │ │ │ ├── SettingModule.kt │ │ │ │ │ └── UISettingFragment.kt │ │ │ │ └── widgets/ │ │ │ │ └── expand/ │ │ │ │ ├── AttrFileItemView.kt │ │ │ │ ├── AttrStrItemView.kt │ │ │ │ ├── ExpandAttrStrLayout.kt │ │ │ │ ├── ExpandFileAttrView.kt │ │ │ │ └── ExpandStrAttrView.kt │ │ │ ├── jni/ │ │ │ │ ├── CMakeLists.txt │ │ │ │ ├── aes/ │ │ │ │ │ ├── aes.h │ │ │ │ │ ├── aes_cbc.c │ │ │ │ │ ├── aes_core.c │ │ │ │ │ ├── aes_ecb.c │ │ │ │ │ ├── aes_locl.h │ │ │ │ │ ├── cbc128.c │ │ │ │ │ ├── main_test.c │ │ │ │ │ └── modes.h │ │ │ │ └── encrypt_str.cpp │ │ │ └── res/ │ │ │ ├── anim/ │ │ │ │ ├── dialog_y_enter.xml │ │ │ │ ├── dialog_y_exit.xml │ │ │ │ ├── translate_bottom_in.xml │ │ │ │ ├── translate_left_in.xml │ │ │ │ ├── translate_left_out.xml │ │ │ │ ├── translate_right_in.xml │ │ │ │ └── translate_right_out.xml │ │ │ ├── color/ │ │ │ │ └── selector_blue_gray_text_bg.xml │ │ │ ├── drawable/ │ │ │ │ ├── bg_circle.xml │ │ │ │ ├── bg_ed.xml │ │ │ │ ├── bg_gray_radius_4.xml │ │ │ │ ├── bg_ime_entry.xml │ │ │ │ ├── bg_ime_key.xml │ │ │ │ ├── bg_line.xml │ │ │ │ ├── bg_str_attr.xml │ │ │ │ ├── bg_white_radius_2.xml │ │ │ │ ├── bg_white_radius_32.xml │ │ │ │ ├── bg_white_radius_4.xml │ │ │ │ ├── bg_white_radius_8.xml │ │ │ │ ├── ic_add.xml │ │ │ │ ├── ic_add_24px.xml │ │ │ │ ├── ic_add_blue_24px.xml │ │ │ │ ├── ic_add_photo_alternate_24px.xml │ │ │ │ ├── ic_alipay.xml │ │ │ │ ├── ic_anim.xml │ │ │ │ ├── ic_app.xml │ │ │ │ ├── ic_arrow_left.xml │ │ │ │ ├── ic_arrow_left_black.xml │ │ │ │ ├── ic_attr_file.xml │ │ │ │ ├── ic_attr_str.xml │ │ │ │ ├── ic_auto.xml │ │ │ │ ├── ic_auto_fill.xml │ │ │ │ ├── ic_baseline_arrow_drop_down_24.xml │ │ │ │ ├── ic_baseline_arrow_drop_up_24.xml │ │ │ │ ├── ic_baseline_bug_report_24.xml │ │ │ │ ├── ic_baseline_casino_24.xml │ │ │ │ ├── ic_baseline_confirmation_number_24.xml │ │ │ │ ├── ic_baseline_content_copy_24.xml │ │ │ │ ├── ic_baseline_edit_24.xml │ │ │ │ ├── ic_baseline_event_busy_24.xml │ │ │ │ ├── ic_baseline_g_translate_24.xml │ │ │ │ ├── ic_baseline_label_24.xml │ │ │ │ ├── ic_baseline_language_24.xml │ │ │ │ ├── ic_baseline_link_24.xml │ │ │ │ ├── ic_baseline_open_with.xml │ │ │ │ ├── ic_baseline_photo_camera_24.xml │ │ │ │ ├── ic_change.xml │ │ │ │ ├── ic_clipbord.xml │ │ │ │ ├── ic_close.xml │ │ │ │ ├── ic_create_time.xml │ │ │ │ ├── ic_del.xml │ │ │ │ ├── ic_delete_sweep.xml │ │ │ │ ├── ic_detail_edit.xml │ │ │ │ ├── ic_done.xml │ │ │ │ ├── ic_done_all.xml │ │ │ │ ├── ic_download_24px.xml │ │ │ │ ├── ic_dropbox.xml │ │ │ │ ├── ic_eco.xml │ │ │ │ ├── ic_fab_dir.xml │ │ │ │ ├── ic_favorite_24px.xml │ │ │ │ ├── ic_feedback_24px.xml │ │ │ │ ├── ic_file_24px.xml │ │ │ │ ├── ic_fingerprint.xml │ │ │ │ ├── ic_folder_24px.xml │ │ │ │ ├── ic_ftp.xml │ │ │ │ ├── ic_google_drive.xml │ │ │ │ ├── ic_google_play.xml │ │ │ │ ├── ic_help_filled.xml │ │ │ │ ├── ic_history.xml │ │ │ │ ├── ic_history_record.xml │ │ │ │ ├── ic_http.xml │ │ │ │ ├── ic_image_blue_24px.xml │ │ │ │ ├── ic_image_broken_24px.xml │ │ │ │ ├── ic_ime_backspace.xml │ │ │ │ ├── ic_ime_close.xml │ │ │ │ ├── ic_ime_enter.xml │ │ │ │ ├── ic_ime_keyboard.xml │ │ │ │ ├── ic_ime_lock.xml │ │ │ │ ├── ic_ime_other_info.xml │ │ │ │ ├── ic_ime_password.xml │ │ │ │ ├── ic_ime_user.xml │ │ │ │ ├── ic_img_choose.xml │ │ │ │ ├── ic_info_filled.xml │ │ │ │ ├── ic_keepassa.xml │ │ │ │ ├── ic_key.xml │ │ │ │ ├── ic_key_gray.xml │ │ │ │ ├── ic_keyboard.xml │ │ │ │ ├── ic_language_24px.xml │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── ic_lightbulb_on.xml │ │ │ │ ├── ic_linear_scale.xml │ │ │ │ ├── ic_lock.xml │ │ │ │ ├── ic_lock_24px.xml │ │ │ │ ├── ic_lose_time.xml │ │ │ │ ├── ic_modify_time.xml │ │ │ │ ├── ic_more_read.xml │ │ │ │ ├── ic_net.xml │ │ │ │ ├── ic_new_file.xml │ │ │ │ ├── ic_notice.xml │ │ │ │ ├── ic_onedrive.xml │ │ │ │ ├── ic_other.xml │ │ │ │ ├── ic_out_db.xml │ │ │ │ ├── ic_password.xml │ │ │ │ ├── ic_paypal.xml │ │ │ │ ├── ic_primary_close.xml │ │ │ │ ├── ic_qr_code_scanner.xml │ │ │ │ ├── ic_refresh_black_24dp.xml │ │ │ │ ├── ic_save_24px.xml │ │ │ │ ├── ic_screen_lock_portrait_black_24dp.xml │ │ │ │ ├── ic_search.xml │ │ │ │ ├── ic_security_24px.xml │ │ │ │ ├── ic_security_24px_white.xml │ │ │ │ ├── ic_server.xml │ │ │ │ ├── ic_setting.xml │ │ │ │ ├── ic_setting_lock.xml │ │ │ │ ├── ic_share_24px.xml │ │ │ │ ├── ic_sort_by_char.xml │ │ │ │ ├── ic_sort_down.xml │ │ │ │ ├── ic_sort_up.xml │ │ │ │ ├── ic_ssh.xml │ │ │ │ ├── ic_star.xml │ │ │ │ ├── ic_star_outline.xml │ │ │ │ ├── ic_star_rate.xml │ │ │ │ ├── ic_start_selector.xml │ │ │ │ ├── ic_state_bar.xml │ │ │ │ ├── ic_swap_horiz.xml │ │ │ │ ├── ic_tab_db.xml │ │ │ │ ├── ic_tab_db_gray.xml │ │ │ │ ├── ic_tab_db_selected.xml │ │ │ │ ├── ic_tab_history.xml │ │ │ │ ├── ic_tab_history_selected.xml │ │ │ │ ├── ic_tag.xml │ │ │ │ ├── ic_text_fields_24px.xml │ │ │ │ ├── ic_theme_style.xml │ │ │ │ ├── ic_title_24px.xml │ │ │ │ ├── ic_token_blue.xml │ │ │ │ ├── ic_token_grey.xml │ │ │ │ ├── ic_token_txt_grey.xml │ │ │ │ ├── ic_totp.xml │ │ │ │ ├── ic_undo_entry.xml │ │ │ │ ├── ic_up.xml │ │ │ │ ├── ic_user.xml │ │ │ │ ├── ic_view.xml │ │ │ │ ├── ic_view_black.xml │ │ │ │ ├── ic_view_headline_24px.xml │ │ │ │ ├── ic_view_off.xml │ │ │ │ ├── ic_view_off_black.xml │ │ │ │ ├── ic_visibility_off.xml │ │ │ │ ├── ripple_primary_selector.xml │ │ │ │ ├── ripple_white_selector.xml │ │ │ │ ├── selector_ic_tab_db.xml │ │ │ │ ├── selector_ic_tab_history.xml │ │ │ │ ├── selector_ic_tab_token.xml │ │ │ │ ├── selector_pass_visibility.xml │ │ │ │ └── selector_password.xml │ │ │ ├── drawable-v21/ │ │ │ │ ├── ripple_primary_selector.xml │ │ │ │ └── ripple_white_selector.xml │ │ │ ├── drawable-v24/ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── layout/ │ │ │ │ ├── activity_auto_fill_entry_search.xml │ │ │ │ ├── activity_change_db.xml │ │ │ │ ├── activity_collection.xml │ │ │ │ ├── activity_create_db.xml │ │ │ │ ├── activity_entry_detail_new.xml │ │ │ │ ├── activity_entry_edit_new.xml │ │ │ │ ├── activity_fingerprint.xml │ │ │ │ ├── activity_generate_pass.xml │ │ │ │ ├── activity_generate_pass_new.xml │ │ │ │ ├── activity_group_detail.xml │ │ │ │ ├── activity_group_dir.xml │ │ │ │ ├── activity_icon.xml │ │ │ │ ├── activity_launcher.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── activity_markdown_editor.xml │ │ │ │ ├── activity_only_list.xml │ │ │ │ ├── activity_qr_code_scanner.xml │ │ │ │ ├── activity_setting.xml │ │ │ │ ├── android_simple_dropdown_item_1line.xml │ │ │ │ ├── dialog_add_attr_str.xml │ │ │ │ ├── dialog_add_group.xml │ │ │ │ ├── dialog_add_more.xml │ │ │ │ ├── dialog_choose_tag.xml │ │ │ │ ├── dialog_cloud_file_list.xml │ │ │ │ ├── dialog_create_tag.xml │ │ │ │ ├── dialog_create_totp.xml │ │ │ │ ├── dialog_donate.xml │ │ │ │ ├── dialog_entry_icon.xml │ │ │ │ ├── dialog_img_viewer.xml │ │ │ │ ├── dialog_loading.xml │ │ │ │ ├── dialog_modify_pass.xml │ │ │ │ ├── dialog_msg.xml │ │ │ │ ├── dialog_other_info.xml │ │ │ │ ├── dialog_otp_modify.xml │ │ │ │ ├── dialog_pass_key.xml │ │ │ │ ├── dialog_path_type.xml │ │ │ │ ├── dialog_play_donate.xml │ │ │ │ ├── dialog_quick_unlock.xml │ │ │ │ ├── dialog_search.xml │ │ │ │ ├── dialog_timer.xml │ │ │ │ ├── dialog_tip.xml │ │ │ │ ├── dialog_totp_display.xml │ │ │ │ ├── dialog_upgrade.xml │ │ │ │ ├── dialog_webdav_login_new.xml │ │ │ │ ├── fragment_change_db.xml │ │ │ │ ├── fragment_create_db_first.xml │ │ │ │ ├── fragment_create_db_second.xml │ │ │ │ ├── fragment_entry_record.xml │ │ │ │ ├── fragment_fingerprint_close.xml │ │ │ │ ├── fragment_fingerprint_desx.xml │ │ │ │ ├── fragment_list.xml │ │ │ │ ├── fragment_only_list.xml │ │ │ │ ├── fragment_open_db.xml │ │ │ │ ├── item_app_icon.xml │ │ │ │ ├── item_auto_fill.xml │ │ │ │ ├── item_choose_tag.xml │ │ │ │ ├── item_cloud_file_list.xml │ │ │ │ ├── item_entry.xml │ │ │ │ ├── item_entry_other_info.xml │ │ │ │ ├── item_icon.xml │ │ │ │ ├── item_ime_entry.xml │ │ │ │ ├── item_mian_content.xml │ │ │ │ ├── item_path_type.xml │ │ │ │ ├── item_search_record.xml │ │ │ │ ├── item_search_result.xml │ │ │ │ ├── item_simple.xml │ │ │ │ ├── layout_action_bar.xml │ │ │ │ ├── layout_chip_harvest.xml │ │ │ │ ├── layout_dialog_button.xml │ │ │ │ ├── layout_dialog_title.xml │ │ │ │ ├── layout_empty_fill.xml │ │ │ │ ├── layout_entry_attachment.xml │ │ │ │ ├── layout_entry_card_base_info.xml │ │ │ │ ├── layout_entry_card_list.xml │ │ │ │ ├── layout_entry_card_note.xml │ │ │ │ ├── layout_entry_card_tag.xml │ │ │ │ ├── layout_entry_create_str_card.xml │ │ │ │ ├── layout_entry_str.xml │ │ │ │ ├── layout_expand_attr_child.xml │ │ │ │ ├── layout_expand_child_file.xml │ │ │ │ ├── layout_expand_child_str.xml │ │ │ │ ├── layout_expand_title.xml │ │ │ │ ├── layout_kpa_ime.xml │ │ │ │ ├── layout_loading.xml │ │ │ │ ├── layout_otp_create_default.xml │ │ │ │ └── layout_otp_create_menu.xml │ │ │ ├── menu/ │ │ │ │ ├── entry_detail_file_summary.xml │ │ │ │ ├── entry_detail_fun_menu.xml │ │ │ │ ├── entry_detail_text_summary.xml │ │ │ │ ├── entry_modify_file_summary.xml │ │ │ │ ├── entry_modify_str_summary.xml │ │ │ │ ├── menu_entry_detail.xml │ │ │ │ ├── menu_entry_edit.xml │ │ │ │ ├── menu_group_detail.xml │ │ │ │ ├── pop_create_entry_summary.xml │ │ │ │ ├── pop_dele_history_record.xml │ │ │ │ ├── pop_entry_summary.xml │ │ │ │ └── pop_group_summary.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── raw/ │ │ │ │ ├── auth_config_single_account_debug.json │ │ │ │ ├── auth_config_single_account_release.json │ │ │ │ └── notices.xml │ │ │ ├── transition/ │ │ │ │ ├── changebounds_with_arcmotion.xml │ │ │ │ ├── fade_enter.xml │ │ │ │ ├── fade_exit.xml │ │ │ │ ├── slide_enter.xml │ │ │ │ ├── slide_exit.xml │ │ │ │ ├── slide_reeter.xml │ │ │ │ └── slide_return.xml │ │ │ ├── values/ │ │ │ │ ├── arrays.xml │ │ │ │ ├── attrs.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── ids.xml │ │ │ │ ├── pre_key.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── values-ar/ │ │ │ │ └── strings.xml │ │ │ ├── values-cs/ │ │ │ │ └── strings.xml │ │ │ ├── values-de-rDE/ │ │ │ │ ├── arrays.xml │ │ │ │ └── strings.xml │ │ │ ├── values-es/ │ │ │ │ └── strings.xml │ │ │ ├── values-fon/ │ │ │ │ └── strings.xml │ │ │ ├── values-fr/ │ │ │ │ └── strings.xml │ │ │ ├── values-fr-rCA/ │ │ │ │ ├── arrays.xml │ │ │ │ └── strings.xml │ │ │ ├── values-ja/ │ │ │ │ └── strings.xml │ │ │ ├── values-nb-rNO/ │ │ │ │ └── strings.xml │ │ │ ├── values-night/ │ │ │ │ └── colors.xml │ │ │ ├── values-nl/ │ │ │ │ └── strings.xml │ │ │ ├── values-pl/ │ │ │ │ └── strings.xml │ │ │ ├── values-pt/ │ │ │ │ └── strings.xml │ │ │ ├── values-pt-rBR/ │ │ │ │ └── strings.xml │ │ │ ├── values-ru-rRU/ │ │ │ │ ├── arrays.xml │ │ │ │ └── strings.xml │ │ │ ├── values-tr/ │ │ │ │ └── strings.xml │ │ │ ├── values-uk-rUA/ │ │ │ │ └── strings.xml │ │ │ ├── values-v29/ │ │ │ │ └── styles.xml │ │ │ ├── values-zh-rCN/ │ │ │ │ └── strings.xml │ │ │ ├── values-zh-rTW/ │ │ │ │ ├── arrays.xml │ │ │ │ └── strings.xml │ │ │ ├── xml/ │ │ │ │ ├── app_setting.xml │ │ │ │ ├── aria_fileprovider_paths.xml │ │ │ │ ├── auto_fill_service_configuration.xml │ │ │ │ ├── db_setting.xml │ │ │ │ ├── input_method.xml │ │ │ │ ├── keyboard_pass_entry.xml │ │ │ │ ├── network_security_config.xml │ │ │ │ └── ui_setting.xml │ │ │ ├── xml-v25/ │ │ │ │ └── shortcuts.xml │ │ │ └── xml-v28/ │ │ │ └── auto_fill_service_configuration.xml │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── lyy/ │ │ └── keepassa/ │ │ ├── LanguageTest.kt │ │ ├── LockTest.java │ │ └── PasswordBuildTest.kt │ └── 打包命令.txt ├── build.gradle ├── gradle/ │ ├── testConfig.gradle │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── libs.versions.toml ├── localMaven/ │ └── com/ │ └── alibaba/ │ └── arouter-register-asm7/ │ ├── 1.0.2/ │ │ ├── arouter-register-asm7-1.0.2-javadoc.jar │ │ ├── arouter-register-asm7-1.0.2-javadoc.jar.md5 │ │ ├── arouter-register-asm7-1.0.2-javadoc.jar.sha1 │ │ ├── arouter-register-asm7-1.0.2-sources.jar │ │ ├── arouter-register-asm7-1.0.2-sources.jar.md5 │ │ ├── arouter-register-asm7-1.0.2-sources.jar.sha1 │ │ ├── arouter-register-asm7-1.0.2.jar │ │ ├── arouter-register-asm7-1.0.2.jar.md5 │ │ ├── arouter-register-asm7-1.0.2.jar.sha1 │ │ ├── arouter-register-asm7-1.0.2.pom │ │ ├── arouter-register-asm7-1.0.2.pom.md5 │ │ └── arouter-register-asm7-1.0.2.pom.sha1 │ ├── maven-metadata.xml │ ├── maven-metadata.xml.md5 │ └── maven-metadata.xml.sha1 └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cz-config.js ================================================ 'use strict'; module.exports = { types: [ {value: ':sparkles:', name: '特性: 一个新的特性'}, {value: ':bug:', name: '修复: 修复一个Bug'}, {value: ':books:', name: '文档: 变更的只有文档'}, {value: ':racehorse:', name: '性能: 提升性能'}, {value: ':rotating_light:', name: '测试: 添加一个测试'}, {value: ':hammer:', name: '回滚: 代码回退'}, // {value: ':tada:', name: '回滚: 代码回退'} ], scopes: [ {name: 'app'}, // {name: 'Demo'}, // {name: 'Frame'}, // {name: 'KeepassApi'}, // {name: 'IBaseApi'}, // {name: 'HWImp'}, // {name: 'PlayImp'} ], allowTicketNumber: false, isTicketNumberRequired: false, ticketNumberPrefix: 'TICKET-', ticketNumberRegExp: '\\d{1,5}', // it needs to match the value for field type. Eg.: 'fix' /* scopeOverrides: { fix: [ {name: 'merge'}, {name: 'style'}, {name: 'e2eTest'}, {name: 'unitTest'} ] }, */ // override the messages, defaults are as follows messages: { type: '选择一种你的提交类型:', scope: '选择一个scope (可选):', // used if allowCustomScopes is true customScope: 'Denote the SCOPE of this change:', subject: '短说明:\n', body: '长说明,使用"|"换行(可选):\n', breaking: '非兼容性说明 (可选):\n', footer: '关联关闭的issue,例如:#31, #34(可选):\n', confirmCommit:'确定提交说明?' }, allowCustomScopes: true, allowBreakingChanges: ['特性', '修复'], // skip any questions you want skipQuestions: ['body'], // limit subject length subjectLimit: 100 }; ================================================ FILE: .gitignore ================================================ *.iml .gradle .idea /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild .cxx app/build Frame/build KeepassLib/build node_modules cp_buglyQqUploadSymbolLib.jar /buglybin local.properties /VersionManager/build/ app/google-services.json ================================================ FILE: KeepassA_privacy_policy.html ================================================

隐私政策
本应用尊重并保护所有使用服务用户的个人隐私权。为了给您提供更准确、更有个性化的服务,本应用会按照本隐私权政策的规定使用和披露您的个人信息。但本应用将以高度的勤勉、审慎义务对待这些信息。除本隐私权政策另有规定外,在未征得您事先许可的情况下,本应用不会将这些信息对外披露或向第三方提供。本应用会不时更新本隐私权政策。 您在同意本应用服务使用协议之时,即视为您已经同意本隐私权政策全部内容。本隐私权政策属于本应用服务使用协议不可分割的一部分。

  1. 适用范围
    (a) 在您注册本应用帐号时,您根据本应用要求提供的个人注册信息;
    (b) 在您使用本应用网络服务,或访问本应用平台网页时,本应用自动接收并记录的您的浏览器和计算机上的信息,包括但不限于您的 IP 地址、浏览器的类型、使用的语言、访问日期和时间、软硬件特征信息及您需求的网页记录等数据;
    (c) 本应用通过合法途径从商业伙伴处取得的用户个人数据。
    您了解并同意,以下信息不适用本隐私权政策:
    (a) 您在使用本应用平台提供的搜索服务时输入的关键字信息;
    (b) 本应用收集到的您在本应用发布的有关信息数据,包括但不限于参与活动、成交信息及评价详情;
    (c) 违反法律规定或违反本应用规则行为及本应用已对您采取的措施。
  2. 信息使用
    (a) 本应用不会向任何无关第三方提供、出售、出租、分享或交易您的个人信息,除非事先得到您的许可,或该第三方和本应用(含本应用关联公司)单独或共同为您提供服务,且在该服务结束后,其将被禁止访问包括其以前能够访问的所有这些资料。
    (b) 本应用亦不允许任何第三方以任何手段收集、编辑、出售或者无偿传播您的个人信息。任何本应用平台用户如从事上述活动,一经发现,本应用有权立即终止与该用户的服务协议。
    (c) 为服务用户的目的,本应用可能通过使用您的个人信息,向您提供您感兴趣的信息,包括但不限于向您发出产品和服务信息,或者与本应用合作伙伴共享信息以便他们向您发送有关其产品和服务的信息(后者需要您的事先同意)。
  3. 信息披露
    在如下情况下,本应用将依据您的个人意愿或法律的规定全部或部分的披露您的个人信息:
    (a) 经您事先同意,向第三方披露;
    (b) 为提供您所要求的产品和服务,而必须和第三方分享您的个人信息;
    (c) 根据法律的有关规定,或者行政或司法机构的要求,向第三方或者行政、司法机构披露;
    (d) 如您出现违反中国有关法律、法规或者本应用服务协议或相关规则的情况,需要向第三方披露;
    (e) 如您是适格的知识产权投诉人并已提起投诉,应被投诉人要求,向被投诉人披露,以便双方处理可能的权利纠纷;
    (f) 在本应用平台上创建的某一交易中,如交易任何一方履行或部分履行了交易义务并提出信息披露请求的,本应用有权决定向该用户提供其交易对方的联络方式等必要信息,以促成交易的完成或纠纷的解决。
    (g) 其它本应用根据法律、法规或者网站政策认为合适的披露。
  4. 信息存储和交换
    本应用收集的有关您的信息和资料将保存在本应用及(或)其关联公司的服务器上,这些信息和资料可能传送至您所在国家、地区或本应用收集信息和资料所在地的境外并在境外被访问、存储和展示。
  5. Cookie 的使用
    (a) 在您未拒绝接受 cookies 的情况下,本应用会在您的计算机上设定或取用 cookies ,以便您能登录或使用依赖于 cookies 的本应用平台服务或功能。本应用使用 cookies 可为您提供更加周到的个性化服务,包括推广服务。
    (b) 您有权选择接受或拒绝接受 cookies。您可以通过修改浏览器设置的方式拒绝接受 cookies。但如果您选择拒绝接受 cookies,则您可能无法登录或使用依赖于 cookies 的本应用网络服务或功能。
    (c) 通过本应用所设 cookies 所取得的有关信息,将适用本政策。
  6. 信息安全
    (a) 本应用帐号均有安全保护功能,请妥善保管您的用户名及密码信息。本应用将通过对用户密码进行加密等安全措施确保您的信息不丢失,不被滥用和变造。尽管有前述安全措施,但同时也请您注意在信息网络上不存在 “完善的安全措施”。
    (b) 在使用本应用网络服务进行网上交易时,您不可避免的要向交易对方或潜在的交易对
  7. 本隐私政策的更改
    (a) 如果决定更改隐私政策,我们会在本政策中、本公司网站中以及我们认为适当的位置发布这些更改,以便您了解我们如何收集、使用您的个人信息,哪些人可以访问这些信息,以及在什么情况下我们会透露这些信息。
    (b) 本公司保留随时修改本政策的权利,因此请经常查看。如对本政策作出重大更改,本公司会通过网站通知的形式告知。
    方披露自己的个人信息,如联络方式或者邮政地址。请您妥善保护自己的个人信息,仅在必要的情形下向他人提供。如您发现自己的个人信息泄密,尤其是本应用用户名及密码发生泄露,请您立即联络本应用客服,以便本应用采取相应措施。
================================================ FILE: LICENSE ================================================ Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: README.md ================================================ # KeepassA ![KeepassA](https://github.com/AriaLyy/KeepassA/blob/master/app/src/main/res/mipmap-xxhdpi/ic_launcher.png)
## Features A password-management software with a simple interface and operation. \ Fully compatible with the KeePass 2.x databases. \ These features were added: 1. Auto-fill service. For passwords, based on the native Android framework. 2. Fingerprint unlock. Use the first fingerprint key to unlock the database. 3. Quick unlock. No need to enter a long database password every time. 4. Dropbox/WebDAV syncing. Sync the KeePass database between Linux, macOS, Windows, Android, iOS and so on. 5. TOTP and OTP secondary verification codes. Get the second verification code with a OTP app. 6. Secondary verification codes from Steam. 7. Open historical records. Quickly find the last opened record. 8. Shortcuts. Quickly open the search and create items on the homescreen. ## Contributions * Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**. * Help **[translate](https://hosted.weblate.org/projects/keepassa/string/)** KeepassA to your language (on [Hosted Weblate](https://hosted.weblate.org/projects/keepassa/) or by sending a [pull request](https://help.github.com/articles/about-pull-requests/)). * Help translate KeePassA by sending a [pull request](https://help.github.com/articles/about-pull-requests/). ## Contributor [@DominicDesbiens](https://github.com/DominicDesbiens) ## Donate for a better service and a quicker development of features you want.
## Download [Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.lyy.keepassa) [Get it on Google Play](https://play.google.com/store/apps/details?id=com.lyy.keepassa) ## Q&A Other questions? Maybe they are answered by reading the [issues](https://github.com/AriaLyy/KeepassA/issues)? ## Other KeePass software * [KeePass](https://keepass.info/). The original and official project for the desktop. * [KeePassDX](https://www.keepassdx.com/). An alternative for Android. * [KeePassXC](https://keepassxc.org/). An alternative for Linux, Windows, and Android. * [KeeWeb](https://keeweb.info/). A web version compatible with KeePass files. ## License ``` Copyright (C) 2020–2022 AriaLyy(https://github.com/AriaLyy/KeepassA) This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, you can obtain one at http://mozilla.org/MPL/2.0/. ``` ================================================ FILE: VersionManager/build.gradle ================================================ /* * This file was generated by the Gradle 'init' task. * * This generated file contains a sample Gradle plugin project to get you started. * For more details take a look at the Writing Custom Plugins chapter in the Gradle * User Manual available at https://docs.gradle.org/7.3.3/userguide/custom_plugins.html * This project uses @Incubating APIs which are subject to change. */ plugins { // Apply the Java Gradle plugin development plugin to add support for developing Gradle plugins id 'groovy' id 'java-gradle-plugin' id 'version-catalog' id 'maven-publish' } // repositories { // // Use Maven Central for resolving dependencies. // mavenCentral() // } dependencies { implementation gradleApi() implementation localGroovy() implementation(libs.groovy.core) // implementation fileTree(dir: 'libs', include: ['*.jar']) } testing { suites { // Configure the built-in test suite test { // Use JUnit Jupiter test framework useJUnitJupiter('5.7.2') } // Create a new test suite functionalTest(JvmTestSuite) { dependencies { // functionalTest test suite depends on the production code in tests implementation project } targets { all { // This test suite should run after the built-in test suite has run its tests testTask.configure { shouldRunAfter(test) } } } } } } gradlePlugin { // Define the plugin plugins { greeting { id = 'com.alg.plugin.version.greeting' implementationClass = 'com.alg.plugin.version.VersionManagerPlugin' } } } gradlePlugin.testSourceSets(sourceSets.functionalTest) tasks.named('check') { // Include functionalTest as part of the check lifecycle dependsOn(testing.suites.functionalTest) } catalog { // declare the aliases, bundles and versions in this block versionCatalog { from files('../libs.versions.toml') } } publishing { publications { maven(MavenPublication) { groupId = 'com.lyy.kpa.version' artifactId = 'catalog' version = '0.0.3' from components.versionCatalog } } } ================================================ FILE: VersionManager/src/functionalTest/java/com/alg/plugin/version/VersionManagerPluginFunctionalTest.java ================================================ /* * This Java source file was generated by the Gradle 'init' task. */ package com.alg.plugin.version; import java.io.File; import java.io.IOException; import java.io.Writer; import java.io.FileWriter; import java.nio.file.Files; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.BuildResult; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.*; /** * A simple functional test for the 'com.alg.plugin.version.greeting' plugin. */ class VersionManagerPluginFunctionalTest { @TempDir File projectDir; private File getBuildFile() { return new File(projectDir, "build.gradle"); } private File getSettingsFile() { return new File(projectDir, "settings.gradle"); } @Test void canRunTask() throws IOException { writeString(getSettingsFile(), ""); writeString(getBuildFile(), "plugins {" + " id('com.alg.plugin.version.greeting')" + "}"); // Run the build GradleRunner runner = GradleRunner.create(); runner.forwardOutput(); runner.withPluginClasspath(); runner.withArguments("greeting"); runner.withProjectDir(projectDir); BuildResult result = runner.build(); // Verify the result assertTrue(result.getOutput().contains("Hello from plugin 'com.alg.plugin.version.greeting'")); } private void writeString(File file, String string) throws IOException { try (Writer writer = new FileWriter(file)) { writer.write(string); } } } ================================================ FILE: VersionManager/src/main/java/com/alg/plugin/version/VersionManagerPlugin.java ================================================ /* * This Java source file was generated by the Gradle 'init' task. */ package com.alg.plugin.version; import org.gradle.api.Project; import org.gradle.api.Plugin; /** * A simple 'hello world' plugin. */ public class VersionManagerPlugin implements Plugin { public void apply(Project project) { // Register a task project.getTasks().register("greeting", task -> { task.doLast(s -> System.out.println("Hello from plugin 'com.alg.plugin.version.greeting'")); }); } } ================================================ FILE: VersionManager/src/test/java/com/alg/plugin/version/VersionManagerPluginTest.java ================================================ /* * This Java source file was generated by the Gradle 'init' task. */ package com.alg.plugin.version; import org.gradle.testfixtures.ProjectBuilder; import org.gradle.api.Project; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; /** * A simple unit test for the 'com.alg.plugin.version.greeting' plugin. */ class VersionManagerPluginTest { @Test void pluginRegistersATask() { // Create a test project and apply the plugin Project project = ProjectBuilder.builder().build(); project.getPlugins().apply("com.alg.plugin.version.greeting"); // Verify the result assertNotNull(project.getTasks().findByName("greeting")); } } ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/AndResGuard.gradle ================================================ apply plugin: 'AndResGuard' buildscript { repositories { jcenter() google() } dependencies { classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21' } } andResGuard { mappingFile = file("resource_mapping.txt") mappingFile = null // 对于发布于Google Play的APP,建议不要使用7Zip压缩,因为这个会导致Google Play的优化Patch算法失效. https://github.com/shwenzhang/AndResGuard/issues/233 use7zip = false useSign = true // 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字 keepRoot = false // 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小 fixedResName = "arg" // 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源 mergeDuplicatedRes = true whiteList = [ // for your icon "R.drawable.icon", // for fabric "R.string.com.crashlytics.*", // for google-services "R.string.google_app_id", "R.string.gcm_defaultSenderId", "R.string.default_web_client_id", "R.string.ga_trackingId", "R.string.firebase_database_url", "R.string.google_api_key", "R.string.google_crash_reporting_api_key", // 不混淆矢量图 "R.drawable.cc*", // 不混淆启动图 "R.drawable.ic_launcher_foreground", "R.mipmap.ic_launcher" ] compressFilePattern = [ "*.png", "*.jpg", "*.jpeg", "*.gif", ] sevenzip { artifact = 'com.tencent.mm:SevenZip:1.2.21' //path = "/usr/local/bin/7za" } /** * 可选: 如果不设置则会默认覆盖assemble输出的apk **/ finalApkBackupPath = "${project.buildDir}/AndResGuardFinal.apk" /** * 可选: 指定v1签名时生成jar文件的摘要算法 * 默认值为“SHA-1” **/ // digestalg = "SHA-256" } ================================================ FILE: app/build.gradle ================================================ plugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-kapt' id 'com.tencent.vasdolly' id 'com.alibaba.arouter' id 'kotlin-parcelize' id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' } android { compileSdkVersion libs.versions.compilesdk.get().toInteger() buildToolsVersion libs.versions.buildtools.get() compileOptions { // Flag to enable support for the new language APIs // coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = "1.8" } defaultConfig { applicationId "com.lyy.keepassa" minSdkVersion libs.versions.minSdk.get().toInteger() targetSdkVersion libs.versions.targetsdk.get().toInteger() versionCode 71 versionName "2.4.7" multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // so 版本 ndk.abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' // 只保留以下语言包 resConfigs "zh-rCN", "zh-rTW", "en", "fr-rCA", "nb-rNO", "ru-rRU", "fr", "de-rDE", "pl", "tr", "uk-rUA", "es" } signingConfigs { release { def properties = new Properties() def inputStream = project.rootProject.file('local.properties').newDataInputStream() properties.load(inputStream) storeFile file(properties.getProperty('storeFile')) storePassword properties.getProperty('storePassword') keyAlias properties.getProperty('keyAlias') keyPassword properties.getProperty('keyPassword') v1SigningEnabled true // v1 签名 v2SigningEnabled true // v2 签名 } } buildTypes { debug { debuggable = true // 下面两个是debug-db的参数 resValue("string", "PORT_NUMBER", "10086") // 端口 resValue("string", "DB_PASSWORD_keepassA.db", "stVz7QxFgzA7yMnH") // sqlcipher 加密密码 signingConfig signingConfigs.release firebaseCrashlytics { mappingFileUploadEnabled false } } release { //zipAlignEnabled true //开启Zipalign优化 debuggable false // debuggable true minifyEnabled true shrinkResources true // 移除无用资源 // multiDexKeepFile file('multidex-config.txt') proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' // wcdb 需要增加这个混淆 signingConfig signingConfigs.release firebaseCrashlytics { mappingFileUploadEnabled true } } } buildFeatures { dataBinding = true // for view binding : viewBinding = true } testOptions { unitTests.includeAndroidResources = true unitTests.returnDefaultValues = true } externalNativeBuild { cmake { path "src/main/jni/CMakeLists.txt" } } lintOptions { checkReleaseBuilds false // Or, if you prefer, you can continue to check for errors in release builds, // but continue the build even when errors are found: // abortOnError false } flavorDimensions "app" // 渠道配置 https://developer.android.google.cn/studio/build/build-variants.html?hl=zh-cn productFlavors { dev { dimension "app" } } ndkVersion '21.4.7075529' } kapt { arguments { arg('room.schemaLocation', "$projectDir/schemas") //指定room.schemaLocation生成的文件路径,用于版本升级 arg('AROUTER_MODULE_NAME', project.getName() + "_kpa") // arouter // 生成的文档路径 : build/generated/source/apt/(debug or release)/com/alibaba/android/arouter/docs/arouter-map-of-${moduleName}.json // arg('AROUTER_GENERATE_DOC', "enable") // arouter,release 不能打开这个 // eventbus索引,https://greenrobot.org/eventbus/documentation/subscriber-index/ arg('eventBusIndex', 'com.lyy.keepassa.KpaEventBusIndex') // 将会自动创建KpaEventBusIndex } } configurations { // 配置不同的渠道加载不同的module playImplementation {} } /** * 根据已有基础包重新生成多渠道包 * 如果是自动生成渠道包,见https://github.com/Tencent/VasDolly */ rebuildChannel { //指定渠道文件 channelFile = file("${project.getProjectDir()}/channel") // baseDebugApk = 已有Debug APK // 已有Release APK // baseReleaseApk = new File("${project.buildDir}/AndResGuardFinal.apk") baseApk = new File("${project.buildDir}/outputs/apk/dev/release/app-dev-release.apk") //默认为new File(project.buildDir, "rebuildChannel/debug") // debugOutputDir = Debug渠道包输出目录 //默认为new File(project.buildDir, "rebuildChannel/release") // Release渠道包输出目录 outputDir = new File("${project.buildDir}/outputs/channels") //快速模式:生成渠道包时不进行校验(速度可以提升10倍以上,默认为false) fastMode = false //低内存模式(仅针对V2签名,默认为false):只把签名块、中央目录和EOCD读取到内存,不把最大头的内容块读取到内存,在手机上合成APK时,可以使用该模式 lowMemory = false } // 配置 AndResGuard 于 channel 前执行 // 这几行代码表示reBuildChannel任务是依赖于resguardRelease任务的,也就是说先执行完资源混淆后再执行多渠道打包。 afterEvaluate { tasks.getByName('reBuildChannel') { dependsOn(assembleRelease) // 暂时使用这个 // 不要使用,1.2.21打包出来的apk,消息对话框布局错乱 // dependsOn('resguardRelease') } } boolean isReleaseBuildType() { for (String s : gradle.startParameter.taskNames) { println("builtType: " + s) if (s.contains("Release") | s.contains("release") | s.contains("reBuildChannel")) { return true } } return false } // 替换AndroidManifest 参数 // https://developer.android.com/studio/known-issues#variant_api android.applicationVariants.all { variant -> variant.outputs.all { output -> output.processManifest.doLast { // Stores the path to the maifest. String manifestPath = "${multiApkManifestOutputDirectory.asFile.get()}/AndroidManifest.xml" println("版本类型:" + isReleaseBuildType()) // def keyHash = "5qMkIKbIM5Lx7VbajX8mT3rKXpE=" // if (isReleaseBuildType()) { // keyHash = "xv3czaACtp98KvuSqP3GRLCxjrc=" // } def keyHash = "xv3czaACtp98KvuSqP3GRLCxjrc=" // 替换one drive 配置 def updatedContent = file(manifestPath).getText('UTF-8') .replaceAll("msal_package_name", "com.lyy.keepassa") // 包名 .replaceAll("msal_signature_hash", keyHash) // 当前签名hash file(manifestPath).write(updatedContent, 'UTF-8') } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) // 测试模块 testImplementation libs.test.junit androidTestImplementation libs.bundles.android.test // debugImplementation 'com.amitshekhar.android:debug-db:1.0.6' // 数据库如果加密的话使用这个 // debugImplementation 'com.amitshekhar.android:debug-db-encrypt:1.0.6' // keepassA组件 implementation 'me.laoyuyu.keepassa:KeepassIcon:0.4.0' implementation 'me.laoyuyu.keepassa:KeepassLib:0.4.1' // implementation project(path: ':KpaLib') implementation 'me.laoyuyu.keepassa:KPAFrame:0.4.1' implementation 'me.laoyuyu.keepassa:UIWidget:0.4.2' // kotlin implementation(libs.bundles.android.kotlin) // =========================== android 组件 start ===================================== implementation(libs.bundles.jetpack.ui) // jetpack-ui implementation(libs.bundles.jetpack.lifecycle) // lifecycle implementation(libs.jetpack.workmanager) // workmanager implementation(libs.jetpack.room.runtime) // room implementation(libs.jetpack.room.ktx) // room kapt(libs.jetpack.room.compiler) implementation(libs.google.tink) implementation(libs.jetpack.autofill) // 自动填充工具 implementation(libs.jetpack.biometric) // 生物识别 implementation(libs.jetpack.multidex) // =========================== android 组件 end ===================================== // =========================== 三方库 start ===================================== // implementation libs.third implementation(libs.third.jodatime) implementation(libs.third.richtext) // 富文本 implementation(libs.third.licensesdialog) //开源许可对话框 implementation(libs.third.immersionbar) // 状态栏 implementation(libs.third.subsampling) // 大图浏览 implementation(libs.third.eventbus) kapt(libs.third.eventbus.compiler) implementation(libs.third.lottie) // json 动画库 implementation(libs.third.glide) kapt(libs.third.glide.compiler) implementation(libs.third.dropbox) implementation(libs.third.webdav) implementation(libs.bundles.tencent.wcdb) implementation(libs.tencent.vasdolly) implementation(libs.third.protector) // XP/调试/多开/模拟器/root 判断 implementation(libs.third.autosize) implementation(libs.google.gson) implementation(libs.third.freereflection) implementation(libs.microsoft.msal) implementation(libs.squareup.retrofit) kapt(libs.ali.third.arouter.compiler) implementation(libs.third.zxing.embedded) { transitive = false } implementation(libs.google.zxing) implementation(libs.jetpack.palette.ktx) // mmkv implementation(libs.tencent.mmkv) // xlog implementation 'com.tencent.mars:mars-xlog:1.2.5' // // =========================== 三方库 end ===================================== implementation(libs.google.gms.billing) implementation(libs.google.gms.base) //firebase implementation platform('com.google.firebase:firebase-bom:32.7.1') implementation 'com.google.firebase:firebase-crashlytics-ktx' } gradle.startParameter.taskNames.forEach { if (it.contains("Test")){ apply from: "${rootDir.path}/gradle/testConfig.gradle" } } //apply from: 'firebase.gradle' ================================================ FILE: app/channel ================================================ play xiaomi coolapk huawei github ================================================ FILE: app/firebase.gradle ================================================ apply { plugin "com.android.application" } android.applicationVariants.configureEach { variant -> tasks.configureEach { if (it.name.contains("assemble") && it.name.contains("Release")) { println("开始上传符号表") it.doLast { def cmd = "${project.rootDir.absolutePath}/gradlew uploadCrashlyticsMappingFileDevRelease" def p = cmd.execute() p.waitForProcessOutput(System.out, System.err) } } } } ================================================ FILE: app/multidex-config.txt ================================================ org/greenrobot/eventbus/EventBus.class ================================================ FILE: app/proguard-rules.pro ================================================ # 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 ################################### 混淆配置 start ########################################### #指定代码的压缩级别 -optimizationpasses 5 #包明不混合大小写 -dontusemixedcaseclassnames #不去忽略非公共的库类 -dontskipnonpubliclibraryclasses #优化 不优化输入的类文件 -dontoptimize #预校验 -dontpreverify #混淆时是否记录日志 -verbose # 混淆时所采用的算法 -optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/* #-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* #忽略警告 #-ignorewarning -keepattributes SourceFile,LineNumberTable # Keep file names and line numbers. -keep public class * extends java.lang.Exception # Optional: Keep custom exceptions. ################################### 混淆配置 end ############################################ ################## 记录生成的日志数据,gradle build时在本项目根目录输出 ################# ####### 输出文件夹 build/outputs/mapping #apk 包内所有 class 的内部结构 -dump build/outputs/mapping/class_files.txt #未混淆的类和成员 -printseeds build/outputs/mapping/kpa_seeds.txt #列出从 apk 中删除的代码 -printusage build/outputs/mapping/kpa_unused.txt #混淆前后的映射 -printmapping build/outputs/mapping/kpa_mapping.txt ################## 记录生成的日志数据,gradle build时 在本项目根目录输出-end ################# ################## 常用属性配置-start ################## # 保护注解 -keepattributes *Annotation* # 保护support v4 包 -dontwarn android.support.v4.app.** # * 匹配任意长度字符,不包含包名分隔符 (.) # ** 匹配任意长度字符,包含包名分隔符 (.) # *** 匹配任意参数类型 -keep class android.support.v4.app.**{ *; } # 保护andorid x -keep class com.google.android.material.** {*;} -keep class androidx.** {*;} -keep public class * extends androidx.** -keep interface androidx.** {*;} -dontwarn com.google.android.material.** -dontnote com.google.android.material.** -dontwarn androidx.** # 保护一些奇葩的问题 -dontwarn org.xmlpull.v1.XmlPullParser -dontwarn org.xmlpull.v1.XmlSerializer -keep class org.xmlpull.v1.* {*;} # 保护JS接口 -keepclassmembers class * { @android.webkit.JavascriptInterface ; } ##保持 native 方法不被混淆 -keepclasseswithmembernames class * { native ; } ##保持 Parcelable 不被混淆 -keep class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator *; *; } ##保持 Serializable 不被混淆 -keepnames class * implements java.io.Serializable -keep class * implements java.io.Serializable{ *; } # #保持 Serializable 不被混淆并且enum 类也不被混淆 -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; private static final java.io.ObjectStreamField[] serialPersistentFields; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); } #避免混淆泛型 如果混淆报错建议关掉 #–keepattributes Signature # webview + js -keepattributes *JavascriptInterface* ################## 常用属性配置-end ################## ################## kotlin-start ################## -keep class kotlin.** { *; } -keep class kotlin.Metadata { *; } -dontwarn kotlin.** -keepclassmembers class **$WhenMappings { ; } -keepclassmembers class kotlin.Metadata { public ; } -assumenosideeffects class kotlin.jvm.internal.Intrinsics { static void checkParameterIsNotNull(java.lang.Object, java.lang.String); } ################## kotlin-end ################## ################# eventbus-start ################## -keepclassmembers class * { @org.greenrobot.eventbus.Subscribe ; } -keep enum org.greenrobot.eventbus.ThreadMode { *; } # And if you use AsyncExecutor: -keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent { (java.lang.Throwable); } ################# eventbus-end ################## ################# glide-start ################## -keep public class * implements com.bumptech.glide.module.GlideModule -keep public class * extends com.bumptech.glide.module.AppGlideModule -keep public enum com.bumptech.glide.load.ImageHeaderParser$** { **[] $VALUES; public *; } # for DexGuard only #-keepresourcexmlelements manifest/application/meta-data@value=GlideModule ################# glide-end ################## ################# immersionbar-start ################## -keep class com.gyf.immersionbar.* {*;} -dontwarn com.gyf.immersionbar.** ################# immersionbar-end ################## ################# room-start ################## -keep class * extends android.arch.persistence.room.RoomDatabase { *; } # suport -keep class * extends androidx.room.RoomDatabase { *; } # androidx ################# room-end ################## ################# bugly-start ################## -dontwarn com.tencent.bugly.** -keep public class com.tencent.bugly.**{*;} ################# bugly-end ################## ################# sqlcipher-start ################## -dontwarn net.sqlcipher.** -keep class net.sqlcipher.** {*;} ################# sqlcipher-end ################## ################# wcdb-start ################## # https://github.com/Tencent/wcdb/blob/master/android/wcdb/proguard-rules.pro # Keep all native methods, their classes and any classes in their descriptors -keepclasseswithmembers,includedescriptorclasses class com.tencent.wcdb.** { native ; } # Keep all exception classes -keep class com.tencent.wcdb.**.*Exception # Keep classes referenced in JNI code -keep class com.tencent.wcdb.database.WCDBInitializationProbe { ; } -keep,includedescriptorclasses class com.tencent.wcdb.database.SQLiteCustomFunction { *; } -keep class com.tencent.wcdb.database.SQLiteDebug$* { *; } -keep class com.tencent.wcdb.database.SQLiteCipherSpec { ; } -keep interface com.tencent.wcdb.support.Log$* { *; } # Keep methods used as callbacks from JNI code -keep class com.tencent.wcdb.repair.RepairKit { int onProgress(java.lang.String, int, long); } -keep class com.tencent.wcdb.database.SQLiteConnection { void notifyCheckpoint(java.lang.String, int); void notifyChange(java.lang.String, java.lang.String, long[], long[], long[]); } ################# wcdb-end ################## ################# webdav-start ################## -dontwarn org.simpleframework.xml.stream.** -keep class org.simpleframework.xml.**{ *; } -keepclassmembers,allowobfuscation class * { @org.simpleframework.xml.* ; @org.simpleframework.xml.* (...); } ## Sardine Android model classes: needed for XML serialization -keep class com.thegrizzlylabs.sardineandroid.model.**{ *; } ## 防止某些奇葩情况下导致的崩溃问题 -keep class javax.xml.namespace.**{ public ; } ## OkHTTP -dontwarn okhttp3.internal.platform.ConscryptPlatform ################# webdav-end ################## ################# arouter-start ################## -keep public class com.alibaba.android.arouter.routes.**{*;} -keep public class com.alibaba.android.arouter.facade.**{*;} -keep class * implements com.alibaba.android.arouter.facade.template.ISyringe{*;} # 如果使用了 byType 的方式获取 Service,需添加下面规则,保护接口 -keep interface * implements com.alibaba.android.arouter.facade.template.IProvider # 如果使用了 单类注入,即不定义接口实现 IProvider,需添加下面规则,保护实现 # -keep class * implements com.alibaba.android.arouter.facade.template.IProvider ################# arouter-end ################## ################# xlog-start ################## -keep class com.tencent.mars.** { *; } ################# xlog-end ################## ################# viewbinding-start ################## -keep class * implements androidx.viewbinding.ViewBinding { public * inflate(android.view.LayoutInflater, android.view.ViewGroup, boolean); } ################# viewbinding-end ################## -keep class com.com.lyy.keepassa.baseapi.*{ *; } -dontwarn com.com.lyy.keepassa.baseapi.** -keep class * implements com.lyy.keepassa.baseapi.INotFreeLibService{ *; } -keep class com.lyy.keepassa.view.setting.UISettingFragment -keep class com.lyy.keepassa.service.autofill.AutofillService{ *; } ================================================ FILE: app/schemas/com.lyy.keepassa.dao.AppDatabase/1.json ================================================ { "formatVersion": 1, "database": { "version": 1, "identityHash": "2d9d3ab7e899171e967d44eb971ec5bb", "entities": [ { "tableName": "OpenDbRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `time` INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT 'AFS', `uri` TEXT NOT NULL, `keyUri` TEXT NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "time", "columnName": "time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true, "defaultValue": "'AFS'" }, { "fieldPath": "uri", "columnName": "uri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "keyUri", "columnName": "keyUri", "affinity": "TEXT", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "EntryRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userName` TEXT NOT NULL, `title` TEXT NOT NULL, `uuid` BLOB NOT NULL, `time` INTEGER NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userName", "columnName": "userName", "affinity": "TEXT", "notNull": true }, { "fieldPath": "title", "columnName": "title", "affinity": "TEXT", "notNull": true }, { "fieldPath": "uuid", "columnName": "uuid", "affinity": "BLOB", "notNull": true }, { "fieldPath": "time", "columnName": "time", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "SearchRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `time` INTEGER NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "title", "columnName": "title", "affinity": "TEXT", "notNull": true }, { "fieldPath": "time", "columnName": "time", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2d9d3ab7e899171e967d44eb971ec5bb')" ] } } ================================================ FILE: app/schemas/com.lyy.keepassa.dao.AppDatabase/2.json ================================================ { "formatVersion": 1, "database": { "version": 2, "identityHash": "60d6ebad122bd7d8a8f58987ecdcd485", "entities": [ { "tableName": "DbRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `time` INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT 'AFS', `localDbUri` TEXT NOT NULL, `cloudDiskPath` TEXT, `keyUri` TEXT NOT NULL, `dbName` TEXT NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "time", "columnName": "time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true, "defaultValue": "'AFS'" }, { "fieldPath": "localDbUri", "columnName": "localDbUri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "cloudDiskPath", "columnName": "cloudDiskPath", "affinity": "TEXT", "notNull": false }, { "fieldPath": "keyUri", "columnName": "keyUri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "dbName", "columnName": "dbName", "affinity": "TEXT", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "EntryRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dbFileUri` TEXT NOT NULL, `userName` TEXT NOT NULL, `title` TEXT NOT NULL, `uuid` BLOB NOT NULL, `time` INTEGER NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "dbFileUri", "columnName": "dbFileUri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userName", "columnName": "userName", "affinity": "TEXT", "notNull": true }, { "fieldPath": "title", "columnName": "title", "affinity": "TEXT", "notNull": true }, { "fieldPath": "uuid", "columnName": "uuid", "affinity": "BLOB", "notNull": true }, { "fieldPath": "time", "columnName": "time", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "SearchRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `time` INTEGER NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "title", "columnName": "title", "affinity": "TEXT", "notNull": true }, { "fieldPath": "time", "columnName": "time", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "CloudServiceInfo", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userName` TEXT, `password` TEXT, `cloudPath` TEXT NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userName", "columnName": "userName", "affinity": "TEXT", "notNull": false }, { "fieldPath": "password", "columnName": "password", "affinity": "TEXT", "notNull": false }, { "fieldPath": "cloudPath", "columnName": "cloudPath", "affinity": "TEXT", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '60d6ebad122bd7d8a8f58987ecdcd485')" ] } } ================================================ FILE: app/schemas/com.lyy.keepassa.dao.AppDatabase/3.json ================================================ { "formatVersion": 1, "database": { "version": 3, "identityHash": "974ef59b197782c4a7327ff2238e3160", "entities": [ { "tableName": "DbHistoryRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `time` INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT 'AFS', `localDbUri` TEXT NOT NULL, `cloudDiskPath` TEXT, `keyUri` TEXT NOT NULL, `dbName` TEXT NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "time", "columnName": "time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true, "defaultValue": "'AFS'" }, { "fieldPath": "localDbUri", "columnName": "localDbUri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "cloudDiskPath", "columnName": "cloudDiskPath", "affinity": "TEXT", "notNull": false }, { "fieldPath": "keyUri", "columnName": "keyUri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "dbName", "columnName": "dbName", "affinity": "TEXT", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "EntryRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dbFileUri` TEXT NOT NULL, `userName` TEXT NOT NULL, `title` TEXT NOT NULL, `uuid` BLOB NOT NULL, `time` INTEGER NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "dbFileUri", "columnName": "dbFileUri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userName", "columnName": "userName", "affinity": "TEXT", "notNull": true }, { "fieldPath": "title", "columnName": "title", "affinity": "TEXT", "notNull": true }, { "fieldPath": "uuid", "columnName": "uuid", "affinity": "BLOB", "notNull": true }, { "fieldPath": "time", "columnName": "time", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "SearchRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `time` INTEGER NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "title", "columnName": "title", "affinity": "TEXT", "notNull": true }, { "fieldPath": "time", "columnName": "time", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "CloudServiceInfo", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userName` TEXT, `password` TEXT, `cloudPath` TEXT NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userName", "columnName": "userName", "affinity": "TEXT", "notNull": false }, { "fieldPath": "password", "columnName": "password", "affinity": "TEXT", "notNull": false }, { "fieldPath": "cloudPath", "columnName": "cloudPath", "affinity": "TEXT", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "QuickUnLockRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dbUri` TEXT NOT NULL, `dbPass` TEXT NOT NULL, `keyPath` TEXT, `isUseKey` INTEGER NOT NULL, `isUseFingerprint` INTEGER NOT NULL, `passIv` BLOB)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "dbUri", "columnName": "dbUri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "dbPass", "columnName": "dbPass", "affinity": "TEXT", "notNull": true }, { "fieldPath": "keyPath", "columnName": "keyPath", "affinity": "TEXT", "notNull": false }, { "fieldPath": "isUseKey", "columnName": "isUseKey", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "isUseFingerprint", "columnName": "isUseFingerprint", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "passIv", "columnName": "passIv", "affinity": "BLOB", "notNull": false } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '974ef59b197782c4a7327ff2238e3160')" ] } } ================================================ FILE: app/schemas/com.lyy.keepassa.dao.AppDatabase/4.json ================================================ { "formatVersion": 1, "database": { "version": 4, "identityHash": "974ef59b197782c4a7327ff2238e3160", "entities": [ { "tableName": "DbHistoryRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `time` INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT 'AFS', `localDbUri` TEXT NOT NULL, `cloudDiskPath` TEXT, `keyUri` TEXT NOT NULL, `dbName` TEXT NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "time", "columnName": "time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true, "defaultValue": "'AFS'" }, { "fieldPath": "localDbUri", "columnName": "localDbUri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "cloudDiskPath", "columnName": "cloudDiskPath", "affinity": "TEXT", "notNull": false }, { "fieldPath": "keyUri", "columnName": "keyUri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "dbName", "columnName": "dbName", "affinity": "TEXT", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "EntryRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dbFileUri` TEXT NOT NULL, `userName` TEXT NOT NULL, `title` TEXT NOT NULL, `uuid` BLOB NOT NULL, `time` INTEGER NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "dbFileUri", "columnName": "dbFileUri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userName", "columnName": "userName", "affinity": "TEXT", "notNull": true }, { "fieldPath": "title", "columnName": "title", "affinity": "TEXT", "notNull": true }, { "fieldPath": "uuid", "columnName": "uuid", "affinity": "BLOB", "notNull": true }, { "fieldPath": "time", "columnName": "time", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "SearchRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `time` INTEGER NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "title", "columnName": "title", "affinity": "TEXT", "notNull": true }, { "fieldPath": "time", "columnName": "time", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "CloudServiceInfo", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userName` TEXT, `password` TEXT, `cloudPath` TEXT NOT NULL)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userName", "columnName": "userName", "affinity": "TEXT", "notNull": false }, { "fieldPath": "password", "columnName": "password", "affinity": "TEXT", "notNull": false }, { "fieldPath": "cloudPath", "columnName": "cloudPath", "affinity": "TEXT", "notNull": true } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "QuickUnLockRecord", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dbUri` TEXT NOT NULL, `dbPass` TEXT NOT NULL, `keyPath` TEXT, `isUseKey` INTEGER NOT NULL, `isUseFingerprint` INTEGER NOT NULL, `passIv` BLOB)", "fields": [ { "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "dbUri", "columnName": "dbUri", "affinity": "TEXT", "notNull": true }, { "fieldPath": "dbPass", "columnName": "dbPass", "affinity": "TEXT", "notNull": true }, { "fieldPath": "keyPath", "columnName": "keyPath", "affinity": "TEXT", "notNull": false }, { "fieldPath": "isUseKey", "columnName": "isUseKey", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "isUseFingerprint", "columnName": "isUseFingerprint", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "passIv", "columnName": "passIv", "affinity": "BLOB", "notNull": false } ], "primaryKey": { "columnNames": [ "uid" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '974ef59b197782c4a7327ff2238e3160')" ] } } ================================================ FILE: app/src/androidTest/java/com/lyy/keepassa/AutoFillTest.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa import androidx.autofill.HintConstants import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith import kotlin.reflect.full.memberProperties @RunWith(AndroidJUnit4::class) class AutoFillTest { @Test fun getMember() { val hintClazz = HintConstants::class hintClazz.memberProperties.forEach { member-> println("${member.name} -> ${member.call()}") } } } ================================================ FILE: app/src/androidTest/java/com/lyy/keepassa/ComposeKeeTrayTotpTest.kt ================================================ package com.lyy.keepassa import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.util.totp.ComposeKeeTrayTotp import org.junit.Test import org.junit.runner.RunWith /** * @Author laoyuyu * @Description * @Date 1:50 PM 2024/2/22 **/ @RunWith(AndroidJUnit4::class) class ComposeKeeTrayTotpTest { @Test fun testGetOtpPass() { val entry = PwEntryV4() entry.strings = hashMapOf().apply { put("STR_URL", ProtectedString(false, "sss")) put("TOTP Settings", ProtectedString(false, "30;6")) put("TOTP Seed", ProtectedString(true, "123")) } val token = ComposeKeeTrayTotp.getOtpPass(entry) println("token = $token") } } ================================================ FILE: app/src/androidTest/java/com/lyy/keepassa/ComposeKeepassTest.kt ================================================ package com.lyy.keepassa import androidx.test.ext.junit.runners.AndroidJUnit4 import com.lyy.keepassa.entity.TimeOtp2Bean import com.lyy.keepassa.util.totp.ComposeKeepass import com.lyy.keepassa.util.totp.SecretHexType import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk import org.junit.Before import org.junit.Test import org.junit.runner.RunWith /** * @Author laoyuyu * @Description * @Date 9:57 AM 2024/2/22 **/ @RunWith(AndroidJUnit4::class) class ComposeKeepassTest { // @MockK // lateinit var timeOtp2Bean: TimeOtp2Bean @Before fun setUp() { // 在每个测试方法开始前初始化所有标注了@MockK的字段 MockKAnnotations.init(this) } @Test fun testHandleTotp() { val timeOtp2Bean = mockk() every { timeOtp2Bean.secret } returns "123" every { timeOtp2Bean.secretType } returns SecretHexType.BASE_32 every { timeOtp2Bean.period } returns 30 every { timeOtp2Bean.digits } returns 6 every { timeOtp2Bean.algorithm } returns HashAlgorithm.SHA1 val instance = ComposeKeepass::class.java.getField("INSTANCE").get(null) val method = ComposeKeepass::class.java.getDeclaredMethod("handleTotp", TimeOtp2Bean::class.java) method.isAccessible = true val token = method.invoke(instance, timeOtp2Bean) println("otp: $token") } } ================================================ FILE: app/src/androidTest/java/com/lyy/keepassa/KeepassDbTest.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa import android.content.Context import android.net.Uri import android.util.Log import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keepassdroid.Database import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroup import com.keepassdroid.database.PwGroupId import com.keepassdroid.database.PwGroupV4 import com.keepassdroid.database.helper.CreateDBHelper import com.keepassdroid.database.helper.KDBHandlerHelper import com.lyy.keepassa.util.QuickUnLockUtil import org.junit.Test import org.junit.runner.RunWith import java.io.File /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class KeepassDbTest { private val TAG = "KeepassDbTest" private val context: Context = ApplicationProvider.getApplicationContext() // private val dbName = "test.kdbx" // private val pass = "123456" // private val keyName = "db.key" private val dbName = "yuyu_pw_db.kdbx" private val pass = "Xiaotaiyan123" private val keyName = "yuyu_pw_db.key" @Test fun useAppContext() { Log.d(TAG, "pkgName = ${context.packageName}") val f = context.filesDir Log.d(TAG, "fileName = ${f.name}") // EditHelper.getInstance(context) // .openDb("test.kdbx", f.path, "123456", "/Users/aria/Downloads/kpa/db.key") } @Test fun createDb() { val dir = context.filesDir val cdb = CreateDBHelper(context, dbName, Uri.fromFile(File(dir, dbName))) cdb.setPass(pass, null) cdb.setKeyFile(Uri.fromFile(File(dir, keyName))) val db = cdb.build() // 创建完成后可以直接读取数据库 Log.d(TAG, "create db ${if (db != null) "success" else "fail"}") // if (db != null) { // readDb(db) // } } @Test fun openDb() { val dir = context.filesDir.path + "/" val db = KDBHandlerHelper.getInstance(context) .openDb(dbName, dir + dbName, pass, dir + keyName) // .openDb(dbName, dir + dbName, pass, null) Log.d(TAG, "open db ${if (db != null) "success" else "fail"}") // if (db != null) { // readDb(db) // } } /** * 增加组 */ @Test fun addGroup() { // val db = getDb() // val group = KDBHandlerHelper.getInstance(context) // .createGroup(db, "test", 45, null) // Log.d(TAG, "创建组:${if (group != null) "成功" else "失败"}") // readDb(db) } @Test fun addEntry() { val db = getDb() val entry = PwEntryV4() } @Test fun testCustomData() { val dir = context.filesDir val cdb = CreateDBHelper(context, dbName, Uri.fromFile(File(dir, dbName))) cdb.setPass(pass, null) cdb.setKeyFile(Uri.fromFile(File(dir, keyName))) val db = cdb.build() // 创建自定义数据 val entryV4 = PwEntryV4() entryV4.parent = db.pm.rootGroup as PwGroupV4 entryV4.setString(PwEntryV4.STR_TITLE, "title", false) entryV4.customData["sss"] = "ggggg" KDBHandlerHelper.getInstance(context).saveEntry(db, entryV4) } private fun getDb(): Database { val dir = context.filesDir.path + "/" return KDBHandlerHelper.getInstance(context) .openDb(dbName, dir + dbName, pass, dir + keyName) } /** * 读取数据库信息 */ private fun readDb(db: Database) { val pm = db.pm for (i in pm.groups) { readGroup(i.key, i.value) } for (i in pm.entries) { readEntry(i.value) } } /** * 读取组信息 */ private fun readGroup( id: PwGroupId, group: PwGroup ) { Log.d( TAG, "groupId = ${id}, groupName = ${group.name}, " + "parent = ${if (group.parent == null) "root" else group.parent.name}" ) for (childGroup in group.childGroups) { readGroup(childGroup.id, childGroup) } for (entry in group.childEntries) { readEntry(entry) } } /** * 读取条目 */ private fun readEntry(entry: PwEntry) { Log.d( TAG, "userName = ${entry.username}, url = ${entry.url}, " + "title = ${entry.title}, des = ${entry.notes}, " + "parent = ${entry.parent}, password = ${entry.password}" ) } /** * 测试加密解密 */ @Test fun encryptStr(){ val eStr = QuickUnLockUtil.encryptStr("gaJj07uacPAAAAAAAAAANB5oOxdcoY0cwIsjYnwH1yPyws3YUyciD_nfBMoabgG7") println("加密字符串:$eStr") // val dStr = QuickUnLockUtil.decryption("gaJj07uacPAAAAAAAAAANB5oOxdcoY0cwIsjYnwH1yPyws3YUyciD_nfBMoabgG7") val dStr = QuickUnLockUtil.decryption(eStr) println("解密字符串:$dStr") } } ================================================ FILE: app/src/androidTest/java/com/lyy/keepassa/UrlTest.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa import android.net.Uri import androidx.test.ext.junit.runners.AndroidJUnit4 import com.arialyy.frame.util.RegularRule import org.junit.Test import org.junit.runner.RunWith import java.io.IOException import java.net.URL @RunWith(AndroidJUnit4::class) class UrlTest { @Test fun domainTest(){ val temp = "twitter.com" val topDomain = Regex(RegularRule.DOMAIN_TOP, RegexOption.IGNORE_CASE).find(temp) println("topDomain = ${topDomain?.value}") } @Test fun testUrl(){ try { val url = URL("http://www.runoob.com/ssss/ssxxx/index.html?language=cn#j2se") System.out.println("URL 为:" + url.toString()) System.out.println("协议为:" + url.getProtocol()) System.out.println("验证信息:" + url.getAuthority()) System.out.println("文件名及请求参数:" + url.getFile()) System.out.println("主机名:" + url.getHost()) System.out.println("路径:" + url.getPath()) System.out.println("端口:" + url.getPort()) System.out.println("默认端口:" + url.getDefaultPort()) System.out.println("请求参数:" + url.getQuery()) System.out.println("定位位置:" + url.getRef()) } catch (e: IOException) { e.printStackTrace() } } @Test fun testUri(){ try { val url = Uri.parse("http://www.runoob.com/ssss/ssxxx/index.html?language=cn&q=1#j2se") println("URL 为:$url") println("协议为:" + url.scheme) println("验证信息:" + url.authority) println("文件名及请求参数:" + url.query) println("主机名:" + url.host) println("路径:" + url.path) println("最后一段路径:" + url.lastPathSegment) println("端口:" + url.port) println("请求参数:" + url.query) println("请求参数key:" + url.queryParameterNames) println("请求参数language:" + url.getQueryParameters("language")) println("定位位置:" + url.fragment) } catch (e: IOException) { e.printStackTrace() } } } ================================================ FILE: app/src/androidTest/java/com/lyy/keepassa/UtilTest.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa import com.arialyy.frame.util.KeyStoreUtil import com.blankj.utilcode.util.EncryptUtils import com.lyy.keepassa.view.StorageType import org.junit.Test class UtilTest { @Test fun emunTEst(){ println(StorageType.AFS.name) } @Test fun keyStoreUtil(){ val keyStoreUtil = KeyStoreUtil() val p = keyStoreUtil.encryptData(keyStoreUtil.getEncryptCipher(), "123456") println("密文: ${p.first}") val end = keyStoreUtil.decryptData(keyStoreUtil.getDecryptCipher(p.second), p.first) println("明文:$end") } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/assets/fingerprint_anim.json ================================================ {"v":"4.6.3","fr":25,"ip":0,"op":39,"w":650,"h":230,"nm":"Fingerprint","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 3","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[325,115,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[20.666,6.495],[-24.5,6],[-12.496,-7.37],[4.5,18.5],[32.5,-6.5],[-19,-58]],"o":[[-52.5,-16.5],[28.289,-6.928],[19.5,11.5],[-7.796,-32.05],[-27.263,5.453],[19.074,58.225]],"v":[[26,78],[-6.5,1.5],[30,38.5],[60.5,9.5],[-15.5,-36],[-55,53.5]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.3058823529411765,0.5215686274509804,0.8588235294117647,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":9},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0_1_0p333_0"],"t":1,"s":[0],"e":[100]},{"t":28}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim"}],"ip":0,"op":39,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[325,115,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[24,-3.5],[3,40.5]],"o":[[-24,3.5],[-3,-40.5]],"v":[[51.5,58.5],[-1,17.5]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.3058823529411765,0.5215686274509804,0.8588235294117647,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":9},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[102.868,99.902],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[43.527,56.362],[27.5,-42.5]],"o":[[-39,-50.5],[-27.5,42.5]],"v":[[68.5,-22.5],[-69,-20.5]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.3058823529411765,0.5215686274509804,0.8588235294117647,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":9},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0_1_0p333_0"],"t":5,"s":[0],"e":[100]},{"t":19}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim"}],"ip":0,"op":39,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 1","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[325,115,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[48.5,-7.5],[-33,-25]],"o":[[0,0],[-44.485,6.879],[35.509,26.901]],"v":[[41,24],[-8.5,-17],[-16,76.5]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.3058823529411765,0.5215686274509804,0.8588235294117647,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":9},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[58,-30]],"o":[[0,0],[-58,30]],"v":[[49.5,-62.5],[-50,-62.5]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.3058823529411765,0.5215686274509804,0.8588235294117647,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":9},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0_1_0p333_0"],"t":0,"s":[0],"e":[100]},{"t":19}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim"}],"ip":0,"op":39,"st":0,"bm":0,"sr":1}]} ================================================ FILE: app/src/main/assets/headAnim.json ================================================ {"v":"5.3.4","fr":30,"ip":0,"op":69,"w":150,"h":150,"nm":"loading","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"mask","td":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[100],"e":[60]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":15,"s":[60],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":30,"s":[100],"e":[60]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":45,"s":[60],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":60,"s":[100],"e":[60]},{"t":75.0000030548126}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[27,14.499999999999998,0],"ix":2},"a":{"a":0,"k":[0,-60,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0,0,0.333],"y":[0,0,0]},"n":["0_1_0_0","0_1_0_0","0p667_1_0p333_0"],"t":0,"s":[100,0,100],"e":[100,99,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[1,1,0.167],"y":[0,0,0]},"n":["0p833_1_1_0","0p833_1_1_0","0p833_1_0p167_0"],"t":40,"s":[100,99,100],"e":[100,0,100]},{"t":57.0000023216576}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"path-mask-1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"fill-mask-1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"トランスフォーム"}],"nm":"rect-mask-1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180.00000733155,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"1","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[31.28,74.311,0],"ix":2},"a":{"a":0,"k":[31.53,59.114,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[30.902,0.376],[62.81,0.25],[32.158,117.851],[0.25,117.978]],"c":true},"ix":2},"nm":"rect","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"g":{"p":3,"k":{"a":0,"k":[0,1,0.612,1,0.5,0.5,0.306,1,1,0,0,1],"ix":9}},"s":{"a":0,"k":[0,110],"ix":5},"e":{"a":0,"k":[0,0],"ix":6},"t":1,"nm":"fill-1","mn":"ADBE Vector Graphic - G-Fill","hd":false}],"ip":0,"op":91.000003706506,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"mask 2","td":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":7,"s":[100],"e":[60]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":22,"s":[60],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":37,"s":[100],"e":[60]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":52,"s":[60],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":67,"s":[100],"e":[60]},{"t":82.0000033399285}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[72.5,32.5,0],"ix":2},"a":{"a":0,"k":[0,-60,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0,0,0.333],"y":[0,0,0]},"n":["0_1_0_0","0_1_0_0","0p667_1_0p333_0"],"t":7,"s":[100,0,100],"e":[100,99,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[1,1,0.167],"y":[0,0,0]},"n":["0p833_1_1_0","0p833_1_1_0","0p833_1_0p167_0"],"t":43,"s":[100,99,100],"e":[100,0,100]},{"t":60.0000024438501}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"path-mask-2","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"fill-mask-2","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"トランスフォーム"}],"nm":"rect-mask-2","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":7.00000028511585,"op":187.000007616666,"st":7.00000028511585,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"2","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[68.526,91.757,0],"ix":2},"a":{"a":0,"k":[31.474,59.114,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[30.902,0.376],[62.699,0.25],[32.047,117.852],[0.25,117.977]],"c":true},"ix":2},"nm":"rect2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"g":{"p":3,"k":{"a":0,"k":[0,0.953,1,0.498,0.5,0.976,0.806,0.714,1,1,0.612,0.929],"ix":9}},"s":{"a":0,"k":[0,110],"ix":5},"e":{"a":0,"k":[0,10],"ix":6},"t":1,"nm":"fill2","mn":"ADBE Vector Graphic - G-Fill","hd":false}],"ip":0,"op":91.000003706506,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"mask 3","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[123,0.5,0],"ix":2},"a":{"a":0,"k":[0,-60,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0,0,0.333],"y":[0,0,0]},"n":["0_1_0_0","0_1_0_0","0p667_1_0p333_0"],"t":15,"s":[100,0,100],"e":[100,99,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[1,1,0.167],"y":[0,0,0]},"n":["0p833_1_1_0","0p833_1_1_0","0p833_1_0p167_0"],"t":45,"s":[100,99,100],"e":[100,0,100]},{"t":62.0000025253118}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"path-mask-3","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"fill-mask-3","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"トランスフォーム"}],"nm":"rect-mask-3","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":15.0000006109625,"op":195.000007942513,"st":15.0000006109625,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"3","tt":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":15,"s":[100],"e":[60]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":30,"s":[60],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":45,"s":[100],"e":[60]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":60,"s":[60],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":75,"s":[100],"e":[60]},{"t":90.0000036657751}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[118.72,59.485,0],"ix":2},"a":{"a":0,"k":[31.53,59.115,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[30.902,0.377],[62.81,0.25],[32.158,117.852],[0.25,117.979]],"c":true},"ix":2},"nm":"rect3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"g":{"p":3,"k":{"a":0,"k":[0,0.498,1,0.867,0.5,0.457,0.725,0.933,1,0.416,0.451,1],"ix":9}},"s":{"a":0,"k":[0,110],"ix":5},"e":{"a":0,"k":[0,10],"ix":6},"t":1,"nm":"fill3","mn":"ADBE Vector Graphic - G-Fill","hd":false}],"ip":0,"op":91.000003706506,"st":0,"bm":0}],"markers":[]} ================================================ FILE: app/src/main/assets/loadingAnimation.json ================================================ {"assets":[],"layers":[{"ddd":0,"ind":0,"ty":4,"nm":"形状图层 5","ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":8,"s":[100],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":24,"s":[30],"e":[100]},{"t":40}]},"r":{"k":0},"p":{"k":[187.875,77.125,0]},"a":{"k":[-76.375,-2.875,0]},"s":{"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":8,"s":[100,100,100],"e":[200,200,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":24,"s":[200,200,100],"e":[100,100,100]},{"t":40}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[18,18]},"p":{"k":[0,0]},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"k":[1,1,1,1]},"o":{"k":100},"w":{"k":0},"lc":1,"lj":1,"ml":4,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"k":[0.87,0.42,0.56,1]},"o":{"k":100},"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"k":[-76.482,-3.482],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":40,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":1,"ty":4,"nm":"形状图层 4","ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":6,"s":[100],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":22,"s":[30],"e":[100]},{"t":36}]},"r":{"k":0},"p":{"k":[162.125,76.625,0]},"a":{"k":[-76.375,-2.875,0]},"s":{"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":6,"s":[100,100,100],"e":[200,200,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":22,"s":[200,200,100],"e":[100,100,100]},{"t":36}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[18,18]},"p":{"k":[0,0]},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"k":[1,1,1,1]},"o":{"k":100},"w":{"k":0},"lc":1,"lj":1,"ml":4,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"k":[0.81,0.55,0.82,1]},"o":{"k":100},"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"k":[-76.482,-3.482],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":40,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"形状图层 3","ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":4,"s":[100],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":20,"s":[30],"e":[100]},{"t":32}]},"r":{"k":0},"p":{"k":[135.625,76.625,0]},"a":{"k":[-76.375,-2.875,0]},"s":{"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":4,"s":[100,100,100],"e":[200,200,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":20,"s":[200,200,100],"e":[100,100,100]},{"t":32}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[18,18]},"p":{"k":[0,0]},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"k":[1,1,1,1]},"o":{"k":100},"w":{"k":0},"lc":1,"lj":1,"ml":4,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"k":[0.47,0.31,0.62,1]},"o":{"k":100},"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"k":[-76.482,-3.482],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":40,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"形状图层 2","ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":2,"s":[100],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":16,"s":[30],"e":[100]},{"t":28}]},"r":{"k":0},"p":{"k":[109.375,76.625,0]},"a":{"k":[-76.625,-3.125,0]},"s":{"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":2,"s":[100,100,100],"e":[200,200,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":16,"s":[200,200,100],"e":[100,100,100]},{"t":28}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[18,18]},"p":{"k":[0,0]},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"k":[1,1,1,1]},"o":{"k":100},"w":{"k":0},"lc":1,"lj":1,"ml":4,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"k":[0.54,0.81,0.89,1]},"o":{"k":100},"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"k":[-76.482,-3.482],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":40,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"形状图层 1","ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":0,"s":[100],"e":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"n":["0p833_0p833_0p333_0"],"t":12,"s":[30],"e":[100]},{"t":24}]},"r":{"k":0},"p":{"k":[82.625,76.625,0]},"a":{"k":[-76.625,-3.375,0]},"s":{"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":0,"s":[100,100,100],"e":[200,200,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":12,"s":[200,200,100],"e":[100,100,100]},{"t":24}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"k":[18,18]},"p":{"k":[0,0]},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"k":[1,1,1,1]},"o":{"k":100},"w":{"k":0},"lc":1,"lj":1,"ml":4,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"k":[0.34,0.45,0.78,1]},"o":{"k":100},"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"k":[-76.482,-3.482],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":40,"st":0,"bm":0,"sr":1}],"v":"4.5.4","ddd":0,"ip":0,"op":40,"fr":24,"w":280,"h":160} ================================================ FILE: app/src/main/assets/lockedAnim.json ================================================ {"v":"5.3.4","fr":30,"ip":0,"op":303,"w":1125,"h":1125,"nm":"Lottie LOCKED","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"LOCKED Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":0,"s":[0],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.9],"y":[-0.103]},"n":["0p667_1_0p9_-0p103"],"t":23.521,"s":[100],"e":[0]},{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":50.25,"s":[0],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.9],"y":[-0.103]},"n":["0p667_1_0p9_-0p103"],"t":73.771,"s":[100],"e":[0]},{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":100.5,"s":[0],"e":[100]},{"i":{"x":[0],"y":[1.009]},"o":{"x":[0.9],"y":[0.008]},"n":["0_1p009_0p9_0p008"],"t":124,"s":[100],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.9],"y":[-0.103]},"n":["0p667_1_0p9_-0p103"],"t":124.021,"s":[100],"e":[0]},{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":150.729,"s":[0],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.9],"y":[-0.103]},"n":["0p667_1_0p9_-0p103"],"t":174.25,"s":[100],"e":[0]},{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":200.979,"s":[0],"e":[100]},{"i":{"x":[0],"y":[1.2]},"o":{"x":[0.9],"y":[0.193]},"n":["0_1p2_0p9_0p193"],"t":224.5,"s":[100],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.9],"y":[-0.103]},"n":["0p667_1_0p9_-0p103"],"t":225,"s":[100],"e":[0]},{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":251.729,"s":[0],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.9],"y":[-0.103]},"n":["0p667_1_0p9_-0p103"],"t":275.25,"s":[100],"e":[0]},{"t":301.978515625}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[562.5,590.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-114.519,0],[-114.519,-10.92],[-154.069,-10.92],[-154.069,-50.47],[-164.919,-50.47],[-164.919,0]],"c":true},"ix":2},"nm":"L","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.58431372549,0.976470648074,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"L","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.006,-2.006],[0,-2.893],[0,0],[-2.007,-2.006],[-2.894,0],[0,0],[-2.03,2.03],[0,2.847],[0,0],[2.03,2.03],[2.846,0],[0,0]],"o":[[-2.007,2.007],[0,0],[0,2.894],[2.006,2.007],[0,0],[2.846,0],[2.03,-2.03],[0,0],[0,-2.846],[-2.03,-2.03],[0,0],[-2.894,0]],"v":[[-107.589,-47.39],[-110.599,-40.04],[-110.599,-10.36],[-107.589,-3.01],[-100.239,0],[-70.559,0],[-63.244,-3.045],[-60.199,-10.36],[-60.199,-40.04],[-63.244,-47.355],[-70.559,-50.4],[-100.239,-50.4]],"c":true},"ix":2},"nm":"O","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-71.189,-39.48],[-71.189,-10.92],[-99.749,-10.92],[-99.749,-39.48]],"c":true},"ix":2},"nm":"O","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.58431372549,0.976470648074,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"O","np":5,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.006,-2.006],[0,-2.893],[0,0],[-2.007,-2.006],[-2.894,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[-2.894,0],[-2.007,2.007],[0,0],[0,2.894],[2.006,2.007],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-2.24,-50.4],[-42.14,-50.4],[-49.49,-47.39],[-52.5,-40.04],[-52.5,-10.36],[-49.49,-3.01],[-42.14,0],[-2.24,0],[-2.24,-10.92],[-41.65,-10.92],[-41.65,-39.48],[-2.24,-39.48]],"c":true},"ix":2},"nm":"C","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.58431372549,0.976470648074,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"C","np":3,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[32.199,-38.15],[25.899,-30.66],[16.029,-30.66],[16.029,-50.4],[5.109,-50.4],[5.109,0],[16.029,0],[16.029,-19.74],[25.899,-19.74],[42.419,0],[53.759,0],[53.759,-3.78],[35.769,-25.2],[53.759,-46.62],[53.759,-50.4],[42.419,-50.4]],"c":true},"ix":2},"nm":"K","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.58431372549,0.976470648074,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"K","np":3,"cix":2,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[60.969,-50.4],[60.969,0],[107.519,0],[107.519,-10.92],[71.959,-10.92],[71.959,-19.74],[100.589,-19.74],[100.589,-30.66],[71.959,-30.66],[71.959,-39.48],[107.519,-39.48],[107.519,-50.4]],"c":true},"ix":2},"nm":"E","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.58431372549,0.976470648074,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"E","np":3,"cix":2,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-2.03,2.03],[0,2.847],[0,0],[2.03,2.03],[2.893,0],[0,0]],"o":[[0,0],[2.893,0],[2.03,-2.03],[0,0],[0,-2.846],[-2.03,-2.03],[0,0],[0,0]],"v":[[114.589,0],[154.559,0],[161.944,-3.045],[164.989,-10.36],[164.989,-40.04],[161.944,-47.355],[154.559,-50.4],[114.589,-50.4]],"c":true},"ix":2},"nm":"D","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[153.999,-39.48],[153.999,-10.92],[125.439,-10.92],[125.439,-39.48]],"c":true},"ix":2},"nm":"D","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.58431372549,0.976470648074,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"D","np":5,"cix":2,"ix":6,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":303,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 5","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[562.5,562.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":1,"ty":"el","s":{"a":0,"k":[1015,1015],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.466,1,1,1,1,1,1,1,0,1,0.882,0.5,1,0],"ix":9}},"s":{"a":0,"k":[0,0],"ix":5},"e":{"a":0,"k":[492,0],"ix":6},"t":2,"h":{"a":0,"k":0,"ix":7},"a":{"a":0,"k":0,"ix":8},"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false}],"ip":0,"op":303,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"comp 3","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[578.5,1349.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[-100,197,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":23,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":709,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":630}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[537,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 16","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":19,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":19,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":242,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":664}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[231,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 15","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":21,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":21,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":662,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":461}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[496,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 14","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":27,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":457,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":307}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-124,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 13","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":642,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":298,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":490}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[663,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":2,"cix":2,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":23,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":27,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":435,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":418}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[20,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 11","np":2,"cix":2,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":261,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":948,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":539}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[277,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":27,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":457,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":655}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-229,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":242,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":393,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":324}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[98,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":39,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":80,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":605,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":469}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-60,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":74,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":1076,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":382}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[261,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":16,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":27,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":457,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":547}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-161,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":642,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":298,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":655}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[158,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":27,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":435,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":309}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[400,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":36,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":25,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":702,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":644}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[609,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":27,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":457,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":-53,"s":[556],"e":[-4793]},{"t":732}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":16,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":303,"st":-53,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 4","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[562.5,562.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":1,"ty":"el","s":{"a":0,"k":[1015,1015],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.466,1,1,1,1,1,1,1,0,1,0.882,0.5,1,0],"ix":9}},"s":{"a":0,"k":[0,0],"ix":5},"e":{"a":0,"k":[492,0],"ix":6},"t":2,"h":{"a":0,"k":0,"ix":7},"a":{"a":0,"k":0,"ix":8},"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false}],"ip":0,"op":303,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"comp 4","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[562.5,1349.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,197,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":23,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":709,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":407}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[537,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 16","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":19,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":19,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":242,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":441}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[231,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 15","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":21,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":21,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":662,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":393}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[496,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 14","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":27,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":457,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":359}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-124,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 13","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":642,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":298,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":411}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[663,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":2,"cix":2,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":23,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":27,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":435,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":422}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[20,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 11","np":2,"cix":2,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":261,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":948,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":474}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[277,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":27,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":457,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":377}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-229,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":242,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":393,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":381}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[98,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":39,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":80,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":605,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":342.583984375}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-60,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":74,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":1076,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":421}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[261,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":16,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":27,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":457,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":375}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-161,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":27,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":435,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":387}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[400,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":36,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":25,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":702,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":421}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[609,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-216.5,-846],[-216.5,-34]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.870588295133,0.894117706897,1],"ix":3},"o":{"a":0,"k":80,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"d":[{"n":"d","nm":"dash","v":{"a":0,"k":27,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":457,"ix":2}},{"n":"o","nm":"offset","v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[556],"e":[-4793]},{"t":509}],"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":15,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":303,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":0,"s":[0],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.9],"y":[-0.103]},"n":["0p667_1_0p9_-0p103"],"t":23.521,"s":[100],"e":[0]},{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":50.25,"s":[0],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.9],"y":[-0.103]},"n":["0p667_1_0p9_-0p103"],"t":73.771,"s":[100],"e":[0]},{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":100.5,"s":[0],"e":[100]},{"i":{"x":[0],"y":[1.009]},"o":{"x":[0.9],"y":[0.008]},"n":["0_1p009_0p9_0p008"],"t":124,"s":[100],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.9],"y":[-0.103]},"n":["0p667_1_0p9_-0p103"],"t":124.021,"s":[100],"e":[0]},{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":150.729,"s":[0],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.9],"y":[-0.103]},"n":["0p667_1_0p9_-0p103"],"t":174.25,"s":[100],"e":[0]},{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":200.979,"s":[0],"e":[100]},{"i":{"x":[0],"y":[1.2]},"o":{"x":[0.9],"y":[0.193]},"n":["0_1p2_0p9_0p193"],"t":224.5,"s":[100],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.9],"y":[-0.103]},"n":["0p667_1_0p9_-0p103"],"t":225,"s":[100],"e":[0]},{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":251.729,"s":[0],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.9],"y":[-0.103]},"n":["0p667_1_0p9_-0p103"],"t":275.25,"s":[100],"e":[0]},{"i":{"x":[0],"y":[1.094]},"o":{"x":[0.333],"y":[0]},"n":["0_1p094_0p333_0"],"t":301.979,"s":[0],"e":[100]},{"i":{"x":[0],"y":[1.009]},"o":{"x":[0.9],"y":[0.008]},"n":["0_1p009_0p9_0p008"],"t":325.479,"s":[100],"e":[100]},{"t":325.5}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[568.5,566,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[89.464,43.464,100],"ix":6}},"ao":0,"shapes":[{"d":1,"ty":"el","s":{"a":0,"k":[678,678],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"g":{"p":3,"k":{"a":0,"k":[0,0.584,0.976,1,0.39,0.584,0.976,1,1,0.584,0.976,1,0,0.5,0.321,0.25,1,0],"ix":9}},"s":{"a":0,"k":[0,0],"ix":5},"e":{"a":0,"k":[303,0],"ix":6},"t":2,"h":{"a":0,"k":0,"ix":7},"a":{"a":0,"k":0,"ix":8},"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false}],"ip":0,"op":390,"st":0,"bm":0}],"markers":[]} ================================================ FILE: app/src/main/assets/version_log/version_log_de_rDE.md ================================================ #### 1.0.8 - Das Absturzproblem beim Öffnen der Schnittstelle zum Entsperren von Fingerabdrücken wurde behoben #### 1.0.7 - Dialogfeld "Verlaufsversion hinzufügen" -Erhöhen Sie die Vibrationsrückmeldung -Fix das Problem, dass Dropbox Open Record nicht gespeichert werden kann #### 1.0.6 -Fix einige böse Fehler -Add [Fingerprint Unlock] (route: //keepassA.com/kpa? Activity = FingerprintActivity) #### 1.0.4 -Fix einige böse Fehler #### 1.0.3 -Fix einige böse Fehler #### 1.0.2 -Fix einige böse Fehler #### 1.0.1 -Fix einige böse Fehler ================================================ FILE: app/src/main/assets/version_log/version_log_en.md ================================================ ### 2.4.7 (2024/04/30) * fix: Fix bug ### 2.4.6 (2024/03/20) * new: New password generation ui * fix: Fix bug ### 2.4.5 (2024/03/07) * fix: Fix bug ### 2.4.2(2024/02/26) * new: New details page * new: Support Keepass TOTP * opt: Night mode color optimization * fix: Fixed edit page icon being gray instead of image * fix: Fixed display issue on edit entry page * fix: Fixed the problem that the copied token is invalid after totp is automatically refreshed * fix: Fix experience issues ### 2.4.1 (2023/05/07) * new: In-app browser auto-fill * new: Fetching log functions * opt: Animation effect * opt: The flow of auto-fill * opt: Auto-fill of web pages * fix: The problem of Chinese path synchronization failure in NutCloud ### 2.4.0 (2023/04/30) * 【Add】Spanish * 【Add】Auto-fill permission pop-up alerts * 【Add】Pre-set header mode, fix the nut cloud webdav can not log in the problem * 【Fix】Fix the issue of no auto-fill option * 【Fix】Fix the issue that some apps can't auto-fill * 【Fix】Fix the issue of failed auto-fill save ### 2.3.2 (2022/10/07) - 【Add】Nextcloud support * 【Fix】Fix some known issues * 【Fix】The problem of url not being displayed ### 2.3.1 (2022/06/13) - 【Fix】bugs ### 2.3.0 (2022/06/12) - 【Add】Collection function - 【Add】Auto save db after entering the background - 【Add】Ukrainian,thank for [@IhorHordiichuk](https://github.com/IhorHordiichuk) - 【Optimize】Open WebDav Process flow ### 2.1.10 (2022/01/05) - 【Optimize】TOTP display ![totp_bar](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/totpDisplay.png) - 【Fix】Failed to create webdav database - 【Fix】Failed to set fingerprint ### 2.1.8 (2021/12/31) Happy New Year!! - 【Add】Network retry,Improve link stability - 【Add】Turkish - 【Add】Night mode,[Setting->Theme Style](route://keepassA.com/kpa?activity=SettingActivity&type=app&scrollKey=setKeyUiSetting) - 【Add】TOTP Bar switch,[Setting->TOTP](route://keepassA.com/kpa?activity=SettingActivity&type=app&scrollKey=setKeyUiSetting) ![totp_bar](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/totpBar.png) - 【Fix】After the data is created, the quick unlock is opened instead of the home page - 【Fix】The problem of fingerprint unlocking - 【Fix】Failed to move item - 【Optimize】Browser auto-fill #### 2.1.7(2021/10/18) - 【Fix】Dialog layout is out of order - 【Fix】Abnormal expiration time display problem #### 2.1.6 (2021/10/17) - 【Fix】Some carsh bug - 【Optimize】Logic of expiration time setting #### 2.1.5 (2021/10/06) - 【New】Polish - 【Fix】After modifying the data, the database cannot be opened #### 2.1.4 (2021/9/2) - 【New】German translation - 【Optimize】Update some multilingual translations - 【Fix】The password generation tool has a high probability of selecting data without numbers. - 【Fix】The problem of failure to open some KDBV4 DB #### 2.1.3(2021/7/13) - 【New】KDBV4 support - 【New】Added reminder for expired entries - 【Fix】Group can be moved to itself - 【Fix】Other fields of the security keyboard cannot be filled - 【Fix】History is not sorted by time - 【Fix】Unable to start the quick unlock interface - 【Optimize】Safe keyboard multi-entry de-duplication #### 2.1.2 (2021/5/29) - 【New】Allow password to be empty - 【New】OneDrive added prompt description - 【Fix】The first time to install, there is no waiting animation to load - 【Fix】Android 11 does not pop up auto-fill options - 【Fix】When creating an entry on the homepage to a group, the number of entries on the homepage does not increase - 【Fix】After changing the password, the icon becomes the default icon - 【Optimize】Homepage slide to increase fade animation - 【Optimize】Animation details #### 2.1.1(2021/5/6) - 【Fix】webview Crash Problem #### 2.1(20201/5/5) - 【New】OneDrive support - 【New】Added French translation - 【New】Compatible with API30 - 【Fix】Crash when saving notes - 【Fix】When the screen is horizontal, the size of the interface is enlarged - 【Fix】Problems caused by multiple Unlock of WebDav #### 2.0.2 (2021/4/1) - 【New】Close loading animation selection[Setting](route://keepassA.com/kpa?activity=SettingActivity) -> UI settings - 【New】Does not automatically lock function[Setting](route://keepassA.com/kpa?activity=SettingActivity&type=app) -> Database settings - 【New】Russian(40%),Thank[@KovalevArtem](https://github.com/KovalevArtem) - 【New】Norwegian(50%),Thank[@Allan Nordhøy](https://github.com/comradekingu) - 【Optimize】Optimize English translation, Thank[@comradekingu](https://github.com/comradekingu) - 【Fix】An animation problem - 【Fix】An application setting crash problem #### 2.0.1(2021/3/27) - 【Fix】Webdav login problem - 【Fix】Multilingual issues #### 2.0(2021/3/23) - 【New】The recycle bin does not display the add group\entry button - 【New】Whether to hide the status bar,[Setting](route://keepassA.com/kpa?activity=SettingActivity) -> Ui Setting - 【New】Key login only - 【Optimize】Lots of animations - 【Optimize】The default icons are all MD style icons - 【Optimize】Icon selection will use the method that pops up at the bottom - 【Fix】The problem of invalid timing lock database - 【Fix】The problem of missing jump animation - 【Fix】Unable to switch historical data #### 1.8.4 (2021/3/4) - Refactor the logic of creating a database - Increase the function of creating a database on Webdav ![webdavCreate](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/webdavCreate.png) - Fix some crashes #### 1.8.3 (2021/2/25) - Fix some crashes - Upgrade kotlin version - Remove kotlin-android-extensions #### 1.8.2 (2021/2/17) - Fix the crash of the software after the screen is locked - Fix the crash of getting steam totp - Upgrade kotlin version #### 1.8.1 (2021/2/14) - Fix database loading due to incomplete reading - Fix asserts - Add Argon2 ID support - Add keyfile 2.0 support #### 1.8 (2021/2/14) - Fix a problem that webdav cannot be opened due to cache - Optimize the speed of opening the database #### 1.7 (2021/2/4) - Add, When the package name does not match, the keyword will be matched from the url - Add, Reference entry hint - Add, [When screen lock, automatic lock database function](route://keepassA.com/kpa?activity=SettingActivity&type=app) - Add, note add editer - Add, User name history record function ![user_drop_down_list](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/userDropdownList.png) - Add, Search ignores items in the recycle bin - Fix, In night mode, auto-fill text cannot be displayed - Fix, Some applications cannot be automatically filled - Fix, Some crash issues #### 1.6 (2020/11/28) - Auto-fill module, after fixing the associated entry, return to the application, the associated data cannot be displayed - Auto-fill module, fix the problem that the input box data is filled incorrectly when multiple input boxes are filled - Auto-fill module, add browser filling function - Auto-fill module, add other buttons - Setup module, After the unlock database, the homepage will display all entries first, [Click Settings](route://keepassA.com/kpa?activity=SettingActivity&type=db) - Item details module,Note field automatically increase expansion and contraction functions ![ime](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/noteExpand.png) - Optimize the homepage flickering problem - Thank [@DominicDesbiens](https://github.com/DominicDesbiens) for providing the Canadian French translation #### 1.5 (2020/11/5) - Add group search function - Add open source protocol description - Increase the function of moving items and groups ![ime](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/moveData.png) - Fix the problem that the secondary group cannot be added - Fix a crash caused by webdav obtaining file information - Fix some nasty crashes #### 1.4.1 (2020/10/29) - Upgrade android studio to 4.1 - Search and add highlight keywords - Fix a crash problem of quick unlock #### 1.4 (2020/10/28) - Upgrade kotlin version - Add group sorting function - Add field reference function - Add [safe keyboard](route://keepassA.com/kpa?activity=ime) ![ime](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/ime.png) - Fix Quickly unlock the interface, the problem that all short passwords cannot be deleted - Fix the crash problem caused by webdav login timeout - Fix some annoying crash problems #### 1.3 (2020/9/22) - Add the TOTP token setting function, edit the entry, click the add more button to display the function interface ![otp_setting](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/otpsetting.png) #### 1.2 (2020/9/2) - Fix some nasty crashes #### 1.1 (2020/8/21) - KeepassA is open source and has been hosted on [github](https://github.com/AriaLyy/KeepassA) - Fixed some annoying bugs #### 1.0.1.2 (2020/7/22) - Refactored [Fingerprint Unlock Setting Interface](route://keepassA.com/kpa?activity=FingerprintActivity) - Add drop-down synchronization database function - Add mobile phone root/emulator detection - Add the function of directly deleting entries/groups - Fix the problem that http addresses with special characters cannot be recognized - Fix the problem that the auto-fill service cannot save data #### 1.0.1.0 (2020/7/10) - Fixed the issue of clicking the notification bar to notify the error when the database is locked - TOTP increase countdown effect - Fixed the problem that the same entry always prompts when synchronizing the database - Fix some nasty crashes #### 1.0.0.9 (2020/7/11) - Fix the problem that after quick lock manually, you cannot enter other pages - Fix the problem that the notification bar icon is too small - Fixed the crash problem when opening the fingerprint unlock interface - Fixed the crash of auto-fill service - Optimize the logic of fingerprint unlock function #### 1.0.0.7 (2020/6/14) - Add history version dialog - Increase vibration feedback - Fix the problem that the dropbox open record cannot be saved #### 1.0.0.6 (2020/6/7) - Fix some nasty bugs - add [Fingerprint unlock](route://keepassA.com/kpa?activity=FingerprintActivity) #### 1.0.0.4 (2020/6/6) - Fix some nasty bugs #### 1.0.0.3 (2020/5/13) - Fix some nasty bugs #### 1.0.0.2 (2020/5/10) - Fix some nasty bugs #### 1.0.0.1 (2020/5/3) - Fix some nasty bugs ================================================ FILE: app/src/main/assets/version_log/version_log_ru_rRU.md ================================================ #### 1.0.8 -Исправлена ​​проблема сбоя при открытии интерфейса разблокировки отпечатков пальцев #### 1.0.7 -Добавить диалог версии истории -Повышение вибрации обратной связи -Исправить проблему, что открытая запись Dropbox не может быть сохранена #### 1.0.6 -Исправить некоторые неприятные ошибки -Добавить [Отпечаток пальца] (маршрут: //keepassA.com/kpa? Activity = FingerprintActivity) #### 1.0.4 -Исправить некоторые неприятные ошибки #### 1.0.3 -Исправить некоторые неприятные ошибки #### 1.0.2 -Исправить некоторые неприятные ошибки #### 1.0.1 -Исправить некоторые неприятные ошибки ================================================ FILE: app/src/main/assets/version_log/version_log_zh_CN.md ================================================ ### 2.4.7 (2024/04/30) * fix: 修复一些已知问题 ### 2.4.6 (2024/03/20) * new: 新的密码生成界面 * fix: 修复一些已知问题 ### 2.4.5 (2024/03/07) * fix: 修复一些已知问题 ### 2.4.2(2024/02/26) * new: 新的详情页 * new: 支持Keepass TOTP * opt: 夜间模式颜色优化 * fix: 修复编辑页图标是灰色而不是图像 * fix: 修复编辑条目页面的显示问题 * fix: 修复totp自动刷新后,复制的令牌无效的问题 * fix: 修复体验上的问题 ### 2.4.1 (2023/05/07) * new: 应用内浏览器自动填充 * new: 增加取出日志的功能 * opt: 动画效果 * opt: 优化自动填充的流程 * opt: 优化网页的自动填充 * fix: 修复坚果云中文路径同步失败的问题 ### 2.4.0 (2023/04/30) * 【增加】西班牙语 * 【增加】自动填充权限弹窗提示 * 【增加】预置header模式,修复某些坚果云webdav无法登录的问题 * 【修复】修复没有自动填充选项的问题 * 【修复】某些app不能自动填充的问题 * 【修复】修复自动填充保存失败的问题 ### 2.3.1 (2022/10/07) * 【增加】隐私说明弹窗 * 【增加】Nextcloud 支持 * 【修复】一些已发现的bug * 【修复】url不显示的问题 ### 2.3.0(2022/06/12) - 【新增】收藏功能 - 【新增】后在自动保存数据库 - 【新增】增加乌克兰语,感谢[@IhorHordiichuk](https://github.com/IhorHordiichuk) - 【优化】优化webdav打开流程,现在流程更加人性化 ### 2.1.10 (2022/01/05) - 【优化】totp显示优化 ![totp_bar](https://gitee.com/laoyuyu/blog/raw/master/img/totpDisplay.png) - 【修复】创建webdav数据库失败的问题 - 【修复】设置指纹失败的问题 ### 2.1.8 (2021/12/31) 新年快乐!! - 【新增】网络重试,提升OneDrive\Dropbox\webdav的网络连接稳定性 - 【新增】土耳其语 - 【新增】夜间模式,[应用设置->主题设置](route://keepassA.com/kpa?activity=SettingActivity&type=app&scrollKey=setKeyUiSetting) - 【新增】TOTP栏开关[应用设置->界面设置](route://keepassA.com/kpa?activity=SettingActivity&type=app&scrollKey=setKeyUiSetting) ![totp_bar](https://gitee.com/laoyuyu/blog/raw/master/keepassA/totpBar.png) - 【修复】创建完成数据后,打开的是快速解锁而不是主页 - 【修复】指纹解锁的问题 - 【修复】移动条目失败的问题 - 【优化】浏览器自动填充 #### 2.1.7(2021/10/18) - 【修复】对话框布局错乱问题 - 【修复】失效时间异常显示问题 #### 2.1.6 (2021/10/17) - 【修复】修复一些崩溃问题 - 【优化】失效时间设置的逻辑 #### 2.1.5 (2021/10/06) - 【新增】波兰语 - 【修复】修改数据后,无法打开数据库 #### 2.1.4 (2021/9/2) - 【新增】德语翻译 - 【优化】更新部分多语言翻译 - 【修复】密码生成工具选中数据大概率没有数字的问题 - 【修复】打开某些KDBV4失败的问题 #### 2.1.3(2021/7/13) - 【新增】KDBV4支持 - 【新增】过期的条目增加提示(中横线) - 【修复】群组可以移动到自己 - 【修复】安全键盘其它字段无法填充 - 【修复】历史记录没有按时间排序 - 【修复】无法启动快速解锁界面 - 【优化】安全键盘多条目去重 #### 2.1.2 (2021/5/29) - 【新增】允许密码为空 - 【新增】OneDrive增加提示说明 - 【修复】第一次安装,没有加载等待动画 - 【修复】android 11 没有弹出自动填充选择项 - 【修复】首页创建条目到群组时,首页的条目数量提示没有增加 - 【修复】修改密码后,图标变为默认图标 - 【优化】首页滑动增加渐隐动画 - 【优化】动画细节 #### 2.1.1(2021/5/6) - 【修复】webview崩溃问题 #### 2.1(20201/5/5) - 【新增】OneDrive支持 - 【新增】增加法语翻译 - 【新增】兼容API30 - 【修复】保存备注出现的崩溃 - 【修复】横屏时,界面尺寸被放大 - 【修复】WebDav多次Unlock导致的问题 #### 2.0.2 (2021/4/1) - 【新增】关闭加载动画选择项[设置](route://keepassA.com/kpa?activity=SettingActivity) -> 界面设置 - 【新增】不自动锁定功能[设置](route://keepassA.com/kpa?activity=SettingActivity&type=app) -> 数据库设置 - 【新增】俄语(40%),感谢[@KovalevArtem](https://github.com/KovalevArtem) - 【新增】挪威语(50%),感谢[@Allan Nordhøy](https://github.com/comradekingu) - 【优化】优化英文翻译,感谢[@Allan Nordhøy](https://github.com/comradekingu) - 【修复】一个动画关闭掉的问题 - 【修复】点击应设置崩溃的问题 #### 2.0.1(2021/3/27) - 【修复】webdav登陆问题 - 【修复】多语言问题 #### 2.0(2021/3/23) - 【新增】回收站不显示添加群组\条目按钮 - 【新增】是否隐藏状态栏,[点击设置](route://keepassA.com/kpa?activity=SettingActivity) -> 界面设置 - 【新增】仅密钥登陆 - 【优化】大量动画 - 【优化】默认图标全采用MD风格图标 - 【优化】图标选择将使用底部弹出的的方式 - 【修复】定时锁定数据库无效的问题 - 【修复】跳转动画丢失的问题 - 【修复】无法切换历史数据 #### 1.8.4(2021/3/4) - 重构创建数据库的逻辑 - 增加在Webdav上创建数据库的功能 ![webdavCreate](https://gitee.com/laoyuyu/blog/raw/master/keepassA/webdavCreate.png) - 修复一些崩溃问题 #### 1.8.3 (2021/2/25) - 修复一些崩溃问题 - 升级kotlin版本 - 移除kotlin-android-extensions #### 1.8.2 (2021/2/17) - 修复屏幕锁定后,点击通知进入软件出现的崩溃问题 - 修复获取steam totp 崩溃问题 - 升级kotlin 版本 #### 1.8.1 (2021/2/14) - 修复数据库一直加载的问题 - 修复一个附件问题 - 增加 Argon2 ID 支持 - 增加 keyfile 2.0 支持 #### 1.8 (2021/2/14) - 修复一个缓存问题导致的webdav打不开的问题 - 优化打开数据库的速度 #### 1.7 (2021/2/4) - 增加当包名不匹配时,将会从url匹配关键字 - 增加参考条目提示 - 增加[屏幕锁定,自动锁定数据库功能](route://keepassA.com/kpa?activity=SettingActivity&type=app) - note增加编辑功能 - 增加用户名历史记录功能 ![user_drop_down_list](https://gitee.com/laoyuyu/blog/raw/master/keepassA/userDropdownList.png) - 搜索忽略回收站中的条目 - 修复夜间模式下,自动填充文字无法显示的问题 - 修复某些应用无法自动填充的问题 - 修复一些崩溃问题 #### 1.6 (2020/11/28) - 自动填充模块,修复关联条目后,返回到应用,已关联的数据无法显示的问题 - 自动填充模块,修复多个输入框时,输入框数据填充错误的问题 - 自动填充模块,增加浏览器填充功能 - 自动填充模块,增加其它按钮 - 设置模块,增加解锁数据库后,主页优先显示所有条目功能,[点击设置](route://keepassA.com/kpa?activity=SettingActivity&type=db) - 条目详情模块,note 自动增加展开和收缩功能 ![note_expand](https://gitee.com/laoyuyu/blog/raw/master/keepassA/noteExpand.png) - 优化主页闪烁问题 - 感谢[@DominicDesbiens](https://github.com/DominicDesbiens)提供了加拿大法语翻译 #### 1.5 (2020/11/5) - 增加群组搜索功能 - 增加开放源码协议说明 - 增加移动条目和群组的功能 ![ime](https://gitee.com/laoyuyu/blog/raw/master/keepassA/moveData.png) - 修复无法增加二级群组的问题 - 修复一个webdav获取文件信息导致的崩溃问题 - 修复一些讨厌的崩溃问题 #### 1.4.1 (2020/10/29) - 升级android studio 到4.1 - 搜索增加高亮关键字 - 修复快速解锁的一个崩溃问题 #### 1.4 (2020/10/28) - 升级kotlin版本 - 增加群组排序功能 - 增加字段引用功能 - 增加[安全键盘](route://keepassA.com/kpa?activity=ime) ![ime](https://gitee.com/laoyuyu/blog/raw/master/keepassA/ime.png) - 修复快速解锁界面,无法删除所有短密码的问题 - 修复webdav登陆超时导致的崩溃问题 - 修复一些讨厌的崩溃问题 #### 1.3 (2020/9/22) - 增加TOTP令牌设置功能,编辑条目,点击添加更多按钮便可以显示该功能界面 ![otp_setting](https://gitee.com/laoyuyu/blog/raw/master/keepassA/otpsetting.png) #### 1.2 (2020/9/2) - 修复一些讨厌的崩溃问题 #### 1.1 (2020/8/21) - KeepassA开源了,已经托管在[github](https://github.com/AriaLyy/KeepassA)上 - 修复了一些讨厌的bug #### 1.0.1.2 (2020/8/7) - 重构[指纹解锁设置界面](route://keepassA.com/kpa?activity=FingerprintActivity) - 增加下拉同步数据库功能 - 增加手机root/模拟器检测 - 增加直接删除条目/群组功能 - 修复无法识别含有特殊字符的http地址的问题 - 修复自动填充服务,无法保存数据的问题 #### 1.0.1.1 (2020/7/22) - 增加[webdav](route://keepassA.com/kpa?activity=WebDavLoginDialog) - 修复小米手机增加指纹后,再进行指纹解锁导致的崩溃问题 #### 1.0.1.0 (2020/7/10) - 修复数据库已锁定后,点击通知栏通知跳转错误的问题 - TOTP增加倒计时效果 - 修复同步数据库时,相同条目总是提示的问题 - 修复一些讨厌的崩溃问题 #### 1.0.0.9 (2020/7/11) - 修复手动进行快速锁定后,无法进入其它页面的问题 - 修复通知栏图标过小的问题 - 修复打开指纹解锁界面崩溃的问题 - 修复自动填充服务崩溃的问题 - 优化指纹解锁功能的逻辑 #### 1.0.0.7 (2020/6/14) - 增加历史版本对话框 - 增加震动反馈 - 修复dropbox打开记录无法保存的问题 #### 1.0.0.6 (2020/6/7) - 修复一些讨厌bug - 增加[指纹解锁](route://keepassA.com/kpa?activity=FingerprintActivity) #### 1.0.0.4 (2020/6/6) - 修复一些讨厌的bug #### 1.0.0.3 (2020/5/13) - 修复一些讨厌的bug #### 1.0.0.2 (2020/5/10) - 修复一些讨厌的bug #### 1.0.0.1 (2020/5/3) - 修复一些讨厌的bug ================================================ FILE: app/src/main/assets/version_log/version_log_zh_TW.md ================================================ #### 2.0.1(2021/3/27) - [修復]webdav登陸問題 - [修復]多語言問題 #### 2.0(2021/3/23) - [新增]回收站不顯示添加群組\條目按鈕 - [新增]是否隱藏狀態欄,[點擊設置](route://keepassA.com/kpa?activity=SettingActivity) -> 界面設置 - [新增]僅密鑰登陸 - [優化]大量動畫 - [優化]默認圖標全採用MD風格圖標 - [優化]圖標選擇將使用底部彈出的的方式 - [修復]定時鎖定數據庫無效的問題 - [修復]跳轉動畫丟失的問題 - [修復]無法切換歷史數據 #### 1.8.4 (2021/3/4) - 重構創建數據庫的邏輯 - 增加在Webdav上創建數據庫的功能 ![webdavCreate](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/webdavCreate.png) - 修復一些崩潰問題 #### 1.8.3 (2021/2/25) - 修復一些崩潰問題 - 升級kotlin版本 - 移除kotlin-android-extensions #### 1.8.2 (2021/2/17) - 修復屏幕鎖定後,點擊通知進入軟件出現的崩潰問題 - 修復獲取steam totp 崩潰問題 - 升級kotlin 版本 #### 1.8.1 (2021/2/15) - 修復數據庫一直加載的問題 - 修復一個附件問題 - 增加Argon2 ID 支持 - 增加keyfile 2.0 支持 #### 1.8 (2021/2/14) - 修復一個緩存問題導致的webdav打不開的問題 - 優化打開數據庫的速度 #### 1.7 (2021/2/4) - 增加當包名不匹配時,將會從url匹配關鍵字 - 增加參考條目提示 - 增加[屏幕鎖定,自動鎖定數據庫功能](route://keepassA.com/kpa?activity=SettingActivity&type=app) - note增加編輯功能 - 增加用戶名歷史記錄功能 ![user_drop_down_list](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/userDropdownList.png) - 搜索忽略回收站中的條目 - 修復夜間模式下,自動填充文字無法顯示的問題 - 修復某些應用無法自動填充的問題 - 修復一些崩潰問題 #### 1.6 (2020/11/28) - 自動填充模塊,修復關聯條目後,返回到應用,已關聯的數據無法顯示的問題 - 自動填充模塊,修復多個輸入框時,輸入框數據填充錯誤的問題 - 自動填充模塊,增加瀏覽器填充功能 - 自動填充模塊,增加其它按鈕 - 設置模塊,增加解鎖數據庫後,主頁優先顯示所有條目功能,[點擊設置](route://keepassA.com/kpa?activity=SettingActivity&type=db) - 條目詳情模塊,note 自動增加展開和收縮功能 ![ime](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/noteExpand.png) - 優化主頁閃爍問題 - 感謝[@DominicDesbiens](https://github.com/DominicDesbiens)提供了加拿大法語翻譯 #### 1.5 (2020/11/5) - 增加群組搜索功能 - 增加開放源碼協議說明 - 增加移動條目和群組的功能 ![ime](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/moveData.png) - 修復無法增加二級群組的問題 - 修復一個webdav獲取文件信息導致的崩潰問題 - 修復一些討厭的崩潰問題 #### 1.4.1 (2020/10/29) - 升級android studio 到4.1 - 搜索增加高亮關鍵字 - 修復快速解鎖的一個崩潰問題 #### 1.4 (2020/10/28) - 升級kotlin版本 - 增加群組排序功能 - 增加字段引用功能 - 增加[安全鍵盤](route://keepassA.com/kpa?activity=ime) ![ime](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/ime.png) - 修復快速解鎖界面,無法刪除所有短密碼的問題 - 修復webdav登陸超時導致的崩潰問題 - 修復一些討厭的崩潰問題 #### 1.3 (2020/9/22) - 增加TOTP令牌設置功能,編輯條目,點擊添加更多按鈕便可以顯示該功能界面 ![otp_setting](https://raw.githubusercontent.com/AriaLyy/KeepassA/master/img/otpsetting.png) #### 1.2 (2020/9/2) - 修復一些討厭的崩潰問題 #### 1.1 (2020/8/21) - KeepassA開源了,已經託管在[github](https://github.com/AriaLyy/KeepassA)上 - 修復了一些討厭的bug #### 1.0.1.2 (2020/7/22) - 重構[指紋解鎖設置界面](route://keepassA.com/kpa?activity=FingerprintActivity) - 增加下拉同步數據庫功能 - 增加手機root/模擬器檢測 - 增加直接刪除條目/群組功能 - 修復無法識別含有特殊字符的http地址的問題 - 修復自動填充服務,無法保存數據的問題 #### 1.0.1.0 (2020/7/10) - 修復數據庫已鎖定後,點擊通知欄通知跳轉錯誤的問題 - TOTP增加倒計時效果 - 修復同步數據庫時,相同條目總是提示的問題 - 修復一些討厭的崩潰問題 #### 1.0.0.9 (2020/7/11) - 修復手動進行快速鎖定後,無法進入其它頁面的問題 - 修復通知欄圖標過小的問題 - 修復打開指紋解鎖界面崩潰的問題 - 修復自動填充服務崩潰的問題 - 優化指紋解鎖功能的邏輯 #### 1.0.0.7 (2020/6/14) - 增加歷史版本對話框 - 增加震動反饋 - 修復dropbox打開記錄無法保存的問題 #### 1.0.0.6 (2020/6/7) - 修復一些討厭bug - 增加[指紋解鎖](route://keepassA.com/kpa?activity=FingerprintActivity) #### 1.0.0.4 (2020/6/6) - 修復一些討厭bug #### 1.0.0.3 (2020/5/13) - 修復一些討厭bug #### 1.0.0.2 (2020/5/10) - 修復一些討厭bug #### 1.0.0.1 (2020/5/3) - 修復一些討厭bug ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/AnimState.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.base enum class AnimState { ALL, NOT_ANIM } ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/BaseActivity.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.base import android.annotation.SuppressLint import android.app.Activity import android.app.ActivityOptions import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Looper import android.util.Pair import android.view.View import android.view.WindowManager import androidx.appcompat.widget.Toolbar import androidx.databinding.ViewDataBinding import com.arialyy.frame.core.AbsActivity import com.arialyy.frame.util.ReflectionUtil import com.blankj.utilcode.util.AppUtils import com.blankj.utilcode.util.ScreenUtils import com.gyf.immersionbar.ImmersionBar import com.lyy.keepassa.R import com.lyy.keepassa.base.AnimState.NOT_ANIM import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KdbUtil.isNull import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.LanguageUtil import me.jessyan.autosize.AutoSizeConfig import timber.log.Timber import java.lang.reflect.Field /** * Created by Lyy on 2016/9/27. */ abstract class BaseActivity : AbsActivity() { protected lateinit var toolbar: Toolbar private var animState = AnimState.ALL companion object { var showStatusBar = false } override fun initData(savedInstanceState: Bundle?) { try { toolbar = findViewById(R.id.kpa_toolbar) toolbar.setNavigationOnClickListener { finishAfterTransition() } } catch (e: Exception) { Timber.w(e) } } open fun useAnim() = AnimState.ALL override fun onPreInit(): Boolean { if (!KeepassAUtil.instance.isHomeActivity(this) && (BaseApp.KDB.isNull() || BaseApp.dbRecord == null) ) { BaseApp.isLocked = true HitUtil.toaskShort(getString(R.string.notify_db_locked)) // Cannot be used finishAfterTransition(), because binding invalid finish() return false } return true } override fun onCreate(savedInstanceState: Bundle?) { AutoSizeConfig.getInstance().screenHeight = ScreenUtils.getScreenHeight() AutoSizeConfig.getInstance().screenWidth = ScreenUtils.getScreenWidth() super.onCreate(savedInstanceState) // 进入系统多任务,界面变空白,设置无法截图 if (!AppUtils.isAppDebug()) { window.setFlags( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE ) } animState = useAnim() setWindowAnim() handleStatusBar() } private fun handleStatusBar() { ImmersionBar.with(this) .statusBarColor(R.color.background_color) .autoDarkModeEnable(true) .autoStatusBarDarkModeEnable(true, 0.2f) //自动状态栏字体变色,必须指定状态栏颜色才可以自动变色哦 .flymeOSStatusBarFontColor(R.color.text_black_color) .fitsSystemWindows(true) // .hideBar(FLAG_HIDE_STATUS_BAR) .autoNavigationBarDarkModeEnable(true, 0.2f) // 自动导航栏图标变色,必须指定导航栏颜色才可以自动变色哦 .navigationBarColor(R.color.background_color) .statusBarDarkFont( true, 0.2f ) //原理:如果当前设备支持状态栏字体变色,会设置状态栏字体为黑色,如果当前设备不支持状态栏字体变色,会使当前状态栏加上透明度,否则不执行透明度 .init() return } override fun attachBaseContext(newBase: Context?) { super.attachBaseContext(LanguageUtil.setLanguage(newBase!!, BaseApp.currentLang)) } private fun setWindowAnim() { if (animState == NOT_ANIM) { return } // salide 为滑入,其它动画效果参考:https://github.com/lgvalle/Material-Animations // A -> B, B的进入动画 // window.enterTransition = TransitionInflater.from(this) // .inflateTransition(R.transition.slide_enter) // A -> B, A的退出动画 // window.exitTransition = TransitionInflater.from(this) // .inflateTransition(R.transition.slide_exit) // // A <- B, B的返回动画 // window.returnTransition = TransitionInflater.from(this) // .inflateTransition(R.transition.slide_return) // // // A <- B, A的进入动画 // window.reenterTransition = TransitionInflater.from(this) // .inflateTransition(R.transition.slide_reeter) // A -> B, B的enter动画和A的exit动画是否同时执行,false 禁止 // window.allowEnterTransitionOverlap = true // A <- B, A的reenter和B的return动画是否同时执行,false 禁止 // window.allowReturnTransitionOverlap = true // reenterTransition、returnTransition 是方向动画 // EnterTransition <-> ReturnTransition // ExitTransition <-> ReenterTransition } protected fun showQuickUnlockDialog() { KeepassAUtil.instance.lock() finish() } override fun onRestart() { super.onRestart() Timber.d("onRestart") if (!KeepassAUtil.instance.isHomeActivity(this) && (BaseApp.KDB.isNull() || BaseApp.isLocked)) { BaseApp.handler.postDelayed({ KeepassAUtil.instance.lock() finish() }, 150) return } } var isStartOtherActivity = false override fun startActivity( intent: Intent?, options: Bundle? ) { super.startActivity(intent, options) isStartOtherActivity = true // overridePendingTransition(R.anim.translate_right_in, R.anim.translate_left_out) } /** * Android10 Activity的onStop方法可能会导致共享元素动画失效,通过反射注入恢复共享元素动画 * @param activity */ @SuppressLint("PrivateApi") private fun updateResume(activity: Activity) { if (!isStartOtherActivity) { return } Looper.myQueue() .addIdleHandler { try { Timber.d("updateResume") ActivityOptions.makeSceneTransitionAnimation(this) val stateField: Field = ReflectionUtil.getField( Activity::class.java, "mActivityTransitionState" ) val stateObj = stateField.get(activity) val activityTransitionStateClazz = classLoader.loadClass("android.app.ActivityTransitionState") val mPendingExitNamesField: Field = ReflectionUtil.getField( activityTransitionStateClazz, "mPendingExitNames" ) val b = buildSharedElements() mPendingExitNamesField.set(stateObj, b) } catch (e: java.lang.Exception) { Timber.e(e) } return@addIdleHandler false } } /** * @param sharedElements 共享元素属性 */ open fun buildSharedElements(vararg sharedElements: Pair): ArrayList { val names = ArrayList() for (i in sharedElements.indices) { val sharedElement: Pair = sharedElements[i] val sharedElementName = sharedElement.second ?: throw IllegalArgumentException("Shared element name must not be null") names.add(sharedElementName) val view = sharedElement.first ?: throw IllegalArgumentException("Shared element must not be null") // views.add(sharedElement.first) } return names } override fun onResume() { super.onResume() // 启动定时器 KeepassAUtil.instance.startLockTimer(this) // updateResume(this) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/BaseApp.java ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.base; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Handler; import android.os.Looper; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDelegate; import androidx.multidex.MultiDexApplication; import androidx.preference.PreferenceManager; import com.alibaba.android.arouter.launcher.ARouter; import com.arialyy.frame.core.AbsFrame; import com.arialyy.frame.router.Routerfit; import com.keepassdroid.Database; import com.lyy.keepassa.R; import com.lyy.keepassa.common.PassType; import com.lyy.keepassa.dao.AppDatabase; import com.lyy.keepassa.entity.DbHistoryRecord; import com.lyy.keepassa.receiver.ScreenLockReceiver; import com.lyy.keepassa.router.ServiceRouter; import com.lyy.keepassa.service.feat.KpaSdkService; import com.lyy.keepassa.util.CommonKVStorage; import com.lyy.keepassa.util.LanguageUtil; import com.lyy.keepassa.view.StorageType; import java.util.Locale; import me.weishu.reflection.Reflection; public class BaseApp extends MultiDexApplication { public static BaseApp APP; public static Handler handler; public static Database KDB; @Nullable public static DbHistoryRecord dbRecord; public static AppDatabase appDatabase; public static String dbName = ""; public static String dbFileName = ""; public static String dbVersion = "Keepass 4.0"; // SHA 256 加密后的数据库主密码 public static String dbPass = ""; public static String shortPass = ""; // SHA 256 加密后的密钥路径 public static String dbKeyPath = ""; public static boolean isV4 = true; public static Locale currentLang = Locale.ENGLISH; public static Boolean isLocked = true; public static int passType = PassType.INSTANCE.getONLY_PASS(); public static boolean isAFS() { return dbRecord == null || StorageType.valueOf(dbRecord.getType()) == StorageType.AFS; } @Override protected void attachBaseContext(Context base) { currentLang = setLanguage(base); //super.attachBaseContext(LanguageUtil.INSTANCE.setLanguage(base, currentLang)); super.attachBaseContext(base); setThemeStyle(); Reflection.unseal(base); } private void setThemeStyle() { String mode = PreferenceManager.getDefaultSharedPreferences(this) .getString(getString(R.string.set_key_theme_style), "0"); switch (mode) { case "0": AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); break; case "1": AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); break; case "2": AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); break; } } @Override public void onCreate() { super.onCreate(); AbsFrame.init(this); APP = this; handler = new Handler(Looper.getMainLooper()); ARouter.init(this); // 尽可能早,推荐在Application中初始化 KpaSdkService kpaSdkService = Routerfit.INSTANCE.create(ServiceRouter.class, null).getKpaSdkService(); kpaSdkService.preInitSdk(this); initReceiver(); if (CommonKVStorage.INSTANCE.getBoolean(Constance.IS_AGREE_PRIVACY_AGREEMENT, false)) { kpaSdkService.initThirdSdk(this); } } /** * init receiver */ public void initReceiver() { boolean isNeedRegScreenLockReceiver = PreferenceManager.getDefaultSharedPreferences(BaseApp.APP) .getBoolean(getString(R.string.set_key_lock_screen_auto_lock_db), false); if (isNeedRegScreenLockReceiver) { ScreenLockReceiver receiver = new ScreenLockReceiver(); IntentFilter inf = new IntentFilter(); inf.addAction(Intent.ACTION_SCREEN_OFF); inf.addAction(Intent.ACTION_USER_PRESENT); registerReceiver(receiver, inf); } } /** * 设置语言 * 优先读取保存的语言,如果配置的语言存在,设置该语言为app的语言 * 如果没有已记录的语言,读取系统当前语言 * 如果系统语言不在支持列表的[LanguageUtil.SUPPORT_LAN]中,将app语言设置为英文 * 如果系统语言在支持列表中,设置该语言为app的语言 */ private Locale setLanguage(Context context) { Locale lang = LanguageUtil.INSTANCE.getDefLanguage(context); if (lang != null) { currentLang = lang; } else { Locale def = LanguageUtil.INSTANCE.getSysCurrentLan(); lang = new Locale(def.getLanguage(), def.getCountry()); if (LanguageUtil.SUPPORT_LAN.contains(lang)) { LanguageUtil.INSTANCE.setLanguage(context, lang); } else { LanguageUtil.INSTANCE.setLanguage(context, Locale.ENGLISH); } LanguageUtil.INSTANCE.saveLanguage(context, lang); } return lang; } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/BaseBottomSheetDialogFragment.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.base import android.content.Context import android.os.Bundle import androidx.databinding.ViewDataBinding import com.arialyy.frame.core.AbsBottomSheetDialogFragment import com.lyy.keepassa.util.LanguageUtil abstract class BaseBottomSheetDialogFragment : AbsBottomSheetDialogFragment() { override fun onAttach(context: Context) { super.onAttach(LanguageUtil.setLanguage(context, BaseApp.currentLang)) } override fun init(savedInstanceState: Bundle?) { } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/BaseDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.base import android.content.Context import android.os.Build.VERSION import android.view.View import androidx.databinding.ViewDataBinding import androidx.fragment.app.FragmentManager import com.arialyy.frame.base.FrameDialog import com.lyy.keepassa.util.LanguageUtil import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 2021/1/13 **/ abstract class BaseDialog : FrameDialog() { private var onDismissListener: OnDialogDismissListener? = null fun setOnDismissListener(dismissListener: OnDialogDismissListener) { this.onDismissListener = dismissListener } override fun initData() { super.initData() dialog?.window?.decorView?.setOnSystemUiVisibilityChangeListener { _ -> val uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or //布局位于状态栏下方 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY dialog?.window?.decorView?.systemUiVisibility = uiOptions } } override fun onAttach(context: Context) { super.onAttach(LanguageUtil.setLanguage(context, BaseApp.currentLang)) } override fun show(manager: FragmentManager, tag: String?) { if (manager.isStateSaved) { Timber.d("ac 已经保存状态了,不再启动对话框") return } if (isAdded || manager.findFragmentByTag(tag) != null) { Timber.d("fragment 已经被add") return } try { //在每个add事务前增加一个remove事务,防止连续的add,需要使用的commit 而非其他方法 manager.beginTransaction().remove(this).commit() super.show(manager, tag) } catch (e: Exception) { //同一实例使用不同的tag会异常,这里捕获一下 Timber.e(e) } } override fun dismiss() { if (!isVisible) { Timber.d("fragment 还没被加载") return } if (childFragmentManager.isStateSaved) { Timber.d("状态已经保存,不再dismiss") return } onDismissListener?.onDismiss() super.dismiss() } override fun onDestroy() { super.onDestroy() onDismissListener = null } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/BaseFragment.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.base import android.content.Context import android.content.Intent import android.os.Bundle import android.transition.ChangeBounds import android.transition.Slide import android.transition.TransitionSet import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.databinding.ViewDataBinding import com.arialyy.frame.core.AbsFragment import com.lyy.keepassa.util.AutoLockDbUtil import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.LanguageUtil import me.jessyan.autosize.AutoSize abstract class BaseFragment : AbsFragment() { override fun onAttach(context: Context) { super.onAttach(LanguageUtil.setLanguage(context, BaseApp.currentLang)) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // AutoSize.autoConvertDensity(activity, 411f, true); return super.onCreateView(inflater, container, savedInstanceState) } override fun onActivityCreated(savedInstanceState: Bundle?) { // setWindowAnim() super.onActivityCreated(savedInstanceState) } fun getRootView(): View = mRootView private fun setWindowAnim() { // salide 为滑入,其它动画效果参考:https://github.com/lgvalle/Material-Animations // 第一次进入activity的动画 val enterSet = TransitionSet() enterSet.addTransition(Slide(Gravity.END)) .addTransition(ChangeBounds()) // 右边进入左边 enterSet.duration = 400 enterSet.excludeTarget(android.R.id.navigationBarBackground, true) // 导航栏不参与动画 enterSet.excludeTarget(android.R.id.statusBarBackground, true) // 状态栏不参与动画 enterTransition = enterSet // 退出当前activity的动画 val exitSet = TransitionSet() exitSet.addTransition(Slide(Gravity.START)) .addTransition(ChangeBounds()) // 左边到右边 exitSet.duration = 400 exitSet.excludeTarget(android.R.id.navigationBarBackground, true) // 导航栏不参与动画 exitSet.excludeTarget(android.R.id.statusBarBackground, true) // 状态栏不参与动画 exitTransition = exitSet // 重新进入activity的动画 val reEnterSet = TransitionSet() reEnterSet.addTransition(Slide(Gravity.END)) .addTransition(ChangeBounds()) reEnterSet.duration = 400 reEnterSet.excludeTarget(android.R.id.navigationBarBackground, true) // 导航栏不参与动画 reEnterSet.excludeTarget(android.R.id.statusBarBackground, true) // 状态栏不参与动画 returnTransition = reEnterSet } override fun onResume() { super.onResume() KeepassAUtil.instance.startLockTimer(this) } override fun onDelayLoad() { } override fun startActivity( intent: Intent?, options: Bundle? ) { super.startActivity(intent, options) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/BaseModule.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.base import com.arialyy.frame.base.BaseViewModule /** * Created by Lyy on 2016/9/27. */ open class BaseModule : BaseViewModule() ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/BaseService.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.base import android.app.Service import android.content.Context import com.lyy.keepassa.util.LanguageUtil /** * @Author laoyuyu * @Description * @Date 2021/5/6 **/ abstract class BaseService:Service() { override fun attachBaseContext(newBase: Context?) { super.attachBaseContext(LanguageUtil.setLanguage(newBase!!, BaseApp.currentLang)) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/Constance.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.base class Constance { companion object { const val DEBUG = true const val PRE_FILE_NAME = "KeepassA" const val VERSION_CODE = "VersionCode" const val PRE_KEY_START_APP_NUM = "PRE_KEY_START_APP_NUM" const val START_DONATE_JUDGMENT_VALUE = 60 const val KPA_IS_COLLECTION = "KPA_IS_COLLECTION" const val IS_AGREE_PRIVACY_AGREEMENT = "IS_AGREE_PRIVACY_AGREEMENT" } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/DbMigration.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.base import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase object DbMigration { fun MIGRATION_2_3(): Migration { return object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE QuickUnLockRecord (uid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, dbUri TEXT NOT NULL, dbPass TEXT NOT NULL, keyPath TEXT, isUseKey INTEGER NOT NULL, isFullUnlock INTEGER NOT NULL, passIv BLOB NOT NULL)") } } } fun MIGRATION_3_4(): Migration { return object : Migration(3, 4) { override fun migrate(database: SupportSQLiteDatabase) { // modify passIv can set null database.execSQL("CREATE TABLE QuickUnLockRecord_TEMP (uid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, dbUri TEXT NOT NULL, dbPass TEXT NOT NULL, keyPath TEXT, isUseKey INTEGER NOT NULL, isUseFingerprint INTEGER NOT NULL, passIv BLOB)") database.execSQL("INSERT INTO QuickUnLockRecord_TEMP(uid, dbUri, dbPass, keyPath, isUseKey, isUseFingerprint, passIv) SELECT uid, dbUri, dbPass, keyPath, isUseKey, isFullUnlock, passIv FROM QuickUnLockRecord") database.execSQL("DROP TABLE QuickUnLockRecord") database.execSQL("ALTER TABLE QuickUnLockRecord_TEMP RENAME TO QuickUnLockRecord") // renameDbRecord database.execSQL("ALTER TABLE DbRecord RENAME TO DbHistoryRecord") } } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/KeyConstance.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.base object KeyConstance { const val KEY_DONT_SHOW_TIP = "KEY_DONT_SHOW_TIP" const val KEY_LAST_TIP_START_TIME = "KEY_LAST_TIP_START_TIME" const val TOTP = "TOTP" } ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/OnDialogDismissListener.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.base /** * @Author laoyuyu * @Description * @Date 5:56 下午 2022/3/29 **/ interface OnDialogDismissListener { fun onDismiss() } ================================================ FILE: app/src/main/java/com/lyy/keepassa/base/ViewBindingAdapter.kt ================================================ package com.lyy.keepassa.base import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.viewbinding.ViewBinding import java.lang.reflect.ParameterizedType class ViewBindingVH(val b: V) : ViewHolder(b.root) abstract class AbsViewBindingAdapter : RecyclerView.Adapter>() { var data: MutableList = mutableListOf() internal set lateinit var context: Context // 通过反射创建ViewBinding private fun viewBinding(parent: ViewGroup): V { val parameterizedType = this.javaClass.genericSuperclass as ParameterizedType val clazz: Class = parameterizedType.actualTypeArguments[1] as Class val inflateMethod = clazz.getMethod( "inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java ) return inflateMethod.invoke(null, LayoutInflater.from(parent.context), parent, false) as V } fun setData(list: MutableList) { data = list notifyDataSetChanged() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewBindingVH { return ViewBindingVH(viewBinding(parent)) } override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) context = recyclerView.context } override fun getItemCount(): Int { return data.size } abstract fun bindData(binding: V, item: T) open fun bindData(binding: V, item: T, payloads: MutableList) {} override fun onBindViewHolder(holder: ViewBindingVH, position: Int) { bindData(holder.b, data[position]) } override fun onBindViewHolder( holder: ViewBindingVH, position: Int, payloads: MutableList ) { super.onBindViewHolder(holder, position, payloads) bindData(holder.b, data[position], payloads) } } interface IMultipleItem { fun getType(): Int } interface OnMultiItemAdapterListener { fun onCreate(context: Context, parent: ViewGroup, viewType: Int): ViewHolder fun onBind(holder: ViewHolder, position: Int, item: TYPE) fun onDetachedFromWindow(holder: ViewHolder) {} } abstract class AbsMultipleViewBindingAdapter : RecyclerView.Adapter() { private val typeMap = hashMapOf>() var data: MutableList = mutableListOf() internal set fun setData(list: MutableList) { data = list notifyDataSetChanged() } fun addItemType( type: Int, adapter: OnMultiItemAdapterListener ) { typeMap[type] = adapter as OnMultiItemAdapterListener } override fun getItemViewType(position: Int): Int { if (data.isEmpty()) { return -1 } return data[position].getType() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { if (viewType == -1) { throw IllegalArgumentException("viewType类型错误") } return typeMap[viewType]!!.onCreate(parent.context, parent, viewType) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { typeMap[getItemViewType(position)]!!.onBind(holder, position, data[position]) } override fun getItemCount(): Int { return data.size } override fun onViewDetachedFromWindow(holder: ViewHolder) { super.onViewDetachedFromWindow(holder) typeMap[getItemViewType(holder.absoluteAdapterPosition)]?.onDetachedFromWindow(holder) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/common/PassType.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.common /** * @Author laoyuyu * @Description * @Date 2021/3/24 **/ object PassType { val ONLY_PASS = 0 val PASS_AND_KEY = 1 val ONLY_KEY = 2 } ================================================ FILE: app/src/main/java/com/lyy/keepassa/common/SortType.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.common enum class SortType { NONE, CHAR_DESC, CHAR_ASC, TIME_DESC, TIME_ASC } ================================================ FILE: app/src/main/java/com/lyy/keepassa/dao/AppDatabase.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.dao import androidx.room.Database import androidx.room.RoomDatabase import com.lyy.keepassa.entity.CloudServiceInfo import com.lyy.keepassa.entity.DbHistoryRecord import com.lyy.keepassa.entity.EntryRecord import com.lyy.keepassa.entity.QuickUnLockRecord import com.lyy.keepassa.entity.SearchRecord @Database( entities = [DbHistoryRecord::class, EntryRecord::class, SearchRecord::class, CloudServiceInfo::class, QuickUnLockRecord::class ], version = 4 ) abstract class AppDatabase : RoomDatabase() { companion object { const val DB_NAME = "keepassA.db" } abstract fun cloudServiceInfoDao(): CloudServiceInfoDao abstract fun dbRecordDao(): DbRecordDao abstract fun entryRecordDao(): EntryRecordDao abstract fun searchRecordDao(): SearchDao abstract fun quickUnlockDao(): QuickUnlockDao } ================================================ FILE: app/src/main/java/com/lyy/keepassa/dao/CloudServiceInfoDao.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.Query import androidx.room.Update import com.lyy.keepassa.entity.CloudServiceInfo @Dao interface CloudServiceInfoDao { @Update suspend fun update(serviceInfo: CloudServiceInfo) @Query("SELECT * FROM CloudServiceInfo WHERE cloudPath=:uri") suspend fun queryServiceInfo(uri: String): CloudServiceInfo? @Insert suspend fun saveServiceInfo(serviceInfo: CloudServiceInfo) @Update suspend fun updateServiceInfo(serviceInfo: CloudServiceInfo) } ================================================ FILE: app/src/main/java/com/lyy/keepassa/dao/DbRecordDao.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query import androidx.room.Update import com.lyy.keepassa.entity.DbHistoryRecord @Dao interface DbRecordDao { @Query("SELECT * FROM DbHistoryRecord ORDER BY time DESC LIMIT 0, 1") suspend fun getLastRecord(): DbHistoryRecord @Query("SELECT * FROM DbHistoryRecord ORDER BY time DESC") suspend fun getAllRecord(): List @Query("SELECT * FROM DbHistoryRecord WHERE localDbUri=:localDbUri") suspend fun findRecord(localDbUri: String): DbHistoryRecord? @Query("SELECT * FROM DbHistoryRecord WHERE cloudDiskPath=:cloudPath") suspend fun findRecordByCloudPath(cloudPath: String): DbHistoryRecord? @Insert suspend fun saveRecord(record: DbHistoryRecord) @Update suspend fun updateRecord(record: DbHistoryRecord) @Delete suspend fun deleteRecord(record: DbHistoryRecord) } ================================================ FILE: app/src/main/java/com/lyy/keepassa/dao/EntryRecordDao.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query import androidx.room.Update import com.lyy.keepassa.entity.EntryRecord @Dao interface EntryRecordDao { @Query("SELECT COUNT(uid) FROM EntryRecord WHERE dbFileUri = :dbFileUri") suspend fun hasRecord(dbFileUri: String): Int @Query("SELECT * FROM EntryRecord WHERE uuid = :uuid AND dbFileUri = :dbFileUri") suspend fun getRecord( uuid: ByteArray, dbFileUri: String ): EntryRecord? /** * 只获取50条历史记录 */ @Query("SELECT * FROM EntryRecord WHERE dbFileUri = :dbFileUri ORDER BY time DESC LIMIT 50") suspend fun getRecord(dbFileUri: String): List @Insert suspend fun saveRecord(record: EntryRecord) @Update suspend fun updateRecord(record: EntryRecord) @Delete suspend fun delReocrd(record: EntryRecord) } ================================================ FILE: app/src/main/java/com/lyy/keepassa/dao/QuickUnlockDao.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query import androidx.room.Update import com.lyy.keepassa.entity.QuickUnLockRecord @Dao interface QuickUnlockDao { @Query("SELECT * FROM QuickUnLockRecord WHERE dbUri=:dbUri") suspend fun findRecord(dbUri: String): QuickUnLockRecord? @Query("SELECT * FROM QuickUnLockRecord") suspend fun getAllRecord(): List? @Insert suspend fun saveRecord(record: QuickUnLockRecord) @Update suspend fun updateRecord(record: QuickUnLockRecord) @Delete suspend fun deleteRecord(record: QuickUnLockRecord) } ================================================ FILE: app/src/main/java/com/lyy/keepassa/dao/SearchDao.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query import androidx.room.Update import com.lyy.keepassa.entity.SearchRecord @Dao interface SearchDao { @Insert suspend fun saveRecord(record: SearchRecord) @Query("SELECT * FROM SearchRecord WHERE title=:title") suspend fun getRecord(title: String): SearchRecord? @Query("SELECT * FROM SearchRecord ORDER BY time DESC LIMIT 5") suspend fun getSearchRecord(): List @Delete suspend fun delRecord(record: SearchRecord) @Update suspend fun updateRecord(record: SearchRecord) } ================================================ FILE: app/src/main/java/com/lyy/keepassa/entity/AutoFillParam.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.entity import android.os.Parcelable import kotlinx.parcelize.Parcelize /** * 自动填充参数 */ @Parcelize data class AutoFillParam( val apkPkgName: String, // other apk packageName val domain: String? = null, val isSave: Boolean = false, // is save mode val saveUserName: String? = null, // User name at the of saving val savePass: String? = null // Password name at the of saving ) : Parcelable ================================================ FILE: app/src/main/java/com/lyy/keepassa/entity/CloudServiceInfo.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey /** * 云端服务器验证信息,所有字段都是加密的 */ @Entity class CloudServiceInfo( @PrimaryKey(autoGenerate = true) val uid: Int = 0, @ColumnInfo var userName: String? = null, @ColumnInfo var password: String? = null, // 云端路径 @ColumnInfo val cloudPath: String ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/entity/CommonState.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.entity /** * @Author laoyuyu * @Description * @Date 3:45 PM 2024/2/1 **/ enum class CommonState { CREATE, DELETE, MODIFY } ================================================ FILE: app/src/main/java/com/lyy/keepassa/entity/DbHistoryRecord.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.entity import android.net.Uri import android.os.Parcel import android.os.Parcelable import android.os.Parcelable.Creator import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.view.StorageType /** * 历史记录实体 */ @Entity data class DbHistoryRecord( @PrimaryKey(autoGenerate = true) var uid: Int = 0, @ColumnInfo var time: Long, // 打开类型 /** * [StorageType] */ @ColumnInfo(defaultValue = "AFS") var type: String, // 本地数据库uri @ColumnInfo var localDbUri: String, // 云端路径 @ColumnInfo var cloudDiskPath: String? = null, //密钥的路径 @ColumnInfo var keyUri: String, // 数据库名 var dbName: String // val uri:ByteArray ) : Parcelable { constructor(parcel: Parcel) : this( parcel.readInt(), parcel.readLong(), parcel.readString()!!, parcel.readString()!!, parcel.readString(), parcel.readString()!!, parcel.readString()!! ) { } fun getDbPathType(): StorageType { return StorageType.valueOf(type) } fun getDbUri(): Uri { return KeepassAUtil.instance.convertUri(localDbUri)!! } /** * 不能使用 getkeyUri(),否则kotlin 编译会报错 */ fun getDbKeyUri(): Uri? { return KeepassAUtil.instance.convertUri(keyUri) } override fun writeToParcel( parcel: Parcel, flags: Int ) { parcel.writeInt(uid) parcel.writeLong(time) parcel.writeString(type) parcel.writeString(localDbUri) parcel.writeString(cloudDiskPath) parcel.writeString(keyUri) parcel.writeString(dbName) } override fun describeContents(): Int { return 0 } companion object CREATOR : Creator { override fun createFromParcel(parcel: Parcel): DbHistoryRecord { return DbHistoryRecord(parcel) } override fun newArray(size: Int): Array { return arrayOfNulls(size) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/entity/EntryRecord.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @Entity class EntryRecord( @PrimaryKey(autoGenerate = true) val uid: Int = 0, // 数据库的本地文件uri var dbFileUri: String, @ColumnInfo var userName: String, @ColumnInfo var title: String, @ColumnInfo val uuid: ByteArray, @ColumnInfo var time: Long ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/entity/IOtpBean.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.entity import android.os.Parcelable import androidx.annotation.VisibleForTesting import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.util.totp.ComposeKeeTrayTotp import com.lyy.keepassa.util.totp.ComposeKeepass import com.lyy.keepassa.util.totp.ComposeKeepassxc import com.lyy.keepassa.util.totp.SecretHexType import com.lyy.keepassa.util.totp.TokenCalculator import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm import kotlinx.parcelize.Parcelize interface IOtpBean @Parcelize data class OtpBeans( val trayTotp: TrayTotpBean? = null, val keeOtp2: KeeOtp2Bean? = null, val keepassxc: KeepassXcBean? = null, val keeOtp: KeepOtpBean? = null, val googleOtpBean: GoogleOtpBean? = null ) : Parcelable /** * TrayTotp 的实体 */ @Parcelize data class TrayTotpBean( var secret: String, var period: Int, val isSteam: Boolean ) : Parcelable, IOtpBean /** * KeepOtp实体 */ @Parcelize data class KeepOtpBean( val key: ProtectedString ) : Parcelable, IOtpBean /** * Keepass 实体 * [ComposeKeepass#toKeepassOtpMap] */ @Parcelize data class KeepassBean( val otpBean: TimeOtp2Bean? = null, val hmac: HmacOtpBean? = null ) : Parcelable, IOtpBean { } /** * KeeOtp2 的实体 */ @Parcelize data class KeeOtp2Bean( val otpBean: TimeOtp2Bean? = null, val hmac: HmacOtpBean? = null ) : Parcelable, IOtpBean /** * keepassxc的实体 */ @Parcelize data class KeepassXcBean( val host: String = "totp", val title: String, val userName: String, val isSteam: Boolean, var encoder: String = "", var secret: String, val issuer: String, var period: Int, var digits: Int, var algorithm: HashAlgorithm, val counter: String? = "" ) : Parcelable, IOtpBean @Parcelize data class GoogleOtpBean( val secret: String ) : Parcelable, IOtpBean fun GoogleOtpBean.toOtpStringMap(): Map { return hashMapOf().apply { put(ComposeKeepassxc.KEY_SEED, ProtectedString(true, secret)) } } /** * KeeOtp2 插件的otpHmac */ @Parcelize data class HmacOtpBean( val secretType: SecretHexType, /** * HmacOtp-Secret-Hex * HmacOtp-Secret-Base32 * HmacOtp-Secret-Base64 */ val secret: String, /** * HMAC-SHA-1 * HMAC-SHA-256 * HMAC-SHA-512 */ val algorithm: HashAlgorithm, val counter: Int, val len: Int = TokenCalculator.TOTP_DEFAULT_DIGITS ) : Parcelable /** * KeeOtp2 插件的otpbean */ @Parcelize data class TimeOtp2Bean( val secretType: SecretHexType, /** * TimeOtp-Secret-Hex * TimeOtp-Secret-Base32 * TimeOtp-Secret-Base64 */ var secret: String, /** * [6-8] */ var digits: Int, /** * HMAC-SHA-1 * HMAC-SHA-256 * HMAC-SHA-512 */ var algorithm: HashAlgorithm, /** * 更新时间,默认30s */ var period: Int, ) : Parcelable{ } fun KeepassBean.toOtpStringMap(): Map { val map = linkedMapOf() //totp otpBean?.let { map[ComposeKeepass.getSecretType(it.secretType)] = ProtectedString(true, it.secret) map[ComposeKeepass.TimeOtp_Length] = ProtectedString(false, it.digits.toString()) map[ComposeKeepass.TimeOtp_Period] = ProtectedString(false, it.period.toString()) map[ComposeKeepass.TimeOtp_Algorithm] = ProtectedString( false, when (it.algorithm) { HashAlgorithm.SHA256 -> ComposeKeepass.HMAC_SHA_256 HashAlgorithm.SHA512 -> ComposeKeepass.HMAC_SHA_512 else -> ComposeKeepass.HMAC_SHA_1 } ) } // hotp hmac?.let { map[ComposeKeepass.getSecretType(it.secretType)] = ProtectedString(true, it.secret) map[ComposeKeepass.HmacOtp_Counter] = ProtectedString(false, it.counter.toString()) } return map } fun KeepassXcBean.toOtpStringMap(): Map { return hashMapOf().apply { val arithmetic = when (algorithm) { HashAlgorithm.SHA256 -> "SHA256" HashAlgorithm.SHA512 -> "SHA512" else -> "SHA1" } var seedStr = "otpauth://totp/${title}:${userName}?secret=${secret}&period=${period}&digits=${digits}&issuer=${userName}&algorithm=$arithmetic" if (isSteam) { seedStr += "&encoder=steam" } put(ComposeKeepassxc.KEY_SEED, ProtectedString(true, seedStr)) } } fun TrayTotpBean.toOtpStringMap(): Map { return hashMapOf().apply { put(ComposeKeeTrayTotp.KEY_SEED, ProtectedString(true, secret)) put( ComposeKeeTrayTotp.KEY_SETTING, ProtectedString(false, "${period};${if (isSteam) "S" else "6"}") ) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/entity/QuickUnLockRecord.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey /** * 快速解锁信息实体 */ @Entity data class QuickUnLockRecord( @PrimaryKey(autoGenerate = true) val uid: Int = 0, val dbUri: String, var dbPass: String = "", var keyPath: String?, var isUseKey: Boolean = true, var isUseFingerprint: Boolean = false, // 使用指纹解锁 @ColumnInfo(name = "passIv", typeAffinity = ColumnInfo.BLOB) var passIv: ByteArray? = null ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as QuickUnLockRecord if (uid != other.uid) return false if (dbUri != other.dbUri) return false if (dbPass != other.dbPass) return false if (keyPath != other.keyPath) return false if (isUseKey != other.isUseKey) return false if (isUseFingerprint != other.isUseFingerprint) return false if (passIv != null) { if (other.passIv == null) return false if (!passIv.contentEquals(other.passIv)) return false } else if (other.passIv != null) return false return true } override fun hashCode(): Int { var result = uid result = 31 * result + dbUri.hashCode() result = 31 * result + dbPass.hashCode() result = 31 * result + (keyPath?.hashCode() ?: 0) result = 31 * result + isUseKey.hashCode() result = 31 * result + isUseFingerprint.hashCode() result = 31 * result + (passIv?.contentHashCode() ?: 0) return result } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/entity/SearchRecord.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @Entity class SearchRecord { @PrimaryKey(autoGenerate = true) var uid: Int = 0 @ColumnInfo var title: String = "" @ColumnInfo var time: Long = 0 } ================================================ FILE: app/src/main/java/com/lyy/keepassa/entity/SimpleItemEntity.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.entity import android.view.View import androidx.fragment.app.FragmentActivity import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwGroup import com.keepassdroid.database.PwGroupV4 import com.lyy.keepassa.view.menu.EntryPopMenu import com.lyy.keepassa.view.menu.GroupPopMenu class SimpleItemEntity { var title: CharSequence = "" var subTitle: CharSequence = "" var content: CharSequence = "" var icon: Int = 0 var id: Int = -1 var time: Long = 0 lateinit var obj: Any var isSelected: Boolean = false var type: Int = 0 /** * 是否受保护 */ var isProtected = false /** * 是否选中 */ var isCheck = false } enum class EntryType{ TYPE_COLLECTION } /** * show pop menu */ fun SimpleItemEntity.showPopMenu( ac: FragmentActivity, v: View, curx: Int, isInRecycleBin: Boolean = false ) { if (obj is PwGroup) { val pop = GroupPopMenu( ac, v, obj as PwGroupV4, curx, isInRecycleBin ) pop.show() return } if (obj is PwEntry) { val pop = EntryPopMenu( ac, v, obj as PwEntry, curx, isInRecycleBin ) pop.show() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/entity/TagBean.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.entity /** * @Author laoyuyu * @Description * @Date 3:45 PM 2023/10/26 **/ data class TagBean( val tag: String, var isSet: Boolean = false ) : java.io.Serializable ================================================ FILE: app/src/main/java/com/lyy/keepassa/entity/TotpType.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.entity enum class TotpType(val value: String) { DEFAULT("default"), STEAM("steam"), CUSTOM("custom"); companion object { fun from(s: String): TotpType { val tt = values().find { it.value == s } return tt ?: DEFAULT } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/AttrFileEvent.kt ================================================ package com.lyy.keepassa.event import com.keepassdroid.database.security.ProtectedBinary import com.lyy.keepassa.entity.CommonState data class AttrFileEvent( val state: CommonState, val key: String, val file: ProtectedBinary, ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/AttrStrEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.entity.CommonState /** * 创建自定义字段事件 */ data class AttrStrEvent( val state: CommonState, val key: String, val str: ProtectedString, val position: Int = 0 ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/ChangeDbEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event import android.net.Uri import com.lyy.keepassa.view.StorageType /** * 切换数据库的事件 */ data class ChangeDbEvent( /** * 数据库名 */ var dbName: String, /** * 本地文件路径 */ var localFileUri: Uri, /** * 云端文件路径 */ var cloudPath: String? = null, var uriType: StorageType = StorageType.AFS, // uri类型,afs,google drive, var keyUri: Uri? = null ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/CheckEnvEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event /** * @Author laoyuyu * @Description check operating env event * @Date 2020/11/25 **/ class CheckEnvEvent { } ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/CloudFileSelectedEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event import com.lyy.keepassa.view.StorageType data class CloudFileSelectedEvent( val isSelectFile: Boolean, val fileFullPath: String, val storageType: StorageType ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/CollectionEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event import com.keepassdroid.database.PwEntryV4 /** * @Author laoyuyu * @Description * @Date 19:58 下午 2022/3/29 **/ data class CollectionEvent( val state: CollectionEventType = CollectionEventType.COLLECTION_STATE_TOTAL, val collectionNum: Int = 0, val pwEntryV4: PwEntryV4? = null ) enum class CollectionEventType { COLLECTION_STATE_ADD, COLLECTION_STATE_REMOVE, COLLECTION_STATE_TOTAL } ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/DbHistoryEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event /** * @Author laoyuyu * @Description * @Date 2020/12/7 **/ data class DbHistoryEvent(val isEmpty: Boolean) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/DbPathEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event import android.net.Uri import com.lyy.keepassa.view.StorageType /** * 选择数据库事件 */ data class DbPathEvent( /** * 数据库名 */ var dbName: String, /** * 本地数据库uri */ var fileUri: Uri? = null, var storageType: StorageType = StorageType.AFS, // uri类型,afs,google drive /** * 云端路径 */ var cloudDiskPath: String? = null ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/DelAttrFileEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event /** * 删除附件事件 */ data class DelAttrFileEvent( val key: String ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/DelAttrStrEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event import com.keepassdroid.database.security.ProtectedString /** * 删除自定义字段事件 */ data class DelAttrStrEvent( val key: String, val str: ProtectedString ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/EditorEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event /** * @Author laoyuyu * @Description * @Date 2020/12/2 **/ data class EditorEvent( val requestCode: Int, val content: CharSequence? ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/FillInfoEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event /** * 填充时间信息 * @Author laoyuyu * @Description * @Date 2020/10/27 **/ data class FillInfoEvent(val infoStr: CharSequence) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/KeyPathEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event import android.net.Uri /** * 获取key的事件 */ data class KeyPathEvent( val keyUri: Uri, val keyName: String ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/ModifyDbNameEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event data class ModifyDbNameEvent(val dbName: String) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/ModifyPassEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event data class ModifyPassEvent(val pass: String) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/MoveEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroupV4 /** * 恢复数据的事件 */ data class MoveEvent( val type: Int = MOVE_TYPE_GROUP, // 1:群组,2:条目 val entryV4: PwEntryV4? = null, val pwGroupV4: PwGroupV4? = null ){ companion object{ const val MOVE_TYPE_GROUP = 1 // 群组 const val MOVE_TYPE_ENTRY = 2 // 条目 } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/MsgDialogEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event /** * msg dialog 事件 */ data class MsgDialogEvent( /** * @param type 1、确认,2、覆盖、3、取消 * */ val type: Int = 1, val requestCode: Int = 0 // 请求码 ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/MultiChoiceEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event /** * @Author laoyuyu * @Description * @Date 2020/12/25 **/ class MultiChoiceEvent { } ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/ShowTOTPEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event data class ShowTOTPEvent(val show: Boolean = false) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/StateChangeEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroupV4 import com.lyy.keepassa.event.EntryState.UNKNOWN /** * @Author laoyuyu * @Description * @Date 2022/3/30 **/ data class EntryStateChangeEvent( val state: EntryState = UNKNOWN, val pwEntryV4: PwEntryV4? = null, val oldParent: PwGroupV4? = null ) data class GroupStateChangeEvent( val state: EntryState = UNKNOWN, val groupV4: PwGroupV4? = null, val oldParent: PwGroupV4? = null ) enum class EntryState { /** * new entry */ CREATE, DELETE, MODIFY, /** * resume entry from recycle bin or move */ MOVE, SAVE, UNKNOWN } ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/TimeEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event /** * 时间事件 */ data class TimeEvent( val year: Int, val month: Int, val dayOfMonth: Int, val hour: Int, val minute: Int ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/event/WebDavLoginEvent.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.event /** * @Author laoyuyu * @Description * @Date 2:52 下午 2022/4/21 **/ data class WebDavLoginEvent( val uri: String, val userName: String, val pass: String, val loginSuccess: Boolean = false ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/ondrive/DriveItem.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.ondrive /** * @author laoyuyu * @date 2021/2/6 */ data class DriveItem( val id: String, val driveType: String, val name: String ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/ondrive/MsalApi.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.ondrive import com.lyy.keepassa.util.cloud.OneDriveUtil.APP_ROOT_DIR import com.lyy.keepassa.util.cloud.OneDriveUtil.TOKEN_KEY import retrofit2.Response import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST import retrofit2.http.Path /** * @Author laoyuyu * @Description * @Date 2021/2/6 **/ interface MsalApi { /** * @param itemPath 云端的路径,如:/foo.txtNet */ @POST("users/{user-id}/drive/special/$APP_ROOT_DIR:/{item-path}:/createUploadSession") suspend fun createUploadSession( @Header(TOKEN_KEY) authorization: String, @Path("user-id") userId: String, @Path("item-path") itemPath: String ): MsalUploadSession /** * 获取驱动器列表,也就是onedrive 空间信息 */ @GET("users/{userId}/drives") suspend fun getDriveList( @Header(TOKEN_KEY) authorization: String, @Path("userId") userId: String ): MsalResponse> /** * 获取应用的app子文件夹列表 */ @GET("users/{user-id}/drive/items/{item-id}/children") suspend fun getFolderListById( @Header(TOKEN_KEY) authorization: String, @Path("user-id") userId: String, @Path("item-id") itemId: String ): MsalResponse> /** * 获取应用的app文件夹列表 */ @GET("users/{userId}/drive/special/$APP_ROOT_DIR/children") suspend fun getAppFolderList( @Header(TOKEN_KEY) authorization: String, @Path("userId") userId: String ): MsalResponse> /** * 获取单个文件信息 * @param itemId 文件id */ @GET("users/{user-id}/drive/items/{item-id}") suspend fun getFileInfoById( @Header(TOKEN_KEY) authorization: String, @Path("user-id") userId: String, @Path("item-id") itemId: String ): MsalSourceItem? /** * 获取单个文件信息 * @param itemPath 文件在云盘的相对路径,如:/xxx.zip */ @GET("users/{user-id}/drive/special/$APP_ROOT_DIR:/{item-path}") suspend fun getFileInfoByPath( @Header(TOKEN_KEY) authorization: String, @Path("user-id") userId: String, @Path("item-path") itemPath: String ): MsalSourceItem? /** * 删除文件,如果成功,此调用将返回 204 No Content 响应,以指明资源已被删除,没有可返回的内容。 */ @DELETE("users/{userId}/drive/items/{itemId}") suspend fun deleteFile( @Header(TOKEN_KEY) authorization: String, @Path("userId") userId: String, @Path("itemId") itemId: String ): Response } ================================================ FILE: app/src/main/java/com/lyy/keepassa/ondrive/MsalResponse.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.ondrive import androidx.annotation.Keep import com.blankj.utilcode.util.ToastUtils import com.google.gson.Gson import com.google.gson.annotations.SerializedName import com.lyy.keepassa.R import com.lyy.keepassa.util.cloud.OneDriveUtil /** * @Author laoyuyu * @Description * @Date 2021/2/6 **/ @Keep class MsalResponse { @SerializedName("value") val value: T? = null get() { // https://docs.microsoft.com/zh-cn/graph/errors if (error?.code == "unauthenticated") { OneDriveUtil.initOneDrive { if (it) { OneDriveUtil.loadAccount() return@initOneDrive } ToastUtils.showLong(R.string.one_drive_init_failure) } } return field } @SerializedName("error") val error: MsalErrorInfo? = null } @Keep data class MsalErrorInfo( val code: String, // https://docs.microsoft.com/zh-cn/graph/errors#code-property val message: String ) { override fun toString(): String { return Gson().toJson(this) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/ondrive/MsalSourceItem.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.ondrive import androidx.annotation.Keep /** * @Author laoyuyu * @Description * @Date 2021/2/7 * https://docs.microsoft.com/zh-cn/graph/api/resources/driveitem?view=graph-rest-1.0 **/ @Keep data class MsalSourceItem( val id: String, val createdDateTime: String, val lastModifiedDateTime: String, val cTag: String, val eTag: String, val webUrl: String?, // 下载地址 val name: String, val size: Long, val file: MsalFileInfo?, val folder: MsalFolderInfo? ) { fun isFolder() = folder != null } @Keep data class MsalFolderInfo( val childCount: Long ) @Keep data class MsalFileInfo( val mimeType: String, val hashes: MsalFileHashes ) @Keep data class MsalFileHashes( val quickXorHash: String, val sha1Hash: String, val sha256Hash: String ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/ondrive/MsalUploadSession.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.ondrive /** * @Author laoyuyu * @Description * @Date 2021/4/27 * https://docs.microsoft.com/zh-cn/graph/api/resources/uploadsession?view=graph-rest-1.0 **/ data class MsalUploadSession( val uploadUrl: String, // 上传路径 val expirationDateTime: String, // 以 UTC 表示的上载会话过期的日期和时间。在此过期时间之前必须上载完整的文件文件。 val nextExpectedRanges: List // range 0- ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/receiver/ScreenLockReceiver.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.preference.PreferenceManager import com.arialyy.frame.util.ResUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.util.KdbUtil.isNull import com.lyy.keepassa.util.KeepassAUtil /** * @Author laoyuyu * @Description screen receiver, when the user lock screen, lock the db * @Date 2021/2/1 **/ class ScreenLockReceiver : BroadcastReceiver() { override fun onReceive( context: Context?, intent: Intent? ) { // if the user lock screen, lock the db if (intent?.action.equals(Intent.ACTION_SCREEN_OFF) && PreferenceManager.getDefaultSharedPreferences(BaseApp.APP) .getBoolean(context?.getString(R.string.set_key_lock_screen_auto_lock_db), false)) { if (BaseApp.isLocked || BaseApp.KDB.isNull()){ return } KeepassAUtil.instance.lock() return } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/router/ActivityRouter.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.router import androidx.core.app.ActivityOptionsCompat import com.arialyy.frame.router.RouterArgName import com.arialyy.frame.router.RouterPath import com.blankj.utilcode.util.ActivityUtils import com.keepassdroid.database.PwGroupId import com.lyy.keepassa.entity.AutoFillParam import com.lyy.keepassa.view.create.entry.CreateEntryActivity import com.lyy.keepassa.view.create.entry.CreateEnum import com.lyy.keepassa.view.detail.EntryDetailActivityNew import com.lyy.keepassa.view.detail.GroupDetailActivity import com.lyy.keepassa.view.launcher.LauncherActivity import com.lyy.keepassa.view.setting.SettingActivity import java.util.UUID /** * @Author laoyuyu * @Description * @Date 2021/10/17 **/ interface ActivityRouter { @RouterPath(path = "/search/common") fun toCommonSearch( @RouterArgName(name = "apkPkgName") apkPkgName: String? = null, @RouterArgName(name = "onlySearch") onlySearch: Boolean = true ) @RouterPath(path = "/collection/ac") fun toMyCollection(@RouterArgName(name = "opt") opt: ActivityOptionsCompat? = null) @RouterPath(path = "/setting/app") fun toAppSetting( @RouterArgName(name = SettingActivity.KEY_TYPE) type: Int = SettingActivity.TYPE_APP, @RouterArgName(name = "opt") opt: ActivityOptionsCompat? = null, @RouterArgName(name = "scrollKey") scrollKey: String? = null ) @RouterPath(path = "/setting/app") fun toDbSetting( @RouterArgName(name = SettingActivity.KEY_TYPE) type: Int = SettingActivity.TYPE_DB, @RouterArgName(name = "opt") opt: ActivityOptionsCompat? = null ) @RouterPath(path = "/launcher/quickLock") fun toQuickUnlockActivity( @RouterArgName(name = "flag", isFlag = true) flags: Int ) @RouterPath(path = "/entry/detail") fun toEntryDetailActivity( @RouterArgName(name = EntryDetailActivityNew.KEY_ENTRY_ID) entryId: UUID, @RouterArgName(name = "opt") opt: ActivityOptionsCompat? = null ) /** * to group detail */ @RouterPath(path = "/group/detail") fun toGroupDetailActivity( @RouterArgName(name = GroupDetailActivity.KEY_TITLE) groupName: String, @RouterArgName(name = GroupDetailActivity.KEY_GROUP_ID) groupId: PwGroupId, @RouterArgName(name = GroupDetailActivity.KEY_IS_IN_RECYCLE_BIN) isRecycleBin: Boolean = false, @RouterArgName(name = "opt") opt: ActivityOptionsCompat? = null ) /** * create entry */ @RouterPath(path = "/entry/create") fun toCreateEntryActivity( @RouterArgName(name = CreateEntryActivity.PARENT_GROUP_ID) groupId: PwGroupId?, @RouterArgName(name = "opt") opt: ActivityOptionsCompat? = null, @RouterArgName(name = CreateEntryActivity.IS_SHORTCUTS) isFromShortcuts: Boolean = false, @RouterArgName(name = CreateEntryActivity.KEY_TYPE) type: CreateEnum = CreateEnum.CREATE ) /** * edit entry */ @RouterPath(path = "/entry/create") fun toEditEntryActivity( @RouterArgName(name = CreateEntryActivity.KEY_ENTRY) uuid: UUID, @RouterArgName(name = "opt") opt: ActivityOptionsCompat? = ActivityOptionsCompat.makeSceneTransitionAnimation(ActivityUtils.getTopActivity()), @RouterArgName(name = CreateEntryActivity.KEY_TYPE) type: CreateEnum = CreateEnum.MODIFY ) @RouterPath(path = "/entry/create") fun toEditEntryActivity( @RouterArgName(name = LauncherActivity.KEY_AUTO_FILL_PARAM) params: AutoFillParam ) @RouterPath(path = "/main/ac") fun toMainActivity( @RouterArgName(name = "isShortcuts") isShortcuts: Boolean = false, @RouterArgName(name = "shortcutType") shortcutType: Int = 1, @RouterArgName(name = "opt") opt: ActivityOptionsCompat? = null ) @RouterPath(path = "/launcher/createDb") fun toCreateDbActivity( @RouterArgName(name = "opt") opt: ActivityOptionsCompat? = null ) } ================================================ FILE: app/src/main/java/com/lyy/keepassa/router/ContentInterceptor.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.router import android.content.Context import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import com.alibaba.android.arouter.facade.Postcard import com.alibaba.android.arouter.facade.annotation.Interceptor import com.alibaba.android.arouter.facade.callback.InterceptorCallback import com.alibaba.android.arouter.facade.template.IInterceptor import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.router.Routerfit import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.util.KdbUtil.isNull import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.isCanOpenQuickLock import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 4:14 下午 2021/7/7 **/ @Interceptor(priority = 8, name = "ContentInterceptor") class ContentInterceptor : IInterceptor { companion object { val ROUTE_WHITE_LIST = arrayListOf().apply { add("/launcher/activity") add("/launcher/quickLock") add("/launcher/createDb") } } override fun init(context: Context) { // 拦截器的初始化,会在sdk初始化的时候调用该方法,仅会调用一次 } override fun process( postcard: Postcard, callback: InterceptorCallback ) { Timber.d("route path => ${postcard.path}") if (postcard.path in ROUTE_WHITE_LIST) { callback.onContinue(postcard) return } if (BaseApp.KDB.isNull()) { callback.onInterrupt(Exception("kdb is null")) ARouter.getInstance() .build("/launcher/activity") .navigation() return } if (BaseApp.isLocked && BaseApp.APP.isCanOpenQuickLock()) { callback.onInterrupt(Exception("database is locked")) Routerfit.create(ActivityRouter::class.java).toQuickUnlockActivity(FLAG_ACTIVITY_NEW_TASK) return } // Timber.i("拦截:${postcard.path}") callback.onContinue(postcard) // 处理完成,交还控制权 // callback.onInterrupt(new RuntimeException("我觉得有点异常")); // 觉得有问题,中断路由流程 // 以上两种至少需要调用其中一种,否则不会继续路由 } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/router/DeeplinkActivity.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.router import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.arialyy.frame.router.Routerfit import timber.log.Timber import java.net.URLDecoder /** * @Author laoyuyu * @Description * @Date 2021/7/11 **/ class DeeplinkActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Timber.d("uri = ${intent.data}") val shortcutData = intent.getStringExtra("shortcutData") if (!shortcutData.isNullOrEmpty()) { Timber.d("shortcutData = $shortcutData") val uriString = URLDecoder.decode(shortcutData) val uri = Uri.parse(uriString) handleFormShortcutRoute(uri) finish() return } } private fun handleFormShortcutRoute(uri: Uri) { val ac = uri.getQueryParameter("ac") if (ac == "createEntry") { Timber.d("to create entry") Routerfit.create(ActivityRouter::class.java).toCreateEntryActivity( groupId = null, isFromShortcuts = true ) return } if (ac == "search") { val type = uri.getQueryParameter("shortcutsType") Timber.d("to search ac") Routerfit.create(ActivityRouter::class.java).toMainActivity(true, type!!.toInt()) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/router/DialogRouter.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.router import android.graphics.drawable.Drawable import com.arialyy.frame.router.DialogArg import com.arialyy.frame.router.RouterArgName import com.arialyy.frame.router.RouterPath import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroupV4 import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.R import com.lyy.keepassa.entity.TagBean import com.lyy.keepassa.view.StorageType import com.lyy.keepassa.view.dialog.CloudFileSelectDialog import com.lyy.keepassa.view.dialog.LoadingDialog import com.lyy.keepassa.view.dialog.OnMsgBtClickListener import com.lyy.keepassa.view.dialog.TimeChangeDialog import com.lyy.keepassa.view.dialog.webdav.WebDavLoginDialogNew import java.util.UUID /** * @Author laoyuyu * @Description * @Date 2021/9/5 **/ interface DialogRouter { @RouterPath(path = "/dialog/chooseTag") @DialogArg(showDialog = true) fun showChooseTagDialog( @RouterArgName(name = "entry") entry: PwEntryV4, @RouterArgName(name = "newTag") newTag: TagBean? = null ) @RouterPath(path = "/dialog/createTag") @DialogArg(showDialog = true) fun showCreateTagDialog() @RouterPath(path = "/dialog/createOtp") @DialogArg(showDialog = true) fun showCreateOtpDialog( @RouterArgName(name = "entryTitle") entryTitle: String, @RouterArgName(name = "entryUserName") entryUserName: String ) @RouterPath(path = "/dialog/otpModify") @DialogArg(showDialog = true) fun showModifyOtpDialog( @RouterArgName(name = "uid") uid: UUID ) @RouterPath(path = "/dialog/customStrDialog") @DialogArg(showDialog = true) fun showCreateCustomDialog( @RouterArgName(name = "position") position: Int = 0, @RouterArgName(name = "key") key: String? = null, @RouterArgName(name = "value") value: ProtectedString? = null ) @RouterPath(path = "/dialog/tipsDialog") @DialogArg(showDialog = true) fun showTipDialog() @RouterPath(path = "/dialog/imgViewer") @DialogArg(showDialog = true) fun showImgViewerDialog( @RouterArgName(name = "imgByteArray") imgByteArray: ByteArray ) @RouterPath(path = "/dialog/cloudFileList") fun getCloudFileListDialog( @RouterArgName(name = "storageType") storageType: StorageType, @RouterArgName(name = "onlyShowDir") onlyShowDir: Boolean = false ): CloudFileSelectDialog @RouterPath(path = "/dialog/cloudFileList") @DialogArg(showDialog = true) fun showCloudFileListDialog( @RouterArgName(name = "storageType") storageType: StorageType, @RouterArgName(name = "onlyShowDir") onlyShowDir: Boolean = false ) @RouterPath(path = "/dialog/webdavLogin") fun getWebDavLoginDialog(): WebDavLoginDialogNew @RouterPath(path = "/dialog/webdavLogin") @DialogArg(showDialog = true) fun showWebDavLoginDialog(): WebDavLoginDialogNew @RouterPath(path = "/dialog/modifyGroup") @DialogArg(showDialog = true) fun showModifyGroupDialog( @RouterArgName(name = "pwGroup") pwGroup: PwGroupV4 ) @RouterPath(path = "/dialog/createGroup") @DialogArg(showDialog = true) fun showCreateGroupDialog( @RouterArgName(name = "parentGroup") parentGroup: PwGroupV4 ) @RouterPath(path = "/dialog/loading") @DialogArg(showDialog = false) fun getLoadingDialog(): LoadingDialog @RouterPath(path = "/dialog/loading") @DialogArg(showDialog = true) fun showLoadingDialog() /** * show play donate dialog */ @RouterPath(path = "/dialog/playDonate") @DialogArg(showDialog = true) fun showPlayDonateDialog() /** * show display dialog * @param uuid don't use UUID, because is that Serializable */ @RouterPath(path = "/dialog/totpDisplay") @DialogArg(showDialog = true) fun showTotpDisplayDialog( @RouterArgName(name = "uuid") uuid: String ) /** * 显示消息对话框 * @param showCountDownTimer 是否显示倒计时 Pair(true, 5) => 显示倒计时,5s */ @RouterPath(path = "/dialog/msgDialog") @DialogArg(showDialog = true) fun showMsgDialog( @RouterArgName(name = "msgTitle", isObject = true) msgTitle: CharSequence = "", @RouterArgName(name = "msgContent", isObject = true) msgContent: CharSequence, @RouterArgName(name = "showCancelBt") showCancelBt: Boolean = true, @RouterArgName(name = "showEnterBt") showEnterBt: Boolean = true, @RouterArgName(name = "showCoverBt") showCoverBt: Boolean = false, @RouterArgName(name = "interceptBackKey") interceptBackKey: Boolean = false, @RouterArgName(name = "enterText", isObject = true) enterText: CharSequence = "", @RouterArgName(name = "cancelText", isObject = true) cancelText: CharSequence = "", @RouterArgName(name = "coverText", isObject = true) coverText: CharSequence = "", @RouterArgName(name = "enterBtTextColor") enterBtTextColor: Int = R.color.text_blue_color, @RouterArgName(name = "cancelBtTextColor") cancelBtTextColor: Int = R.color.text_gray_color, @RouterArgName(name = "coverBtTextColor") coverBtTextColor: Int = R.color.text_blue_color, @RouterArgName( name = "btnClickListener", isObject = true ) btnClickListener: OnMsgBtClickListener? = null, @RouterArgName(name = "msgTitleEndIcon", isObject = true) msgTitleEndIcon: Drawable? = null, @RouterArgName(name = "msgTitleStartIcon", isObject = true) msgTitleStartIcon: Drawable? = null, @RouterArgName(name = "showCountDownTimer") showCountDownTimer: Pair = Pair( false, 5 ) ) /** * 日期选择对话框 */ @RouterPath(path = "/dialog/timeChange") @DialogArg(showDialog = true) fun showTimeChangeDialog(): TimeChangeDialog /** * 日期选择对话框 */ @RouterPath(path = "/dialog/timeChange") fun getTimeChangeDialog(): TimeChangeDialog } ================================================ FILE: app/src/main/java/com/lyy/keepassa/router/FragmentRouter.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.router import androidx.preference.PreferenceFragmentCompat import com.arialyy.frame.router.RouterArgName import com.arialyy.frame.router.RouterPath import com.keepassdroid.database.PwGroup import com.keepassdroid.database.PwGroupId import com.lyy.keepassa.entity.DbHistoryRecord import com.lyy.keepassa.view.dir.DirFragment import com.lyy.keepassa.view.launcher.ChangeDbFragment import com.lyy.keepassa.view.launcher.OpenDbFragment import com.lyy.keepassa.view.main.EntryListFragment import com.lyy.keepassa.view.main.HomeFragment import com.lyy.keepassa.view.setting.AppSettingFragment import com.lyy.keepassa.view.setting.DBSettingFragment /** * @Author laoyuyu * @Description * @Date 3:08 下午 2021/10/27 **/ interface FragmentRouter { @RouterPath(path = "/setting/appFm") fun getAppSettingFragment( @RouterArgName(name = "scrollKey") scrollKey: String? = null ): PreferenceFragmentCompat @RouterPath(path = "/setting/DbFm") fun getDbSettingFragment(): PreferenceFragmentCompat @RouterPath(path = "/group/choose/dir") fun getDirFragment( @RouterArgName(name = DirFragment.KEY_CUR_GROUP) group: PwGroup, @RouterArgName(name = DirFragment.KEY_IS_MOVE_GROUP) isMoveGroup: Boolean, @RouterArgName(name = DirFragment.KEY_IS_RECYCLE_GROUP_ID) recycleGroupId: PwGroupId? ): DirFragment @RouterPath(path = "/main/fragment/home") fun toMainHomeFragment(): HomeFragment @RouterPath(path = "/main/fragment/entry") fun toMainHistoryFragment(@RouterArgName(name = "type") type: String = EntryListFragment.TYPE_HISTORY): EntryListFragment @RouterPath(path = "/main/fragment/entry") fun toMainTOTPFragment(@RouterArgName(name = "type") type: String = EntryListFragment.TYPE_TOTP): EntryListFragment @RouterPath(path = "/launcher/opendb") fun getOpenDbFragment( @RouterArgName(name = "openDbRecord") openDbRecord: DbHistoryRecord ): OpenDbFragment @RouterPath(path = "/launcher/changeDb") fun getChangeDbFragment(): ChangeDbFragment } ================================================ FILE: app/src/main/java/com/lyy/keepassa/router/ServiceRouter.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.router import com.arialyy.frame.router.RouterPath import com.lyy.keepassa.service.feat.KdbHandlerService import com.lyy.keepassa.service.feat.KdbOpenService import com.lyy.keepassa.service.feat.KpaSdkService /** * @Author laoyuyu * @Description * @Date 2:05 下午 2022/3/24 **/ interface ServiceRouter { @RouterPath(path = "/service/kpaSdk") fun getKpaSdkService(): KpaSdkService @RouterPath(path = "/service/kdbHandler") fun getDbSaveService(): KdbHandlerService @RouterPath(path = "/service/kdbOpen") fun getDbOpenService(): KdbOpenService } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/DbOpenNotificationService.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.graphics.Color import android.os.Build import android.os.IBinder import com.arialyy.frame.base.FrameApp.context import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.BaseService import com.lyy.keepassa.util.IconUtil import com.lyy.keepassa.view.launcher.LauncherActivity import com.lyy.keepassa.view.main.MainActivity import com.lyy.keepassa.view.main.QuickUnlockActivity /** * 数据库打开通知 */ class DbOpenNotificationService : BaseService() { companion object { const val KEY_NOTIFY_TYPE = "KEY_NOTIFY_TYPE" // 数据库已解锁 const val NOTIFY_TYPE_OPEN_DB = 1 // 数据库已锁定,启动快速解锁 const val NOTIFY_TYPE_QUICK_UNLOCK_DB = 2 // 数据库已锁定 const val NOTIFY_TYPE_DB_LOCKED = 3 } private val CHANNEL_ID_OPEN_DB = "CHANNEL_OPEN_DB" private var CHANNEL_NAME_OPEN_DB: String = "" // 数据库已解锁的的通知的id private val DB_UNLOCK_ID = 10001 // 数据库启用快速解锁的通知的id private val DB_START_QUICK_UNLOCK = 10002 private lateinit var notificationManager: NotificationManager override fun onCreate() { super.onCreate() CHANNEL_NAME_OPEN_DB = getText(R.string.notify_channel_db_open) as String notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 设置通知归属的渠道 // 设置渠道,8.0上必须需要设置渠道,否则通知无法显示,参数分别为:渠道id,渠道名,优先级 // 优先级 https://developer.android.com/guide/topics/ui/notifiers/notifications?hl=zh-cn#importance val channel = NotificationChannel( CHANNEL_ID_OPEN_DB, CHANNEL_NAME_OPEN_DB, NotificationManager.IMPORTANCE_LOW ) notificationManager.createNotificationChannel(channel) } } override fun onBind(intent: Intent?): IBinder? { return null } override fun onStartCommand( intent: Intent?, flags: Int, startId: Int ): Int { // notificationManager.notify(CHANNEL_OPEN_DB, builder.build()); val notify: Notification var notifyId = DB_UNLOCK_ID when (intent?.getIntExtra(KEY_NOTIFY_TYPE, NOTIFY_TYPE_OPEN_DB) ?: DB_START_QUICK_UNLOCK) { NOTIFY_TYPE_QUICK_UNLOCK_DB -> { notifyId = DB_START_QUICK_UNLOCK notify = createQuickUnlockNotify() } NOTIFY_TYPE_DB_LOCKED -> { notify = createDbLockedNotify() } else -> { notify = createDbUnlockNotify() } } startForeground(notifyId, notify) return super.onStartCommand(intent, flags, startId) } /** * 创建数据库已解锁的通知 */ private fun createDbUnlockNotify(): Notification { return createDbNotify(getText(R.string.unlocked), createMainPending()) } /** * 数据库启用快速解锁 */ private fun createQuickUnlockNotify(): Notification { return createDbNotify( getText(R.string.notify_quick_unlock_start), QuickUnlockActivity.createQuickUnlockPending(this) ) } /** * 数据库已锁定 */ private fun createDbLockedNotify(): Notification { return createDbNotify(getText(R.string.notify_db_locked), LauncherActivity.createLauncherPending(this)) } private fun createDbNotify( contentMsg: CharSequence, pendingIntent: PendingIntent ): Notification { val iconId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { R.drawable.ic_launcher_foreground } else { R.mipmap.ic_launcher } val builder: Notification.Builder = Notification.Builder(this) .setContentTitle(getText(R.string.app_name)) .setContentText("${BaseApp.dbName}: $contentMsg") .setLargeIcon(IconUtil.getBitmapFromDrawable(this, iconId, -1)) .setSmallIcon(R.drawable.ic_security_24px) // 状态栏图标 .setContentIntent(pendingIntent) .setColor(Color.TRANSPARENT) // 大图标右下角的小图标 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { builder.setChannelId(CHANNEL_ID_OPEN_DB) } return builder.build() } /** * 主页 */ private fun createMainPending(): PendingIntent { return Intent(this, MainActivity::class.java).let { notificationIntent -> PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/autofill/AutoFillClickReceiver.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.autofill import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 2021/7/10 **/ class AutoFillClickReceiver: BroadcastReceiver() { companion object{ const val ACTION_CLICK_OTHER = "ACTION_CLICK_OTHER" } override fun onReceive(context: Context, intent: Intent) { Timber.d("action = ${intent.action}") } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/autofill/AutoFillHelper.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.autofill import android.annotation.TargetApi import android.app.assist.AssistStructure import android.content.Context import android.content.IntentSender import android.graphics.Bitmap.Config import android.graphics.BitmapFactory import android.os.Build import android.service.autofill.Dataset import android.service.autofill.FillResponse import android.service.autofill.SaveInfo import android.view.View import android.view.autofill.AutofillId import android.view.autofill.AutofillValue import android.widget.RemoteViews import androidx.annotation.DrawableRes import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwIconCustom import com.keepassdroid.database.PwIconStandard import com.lyy.keepassa.R import com.lyy.keepassa.service.autofill.model.AutoFillFieldMetadataCollection import com.lyy.keepassa.util.IconUtil import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.view.launcher.LauncherActivity import com.lyy.keepassa.view.search.AutoFillEntrySearchActivity import com.lyy.keepassa.widget.toPx import timber.log.Timber /** * This is a class containing helper methods for building Autofill Datasets and Responses. */ @TargetApi(Build.VERSION_CODES.O) object AutoFillHelper { val TAG = javaClass.simpleName /** * 数据库没打开时的view * @param packageName 当前应用的包名(keepassA的包名) */ private fun newRemoteViews( context: Context, packageName: String, remoteViewsText: String, @DrawableRes drawableId: Int ): RemoteViews { val rev = RemoteViews(packageName, R.layout.item_auto_fill) rev.setTextViewText(R.id.text, remoteViewsText) rev.setImageViewResource(R.id.img, drawableId) rev.setViewVisibility(R.id.hint, View.GONE) return rev } /** * Not autofill dataset */ private fun notAutoFill( context: Context, apkPageName: String ): Dataset { val rev = RemoteViews(context.packageName, R.layout.item_auto_fill) rev.setTextViewText(R.id.text, context.resources.getString(R.string.cur_app_not_autofill)) IconUtil.getAppIcon(context, apkPageName) ?.let { rev.setImageViewBitmap(R.id.img, it) } // rev.setOnClickResponse() rev.setViewVisibility(R.id.hint, View.GONE) val db = Dataset.Builder(rev) return db.build() } /** * use other entry, click the entry, jump to the search activity */ private fun otherEntry( context: Context, apkPageName: String, tempFillId: AutofillId, structure: AssistStructure ): Dataset { val rev = RemoteViews(context.packageName, R.layout.item_auto_fill) rev.setTextViewText(R.id.text, context.resources.getString(R.string.other)) rev.setViewVisibility(R.id.hint, View.GONE) IconUtil.getBitmapFromDrawable(context, R.drawable.ic_search, 20.toPx())?.let { rev.setImageViewBitmap(R.id.img, it) } val sender = AutoFillEntrySearchActivity.createSearchPending(context, apkPageName, structure) rev.setOnClickPendingIntent( R.id.llContent, sender ) // newSaveResponse(context, metadata, sender) val db = Dataset.Builder(rev) db.setValue(tempFillId, AutofillValue.forText("")) return db.build() } /** * 创建填充数据,填充用户名,密码 * @param dataSetAuth true 验证通过 * @param apkPageName 第三方apk包名 */ private fun newDataSet( context: Context, metadataList: AutoFillFieldMetadataCollection, entry: PwEntry?, dataSetAuth: Boolean, apkPageName: String ): Dataset? { if (entry == null) { return null } val dataSetBuilder: Dataset.Builder if (!dataSetAuth) { dataSetBuilder = Dataset.Builder( buildRemoteView( context, entry.title, if (entry is PwEntryV4) entry.customIcon else null, entry.icon, entry.username ) ) // 设置点击事件,用于数据库没有打开的情况 val sender = LauncherActivity.getAuthDbIntentSender(context, apkPageName) dataSetBuilder.setAuthentication(sender) } else { dataSetBuilder = Dataset.Builder( buildRemoteView( context, entry.title, if (entry is PwEntryV4) entry.customIcon else null, entry.icon, entry.username ) ) } // 填充数据 val setValueAtLeastOnce = applyDataInfoToFields(entry, metadataList, dataSetBuilder) if (setValueAtLeastOnce) { return dataSetBuilder.build() } return null } /** * 构建用户名view */ private fun buildRemoteView( context: Context, title: String, customIcon: PwIconCustom?, icon: PwIconStandard, account: String ): RemoteViews { val rev = RemoteViews(context.packageName, R.layout.item_auto_fill) rev.setTextViewText(R.id.text, title) rev.setViewVisibility(R.id.hint, if (account.isBlank()) View.GONE else View.VISIBLE) rev.setTextViewText(R.id.hint, account) if (customIcon?.imageData != null && customIcon.imageData.isNotEmpty()) { val byte = customIcon.imageData val option = BitmapFactory.Options() option.inPreferredConfig = Config.RGB_565 option.inDensity = 480 rev.setImageViewBitmap(R.id.img, BitmapFactory.decodeByteArray(byte, 0, byte.size, option)) } else { rev.setImageViewResource(R.id.img, IconUtil.getIconById(icon.iconId)) } return rev } /** * 如果不返回SaveInfo,系统是不会触发保存的 */ fun newSaveResponse( context: Context, metadataList: AutoFillFieldMetadataCollection, sender: IntentSender ): FillResponse { val responseBuilder = FillResponse.Builder() val presentation = newRemoteViews( context, context.packageName, context.getString(R.string.autofill_sign_in_prompt), R.mipmap.ic_launcher ) responseBuilder.setAuthentication(metadataList.autoFillIds.toTypedArray(), sender, presentation) val dataSetBuild = Dataset.Builder() val notUsed = RemoteViews(context.packageName, android.R.layout.simple_list_item_1) val b = applySaveInfoToFields(metadataList, dataSetBuild, notUsed) Timber.d("newSaveResponse applyToFields -> $b") if (b) { responseBuilder.addDataset(dataSetBuild.build()) } /* * 触发系统保存弹窗的条件: * 1、activity 必须要关闭 * 2、editText 中的值必须有更改。如果editText中已经设置了text,这时直接登陆的话,是不会触发弹框的 * 3、必须设置DataSet * 4、也可以手动触发,不需要等待activity关闭,但是需要设置setTriggerId */ val sbi = SaveInfo.Builder(metadataList.saveType, metadataList.autoFillIds.toTypedArray()) // .setOptionalIds(metadataList.autoFillIds.toTypedArray()) .setFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) .build() responseBuilder.setSaveInfo(sbi) return responseBuilder.build() } /** * @param dataSetAuth true 验证通过 * @param apkPageName 第三方apk包名 */ fun newResponse( context: Context, dataSetAuth: Boolean, metadata: AutoFillFieldMetadataCollection, entries: MutableList?, apkPageName: String, structure: AssistStructure ): FillResponse? { val responseBuilder = FillResponse.Builder() entries?.forEach { entry -> val dataSet = newDataSet(context, metadata, entry, dataSetAuth, apkPageName) dataSet?.let(responseBuilder::addDataset) } // // user editText add other item // responseBuilder.addDataset(metadata.tempUserFillId?.let { // otherEntry( // context, // apkPageName, // it, // structure // ) // }) // // pass editText add other item // responseBuilder.addDataset(metadata.tempPassFillId?.let { // otherEntry( // context, // apkPageName, // it, // structure // ) // }) return if (metadata.saveType != 0) { val autoFillIds = metadata.autoFillIds // 设置触发保存的类型 responseBuilder.setSaveInfo( SaveInfo.Builder( metadata.saveType, autoFillIds.toTypedArray() ) .build() ) // val rev = RemoteViews(context.packageName, R.layout.item_auto_fill) // rev.setTextViewText(R.id.text, context.resources.getString(R.string.other)) // // IconUtil.getBitmapFromDrawable(context, R.drawable.ic_search, 20.toPx())?.let { // rev.setImageViewBitmap(R.id.img, it) // } //// rev.setIntent(R.id.llContent, ) // rev.setOnClickPendingIntent(R.id.text, PendingIntent.getBroadcast( // context, // 1, // Intent(AutoFillClickReceiver.ACTION_CLICK_OTHER), // PendingIntent.FLAG_UPDATE_CURRENT // )) // // setTextColor(rev, context) // responseBuilder.setHeader(rev) responseBuilder.build() } else { Timber.d("These fields are not meant to be saved by autofill.") null } } fun isValidHint(hint: String): Boolean { if (hint.contains("user", true) || hint.contains("pass")) { return true } when (hint) { View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE, View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY, View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE, View.AUTOFILL_HINT_EMAIL_ADDRESS, View.AUTOFILL_HINT_PHONE, View.AUTOFILL_HINT_NAME, View.AUTOFILL_HINT_PASSWORD, View.AUTOFILL_HINT_POSTAL_ADDRESS, View.AUTOFILL_HINT_POSTAL_CODE, View.AUTOFILL_HINT_USERNAME, -> return true else -> return false } } /** * 将数据填充到FillResponse中 */ private fun applyDataInfoToFields( pwEntry: PwEntry, autoFillFieldMetadataList: AutoFillFieldMetadataCollection, dataSetBuilder: Dataset.Builder ): Boolean { var setValueAtLeastOnce = false for (hint in autoFillFieldMetadataList.allAutoFillHints) { val fillFields = autoFillFieldMetadataList.getFieldsForHint(hint) ?: continue loop@ for (fillField in fillFields) { val fillId = fillField.autoFillId ?: break val fillType = fillField.autoFillType Timber.w("applyDataInfoToFields, autoFill type -> $fillType, autoFillId -> $fillId") when (fillType) { View.AUTOFILL_TYPE_LIST -> { if (fillField.autoFillField.textValue.isNullOrEmpty()) { continue@loop } val listValue = fillField.getAutoFillOptionIndex(fillField.autoFillField.textValue!!) if (listValue != 1) { dataSetBuilder.setValue(fillId, AutofillValue.forList(listValue)) setValueAtLeastOnce = true } } View.AUTOFILL_TYPE_DATE -> { fillField.autoFillField.dateValue ?: continue@loop dataSetBuilder.setValue( fillId, AutofillValue.forDate(fillField.autoFillField.dateValue!!) ) setValueAtLeastOnce = true } View.AUTOFILL_TYPE_TEXT -> { if (fillField.isPassword) { dataSetBuilder.setValue(fillId, AutofillValue.forText(KdbUtil.getPassword(pwEntry))) } else { dataSetBuilder.setValue(fillId, AutofillValue.forText(KdbUtil.getUserName(pwEntry))) } setValueAtLeastOnce = true } View.AUTOFILL_TYPE_TOGGLE -> { fillField.autoFillField.toggleValue ?: continue@loop dataSetBuilder.setValue( fillId, AutofillValue.forToggle(fillField.autoFillField.toggleValue!!) ) setValueAtLeastOnce = true } else -> Timber.w("Invalid autoFill type -> $fillType") } } } return setValueAtLeastOnce } /** * 将数据填充到FillResponse中 */ private fun applySaveInfoToFields( autoFillFieldMetadataList: AutoFillFieldMetadataCollection, dataSetBuilder: Dataset.Builder, rv: RemoteViews ): Boolean { var setValueAtLeastOnce = false for (hint in autoFillFieldMetadataList.allAutoFillHints) { val fillFields = autoFillFieldMetadataList.getFieldsForHint(hint) ?: continue loop@ for (fillField in fillFields) { val fillId = fillField.autoFillId ?: break val fillType = fillField.autoFillType Timber.w("applySaveInfoToFields, autoFill type -> $fillType, autoFillId -> $fillId") when (fillType) { View.AUTOFILL_TYPE_LIST -> { if (fillField.autoFillField.textValue.isNullOrEmpty()) { continue@loop } val listValue = fillField.getAutoFillOptionIndex(fillField.autoFillField.textValue!!) if (listValue != 1) { dataSetBuilder.setValue(fillId, AutofillValue.forList(listValue)) setValueAtLeastOnce = true } } View.AUTOFILL_TYPE_DATE -> { fillField.autoFillField.dateValue ?: continue@loop dataSetBuilder.setValue( fillId, AutofillValue.forDate(fillField.autoFillField.dateValue!!) ) setValueAtLeastOnce = true } View.AUTOFILL_TYPE_TEXT -> { if (fillField.autoFillField.textValue.isNullOrEmpty()) { Timber.w("applySaveInfoToFields, textValue is null") continue@loop } dataSetBuilder.setValue( fillId, AutofillValue.forText(fillField.autoFillField.textValue), rv ) setValueAtLeastOnce = true } View.AUTOFILL_TYPE_TOGGLE -> { fillField.autoFillField.toggleValue ?: continue@loop dataSetBuilder.setValue( fillId, AutofillValue.forToggle(fillField.autoFillField.toggleValue!!) ) setValueAtLeastOnce = true } else -> Timber.w("Invalid autofill type -> $fillType") } } } return setValueAtLeastOnce } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/autofill/AutoFillService.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.autofill import KDBAutoFillRepository import android.annotation.TargetApi import android.app.assist.AssistStructure import android.content.Context import android.content.IntentSender import android.os.Build import android.os.Build.VERSION_CODES import android.os.CancellationSignal import android.service.autofill.AutofillService import android.service.autofill.FillCallback import android.service.autofill.FillRequest import android.service.autofill.FillResponse import android.service.autofill.SaveCallback import android.service.autofill.SaveRequest import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.ToastUtils import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.AutoFillParam import com.lyy.keepassa.service.autofill.model.AutoFillFieldMetadataCollection import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KLog import com.lyy.keepassa.util.KdbUtil.isNull import com.lyy.keepassa.util.LanguageUtil import com.lyy.keepassa.util.PermissionsUtil import com.lyy.keepassa.util.isCanOpenQuickLock import com.lyy.keepassa.view.create.entry.CreateEntryActivity import com.lyy.keepassa.view.launcher.LauncherActivity import com.lyy.keepassa.view.main.QuickUnlockActivity import com.lyy.keepassa.view.search.AutoFillEntrySearchActivity import timber.log.Timber /** * 自动填充服务 * 官方demo https://github.com/android/input-samples * 官方文档:https://developer.android.com/reference/android/service/autofill/AutofillService */ @TargetApi(VERSION_CODES.O) class AutoFillService : AutofillService() { /** * 接收请求 */ override fun onFillRequest( request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback ) { val isManual = request.flags == FillRequest.FLAG_MANUAL_REQUEST val structure = request.fillContexts[request.fillContexts.size - 1].structure val apkPackageName = structure.activityComponent.packageName if (apkPackageName.equals(packageName, ignoreCase = true)) { // 本应用内不进行填充 return } if (!PermissionsUtil.isCanBackgroundStart()) { ToastUtils.showLong(R.string.hint_open_background_start) return } Timber.d( "onFillRequest(): flags = ${request.flags}, requestId = ${request.id}, clientState = ${ KLog.b( request.clientState ) }" ) cancellationSignal.setOnCancelListener { Timber.w("Cancel autofill not implemented in this sample.") } // Parse AutoFill data in Activity val parser = StructureParser(structure) parser.parseForFill(isManual, apkPackageName) val autoFillFields = parser.autoFillFields val needAuth = BaseApp.KDB == null || BaseApp.isLocked if (autoFillFields.autoFillIds.size <= 0) { Timber.i("autoFillIds is nulll") callback.onSuccess(null) return } // 如果数据库没打开,或者数据库已经锁定,打开登录页面 if (needAuth) { val isOpenQuickLock = BaseApp.APP.isCanOpenQuickLock() if (BaseApp.KDB == null) { openLoginActivity(callback, autoFillFields, apkPackageName, structure) return } if (isOpenQuickLock) { openQuickUnLockActivity(callback, autoFillFields, apkPackageName, structure) return } openLoginActivity(callback, autoFillFields, apkPackageName, structure) return } // 获取填充数据 val datas = if (parser.domainUrl.isEmpty()) { KDBAutoFillRepository.getAutoFillDataByPackageName(apkPackageName) } else { KDBAutoFillRepository.getAutoFillDataByDomain(parser.domainUrl) } Timber.d("entrySize = ${datas?.size}") // 没有匹配的数据,进入搜索界面 if (datas == null) { openSearchActivity(callback, autoFillFields, apkPackageName, structure) return } val response = AutoFillHelper.newResponse(this, !needAuth, autoFillFields, datas, apkPackageName, structure) callback.onSuccess(response) } /** * 启动搜索界面 */ private fun openSearchActivity( callback: FillCallback, autofillFields: AutoFillFieldMetadataCollection, apkPackageName: String, structure: AssistStructure ) { callback.onSuccess( getAuthResponse( autofillFields, AutoFillEntrySearchActivity.getSearchIntentSender(this, apkPackageName, structure) ) ) } /** * 启动快速解锁界面 */ private fun openQuickUnLockActivity( callback: FillCallback, autofillFields: AutoFillFieldMetadataCollection, apkPackageName: String, structure: AssistStructure ) { callback.onSuccess( getAuthResponse( autofillFields, QuickUnlockActivity.getQuickUnlockSenderForResponse(this, apkPackageName, structure) ) ) } /** * 启动登录界面 */ private fun openLoginActivity( callback: FillCallback, autofillFields: AutoFillFieldMetadataCollection, apkPackageName: String, structure: AssistStructure ) { callback.onSuccess( getAuthResponse( autofillFields, LauncherActivity.getAuthDbIntentSender(this, apkPackageName, structure) ) ) } /** * 启动数据库验证界面或数据为空时的匹配界面 */ private fun getAuthResponse( metadataList: AutoFillFieldMetadataCollection, sender: IntentSender ): FillResponse { return AutoFillHelper.newSaveResponse(this, metadataList, sender) } /** * 保存用户数据 */ override fun onSaveRequest( request: SaveRequest, callback: SaveCallback ) { if (Build.VERSION.SDK_INT < VERSION_CODES.P) { val str = ResUtil.getString(R.string.fail_unsupported_Systems_O) callback.onFailure(str) ToastUtils.showLong(str) return } val context = request.fillContexts val structure = context[context.size - 1].structure val apkPackageName = structure.activityComponent.packageName val data = request.clientState Timber.d("onSaveRequest(): data=${KLog.b(data)}") val parser = StructureParser(structure) parser.parseForFill(true, apkPackageName) val needAuth = BaseApp.KDB.isNull() || BaseApp.isLocked // 如果数据库没打开,需要打开登录页面 val p = KDBAutoFillRepository.getUserInfo(parser.autoFillFields) Timber.d("用户信息:$p") if (needAuth) { // This api is only at P callback.onSuccess( LauncherActivity.authAndSaveDb( context = this, apkPackageName = apkPackageName, userName = p.first ?: "", pass = p.second ?: "", if (!BaseApp.KDB.isNull() && BaseApp.APP.isCanOpenQuickLock()) QuickUnlockActivity::class.java else LauncherActivity::class.java ) ) return } if (BaseApp.KDB == null) { // 用户没有登陆成功,保存失败 callback.onFailure(getString(R.string.hint_please_open_database)) return } // KDBAutoFillRepository.saveDataToKdb(this, apkPackageName, parser.autoFillFields) callback.onSuccess( CreateEntryActivity.authAndSaveDb( this, AutoFillParam( apkPkgName = apkPackageName, saveUserName = p.first ?: "", savePass = p.second ?: "", isSave = true ) ) ) HitUtil.toaskLong(getString(R.string.save_db_success)) } override fun onConnected() { Timber.d("onConnected") } override fun onDisconnected() { Timber.d("onDisconnected") // W3cHints.curDomainUrl = "" } override fun attachBaseContext(newBase: Context?) { super.attachBaseContext(LanguageUtil.setLanguage(newBase!!, BaseApp.currentLang)) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/autofill/PackageVerifier.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.autofill /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.annotation.SuppressLint import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.os.Build.VERSION_CODES import timber.log.Timber import java.io.ByteArrayInputStream import java.security.MessageDigest import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.util.Locale object PackageVerifier { val TAG = javaClass.simpleName /** * Verifies if a package is valid by matching its certificate with the previously stored * certificate. */ fun isValidPackage( context: Context, packageName: String ): Boolean { val hash: String try { hash = getCertificateHash(context, packageName) Timber.d( "Hash for $packageName: $hash") } catch (e: Exception) { Timber.w("Error getting hash for $packageName: $e") return false } return verifyHash(context, packageName, hash) } @SuppressLint("PackageManagerGetSignatures") private fun getCertificateHash( context: Context, packageName: String ): String { val pm = context.packageManager val signatures = if (Build.VERSION.SDK_INT >= VERSION_CODES.P) { pm.getPackageInfo( packageName, PackageManager.GET_SIGNING_CERTIFICATES ).signingInfo.apkContentsSigners } else { pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures } val cert = signatures[0].toByteArray() ByteArrayInputStream(cert).use { input -> val factory = CertificateFactory.getInstance("X509") val x509 = factory.generateCertificate(input) as X509Certificate val md = MessageDigest.getInstance("SHA256") val publicKey = md.digest(x509.encoded) return toHexFormat(publicKey) } } private fun toHexFormat(bytes: ByteArray): String { val builder = StringBuilder(bytes.size * 2) for (i in bytes.indices) { var hex = Integer.toHexString(bytes[i].toInt()) val length = hex.length if (length == 1) { hex = "0$hex" } if (length > 2) { hex = hex.substring(length - 2, length) } builder.append(hex.toUpperCase(Locale.ROOT)) if (i < bytes.size - 1) { builder.append(':') } } return builder.toString() } private fun verifyHash( context: Context, packageName: String, hash: String ): Boolean { val prefs = context.applicationContext.getSharedPreferences( "package-hashes", Context.MODE_PRIVATE ) if (!prefs.contains(packageName)) { Timber.d( "Creating intial hash for $packageName") prefs.edit() .putString(packageName, hash) .apply() return true } val existingHash = prefs.getString(packageName, null) if (hash != existingHash) { Timber.w("hash mismatch for ${packageName}: expected ${existingHash}, got $hash") return false } return true } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/autofill/StructureParser.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.autofill import android.annotation.TargetApi import android.app.assist.AssistStructure import android.app.assist.AssistStructure.ViewNode import android.os.Build import android.text.InputType import android.view.View import androidx.autofill.HintConstants import com.lyy.keepassa.service.autofill.model.AutoFillFieldMetadata import com.lyy.keepassa.service.autofill.model.AutoFillFieldMetadataCollection import timber.log.Timber /** * Parser for an AssistStructure object. This is invoked when the Autofill Service receives an * AssistStructure from the client Activity, representing its View hierarchy. In this sample, it * parses the hierarchy and collects autofill metadata from {@link ViewNode}s along the way. */ @TargetApi(Build.VERSION_CODES.O) internal class StructureParser(private val autofillStructure: AssistStructure) { val autoFillFields = AutoFillFieldMetadataCollection() val useFields = ArrayList() val passFields = ArrayList() var domainUrl = "" var pkgName = "" var isW3c = false var isInnerAppW3c = false companion object { // 其它应用editText 可能设置的id名,如:R.id.email val usernameHints = HashSet().also { it.add("email") it.add("e-email") it.add("account") it.add("user_name") it.add("mobile") it.add(HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS) it.add(HintConstants.AUTOFILL_HINT_PHONE) it.add(HintConstants.AUTOFILL_HINT_NAME) it.add(HintConstants.AUTOFILL_HINT_USERNAME) it.add(HintConstants.AUTOFILL_HINT_PERSON_NAME) it.add(HintConstants.AUTOFILL_HINT_PERSON_NAME_GIVEN) it.add(HintConstants.AUTOFILL_HINT_NEW_USERNAME) it.add(HintConstants.AUTOFILL_HINT_POSTAL_ADDRESS) it.add(HintConstants.AUTOFILL_HINT_POSTAL_CODE) } val passHints = HashSet().also { it.add(HintConstants.AUTOFILL_HINT_PASSWORD) it.add(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) it.add("passwort") } /** * key: class name, value: isEditText */ val editTextMap = HashSet() /** * key: class name, value: WebView */ val webViewMap = HashSet() } private fun clear() { autoFillFields.clear() useFields.clear() passFields.clear() } /** * 是否是用户手动 用户手机选择了自动填充,也就是editText获取了焦点才开始弹出 * @param pkgName 目标应用包名 */ fun parseForFill( isManual: Boolean, pkgName: String ) { this.pkgName = pkgName parse(isManual) } /** * Traverse AssistStructure and add ViewNode metadata to a flat list. */ private fun parse(isManual: Boolean) { isW3c = false domainUrl = "" Timber.d("Parsing structure for ${autofillStructure.activityComponent}") val nodeSize = autofillStructure.windowNodeCount clear() for (i in 0 until nodeSize) { parseLocked(autofillStructure.getWindowNodeAt(i).rootViewNode) } // 如果密码为空,默认不弹出选择item,这是为了防止遇到editText就弹出item的情况 if (passFields.isEmpty() && !isManual && !isW3c) { autoFillFields.clear() } } private fun parseLocked(viewNode: ViewNode) { // 处理editText 增加 android:autofillHints 的情况 if (!viewNode.autofillHints.isNullOrEmpty()) { if (isW3c) { getW3CInfo(viewNode) } else { getAndroidViewInfo(viewNode) } } else { if (W3cHints.isBrowser(pkgName)) { // Timber.i("is browser, start get web info") checkW3C(viewNode) if (isW3c) { if (domainUrl.isBlank()) { domainUrl = viewNode.webDomain ?: "" W3cHints.curDomainUrl = domainUrl Timber.d("domainUrl = $domainUrl") } getW3CInfo(viewNode) } } else { val className = viewNode.className if (classIsEditText(className)) { getAndroidViewInfo(viewNode) } else if (classIsWebView(className)) { innerAppWebView(viewNode) return } } } val childrenSize = viewNode.childCount for (i in 0 until childrenSize) { parseLocked(viewNode.getChildAt(i)) } } /** * 内置浏览器 */ private fun innerAppWebView(viewNode: ViewNode) { isInnerAppW3c = true if (domainUrl.isBlank()) { domainUrl = viewNode.webDomain ?: "" W3cHints.curDomainUrl = domainUrl Timber.d("domainUrl = $domainUrl") } getW3CInfo(viewNode) val childrenSize = viewNode.childCount for (i in 0 until childrenSize) { innerAppWebView(viewNode.getChildAt(i)) } } private fun checkIsWebView(clazz: Class<*>): Boolean { if (clazz.name.equals("android.webkit.WebView")) { return true } val sup = clazz.superclass ?: return false if (sup.name.equals("java.lang.Object")) { return false } if (sup.name.equals("android.webkit.WebView")) { return true } return checkIsWebView(sup) } private fun classIsWebView(className: String?): Boolean { if (className.isNullOrEmpty()) return false if (webViewMap.contains(className)) return true try { if (checkIsWebView(Class.forName(className))) { webViewMap.add(className) return true } } catch (e: ClassNotFoundException) { Timber.e(e) } return false } private fun checkIsEditText(clazz: Class<*>): Boolean { if (clazz.name.equals("android.widget.EditText")) { return true } val sup = clazz.superclass ?: return false if (sup.name.equals("java.lang.Object")) { return false } if (sup.name.equals("android.widget.EditText")) { return true } return checkIsEditText(sup) } private fun classIsEditText(className: String?): Boolean { if (className.isNullOrEmpty()) return false if (editTextMap.contains(className)) return true try { if (checkIsEditText(Class.forName(className))) { editTextMap.add(className) return true } } catch (e: ClassNotFoundException) { Timber.e(e) } return false } private fun getAndroidViewInfo(viewNode: ViewNode) { if (isPassword(viewNode)) { addPassField(viewNode) return } if (isUserName(viewNode)) { addUserField(viewNode) return } Timber.d( "not w3c, unknown idEntry = ${viewNode.idEntry}, isFocused = ${viewNode.isFocused}, autofillId = ${viewNode.autofillId}, fillValue = ${viewNode.autofillValue}, inputType = ${viewNode.inputType}, htmlInfo = ${viewNode.htmlInfo}, autofillType = ${viewNode.autofillType}, hint = ${viewNode.hint}, isAccessibilityFocused =${viewNode.isAccessibilityFocused}, idPackage = ${viewNode.idPackage}, isActivated = ${viewNode.isActivated}, visibility = ${viewNode.visibility}, isAssistBlocked = ${viewNode.isAssistBlocked}, isOpaque = ${viewNode.isOpaque}" ) } private fun getW3CInfo(viewNode: ViewNode) { if (viewNode.htmlInfo == null) { return } if (W3cHints.isW3CUserByHints(viewNode)) { Timber.i("addUser by hints") addUserField(viewNode) return } if (W3cHints.isW3CPassByHints(viewNode)) { Timber.i("addPassword by hints") addPassField(viewNode) return } Timber.d( "w3c, unknown idEntry = ${viewNode.idEntry}, isFocused = ${viewNode.isFocused}, autofillId = ${viewNode.autofillId}, fillValue = ${viewNode.autofillValue}, inputType = ${viewNode.inputType}, htmlInfo = ${viewNode.htmlInfo}, autofillType = ${viewNode.autofillType}, hint = ${viewNode.hint}, isAccessibilityFocused =${viewNode.isAccessibilityFocused}, idPackage = ${viewNode.idPackage}, isActivated = ${viewNode.isActivated}, visibility = ${viewNode.visibility}, isAssistBlocked = ${viewNode.isAssistBlocked}, isOpaque = ${viewNode.isOpaque}" ) } /** * Check whether the web page */ private fun checkW3C(viewNode: ViewNode): Boolean { if (isW3c) { return true } isW3c = viewNode.htmlInfo?.tag == "input" || viewNode.className == "android.webkit.WebView" return isW3c } /** * add pass field */ private fun addPassField(viewNode: ViewNode) { if (!isW3c && !isInnerAppW3c && (viewNode.visibility != View.VISIBLE || !viewNode.isFocusable)) { return } autoFillFields.tempPassFillId = viewNode.autofillId Timber.d("pass autofillType = ${viewNode.autofillType}, fillId = ${viewNode.autofillId}, fillValue = ${viewNode.autofillValue}, text = ${viewNode.text}, hint = ${viewNode.hint}, visibility = ${viewNode.visibility}, isActivated = ${viewNode.isActivated}") passFields.add(viewNode) autoFillFields.add(AutoFillFieldMetadata(viewNode, View.AUTOFILL_HINT_PASSWORD)) } /** * add userName field */ private fun addUserField(viewNode: ViewNode) { if (!isW3c && !isInnerAppW3c && (viewNode.visibility != View.VISIBLE || !viewNode.isFocusable)) { return } if (autoFillFields.tempUserFillId == null || viewNode.isFocused) { autoFillFields.tempUserFillId = viewNode.autofillId Timber.d("user autofillType = ${viewNode.autofillType}, fillId = ${viewNode.autofillId}, idEntry = ${viewNode.idEntry}, fillValue = ${viewNode.autofillValue} text = ${viewNode.text}, hint = ${viewNode.hint}, visibility = ${viewNode.visibility}, isActivated = ${viewNode.isActivated}") useFields.add(viewNode) autoFillFields.add(AutoFillFieldMetadata(viewNode, View.AUTOFILL_HINT_USERNAME)) } } /** * 判断是否是用户名输入框 */ private fun isUserName(f: ViewNode): Boolean { if (!isPassword(f) || usernameHints.any { f.idEntry != null && f.idEntry!!.contains(it, ignoreCase = true) } || usernameHints.any { f.hint != null && f.hint!!.contains(it, ignoreCase = true) } ) { if ((f.idEntry != null && f.idEntry!!.contains("search", ignoreCase = false)) || (f.hint != null && f.hint!!.contains("search", ignoreCase = false)) ) { return false } return true } return false } /** * 判断是否是密码输入框 * @return true 密码输入框 */ private fun isPassword(f: ViewNode): Boolean { val inputType = f.inputType if (inputType == InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD || inputType == InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD || inputType == InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD || inputType == InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD || passHints.any { f.idEntry != null && f.idEntry!!.contains(it, ignoreCase = true) } ) { return true } return false } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/autofill/W3cHints.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.autofill import android.annotation.TargetApi import android.app.assist.AssistStructure.ViewNode import android.os.Build import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.ActivityUtils import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import timber.log.Timber import java.util.Locale /** * https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete */ @TargetApi(Build.VERSION_CODES.O) object W3cHints { val CompatBrowsers = setOf( "org.mozilla.firefox", "org.mozilla.firefox_beta", "com.microsoft.emmx", "com.android.chrome", "com.chrome.beta", "com.android.browser", "com.brave.browser", "com.opera.browser", "com.opera.browser.beta", "com.opera.mini.native", "com.chrome.dev", "com.chrome.canary", "com.google.android.apps.chrome", "com.google.android.apps.chrome_dev", "com.yandex.browser", "com.sec.android.app.sbrowser", "com.sec.android.app.sbrowser.beta", "org.codeaurora.swe.browser", "com.amazon.cloud9", "mark.via.gp", "mark.via", "org.bromite.bromite", "org.chromium.chrome", "com.kiwibrowser.browser", "com.ecosia.android", "com.opera.mini.native.beta", "org.mozilla.fennec_aurora", "org.mozilla.fennec_fdroid", "com.qwant.liberty", "com.opera.touch", "org.mozilla.fenix", "org.mozilla.fenix.nightly", "org.mozilla.reference.browser", "org.mozilla.rocket", "org.torproject.torbrowser", "com.vivaldi.browser", "com.mmbox.xbrowser", "info.torapp.uweb" ) const val HONORIFIC_PREFIX = "honorific-prefix" const val NAME = "name" const val GIVEN_NAME = "given-name" const val ADDITIONAL_NAME = "additional-name" const val FAMILY_NAME = "family-name" const val HONORIFIC_SUFFIX = "honorific-suffix" const val USERNAME = "username" const val PWD = "pwd" const val PASSWORD = "password" const val NEW_PASSWORD = "new-password" const val CURRENT_PASSWORD = "current-password" const val ORGANIZATION_TITLE = "organization-title" const val ORGANIZATION = "organization" const val STREET_ADDRESS = "street-address" const val ADDRESS_LINE1 = "address-line1" const val ADDRESS_LINE2 = "address-line2" const val ADDRESS_LINE3 = "address-line3" const val ADDRESS_LEVEL4 = "address-level4" const val ADDRESS_LEVEL3 = "address-level3" const val ADDRESS_LEVEL2 = "address-level2" const val ADDRESS_LEVEL1 = "address-level1" const val COUNTRY = "country" const val COUNTRY_NAME = "country-name" const val POSTAL_CODE = "postal-code" const val CC_NAME = "cc-name" const val CC_GIVEN_NAME = "cc-given-name" const val CC_ADDITIONAL_NAME = "cc-additional-name" const val CC_FAMILY_NAME = "cc-family-name" const val CC_NUMBER = "cc-number" const val CC_EXPIRATION = "cc-exp" const val CC_EXPIRATION_MONTH = "cc-exp-month" const val CC_EXPIRATION_YEAR = "cc-exp-year" const val CC_CSC = "cc-csc" const val CC_TYPE = "cc-type" const val TRANSACTION_CURRENCY = "transaction-currency" const val TRANSACTION_AMOUNT = "transaction-amount" const val LANGUAGE = "language" const val BDAY = "bday" const val BDAY_DAY = "bday-day" const val BDAY_MONTH = "bday-month" const val BDAY_YEAR = "bday-year" const val SEX = "sex" const val URL = "url" const val PHOTO = "photo" // Optional W3C prefixes const val PREFIX_SECTION = "section-" const val SHIPPING = "shipping" const val BILLING = "billing" // W3C prefixes below... const val PREFIX_HOME = "home" const val PREFIX_WORK = "work" const val PREFIX_FAX = "fax" const val PREFIX_PAGER = "pager" // ... require those suffix const val TEL = "tel" const val TEL_COUNTRY_CODE = "tel-country-code" const val TEL_NATIONAL = "tel-national" const val TEL_AREA_CODE = "tel-area-code" const val TEL_LOCAL = "tel-local" const val TEL_LOCAL_PREFIX = "tel-local-prefix" const val TEL_LOCAL_SUFFIX = "tel-local-suffix" const val TEL_EXTENSION = "tel_extension" const val EMAIL = "email" const val TEXT = "text" const val IMPP = "impp" // totp const val ONE_TIME_CODE = "one-time-code" private val PASSWORD_HINT_LIST = arrayListOf(PASSWORD, NEW_PASSWORD, CURRENT_PASSWORD) private val USER_HINT_LIST = arrayListOf(NAME, USERNAME, TEL, GIVEN_NAME, EMAIL, IMPP) private val ATTR_LIST = arrayListOf("name", "type") private val HINT_USER_LABEL by lazy { if (ActivityUtils.getTopActivity() != null){ ActivityUtils.getTopActivity().resources.getStringArray(R.array.auto_fill_hint_label) }else{ BaseApp.APP.resources.getStringArray(R.array.auto_fill_hint_label) } } private val HINT_PASSWORD_LABEL by lazy { ResUtil.getString(R.string.password) } var curDomainUrl = "" /** * 是否是浏览器 */ fun isBrowser(pkgName: String?): Boolean { if (pkgName.isNullOrEmpty()) return false return CompatBrowsers.contains(pkgName) } /** * 是否是用户名 */ fun isW3cUserName(p: android.util.Pair): Boolean { if (p.second.isNullOrEmpty()) { return false } val temp = p.second.lowercase() return USER_HINT_LIST.contains(temp) } /** * 是否是密码 */ fun isW3cPassWord(p: android.util.Pair): Boolean { if (p.second.isNullOrEmpty()) { return false } val temp = p.second.lowercase() return PASSWORD_HINT_LIST.contains(temp) // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion || (p.first == "autocomplete") } fun isW3CUserByHints(viewNode: ViewNode): Boolean { if (!viewNode.htmlInfo?.tag.equals("input", true)) { return false } val hints = viewNode.autofillHints hints?.forEach { val temp = it.lowercase() if (USER_HINT_LIST.contains(temp)) { return true } if (StructureParser.usernameHints.contains(temp)) { return true } if (temp.contains("user", true) || temp.contains("account", true)) { return true } } val names = viewNode.htmlInfo?.attributes names?.forEach { if (it.first.isNullOrEmpty() || it.second.isNullOrEmpty()) { Timber.d("value is null, first = ${it.first}, second = ${it.second}") return@forEach } val name = it.first.lowercase() val value = it.second.lowercase() if (ATTR_LIST.contains(name) && USER_HINT_LIST.contains(value)) { return true } if (name == "label") { HINT_USER_LABEL.forEach { label -> if (value.contains(label)) { return true } } } } return false } fun isW3CPassByHints(viewNode: ViewNode): Boolean { if (!viewNode.htmlInfo?.tag.equals("input", true)) { return false } val hints = viewNode.autofillHints hints?.forEach { val temp = it.lowercase() if (PASSWORD_HINT_LIST.contains(temp)) { return true } if (StructureParser.passHints.contains(temp)) { return true } if (temp.contains("password", true)) { return true } } val names = viewNode.htmlInfo?.attributes names?.forEach { if (it.first.isNullOrEmpty() || it.second.isNullOrEmpty()) { Timber.d("value is null, first = ${it.first}, second = ${it.second}") return@forEach } val name = it.first.lowercase() val value = it.second.lowercase() if (ATTR_LIST.contains(name) && PASSWORD_HINT_LIST.contains(value)) { return true } if (name == "label" && value.contains(HINT_PASSWORD_LABEL)) { return true } } return false } fun isW3cSectionPrefix(hint: String): Boolean { return hint.lowercase(Locale.ROOT) .startsWith(PREFIX_SECTION); } fun isW3cAddressType(hint: String): Boolean { when (hint.lowercase(Locale.ROOT)) { SHIPPING, BILLING -> return true } return false } fun isW3cTypePrefix(hint: String): Boolean { when (hint.lowercase(Locale.ROOT)) { PREFIX_WORK, PREFIX_FAX, PREFIX_HOME, PREFIX_PAGER -> return true } return false } fun isW3cTypeHint(hint: String): Boolean { when (hint.lowercase(Locale.ROOT)) { TEL, TEL_COUNTRY_CODE, TEL_NATIONAL, TEL_AREA_CODE, TEL_LOCAL, TEL_LOCAL_PREFIX, TEL_LOCAL_SUFFIX, TEL_EXTENSION, EMAIL, IMPP -> return true; } Timber.w("Inid W3C type hint: $hint") return false; } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/autofill/datasource/KDBAutoFillRepository.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ import android.content.Context import android.content.pm.PackageManager import android.graphics.Bitmap.CompressFormat.PNG import android.view.View import com.arialyy.frame.config.CommonConstant import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.ToastUtils import com.keepassdroid.database.PwDatabaseV4 import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwEntryV3 import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroupV4 import com.keepassdroid.database.PwIconCustom import com.keepassdroid.database.PwIconStandard import com.keepassdroid.database.SearchParametersV4 import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.service.autofill.model.AutoFillFieldMetadataCollection import com.lyy.keepassa.util.IconUtil import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.util.KpaUtil import timber.log.Timber import java.io.ByteArrayOutputStream import java.util.UUID /** * Singleton autofill data repository that stores autofill fields to SharedPreferences. * Disclaimer: you should not store sensitive fields like user data unencrypted. This is done * here only for simplicity and learning purposes. */ object KDBAutoFillRepository { /** * 通过包名获取填充数据 */ fun getAutoFillDataByPackageName(pkgName: String): MutableList? { if (BaseApp.KDB?.pm == null) { return null } Timber.d("getFillDataByPkgName, pkgName = $pkgName") val listStorage = ArrayList() KdbUtil.searchEntriesByPackageName(pkgName, listStorage) if (listStorage.isEmpty()) { val sp = SearchParametersV4() val strs = pkgName.split(".") // 如果没有,则从url检索 for (s in strs) { if (CommonConstant.domainSuffix.contains(s)) { continue } sp.setupNone() sp.searchInUrls = true sp.searchString = s BaseApp.KDB!!.pm.rootGroup.searchEntries(sp, listStorage) } if (listStorage.isEmpty()) { return null } } return listStorage.toSet().toMutableList() } /** * 通过url获取填充数据 */ fun getAutoFillDataByDomain(domain: String): ArrayList? { Timber.d("getFillDataByDomain, domain = $domain") val listStorage = ArrayList() KdbUtil.searchEntriesByDomain(domain, listStorage) if (listStorage.isEmpty()) { return null } return listStorage } /** * 保存数据到数据库 */ fun saveDataToKdb( context: Context, apkPkgName: String, autofillFields: AutoFillFieldMetadataCollection ) { if (BaseApp.KDB?.pm == null) { Timber.e("数据库为空") return } val listStorage = ArrayList() KdbUtil.searchEntriesByPackageName(apkPkgName, listStorage) val entry: PwEntry if (listStorage.isEmpty()) { if (BaseApp.isV4) { entry = PwEntryV4(BaseApp.KDB!!.pm.rootGroup as PwGroupV4) val icon = IconUtil.getAppIcon(context, apkPkgName) if (icon != null) { val baos = ByteArrayOutputStream() icon.compress(PNG, 100, baos) val datas: ByteArray = baos.toByteArray() val customIcon = PwIconCustom(UUID.randomUUID(), datas) entry.customIcon = customIcon (BaseApp.KDB!!.pm as PwDatabaseV4).putCustomIcons(customIcon) } entry.strings["KP2A_URL_1"] = ProtectedString(false, "androidapp://$apkPkgName") } else { entry = PwEntryV3() entry.setUrl("androidapp://$apkPkgName", BaseApp.KDB!!.pm) } val appName = getAppName(context, apkPkgName) entry.setTitle(appName ?: "newEntry", BaseApp.KDB!!.pm) entry.icon = PwIconStandard(0) KpaUtil.kdbHandlerService.createEntry(entry as PwEntryV4) } else { entry = listStorage[0] Timber.w("已存在含有【$apkPkgName】的条目,将更新条目") } for (hint in autofillFields.allAutoFillHints) { val fillFields = autofillFields.getFieldsForHint(hint) ?: continue for (fillField in fillFields) { fillField.autoFillField.textValue ?: continue if (fillField.autoFillType == View.AUTOFILL_TYPE_TEXT) { if (fillField.isPassword) { entry.setPassword(fillField.autoFillField.textValue, BaseApp.KDB!!.pm) Timber.d("pass = ${fillField.autoFillField.textValue}") } else { entry.setUsername(fillField.autoFillField.textValue, BaseApp.KDB!!.pm) Timber.d("userName = ${fillField.autoFillField.textValue}") } } } } KpaUtil.kdbHandlerService.saveDbByBackground() ToastUtils.showLong(ResUtil.getString(R.string.save_db_success)) Timber.d("密码信息保存成功") } /** * 获取用户名和密码 * @return first 用户名 */ fun getUserInfo(autofillFields: AutoFillFieldMetadataCollection): Pair { var user: String? = null var pass: String? = null for (hint in autofillFields.allAutoFillHints) { val fillFields = autofillFields.getFieldsForHint(hint) ?: continue for (fillField in fillFields) { fillField.autoFillField.textValue ?: continue if (fillField.autoFillType == View.AUTOFILL_TYPE_TEXT) { if (fillField.isPassword) { pass = fillField.autoFillField.textValue } if (!fillField.isPassword) { user = fillField.autoFillField.textValue } } } } return Pair(user, pass) } /** * 获取应用程序名称 */ fun getAppName( context: Context, apkPkgName: String ): String? { try { val packageManager = context.packageManager return packageManager.getApplicationLabel( packageManager.getApplicationInfo( apkPkgName, PackageManager.GET_META_DATA ) ) .toString() } catch (e: Exception) { Timber.e(e) } return null } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/autofill/model/AutoFillFieldMetadata.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.autofill.model import android.annotation.TargetApi import android.app.assist.AssistStructure.ViewNode; import android.os.Build import android.service.autofill.SaveInfo import android.view.View import android.view.autofill.AutofillId import com.lyy.keepassa.service.autofill.AutoFillHelper /** * A stripped down version of a [ViewNode] that contains only autofill-relevant metadata. It also * contains a `saveType` flag that is calculated based on the [ViewNode]'s autofill hints. */ @TargetApi(Build.VERSION_CODES.O) class AutoFillFieldMetadata(viewNode: ViewNode) { var saveType = 0 private set val autoFillHints = HashSet() val autoFillId: AutofillId? = viewNode.autofillId val autoFillType: Int = viewNode.autofillType val autoFillOptions: Array? = viewNode.autofillOptions val isFocused: Boolean = viewNode.isFocused var isPassword: Boolean = false val autoFillField = FilledAutoFillField(viewNode) /** * 处理自定义的情况,也就是控件没有设置android:autofillHints的情况 * @param fileType view的类型 [View.AUTOFILL_HINT_PASSWORD] */ constructor( view: ViewNode, fileType: String ) : this(view) { autoFillHints.add(fileType) updateSaveTypeFromHints() } /** * 处理控件中已经设置了android:autofillHints的情况 */ init { viewNode.autofillHints?.filter(AutoFillHelper::isValidHint) ?.forEach { autoFillHints.add(it) } updateSaveTypeFromHints() } /** * When the [ViewNode] is a list that the user needs to choose a string from (i.e. a spinner), * this is called to return the index of a specific item in the list. */ fun getAutoFillOptionIndex(value: CharSequence): Int { return autoFillOptions?.indexOf(value) ?: -1 } /** * 更新保存类型,和StructureParser.parseLocked 中的需要关联 */ private fun updateSaveTypeFromHints() { saveType = 0 for (hint in autoFillHints) { when (hint) { View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE, View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY, View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE -> { saveType = saveType or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD } View.AUTOFILL_HINT_EMAIL_ADDRESS -> { saveType = saveType or SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS } View.AUTOFILL_HINT_PHONE, View.AUTOFILL_HINT_NAME -> { saveType = saveType or SaveInfo.SAVE_DATA_TYPE_GENERIC } View.AUTOFILL_HINT_PASSWORD -> { isPassword = true saveType = saveType or SaveInfo.SAVE_DATA_TYPE_PASSWORD saveType = saveType and SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS.inv() saveType = saveType and SaveInfo.SAVE_DATA_TYPE_USERNAME.inv() } View.AUTOFILL_HINT_POSTAL_ADDRESS, View.AUTOFILL_HINT_POSTAL_CODE -> { saveType = saveType or SaveInfo.SAVE_DATA_TYPE_ADDRESS } View.AUTOFILL_HINT_USERNAME -> { saveType = saveType or SaveInfo.SAVE_DATA_TYPE_USERNAME } } } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/autofill/model/AutoFillFieldMetadataCollection.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.autofill.model import android.view.autofill.AutofillId import timber.log.Timber /** * Data structure that stores a collection of `AutofillFieldMetadata`s. Contains all of the client's `View` * hierarchy autoFill-relevant metadata. */ data class AutoFillFieldMetadataCollection @JvmOverloads constructor( val autoFillIds: HashSet = HashSet(), val allAutoFillHints: HashSet = HashSet(), val focusedAutoFillHints: HashSet = HashSet() ) { private val TAG = javaClass.simpleName /** * used for "other entry" */ var tempUserFillId: AutofillId? = null var tempPassFillId: AutofillId? = null /** * key -> autoHintString * value -> */ private val autoFillHintsToFieldsMap = HashMap>() var saveType = 0 private set fun clear() { tempUserFillId = null tempPassFillId = null autoFillIds.clear() allAutoFillHints.clear() focusedAutoFillHints.clear() } fun add(autoFillFieldMetadata: AutoFillFieldMetadata) { if (autoFillFieldMetadata.autoFillId == null) { Timber.w("autoFillId == null") return } saveType = saveType or autoFillFieldMetadata.saveType autoFillIds.add(autoFillFieldMetadata.autoFillId) val hintsList = autoFillFieldMetadata.autoFillHints allAutoFillHints.addAll(hintsList) if (autoFillFieldMetadata.isFocused) { focusedAutoFillHints.addAll(hintsList) } autoFillFieldMetadata.autoFillHints.forEach { val fields = autoFillHintsToFieldsMap[it] ?: ArrayList() autoFillHintsToFieldsMap[it] = fields fields.add(autoFillFieldMetadata) } } fun getFieldsForHint(hint: String): MutableList? { return autoFillHintsToFieldsMap[hint] } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/autofill/model/FilledAutoFillField.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.autofill.model import android.annotation.TargetApi import android.app.assist.AssistStructure import android.os.Build import android.view.autofill.AutofillValue import com.google.gson.annotations.Expose import com.lyy.keepassa.service.autofill.AutoFillHelper /** * JSON serializable data class containing the same data as an [AutofillValue]. */ @TargetApi(Build.VERSION_CODES.O) class FilledAutoFillField(viewNode: AssistStructure.ViewNode) { @Expose var textValue: String? = null @Expose var dateValue: Long? = null @Expose var toggleValue: Boolean? = null val autoFillHints = viewNode.autofillHints?.filter(AutoFillHelper::isValidHint)?.toTypedArray() init { viewNode.autofillValue?.let { when { it.isList -> { val index = it.listValue viewNode.autofillOptions?.let { autofillOptions -> if (autofillOptions.size > index) { textValue = autofillOptions[index].toString() } } } it.isDate -> { dateValue = it.dateValue } it.isText -> { // Using toString of AutofillValue.getTextValue in order to save it to // SharedPreferences. textValue = it.textValue.toString() } else -> { } } } } fun isNull(): Boolean { return textValue == null && dateValue == null && toggleValue == null } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/feat/IFeature.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.feat import android.content.Context interface IFeature { fun init(context: Context) } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/feat/KdbHandlerService.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.feat import android.content.Context import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.facade.template.IProvider import com.arialyy.frame.router.Routerfit import com.keepassdroid.database.PwDatabase import com.keepassdroid.database.PwDatabaseV4 import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroup import com.keepassdroid.database.PwGroupV4 import com.keepassdroid.database.PwIconCustom import com.keepassdroid.database.PwIconStandard import com.keepassdroid.database.helper.KDBHandlerHelper import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.event.CollectionEvent import com.lyy.keepassa.event.CollectionEventType import com.lyy.keepassa.event.CollectionEventType.COLLECTION_STATE_ADD import com.lyy.keepassa.event.CollectionEventType.COLLECTION_STATE_REMOVE import com.lyy.keepassa.event.EntryState.CREATE import com.lyy.keepassa.event.EntryState.DELETE import com.lyy.keepassa.event.EntryState.MODIFY import com.lyy.keepassa.event.EntryState.MOVE import com.lyy.keepassa.event.EntryState.SAVE import com.lyy.keepassa.event.EntryStateChangeEvent import com.lyy.keepassa.event.GroupStateChangeEvent import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.KdbUtil.isNull import com.lyy.keepassa.util.cloud.DbSynUtil import com.lyy.keepassa.util.setCollection import com.lyy.keepassa.view.dialog.LoadingDialog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import timber.log.Timber import java.util.concurrent.atomic.AtomicInteger /** * @Author laoyuyu * @Description * @Date 2:03 下午 2022/3/24 **/ @Route(path = "/service/kdbHandler") class KdbHandlerService : IProvider { companion object { const val MIN_TIME = 200L } private var scope = MainScope() private var collectionNum = AtomicInteger(0) /** * collection state flow */ val collectionStateFlow = MutableStateFlow(CollectionEvent()) val entryStateChangeFlow = MutableSharedFlow() val groupStateChangeFlow = MutableStateFlow(GroupStateChangeEvent()) private val collectionEntries = hashSetOf() private val mutex = Mutex() private val loadingDialog: LoadingDialog by lazy { Routerfit.create(DialogRouter::class.java).getLoadingDialog() } private val kdbHelper by lazy { KDBHandlerHelper.getInstance(BaseApp.APP) } fun clearDb() { BaseApp.KDB?.clear(BaseApp.APP) BaseApp.KDB = null collectionEntries.clear() collectionNum.set(0) } fun getCollectionEntries() = collectionEntries fun getCollectionNum() = collectionNum.get() internal fun updateCollectionEntries(collectionEntries: Set) { this.collectionEntries.clear() this.collectionEntries.addAll(collectionEntries) } internal fun updateCollectionNum(newCollectionNum: Int) { collectionNum.set(newCollectionNum) scope.launch { collectionStateFlow.emit( CollectionEvent( state = CollectionEventType.COLLECTION_STATE_TOTAL, collectionNum = collectionNum.get() ) ) } } /** * @param collection true: add collection, false: cancel collection */ fun collection(pwEntryV4: PwEntryV4, collection: Boolean) { pwEntryV4.setCollection(collection) if (collection) { collectionEntries.add(pwEntryV4) } else { collectionEntries.remove(pwEntryV4) } scope.launch { if (collection) { collectionStateFlow.emit( CollectionEvent( COLLECTION_STATE_ADD, collectionNum.incrementAndGet(), pwEntryV4 ) ) return@launch } collectionStateFlow.emit( CollectionEvent( COLLECTION_STATE_REMOVE, collectionNum.decrementAndGet(), pwEntryV4 ) ) } } private fun showLoading() { Timber.d("showLoading, hashCode = ${loadingDialog.hashCode()}") scope.launch { if (loadingDialog.isVisible) { return@launch } loadingDialog.show() } } private fun dismissLoading(delay: Long = 0) { Timber.d("dismissLoading, delay = ${delay}") scope.launch { loadingDialog.dismiss(delay) } } /** * delete group */ fun deleteGroup(pwGroup: PwGroupV4, callback: () -> Unit) { scope.launch { val oldParent = pwGroup.parent withContext(Dispatchers.IO) { if (BaseApp.KDB!!.pm.canRecycle(pwGroup)) { (BaseApp.KDB!!.pm as PwDatabaseV4).recycle(pwGroup) } else { kdbHelper.deleteGroup(BaseApp.KDB, pwGroup, true) } } callback.invoke() groupStateChangeFlow.emit(GroupStateChangeEvent(DELETE, pwGroup, oldParent)) } } /** * only send status */ fun updateEntryStatus(v4Entry: PwEntryV4) { scope.launch { v4Entry.touch(true, true) entryStateChangeFlow.emit( EntryStateChangeEvent( MODIFY, v4Entry, v4Entry.parent ) ) } } /** * move entry from other group * @param targetParent target parent */ fun moveEntry(v4Entry: PwEntryV4, targetParent: PwGroupV4) { scope.launch { val originParent = v4Entry.parent withContext(Dispatchers.IO) { v4Entry.parent.childEntries.remove(v4Entry) if (v4Entry.parent == BaseApp.KDB.pm.recycleBin) { (BaseApp.KDB.pm as PwDatabaseV4).undoRecycle(v4Entry, targetParent) } else { (BaseApp.KDB.pm as PwDatabaseV4).moveEntry(v4Entry, targetParent) } } entryStateChangeFlow.emit( EntryStateChangeEvent( MOVE, v4Entry, originParent ) ) } } /** * delete entry */ fun deleteEntry(v4Entry: PwEntryV4, callback: () -> Unit) { scope.launch { val parent = v4Entry.parent withContext(Dispatchers.IO) { kdbHelper.deleteEntry(BaseApp.KDB, v4Entry, true) } callback.invoke() entryStateChangeFlow.emit(EntryStateChangeEvent(DELETE, v4Entry, parent)) } } /** * update group info and send new state */ fun modifyGroup( groupName: String, icon: PwIconStandard, customIcon: PwIconCustom?, self: PwGroupV4, callback: () -> Unit ) { scope.launch { withContext(Dispatchers.IO) { self.customIcon = customIcon self.icon = icon self.name = groupName if (kdbHelper.save(BaseApp.KDB)) { BaseApp.KDB.dirty.add(self.parent) } } callback.invoke() groupStateChangeFlow.emit(GroupStateChangeEvent(MODIFY, self)) } } /** * create new Group * * @param icon default icon * @param customIcon custom icon */ fun createGroup( groupName: String, icon: PwIconStandard, customIcon: PwIconCustom?, parent: PwGroupV4, callback: (PwGroupV4) -> Unit ) { scope.launch { val tempGroup = withContext(Dispatchers.IO) { val pm: PwDatabase = BaseApp.KDB.pm val group = pm.createGroup() as PwGroupV4 group.initNewGroup(groupName, pm.newGroupId()) group.icon = icon customIcon?.let { group.customIcon = it } pm.addGroupTo(group, parent) return@withContext group } callback.invoke(tempGroup) groupStateChangeFlow.emit(GroupStateChangeEvent(CREATE, tempGroup, null)) } } /** * add new group */ fun createGroup(group: PwGroup) { kdbHelper .createGroup(BaseApp.KDB, group.name, group.icon, group.parent) } fun addGroup(group: PwGroupV4) { BaseApp.KDB?.pm?.addGroupTo(group, group.parent) } /** * add new entry */ fun createEntry(entry: PwEntryV4, parent: PwGroup? = null) { BaseApp.KDB!!.pm.addEntryTo(entry, parent ?: entry.parent) scope.launch { entryStateChangeFlow.emit(EntryStateChangeEvent(CREATE, entry)) } } /** * only add entry */ fun addEntryTo(entry: PwEntryV4, parent: PwGroup) { BaseApp.KDB!!.pm.addEntryTo(entry, parent) } suspend fun saveOnly(needShowLoading: Boolean = false, callback: (Int) -> Unit) { mutex.withLock { withContext(Dispatchers.IO) { if (needShowLoading) { showLoading() } val b = kdbHelper.save(BaseApp.KDB) if (needShowLoading) { dismissLoading() } withContext(Dispatchers.Main) { callback.invoke(if (b) DbSynUtil.STATE_SUCCEED else DbSynUtil.STATE_SAVE_DB_FAIL) entryStateChangeFlow.emit(EntryStateChangeEvent(SAVE)) } } } } /** * save db by background * @param uploadDb true: upload db to cloud * @param callback run in main thread */ fun saveDbByBackground(uploadDb: Boolean = false, callback: (Int) -> Unit = {}) { Timber.d("start save db by background") if (BaseApp.KDB.isNull()) { Timber.d("db is null") return } if (BaseApp.isLocked) { Timber.d("db is locked") return } scope.launch(Dispatchers.IO) { mutex.withLock { BaseApp.KDB?.let { kdb -> val b = kdbHelper.save(kdb) Timber.d("保存后的数据库hash:${kdb.hashCode()},num = ${kdb.pm.entries.size}") delay(1000) if (uploadDb) { val response = DbSynUtil.uploadSyn(BaseApp.dbRecord!!, false) Timber.i(response.msg) withContext(Dispatchers.Main) { callback.invoke(response.code) } entryStateChangeFlow.emit(EntryStateChangeEvent(SAVE)) return@withLock } val code = if (b) DbSynUtil.STATE_SUCCEED else DbSynUtil.STATE_SAVE_DB_FAIL withContext(Dispatchers.Main) { callback.invoke(code) } entryStateChangeFlow.emit(EntryStateChangeEvent(SAVE)) } } } } /** * save db * @param uploadDb true: upload db to cloud * @param isCreate true: is create new db, save that db and upload it. * @param needShowLoading do you need to display the load dialog box? * @param callback run in main thread */ fun saveDbByForeground( uploadDb: Boolean = true, isCreate: Boolean = false, needShowLoading: Boolean = true, callback: (Int) -> Unit = {} ) { Timber.d("saveDbByForeground") scope.launch(Dispatchers.Main) { mutex.withLock { Timber.d("保存前的数据库hash:${BaseApp.KDB.hashCode()},num = ${BaseApp.KDB!!.pm.entries.size}") val b = withContext(Dispatchers.IO) { return@withContext kdbHelper.save(BaseApp.KDB) } Timber.d("保存后的数据库hash:${BaseApp.KDB.hashCode()},num = ${BaseApp.KDB!!.pm.entries.size}") if (uploadDb) { val startTime = System.currentTimeMillis() if (needShowLoading) { showLoading() } val response = withContext(Dispatchers.IO) { return@withContext DbSynUtil.uploadSyn(BaseApp.dbRecord!!, isCreate) } Timber.i(response.msg) val endTime = System.currentTimeMillis() if (needShowLoading) { dismissLoading(if ((endTime - startTime) < MIN_TIME) MIN_TIME else 0L) } callback.invoke(response.code) entryStateChangeFlow.emit(EntryStateChangeEvent(SAVE)) return@launch } val code = if (b) DbSynUtil.STATE_SUCCEED else DbSynUtil.STATE_SAVE_DB_FAIL callback.invoke(code) entryStateChangeFlow.emit(EntryStateChangeEvent(SAVE)) } } } override fun init(context: Context?) { } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/feat/KdbOpenService.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.feat import android.content.Context import android.net.Uri import android.text.TextUtils import androidx.core.net.toFile import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.facade.template.IProvider import com.arialyy.frame.router.Routerfit import com.keepassdroid.Database import com.keepassdroid.database.PwDatabase import com.keepassdroid.database.PwDatabaseV4 import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroupV4 import com.keepassdroid.database.PwIconStandard import com.keepassdroid.database.helper.CreateDBHelper import com.keepassdroid.database.helper.KDBHandlerHelper import com.keepassdroid.utils.UriUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.DbHistoryRecord import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.NotificationUtil import com.lyy.keepassa.util.QuickUnLockUtil import com.lyy.keepassa.util.cloud.DbSynUtil import com.lyy.keepassa.util.cloud.OneDriveUtil import com.lyy.keepassa.util.cloud.WebDavUtil import com.lyy.keepassa.util.isCollectioned import com.lyy.keepassa.view.StorageType import com.lyy.keepassa.view.StorageType.AFS import com.lyy.keepassa.view.StorageType.DROPBOX import com.lyy.keepassa.view.StorageType.ONE_DRIVE import com.lyy.keepassa.view.StorageType.UNKNOWN import com.lyy.keepassa.view.StorageType.WEBDAV import com.lyy.keepassa.view.dialog.LoadingDialog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File /** * @Author laoyuyu * @Description * @Date 11:25 上午 2022/3/28 **/ @Route(path = "/service/kdbOpen") class KdbOpenService : IProvider { private var scope = MainScope() val openDbFlow = MutableSharedFlow(0) private val loadingDialog: LoadingDialog by lazy { Routerfit.create(DialogRouter::class.java).getLoadingDialog() } private fun showLoading() { scope.launch { if (loadingDialog.isVisible) { return@launch } loadingDialog.show() } } private fun dismissLoading() { scope.launch { loadingDialog.dismiss() } } /** * 创建数据库时创建默认的群组 */ private fun createDefaultGroup(context: Context) { val icons = arrayListOf(25, 47, 66, 62, 43) val names = context.resources.getStringArray(R.array.create_normal_group) val pm: PwDatabase = BaseApp.KDB.pm for ((index, name) in names.withIndex()) { val group = pm.createGroup() as PwGroupV4 group.initNewGroup(name, pm.newGroupId()) group.icon = PwIconStandard(icons[index]) // 需要设置默认图标 // 处理回收站 if (icons[index] == 43) { group.enableAutoType = false group.enableSearching = false group.isExpanded = false (BaseApp.KDB.pm as PwDatabaseV4).recycleBinUUID = group.uuid } pm.addGroupTo(group, pm.rootGroup) } } /** * create db * @param keyUri the db key * @param cloudPath cloud path eg: https://dev.jianguo.com * @param storageType [StorageType] */ fun createDb( dbName: String, localDbUri: Uri?, dbPass: String, keyUri: Uri?, cloudPath: String?, storageType: StorageType = UNKNOWN ) { val context = BaseApp.APP scope.launch(Dispatchers.IO) { try { showLoading() // 创建db val cdb = CreateDBHelper(context, dbName, localDbUri) if (keyUri != null) { cdb.setKeyFile(keyUri) BaseApp.dbKeyPath = QuickUnLockUtil.encryptStr(keyUri.toString()) } cdb.setPass(dbPass, null) val db = cdb.build() val success = checkCreatedDb(BaseApp.APP, localDbUri!!, dbPass, keyUri) if (!success){ scope.launch { openDbFlow.emit(null) } Timber.e("create db fail") return@launch } // 保存打开记录 BaseApp.KDB = db BaseApp.dbName = db.pm.name BaseApp.dbFileName = dbName BaseApp.dbPass = QuickUnLockUtil.encryptStr(dbPass) KpaUtil.setEmptyPass(dbPass.isEmpty()) KeepassAUtil.instance.subShortPass() // 创建默认群组 createDefaultGroup(context) BaseApp.dbVersion = "Keepass ${if (PwDatabase.isKDBExtension(dbName)) "3.x" else "4.x"}" BaseApp.isV4 = !PwDatabase.isKDBExtension(dbName) val record = DbHistoryRecord( time = System.currentTimeMillis(), type = storageType.name, localDbUri = localDbUri.toString(), cloudDiskPath = cloudPath, keyUri = keyUri?.toString() ?: "", dbName = dbName ) BaseApp.dbRecord = record BaseApp.isLocked = false // 保存并上传数据库到云端 KpaUtil.kdbHandlerService.saveDbByForeground( uploadDb = true, isCreate = true, needShowLoading = false ) { NotificationUtil.startDbOpenNotify(context) dismissLoading() scope.launch { openDbFlow.emit(BaseApp.KDB) } } } catch (e: Exception) { HitUtil.toaskOpenDbException(e) scope.launch { openDbFlow.emit(null) } Timber.e(e) } } } /** * check db */ private fun checkCreatedDb( context: Context, dbUri: Uri, dbPass: String, keyUri: Uri? ): Boolean { return KDBHandlerHelper.getInstance(context) .openDb(dbUri, dbPass, keyUri) != null } /** * open database * @param needShowLoading Do you need to display the load dialog box? */ fun openDb( context: Context, record: DbHistoryRecord, dbPass: String, needShowLoading: Boolean = true ) { Timber.d("打开数据库") if (needShowLoading) { showLoading() } scope.launch { val db: Database? = withContext(Dispatchers.IO) { var temp: Database? = null try { temp = when (record.getDbPathType()) { AFS -> { openDbFile(context, record.getDbUri(), dbPass, record.getDbKeyUri(), record) } DROPBOX -> { openDropboxDb(context, record, dbPass) } WEBDAV -> { openWebDavDb(context, record, dbPass) } ONE_DRIVE -> { openOneDriveDb(context, record, dbPass) } else -> null } } catch (e: Exception) { HitUtil.toaskOpenDbException(e) Timber.e(e) } temp } if (db != null) { BaseApp.isLocked = false BaseApp.KDB = db NotificationUtil.startDbOpenNotify(context) withContext(Dispatchers.IO) { var collectionNum = 0 val entrySet = hashSetOf() BaseApp.KDB.pm.entries.forEach { if ((it.value as PwEntryV4).isCollectioned()) { entrySet.add(it.value as PwEntryV4) collectionNum++ } } KpaUtil.kdbHandlerService.updateCollectionNum(collectionNum) KpaUtil.kdbHandlerService.updateCollectionEntries(entrySet) } } if (needShowLoading) { dismissLoading() } openDbFlow.emit(db) } } /** * 打开OneDrive数据库 */ private suspend fun openOneDriveDb( context: Context, record: DbHistoryRecord, dbPass: String ): Database? { val channel = Channel() var db: Database? = null OneDriveUtil.initOneDrive { if (!it) { scope.launch { channel.send(null) } return@initOneDrive } OneDriveUtil.loginCallback = object : OneDriveUtil.OnLoginCallback { override fun callback(success: Boolean) { scope.launch { if (!success) { channel.send(null) return@launch } val cacheFile = record.getDbUri() .toFile() val cloudFileInfo = OneDriveUtil.getFileInfo(record.cloudDiskPath!!) if (cacheFile.exists() && cloudFileInfo != null && OneDriveUtil.checkContentHash(cloudFileInfo.contentHash, record.getDbUri()) ) { Timber.i("文件存在,并且hash一致,将使用本地数据库") db = openDbFile(context, record.getDbUri(), dbPass, record.getDbKeyUri(), record) channel.send(db) return@launch } val cachePath = DbSynUtil.downloadOnly(context, record, Uri.fromFile(cacheFile)) db = if (TextUtils.isEmpty(cachePath)) { null } else { openDbFile(context, record.getDbUri(), dbPass, record.getDbKeyUri(), record) } channel.send(db) } } } OneDriveUtil.loadAccount() } repeat(1) { db = channel.receive() } return db } /** * 打开坚果云数据 */ private suspend fun openWebDavDb( context: Context, record: DbHistoryRecord, dbPass: String ): Database? { val dao = BaseApp.appDatabase.cloudServiceInfoDao() val serviceInfo = dao.queryServiceInfo(record.cloudDiskPath!!) if (serviceInfo == null) { HitUtil.toaskShort(context.getString(R.string.invalid_auth)) return null } WebDavUtil.login( serviceInfo.cloudPath, QuickUnLockUtil.decryption(serviceInfo.userName), QuickUnLockUtil.decryption(serviceInfo.password) ) val cacheFile = record.getDbUri() .toFile() val cloudFileInfo = DbSynUtil.getFileInfo(record) if (cacheFile.exists() && (cloudFileInfo == null || DbSynUtil.serviceModifyTime == cloudFileInfo.serviceModifyDate) ) { Timber.i("文件存在,并且云端文件时间和本地保存的时间一致,不会重新从云端下载数据库") return openDbFile(context, record.getDbUri(), dbPass, record.getDbKeyUri(), record) } val cachePath = DbSynUtil.downloadOnly(context, record, Uri.fromFile(cacheFile)) return if (TextUtils.isEmpty(cachePath)) { null } else { openDbFile(context, record.getDbUri(), dbPass, record.getDbKeyUri(), record) } } /** * 打开dropbox的数据库 */ private suspend fun openDropboxDb( context: Context, record: DbHistoryRecord, dbPass: String ): Database? { val cacheFile = record.getDbUri() .toFile() if (cacheFile.exists() && DbSynUtil.serviceModifyTime == DbSynUtil.getFileServiceModifyTime(record) ) { Timber.i("文件存在,并且云端文件时间和本地保存的时间一致,不会重新从云端下载数据库") return openDbFile(context, record.getDbUri(), dbPass, record.getDbKeyUri(), record) } val cachePath = DbSynUtil.downloadOnly(context, record, Uri.fromFile(cacheFile)) return if (TextUtils.isEmpty(cachePath)) { null } else { openDbFile(context, record.getDbUri(), dbPass, record.getDbKeyUri(), record) } } /** * 打开数据库文件 * @param dbUri 如果是AFS,dbUri表示本地文件的Uri;如果是云端文件,表示的是云端文件的路径 */ private suspend fun openDbFile( context: Context, dbUri: Uri, dbPass: String, keyUri: Uri?, record: DbHistoryRecord ): Database? { try { val db = KDBHandlerHelper.getInstance(context) .openDb(dbUri, dbPass, keyUri) if (db != null) { val dbName = UriUtil.getFileNameFromUri(context, dbUri) KpaUtil.setEmptyPass(dbPass.isEmpty()) BaseApp.dbPass = QuickUnLockUtil.encryptStr(dbPass) KeepassAUtil.instance.subShortPass() if (keyUri != null) { BaseApp.dbKeyPath = QuickUnLockUtil.encryptStr(keyUri.toString()) } else { BaseApp.dbKeyPath = null } // BaseApp.KDB?.clear(context) // 保存打开记录 BaseApp.KDB = db BaseApp.dbName = db.pm.name BaseApp.dbFileName = dbName BaseApp.dbVersion = "Keepass ${if (PwDatabase.isKDBExtension(dbName)) "3.x" else "4.x"}" BaseApp.isV4 = !PwDatabase.isKDBExtension(dbName) BaseApp.dbRecord = record KeepassAUtil.instance.saveLastOpenDbHistory(record) if (!BaseApp.isAFS()) { DbSynUtil.updateServiceModifyTime(record) } } return db } catch (e: Exception) { HitUtil.toaskOpenDbException(e) Timber.e(e) } return null } override fun init(context: Context?) { } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/feat/KpaSdkService.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.feat import android.app.Activity import android.app.Application import android.content.Context import androidx.preference.PreferenceManager import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.facade.template.IProvider import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.util.KeyStoreUtil.Companion.keyStorePass import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.AppUtils import com.blankj.utilcode.util.Utils import com.lyy.keepassa.KpaEventBusIndex import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseActivity import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.QuickUnLockUtil import com.lyy.keepassa.view.create.entry.CreateEntryActivity import com.tencent.mmkv.MMKV import com.zzhoujay.richtext.RichText import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 3:24 下午 2022/4/15 **/ @Route(path = "/service/kpaSdk") class KpaSdkService : IProvider { private val scope = MainScope() fun preInitSdk(context: Application) { MMKV.initialize(context) Utils.init(context) RoomFeature.init(context) // 开启kotlin 协程debug if (AppUtils.isAppDebug()) { System.setProperty("kotlinx.coroutines.debug", "on") Timber.plant(Timber.DebugTree()) ARouter.openLog() // 打印日志 ARouter.openDebug() // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险) } scope.launch(Dispatchers.IO) { // 初始化一下时间 KeepassAUtil.instance.isFastClick() keyStorePass = QuickUnLockUtil.getDbPass().toCharArray() val showStatusBar = PreferenceManager.getDefaultSharedPreferences(BaseApp.APP) .getBoolean(ResUtil.getString(R.string.set_key_title_show_state_bar), true) BaseActivity.showStatusBar = showStatusBar EventBus.builder().addIndex(KpaEventBusIndex()).installDefaultEventBus() listenerAppBackground() } } fun initThirdSdk(context: Context) { scope.launch(Dispatchers.IO) { RichText.initCacheDir(context) XLogFeature.init(context) } } private fun listenerAppBackground() { AppUtils.registerAppStatusChangedListener(object : Utils.OnAppStatusChangedListener { override fun onForeground(activity: Activity?) { } override fun onBackground(activity: Activity) { if (activity::class.java.name == CreateEntryActivity::class.java.name){ Timber.w("in CreateEntryActivity, not save") return } KpaUtil.kdbHandlerService.saveDbByBackground(true) XLogFeature.flush() } }) } override fun init(context: Context?) { } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/feat/RoomFeature.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.feat import android.content.Context import androidx.room.Room import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.DbMigration import com.lyy.keepassa.dao.AppDatabase import com.lyy.keepassa.util.QuickUnLockUtil import com.tencent.wcdb.database.SQLiteCipherSpec import com.tencent.wcdb.room.db.WCDBOpenHelperFactory object RoomFeature :IFeature{ override fun init(context: Context) { // 初始化数据库 val cipherSpec = SQLiteCipherSpec() // 指定加密方式,使用默认加密可以省略 .setPageSize(4096) .setKDFIteration(64000) val factory = WCDBOpenHelperFactory() .passphrase(QuickUnLockUtil.getDbPass().toByteArray()) // 指定加密DB密钥,非加密DB去掉此行 .cipherSpec(cipherSpec) // 指定加密方式,使用默认加密可以省略 .writeAheadLoggingEnabled(true) // 打开WAL以及读写并发,可以省略让Room决定是否要打开 .asyncCheckpointEnabled(true) // 打开异步Checkpoint优化,不需要可以省略 BaseApp.appDatabase = Room.databaseBuilder( context, AppDatabase::class.java, AppDatabase.DB_NAME ) .openHelperFactory(factory) .addMigrations(DbMigration.MIGRATION_2_3(), DbMigration.MIGRATION_3_4()) .build() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/feat/XLogFeature.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.feat import android.content.Context import com.blankj.utilcode.util.Utils import com.lyy.keepassa.BuildConfig import com.tencent.mars.xlog.Log import com.tencent.mars.xlog.Xlog import com.tencent.mars.xlog.Xlog.XLogConfig import org.joda.time.DateTime import timber.log.Timber import timber.log.Timber.DebugTree object XLogFeature : IFeature { init { System.loadLibrary("c++_shared") System.loadLibrary("marsxlog") } private val rootDir: String = Utils.getApp().filesDir.absolutePath private val logDir = "$rootDir/marssample/log" private val cacheDir = "$rootDir/marssample/cache" private val logName = "kpa" override fun init(context: Context) { val xlog = Xlog() Log.setLogImp(xlog) Log.setConsoleLogOpen(false) Log.appenderOpen(Xlog.LEVEL_VERBOSE, Xlog.AppednerModeAsync, cacheDir, logDir, logName, 0) setTimberPlant() } fun flush() { Timber.d("写日志到xlog中") Log.appenderFlush() } fun getLogName(): String { val data = DateTime(System.currentTimeMillis()) return "${logName}_${data.toString("yyyyMMdd")}.xlog" } fun getLogPath(): String { flush() return "${logDir}/${getLogName()}" } private fun setTimberPlant() { Timber.plant(object : DebugTree() { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { if (BuildConfig.DEBUG) { return } when (priority) { android.util.Log.DEBUG -> { Log.d(tag, message) } android.util.Log.VERBOSE -> { Log.v(tag, message) } android.util.Log.WARN -> { Log.w(tag, message) } android.util.Log.INFO -> { Log.i(tag, message) } android.util.Log.ERROR -> { Log.e(tag, message) } android.util.Log.ASSERT -> { Log.f(tag, message) } } } }) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/input/CandidatesAdapter.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.input import android.content.Context import android.view.View import android.widget.ImageView import android.widget.TextView import com.arialyy.frame.util.adapter.AbsHolder import com.arialyy.frame.util.adapter.AbsRVAdapter import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.R import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.service.input.CandidatesAdapter.Holder import com.lyy.keepassa.util.IconUtil /** * 候选条目适配器 */ class CandidatesAdapter( context: Context, data: List ) : AbsRVAdapter(context, data) { override fun getViewHolder( convertView: View, viewType: Int ): Holder { return Holder(convertView) } override fun setLayoutId(type: Int): Int { return R.layout.item_ime_entry } override fun bindData( holder: Holder, position: Int, item: SimpleItemEntity ) { val pwEntryV4 = item.obj as PwEntryV4 IconUtil.setEntryIcon(pwEntryV4, holder.icon) holder.text.text = pwEntryV4.title holder.itemView.isSelected = item.isSelected } class Holder(view: View) : AbsHolder(view) { val icon: ImageView = view.findViewById(R.id.icon) val text: TextView = view.findViewById(R.id.text) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/input/EntryOtherInfoAdapter.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.input import android.content.Context import android.text.method.HideReturnsTransformationMethod import android.text.method.PasswordTransformationMethod import android.view.View import android.widget.ImageView import android.widget.TextView import com.arialyy.frame.util.adapter.AbsHolder import com.arialyy.frame.util.adapter.AbsRVAdapter import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.R import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.service.input.EntryOtherInfoAdapter.Holder /** * @Author laoyuyu * @Description 填充其它信息适配器 * @Date 2020/10/25 **/ class EntryOtherInfoAdapter( context: Context, data: List ) : AbsRVAdapter(context, data) { class Holder(v: View) : AbsHolder(v) { val tvHint: TextView = v.findViewById(R.id.tvHint) val tvContent: TextView = v.findViewById(R.id.tvContent) val ivIcon: ImageView = v.findViewById(R.id.ivIcon) } override fun getViewHolder( convertView: View?, viewType: Int ): Holder { return Holder(convertView!!) } override fun setLayoutId(type: Int): Int { return R.layout.item_entry_other_info } override fun bindData( holder: Holder, position: Int, item: SimpleItemEntity ) { holder.tvHint.text = item.title holder.tvContent.text = item.content if (item.isProtected) { holder.ivIcon.visibility = View.VISIBLE holder.ivIcon.isSelected = !item.isSelected // 显示密码 if (item.isSelected) { holder.tvContent.transformationMethod = PasswordTransformationMethod.getInstance() } else { holder.tvContent.transformationMethod = HideReturnsTransformationMethod.getInstance() } holder.ivIcon.setOnClickListener { item.isSelected = !item.isSelected holder.ivIcon.isSelected = item.isSelected notifyItemChanged(position) } return } holder.ivIcon.visibility = View.GONE } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/input/EntryOtherInfoDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.input import android.os.Bundle import androidx.recyclerview.widget.LinearLayoutManager import com.arialyy.frame.util.adapter.RvItemClickSupport import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseActivity import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.databinding.DialogOtherInfoBinding import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.event.FillInfoEvent import com.lyy.keepassa.util.HitUtil import org.greenrobot.eventbus.EventBus import java.util.UUID /** * @Author laoyuyu * @Description 其它信息 * @Date 2020/10/25 **/ class EntryOtherInfoDialog : BaseActivity() { companion object { val KEY_DATA = "KEY_DATA" } private var entryUUID: UUID? = null private val moreInfoList = arrayListOf() private val moreInfoAdapter by lazy { EntryOtherInfoAdapter(this, moreInfoList) } private lateinit var pwEntry: PwEntryV4 override fun setLayoutId(): Int { return R.layout.dialog_other_info } override fun initData(savedInstanceState: Bundle?) { super.initData(savedInstanceState) entryUUID = intent.getSerializableExtra(KEY_DATA) as UUID? if (entryUUID == null) { HitUtil.toaskShort(getString(R.string.hint_not_nore_info)) finish() return } pwEntry = BaseApp.KDB!!.pm.entries[entryUUID] as PwEntryV4 binding.rvList.adapter = moreInfoAdapter binding.rvList.setHasFixedSize(true) binding.rvList.layoutManager = LinearLayoutManager(this) RvItemClickSupport.addTo(binding.rvList) .setOnItemClickListener { _, position, _ -> EventBus.getDefault() .post(FillInfoEvent(moreInfoList[position].content)) finish() } setEntry(pwEntry) } private fun setEntry(pwEntry: PwEntryV4?) { if (pwEntry == null) { HitUtil.toaskShort(getString(R.string.hint_not_nore_info)) finish() return } val list = getStrList(pwEntry) if (list.isNullOrEmpty()) { HitUtil.toaskShort(getString(R.string.hint_not_nore_info)) finish() return } moreInfoList.clear() moreInfoList.addAll(list) moreInfoAdapter.notifyDataSetChanged() } private fun getStrList(pwEntry: PwEntryV4): ArrayList { val list = arrayListOf() if (!pwEntry.url.isNullOrEmpty()) { val url = SimpleItemEntity() url.title = getString(R.string.url) url.content = pwEntry.url url.isProtected = false list.add(url) } if (!pwEntry.notes.isNullOrEmpty()) { val notes = SimpleItemEntity() notes.title = getString(R.string.notice) notes.content = pwEntry.notes notes.isProtected = false list.add(notes) } if (pwEntry.strings.isNotEmpty()) { pwEntry.strings.forEach { if (it.value.toString().isEmpty() || it.key.equals(PwEntryV4.STR_TITLE, true) || it.key.equals(PwEntryV4.STR_USERNAME, true) || it.key.equals(PwEntryV4.STR_PASSWORD, true)){ return@forEach } val item = SimpleItemEntity() item.title = it.key item.content = it.value.toString() item.isSelected = it.value.isProtected item.isProtected = it.value.isProtected list.add(item) } } return list } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/input/InputIMEService.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.service.input import android.content.Context import android.content.Intent import android.inputmethodservice.InputMethodService import android.os.Build import android.os.Bundle import android.os.IBinder import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.autofill.AutofillManager import android.view.inputmethod.EditorInfo import android.view.inputmethod.InlineSuggestionsRequest import android.view.inputmethod.InlineSuggestionsResponse import android.view.inputmethod.InputConnection import android.view.inputmethod.InputMethodManager import android.widget.ImageView import android.widget.TextView import androidx.appcompat.widget.AppCompatImageView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.ResUtil import com.arialyy.frame.util.adapter.RvItemClickSupport import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.event.FillInfoEvent import com.lyy.keepassa.router.ActivityRouter import com.lyy.keepassa.router.ServiceRouter import com.lyy.keepassa.service.autofill.W3cHints import com.lyy.keepassa.util.EventBusHelper import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.util.LanguageUtil import com.lyy.keepassa.util.NotificationUtil import com.lyy.keepassa.util.totp.OtpUtil import com.lyy.keepassa.util.isCanOpenQuickLock import com.lyy.keepassa.view.launcher.LauncherActivity import com.lyy.keepassa.view.main.QuickUnlockActivity import com.lyy.keepassa.view.search.CommonSearchActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode.MAIN import timber.log.Timber /** * 输入法 * https://developer.android.com/guide/topics/text/creating-input-method?hl=zh-cn */ class InputIMEService : InputMethodService(), View.OnClickListener { private var appPkgName: String? = "" private var ic: InputConnection? = null private var curEntry: PwEntry? = null private lateinit var candidatesList: RecyclerView private val candidatesData = arrayListOf() private lateinit var candidatesAdapter: CandidatesAdapter private var imeOption = EditorInfo.IME_ACTION_GO private var curImeView: View? = null private var scope = MainScope() /** * 当 IME 首次显示时,系统会调用 onCreateInputView() 回调。在此方法的实现中,您可以创建要在 IME 窗口中显示的布局,并将布局返回系统。 */ override fun onCreateInputView(): View { val layout = LayoutInflater.from(this) .inflate(R.layout.layout_kpa_ime, null) as ViewGroup candidatesList = layout.findViewById(R.id.rvContent) for (i in 0..layout.childCount) { val child = layout.getChildAt(i) if (child != null && (child is ImageView || child is TextView) && child.isClickable ) { child.setOnClickListener(this) } } curImeView = layout initCandidatesLayout() layout.findViewById(R.id.ivSearch).setOnClickListener { Routerfit.create(ActivityRouter::class.java).toCommonSearch() } scope = MainScope() scope.launch { CommonSearchActivity.searchFlow.collectLatest { curEntry = it showEntryList(arrayListOf().apply { add(it) }) } } return layout } private fun initCandidatesLayout() { candidatesAdapter = CandidatesAdapter(this, candidatesData) candidatesList.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false) candidatesList.setHasFixedSize(true) candidatesList.adapter = candidatesAdapter RvItemClickSupport.addTo(candidatesList) .setOnItemClickListener(object : RvItemClickSupport.OnItemClickListener { var lastPosition = 0 override fun onItemClicked( recyclerView: RecyclerView?, position: Int, v: View? ) { Timber.d("select item, position = $position") val lastItemEntity = candidatesData[lastPosition] val curItemEntity = candidatesData[position] lastItemEntity.isSelected = false curItemEntity.isSelected = true candidatesAdapter.notifyItemChanged(lastPosition) candidatesAdapter.notifyItemChanged(position) lastPosition = position curEntry = curItemEntity.obj as PwEntry } }) } /** * 输入法被唤起,开始输入 */ override fun onStartInputView( info: EditorInfo?, restarting: Boolean ) { super.onStartInputView(info, restarting) EventBusHelper.reg(this) imeOption = info?.imeOptions ?: EditorInfo.IME_ACTION_GO candidatesData.clear() candidatesAdapter.notifyDataSetChanged() candidatesList.visibility = View.GONE curEntry = null ic = currentInputConnection Timber.d("pkgName = ${info?.packageName}, inputType = ${info?.inputType}, fieldName = ${info?.fieldName}, fieldId = ${info?.fieldId}") appPkgName = info?.packageName if (W3cHints.isBrowser(appPkgName) && !checkCanOpenAutoFill()) { if (curImeView == null) { HitUtil.toaskLong(ResUtil.getString(R.string.ime_hint_open_auto_fill)) return } HitUtil.snackLong( curImeView!!, ResUtil.getString(R.string.ime_hint_open_auto_fill), ResUtil.getString(R.string.setting) ) { Routerfit.create(ActivityRouter::class.java, this).toAppSetting( scrollKey = getString(R.string.set_open_auto_fill) ) } return } showEntryList(searchEntry(appPkgName)) } private fun checkCanOpenAutoFill(): Boolean { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { Timber.w("the sdk version ${Build.VERSION.SDK_INT} less than O") return false } val am = getSystemService(AutofillManager::class.java) if (!am.isAutofillSupported) { Timber.w("it not support autofill") return false } if (!am.hasEnabledAutofillServices()) { Timber.w("The auto-fill service is not turned on") return false } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && (am.autofillServiceComponentName?.packageName?.equals(packageName) == false) ) { Timber.w("The auto-fill service is not turned on") return false } return true } override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest? { Timber.d("onCreateInlineSuggestionsRequest") return super.onCreateInlineSuggestionsRequest(uiExtras) } override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean { Timber.d("onInlineSuggestionsResponse") return super.onInlineSuggestionsResponse(response) } /** * 填充数据,如果有多个条目,启动对话框,让用户选择特定的条目 */ override fun onClick(v: View) { when (v.id) { // 锁定 R.id.btLock -> { if (BaseApp.KDB == null || BaseApp.isLocked) { return } if (appPkgName == packageName) { LauncherActivity.startLauncherActivity(this, Intent.FLAG_ACTIVITY_NEW_TASK) } BaseApp.isLocked = true NotificationUtil.startDbLocked(this) if (BaseApp.APP.isCanOpenQuickLock()) { return } curEntry = null candidatesData.clear() Routerfit.create(ServiceRouter::class.java).getDbSaveService().clearDb() Timber.d("数据库已锁定") HitUtil.toaskShort(getString(R.string.notify_db_locked)) return } // 用户名 R.id.btAccount -> { if (!dbIsOpen()) { return } showEntryList(searchEntry(appPkgName)) curEntry?.let { val userName = KdbUtil.getUserName(it) Timber.d("fill user name: $userName") fillData(userName) return } } // 密码 R.id.btPass -> { if (!dbIsOpen()) { return } showEntryList(searchEntry(appPkgName)) curEntry?.let { val pass = KdbUtil.getPassword(it) Timber.d("fill password: $pass") fillData(pass) return } } // 关键软键盘 R.id.btClose -> { requestHideSelf(InputMethodManager.HIDE_NOT_ALWAYS) } // 选择输入法 R.id.btChangeIme -> { val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager imm.showInputMethodPicker() } // totp R.id.btTotp -> { if (!dbIsOpen()) { return } showEntryList(searchEntry(appPkgName)) if (curEntry == null) { return } val totp = OtpUtil.getOtpPass(curEntry as PwEntryV4) if (totp.second.isNullOrEmpty()) { HitUtil.toaskShort(getString(R.string.no_totp_token)) return } else { fillData(totp.second!!) } } // 其它信息 R.id.btOtherInfo -> { if (!dbIsOpen()) { return } showEntryList(searchEntry(appPkgName)) showMoreInfoDialog() } // 回退键 R.id.btBackspace -> { ic?.deleteSurroundingText(1, 0) } // 回车键 R.id.btEnter -> { ic?.performEditorAction(imeOption) } } } /** * 显示更多信息的对话框,点击item自动填充 */ private fun showMoreInfoDialog() { startActivity(Intent(this, EntryOtherInfoDialog::class.java).apply { putExtra(EntryOtherInfoDialog.KEY_DATA, curEntry?.uuid) flags = Intent.FLAG_ACTIVITY_NEW_TASK }) } @Subscribe(threadMode = MAIN) fun onFillOtherInfo(event: FillInfoEvent) { Timber.d("getOtherInfo, info = ${event.infoStr}") MainScope().launch { withContext(Dispatchers.IO) { delay(600) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { requestShowSelf(InputMethodManager.SHOW_IMPLICIT) } else { try { val inm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val field = InputMethodService::class.java.getDeclaredField("mToken") field.isAccessible = true inm.showSoftInputFromInputMethod( field.get(this@InputIMEService) as IBinder, InputMethodManager.SHOW_IMPLICIT ) } catch (e: Exception) { Timber.e(e) } } withContext(Dispatchers.IO) { delay(600) } fillData(event.infoStr.toString()) } } override fun onDestroy() { super.onDestroy() EventBusHelper.unReg(this) scope.cancel() } /** * 如果有多个条目,显示条目列表 */ private fun showEntryList(entries: List) { if (entries.isNullOrEmpty()) { candidatesList.visibility = View.GONE return } if (entries.size == 1) { candidatesList.visibility = View.GONE curEntry = entries[0] return } candidatesData.clear() candidatesList.visibility = View.VISIBLE entries.forEachIndexed { index, pwEntry -> val item = SimpleItemEntity() item.title = pwEntry.title item.obj = pwEntry if (index == 0) { item.isSelected = true curEntry = pwEntry } candidatesData.add(item) } candidatesAdapter.notifyDataSetChanged() } /** * 填充数据 */ private fun fillData(text: String) { ic?.commitText(text, 0) } /** * 判断数据库是否打开,没有打开,启动登陆界面,如果是快速锁定,打开快速解锁界面 */ private fun dbIsOpen(): Boolean { if (BaseApp.KDB == null || BaseApp.isLocked) { if (BaseApp.KDB == null) { LauncherActivity.startLauncherActivity(this, Intent.FLAG_ACTIVITY_NEW_TASK) return false } if (BaseApp.APP.isCanOpenQuickLock()) { QuickUnlockActivity.startQuickUnlockActivity(this, Intent.FLAG_ACTIVITY_NEW_TASK) } return false } return true } /** * 搜索条目 */ private fun searchEntry(pkgName: String?): List { if (pkgName.isNullOrEmpty() || BaseApp.KDB == null) { return emptyList() } val listStorage = ArrayList() if (W3cHints.isBrowser(pkgName)) { Timber.d("curDomain = ${W3cHints.curDomainUrl}") KdbUtil.searchEntriesByDomain(W3cHints.curDomainUrl, listStorage) return listStorage } KdbUtil.searchEntriesByPackageName(pkgName, listStorage) return listStorage } override fun attachBaseContext(newBase: Context?) { super.attachBaseContext(LanguageUtil.setLanguage(newBase!!, BaseApp.currentLang)) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/service/play/PlayServiceUtil.kt ================================================ package com.lyy.keepassa.service.play import android.app.Activity import com.android.billingclient.api.AcknowledgePurchaseParams import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.BillingClient.SkuType import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingResult import com.android.billingclient.api.Purchase import com.android.billingclient.api.Purchase.PurchaseState import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.SkuDetails import com.android.billingclient.api.SkuDetailsParams import com.android.billingclient.api.SkuDetailsResult import com.android.billingclient.api.querySkuDetails import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.ToastUtils import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber /** * https://developer.android.com/google/play/billing/integrate?hl=zh-cn#fetch * @Author laoyuyu * @Description * @Date 1:58 下午 2022/1/20 **/ class PlayServiceUtil { private var isConnected = false /** * 购买回调 */ private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> // To be implemented in a later section. Timber.d("result = ${billingResult}, purchases = $purchases") if (billingResult.responseCode == BillingResponseCode.OK && purchases != null) { Timber.d("purchases success") for (purchase in purchases) { handlePurchase(purchase) } } else if (billingResult.responseCode == BillingResponseCode.USER_CANCELED) { Timber.d("cancel") // Handle an error caused by a user cancelling the purchase flow. } else { // Handle any other error codes. } } private var billingClient = BillingClient.newBuilder(BaseApp.APP) .setListener(purchasesUpdatedListener) .enablePendingPurchases() .build() /** * 确认非消耗型商品的购买交易 */ private fun handlePurchase(purchase: Purchase) { if (purchase.purchaseState == PurchaseState.PURCHASED) { if (!purchase.isAcknowledged) { val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) billingClient.acknowledgePurchase(acknowledgePurchaseParams.build()) { Timber.d("code = ${it.responseCode}") } } } } /** * connect to play */ fun connectPlay(callback: (Boolean) -> Unit) { // loadingDialog.show() billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: BillingResult) { // loadingDialog.dismiss() if (billingResult.responseCode == BillingResponseCode.OK) { // The BillingClient is ready. You can query purchases here. isConnected = true callback.invoke(true) return } ToastUtils.showLong(ResUtil.getString(R.string.error_connect_play)) callback.invoke(false) } override fun onBillingServiceDisconnected() { // Try to restart the connection on the next request to // Google Play by calling the startConnection() method. isConnected = false } }) } /** * 普通商品 */ suspend fun queryInappSkuDetails(): SkuDetailsResult { val skuList = ArrayList() skuList.add("01_rqvz3usue52wu77d7dqhywfjc23ki9ae") skuList.add("02_i5phiiuoccqxrwzhu72caqpfjz2ayrsx") skuList.add("03_9i7njrgcrr4rev554hnz777a7uhxxy2w") val params = SkuDetailsParams.newBuilder() params.setSkusList(skuList).setType(SkuType.INAPP) // leverage querySkuDetails Kotlin extension function return withContext(Dispatchers.IO) { val detail = billingClient.querySkuDetails(params.build()) val skuList = detail.skuDetailsList val skuResult = detail.billingResult Timber.d("get inapp sku success, size = ${skuList?.size}") return@withContext detail } // Process the result. } /** * 订阅商品 */ suspend fun querySubSkuDetails(): SkuDetailsResult { val skuList = ArrayList() skuList.add("11_wdvkaaymasi93y7r5hnrcm7unccqv2a7") skuList.add("21_rgwbquffzynsq23ca3239npc7r7fddnm") skuList.add("14_v9bgagya5emfagnbxizhmeoj223wfsuq") skuList.add("13_w2vn9xn7k9zqga29jrxndrtut4kifipo") val params = SkuDetailsParams.newBuilder() params.setSkusList(skuList).setType(SkuType.SUBS) // leverage querySkuDetails Kotlin extension function return withContext(Dispatchers.IO) { val detail = billingClient.querySkuDetails(params.build()) val skuList = detail.skuDetailsList val skuResult = detail.billingResult Timber.d("get sub sku success, size = ${skuList?.size}") return@withContext detail } } /** * 开始支付流程 */ suspend fun startPlayFlow(ac: Activity, skuDetail: SkuDetails) { val flowParams = BillingFlowParams.newBuilder() .setSkuDetails(skuDetail) .build() val responseCode = billingClient.launchBillingFlow(ac, flowParams).responseCode } fun onDestroy() { billingClient.endConnection() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/AutoLockDbUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.content.Context import androidx.core.content.edit import androidx.preference.PreferenceManager import androidx.work.CoroutineWorker import androidx.work.ExistingWorkPolicy.REPLACE import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager import androidx.work.WorkerParameters import com.arialyy.frame.util.StringUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.Constance import timber.log.Timber import java.util.concurrent.TimeUnit /** * 自动锁定数据库工具 */ class AutoLockDbUtil private constructor() { private var requestTag = "LockDbWork" private val KEY_NAME = "LockTimer" private val KEY_LAST_START_TIME = "LastStartTime" private val sp = BaseApp.APP.getSharedPreferences(Constance.PRE_FILE_NAME, Context.MODE_PRIVATE) private val TIMER_TAG = "AutoLockDbTimer" private val manager by lazy { WorkManager.getInstance(BaseApp.APP) } companion object { private val instance: AutoLockDbUtil by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { AutoLockDbUtil() } fun get(): AutoLockDbUtil { return instance } } /** * 重置定时器 */ fun resetTimer() { Timber.d( "resetTimer") startLockWorker() } /** * cancel timer */ fun cancelTimer(){ manager.cancelAllWorkByTag(TIMER_TAG) } /** * 启动定时器 */ private fun startTimer(workRequest: OneTimeWorkRequest) { val lastStartTime = sp.getLong(KEY_LAST_START_TIME, -1) if (lastStartTime > 0 && System.currentTimeMillis() - lastStartTime <= 3000) { return } Timber.d( "开始自动锁定") sp.edit(true) { putLong(KEY_LAST_START_TIME, System.currentTimeMillis()) } // https://developer.android.com/topic/libraries/architecture/workmanager/how-to/managing-work?hl=zh-cn // 唯一任务 manager.enqueueUniqueWork( "autoLockDb", REPLACE, // 如果有新任务,则取消以前的任务 workRequest ) } /** * 立即启动定时器 */ fun startLockWorkerNow() { val wordRequest = OneTimeWorkRequest.Builder(LockWorker::class.java) .addTag(requestTag) .build() startTimer(wordRequest) } /** * 启动锁定数据库的工作线程 */ private fun startLockWorker() { val time = PreferenceManager.getDefaultSharedPreferences(BaseApp.APP) .getString(BaseApp.APP.getString(R.string.set_key_auto_lock_db_time), "300")!! .toInt() // val time = 10 val wordRequest = OneTimeWorkRequest.Builder(LockWorker::class.java) .addTag(TIMER_TAG) .setInitialDelay(time.toLong(), TimeUnit.SECONDS) .build() startTimer(wordRequest) } /** * 锁定数据库线程任务 * 如果开启了快速解锁,进入快速解锁界面 * 如果没有开启快速解锁,直接进入启动页,并清空数据库 */ class LockWorker( appContext: Context, workerParams: WorkerParameters ) : CoroutineWorker(appContext, workerParams) { override suspend fun doWork(): Result { KeepassAUtil.instance.lock() return Result.success() } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/BarUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.app.Activity import android.content.Context import android.content.Context.WINDOW_SERVICE import android.graphics.PixelFormat import android.graphics.Rect import android.os.Build import android.view.Gravity import android.view.View import android.view.ViewTreeObserver import android.view.Window import android.view.WindowManager import com.arialyy.frame.util.ResUtil import com.gyf.immersionbar.ImmersionBar import com.lyy.keepassa.R import timber.log.Timber import java.lang.reflect.Field import java.lang.reflect.Method /** * @author laoyuyu * @date 2021/3/22 */ object BarUtil { private val TAG = javaClass.simpleName private const val MIUI = 1 private const val FLYME = 2 private const val ANDROID_M = 3 fun showStatusBar( activity: Activity, show: Boolean ) { val window = activity.window var vis: Int = window.decorView.systemUiVisibility if (show) { // setStatusBarLightMode(activity) window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) window.statusBarColor = ResUtil.getColor(R.color.background_color) window.navigationBarColor = ResUtil.getColor(R.color.background_color) window.decorView.systemUiVisibility = (WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or View.SYSTEM_UI_FLAG_VISIBLE or setMode() ) // ImmersionBar.with(activity) // .statusBarColor(R.color.background_color) // .autoStatusBarDarkModeEnable(true, 0.2f) //自动状态栏字体变色,必须指定状态栏颜色才可以自动变色哦 // .flymeOSStatusBarFontColor(R.color.text_black_color) // .fitsSystemWindows(true) //// .hideBar(FLAG_HIDE_STATUS_BAR) // .autoNavigationBarDarkModeEnable(true, 0.2f) // 自动导航栏图标变色,必须指定导航栏颜色才可以自动变色哦 // .navigationBarColor(R.color.background_color) // .statusBarDarkFont( // true, 0.2f // ) //原理:如果当前设备支持状态栏字体变色,会设置状态栏字体为黑色,如果当前设备不支持状态栏字体变色,会使当前状态栏加上透明度,否则不执行透明度 // .init() return } window.navigationBarColor = ResUtil.getColor(R.color.background_color) vis = vis.or(setMode()) vis = vis.or(View.SYSTEM_UI_FLAG_FULLSCREEN) vis = vis.or(View.INVISIBLE) window.decorView.systemUiVisibility = vis } private fun setMode(): Int { var mode = 0 val isNight = KeepassAUtil.instance.isNightMode() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mode = mode.or(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mode = mode.or(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) } return mode } /** * 直接读取系统里状态栏高度的值,但是无法判断状态栏是否显示 */ fun getStatusBarHeight(context: Context):Int { var height = 0 //获取status_bar_height资源的ID val resourceId: Int = context.resources.getIdentifier("status_bar_height", "dimen", "android") if (resourceId > 0) { //根据资源ID获取响应的尺寸值 height = context.resources.getDimensionPixelSize(resourceId) } return height } /** * 状态栏是否隐藏 * @return true 状态栏没有隐藏 */ fun statusBarIsVisible(window: Window):Boolean{ val rectangle = Rect() window.decorView.getWindowVisibleDisplayFrame(rectangle) val statusBarHeight: Int = rectangle.top return statusBarHeight != 0 } /** * @param callback true 状态栏隐藏; false状态栏显示 */ private fun statusBarIsVisible( context: Context, callback: (Boolean) -> Unit ) { val wm = context.getSystemService(WINDOW_SERVICE) as WindowManager? val p = WindowManager.LayoutParams() p.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY //放在左上角 p.gravity = Gravity.START or Gravity.TOP // 不可触摸,不可获得焦点 p.flags = (WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) p.width = 1 p.height = WindowManager.LayoutParams.MATCH_PARENT p.format = PixelFormat.TRANSPARENT val helperWnd = View(context) //View helperWnd; val vto: ViewTreeObserver = helperWnd.viewTreeObserver vto.addOnGlobalLayoutListener { val windowParams = IntArray(2) val screenParams = IntArray(2) helperWnd.getLocationInWindow(windowParams) helperWnd.getLocationOnScreen(screenParams) // 如果状态栏隐藏,返回0,如果状态栏显示则返回高度 Timber.d( "getStatusBarHeight = " + (screenParams[1] - windowParams[1])) val b = screenParams[1] - windowParams[1] == 0 callback.invoke(b) } wm!!.addView(helperWnd, p) } /** * 状态栏亮色模式,设置状态栏黑色文字、图标, * 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android * * @return 1:MIUUI 2:Flyme 3:android6.0 */ fun setStatusBarLightMode(activity: Activity): Int { var result = 0 when { MIUISetStatusBarLightMode(activity, true) -> { result = MIUI } FLYMESetStatusBarLightMode(activity.window, true) -> { result = FLYME } Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> { activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR result = ANDROID_M } } return result } /** * 已知系统类型时,设置状态栏黑色文字、图标。 * 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android * * @param type 1:MIUUI 2:Flyme 3:android6.0 */ fun setStatusBarLightMode( activity: Activity, type: Int ) { when (type) { MIUI -> { MIUISetStatusBarLightMode(activity, true) } FLYME -> { FLYMESetStatusBarLightMode(activity.window, true) } ANDROID_M -> { activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } } } /** * 状态栏暗色模式,清除MIUI、flyme或6.0以上版本状态栏黑色文字、图标 */ fun setStatusBarDarkMode( activity: Activity, type: Int ) { when (type) { MIUI -> { MIUISetStatusBarLightMode(activity, false) } FLYME -> { FLYMESetStatusBarLightMode(activity.window, false) } ANDROID_M -> { activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE } } } /** * 设置状态栏图标为深色和魅族特定的文字风格 * 可以用来判断是否为Flyme用户 * * @param window 需要设置的窗口 * @param dark 是否把状态栏文字及图标颜色设置为深色 * @return boolean 成功执行返回true */ private fun FLYMESetStatusBarLightMode( window: Window?, dark: Boolean ): Boolean { var result = false if (window != null) { try { val lp: WindowManager.LayoutParams = window.attributes val darkFlag: Field = WindowManager.LayoutParams::class.java .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON") val meizuFlags: Field = WindowManager.LayoutParams::class.java .getDeclaredField("meizuFlags") darkFlag.isAccessible = true meizuFlags.isAccessible = true val bit: Int = darkFlag.getInt(null) var value: Int = meizuFlags.getInt(lp) value = if (dark) { value or bit } else { value and bit.inv() } meizuFlags.setInt(lp, value) window.attributes = lp result = true } catch (e: Exception) { } } return result } /** * 需要MIUIV6以上 * * @param dark 是否把状态栏文字及图标颜色设置为深色 * @return boolean 成功执行返回true */ private fun MIUISetStatusBarLightMode( activity: Activity, dark: Boolean ): Boolean { var result = false val window: Window? = activity.window if (window != null) { val clazz: Class<*> = window::class.java try { var darkModeFlag = 0 val layoutParams = Class.forName("android.view.MiuiWindowManager\$LayoutParams") val field: Field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE") darkModeFlag = field.getInt(layoutParams) val extraFlagField: Method = clazz.getMethod( "setExtraFlags", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType ) if (dark) { extraFlagField.invoke(window, darkModeFlag, darkModeFlag) //状态栏透明且黑色字体 } else { extraFlagField.invoke(window, 0, darkModeFlag) //清除黑色字体 } result = true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //开发版 7.7.13 及以后版本采用了系统API,旧方法无效但不会报错,所以两个方式都要加上 if (dark) { activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } else { activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE } } } catch (e: java.lang.Exception) { Timber.e(e) } } return result } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/ClipboardUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.content.ClipData import android.content.ClipboardManager import android.content.Context import androidx.preference.PreferenceManager import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager import androidx.work.Worker import androidx.work.WorkerParameters import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import java.util.concurrent.TimeUnit /** * 剪切板工具 */ class ClipboardUtil private constructor() { private val clipManager: ClipboardManager = BaseApp.APP.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager companion object { private const val CLIP_LABEL = "CLIP_LABEL" private var instance: ClipboardUtil? = null get() { if (field == null) { field = ClipboardUtil() } return field } //细心的小伙伴肯定发现了,这里不用getInstance作为为方法名,是因为在伴生对象声明时,内部已有getInstance方法,所以只能取其他名字 fun get(): ClipboardUtil { return instance!! } } /** * 将数据拷贝到剪切板中,并在30s后清空剪切板 */ public fun copyDataToClip(string: String) { val itemData = ClipData.newPlainText(CLIP_LABEL, string) clipManager.setPrimaryClip(itemData) val time = PreferenceManager.getDefaultSharedPreferences(BaseApp.APP) .getString( BaseApp.APP.getString( R.string.set_key_clean_clip_time ), "30" )!!.toLong() val wordRequest = OneTimeWorkRequest.Builder(CleanClipWord::class.java) .setInitialDelay(time, TimeUnit.SECONDS) .build() WorkManager.getInstance(BaseApp.APP) .enqueue(wordRequest) } /** * 清空剪切板 */ fun cleanClipboard() { clipManager.setPrimaryClip(ClipData.newPlainText(null, "")) } class CleanClipWord( appContext: Context, workerParams: WorkerParameters ) : Worker(appContext, workerParams) { override fun doWork(): Result { get().cleanClipboard() return Result.success() } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/CommonKVStorage.kt ================================================ package com.lyy.keepassa.util import android.os.Parcelable import com.tencent.mmkv.MMKV object CommonKVStorage : KVStorage { private val kv: MMKV by lazy { MMKV.mmkvWithID("com.lyy.kpa", MMKV.MULTI_PROCESS_MODE) } override fun put(key: String, value: String): Boolean = kv.encode(key, value) override fun getString(key: String, defaultValue: String): String = kv.decodeString(key, defaultValue) ?: "" override fun put(key: String, value: Boolean): Boolean = kv.encode(key, value) override fun getBoolean(key: String, defaultValue: Boolean): Boolean = kv.decodeBool(key, defaultValue) override fun put(key: String, value: Int): Boolean = kv.encode(key, value) override fun getInt(key: String, defaultValue: Int): Int = kv.decodeInt(key, defaultValue) override fun put(key: String, value: Long): Boolean = kv.encode(key, value) override fun getLong(key: String, defaultValue: Long): Long = kv.decodeLong(key, defaultValue) override fun put(key: String, value: Float): Boolean = kv.encode(key, value) override fun getFloat(key: String, defaultValue: Float): Float = kv.decodeFloat(key, defaultValue) override fun put(key: String, value: Double): Boolean = kv.encode(key, value) override fun getDouble(key: String, defaultValue: Double): Double = kv.decodeDouble(key, defaultValue) override fun put(key: String, value: Set): Boolean = kv.encode(key, value) override fun getStringSet(key: String, defaultValue: Set): Set = kv.decodeStringSet(key, defaultValue) ?: mutableSetOf() override fun put(key: String, value: Parcelable): Boolean = kv.encode(key, value) override fun get(key: String?, tClass: Class?, defaultValue: T?): T? = kv.decodeParcelable(key, tClass, defaultValue) override fun containsKey(key: String): Boolean = kv.containsKey(key) override fun remove(key: String) = kv.removeValueForKey(key) override fun clean() = kv.clearAll() } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/EncryptUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import com.google.crypto.tink.Aead import com.google.crypto.tink.KeysetHandle import com.google.crypto.tink.aead.AeadKeyTemplates import com.google.crypto.tink.config.TinkConfig /** * 加密工具 * https://developer.android.com/topic/security/data */ object EncryptUtil { var AEAD: Aead? = null /** * * 加密字符串,并将其保存到配置文件中 * * @param str 明文 * @param pass 密钥,一般是放在服务器端,或放在keystore * @return 密文 */ fun encryptStr( str: String, pass: String ): String? { val temp = getAead()?.encrypt( str.toByteArray(Charsets.UTF_8), pass.toByteArray(Charsets.UTF_8) ) if (temp != null) { return String(temp, Charsets.UTF_8) } return null } /** * 解密字符串 * * @param str 密文 * @param pass 密钥,一般是放在服务器端,或放在keystore * @return 明文 */ fun decryptionStr( str: String, pass: String ): String? { val temp = getAead()?.decrypt( str.toByteArray(Charsets.UTF_8), pass.toByteArray(Charsets.UTF_8) ) if (temp != null) { return String(temp, Charsets.UTF_8) } return null } /** * @param dbPass keepass数据库密码 */ fun getAead(): Aead? { if (AEAD == null) { TinkConfig.register() // 生成密钥 val keysetHandle = KeysetHandle.generateNew(AeadKeyTemplates.AES256_GCM) AEAD = keysetHandle.getPrimitive(Aead::class.java) } return AEAD } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/EventBusHelper.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.app.Activity import android.inputmethodservice.InputMethodService import androidx.fragment.app.Fragment import com.lyy.keepassa.view.menu.IPopMenu import org.greenrobot.eventbus.EventBus object EventBusHelper { fun reg(inputService: InputMethodService){ if (!EventBus.getDefault().isRegistered(inputService)) { EventBus.getDefault().register(inputService) } } fun unReg(inputService: InputMethodService) { if (EventBus.getDefault().isRegistered(inputService)) { EventBus.getDefault().unregister(inputService) } } fun reg(activity: Activity) { if (!EventBus.getDefault().isRegistered(activity)) { EventBus.getDefault().register(activity) } } fun unReg(activity: Activity) { if (EventBus.getDefault().isRegistered(activity)) { EventBus.getDefault().unregister(activity) } } fun reg(fragment: Fragment) { if (!EventBus.getDefault().isRegistered(fragment)) { EventBus.getDefault().register(fragment) } } fun unReg(fragment: Fragment) { if (EventBus.getDefault().isRegistered(fragment)) { EventBus.getDefault().unregister(fragment) } } fun reg(popMenu: IPopMenu) { if (!EventBus.getDefault().isRegistered(popMenu)) { EventBus.getDefault().register(popMenu) } } fun unReg(popMenu: IPopMenu) { if (EventBus.getDefault().isRegistered(popMenu)) { EventBus.getDefault().unregister(popMenu) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/Extensions.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.net.Uri import android.os.Bundle import android.os.Parcelable import androidx.fragment.app.Fragment import com.keepassdroid.utils.UriUtil import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.DbHistoryRecord import com.lyy.keepassa.view.StorageType import com.lyy.keepassa.view.StorageType.AFS import timber.log.Timber import java.io.InputStream import java.io.Serializable fun Uri.getBytes(): ByteArray? { var ips: InputStream? = null return try { ips = UriUtil.getUriInputStream(BaseApp.APP, this) ips.readBytes() } catch (e: Exception) { Timber.d(e) null } finally { ips?.close() } } fun Fragment.putArgument( key: String, value: T ) { if (this.arguments == null) { this.arguments = Bundle() } when (value) { is Int -> this.requireArguments() .putInt(key, value) is Boolean -> this.requireArguments() .putBoolean(key, value) is String -> this.requireArguments() .putString(key, value) is CharSequence -> this.requireArguments() .putCharSequence(key, value) is Float -> this.requireArguments() .putFloat(key, value) is Long -> this.requireArguments() .putLong(key, value) is Bundle -> this.requireArguments() .putBundle(key, value) is Serializable -> this.requireArguments() .putSerializable(key, value) is Parcelable -> this.requireArguments() .putParcelable(key, value) is Char -> this.requireArguments() .putChar(key, value) is Byte -> this.requireArguments() .putByte(key, value) else -> error("不支持的类型, $value") } } fun Fragment.getArgument(key: String): T? { if (this.arguments == null) { return null } val d = this.arguments?.get(key) ?: return null return d as T } fun DbHistoryRecord.isAFS(): Boolean { return StorageType.valueOf(this.type) === AFS } //fun Fragment.getArgument(key: String) = BindLoader(key) //private class IntentDelegate(private val key: String) : ReadOnlyProperty { // override fun getValue( // thisRef: U, // property: KProperty<*> // ): T { // @Suppress("UNCHECKED_CAST") // return when (thisRef) { // is Fragment -> thisRef.arguments?.get(key) as T // else -> (thisRef as Activity).intent?.extras?.get(key) as T // } // } //} // //class BindLoader(private val key: String) { // operator fun provideDelegate( // thisRef: U, // prop: KProperty<*> // ): ReadOnlyProperty { // // 创建委托 // return IntentDelegate(key) // } // //} ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/FingerprintUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.annotation.TargetApi import android.content.Context import android.os.Build import android.os.Build.VERSION import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import androidx.annotation.Nullable import androidx.biometric.BiometricManager import timber.log.Timber import java.security.KeyPair import java.security.KeyPairGenerator import java.security.KeyStore import java.security.PrivateKey import java.security.PublicKey import java.security.Signature import java.security.spec.ECGenParameterSpec /** * 指纹工具 * */ object FingerprintUtil { /** * 是否支持生物识别:指纹,面部 * @return true 支持,false 硬件不支持,或者用户没有设置指纹 */ fun hasBiometricPrompt(context: Context): Boolean { if (VERSION.SDK_INT <= Build.VERSION_CODES.M) { return false } val biometricManager = BiometricManager.from(context) var can = false when (biometricManager.canAuthenticate()) { BiometricManager.BIOMETRIC_SUCCESS -> { Timber.d("App can authenticate using biometrics.") can = true } BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> Timber.e("No biometric features available on this device.") BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> Timber.e("Biometric features are currently unavailable.") BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> Timber.e("The user hasn't associated any biometric credentials with their account.") } return can } /** * 生成密钥对 */ @TargetApi(Build.VERSION_CODES.M) @Throws(Exception::class) fun generateKeyPair( keyName: String, invalidatedByBiometricEnrollment: Boolean ): KeyPair? { val keyPairGenerator: KeyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") val builder: KeyGenParameterSpec.Builder = KeyGenParameterSpec.Builder( keyName, KeyProperties.PURPOSE_SIGN ) .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) .setDigests( KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA384, KeyProperties.DIGEST_SHA512 ) // Require the user to authenticate with a biometric to authorize every use of the key .setUserAuthenticationRequired(true) // Generated keys will be invalidated if the biometric templates are added more to user device if (Build.VERSION.SDK_INT >= 24) { builder.setInvalidatedByBiometricEnrollment(invalidatedByBiometricEnrollment) } keyPairGenerator.initialize(builder.build()) return keyPairGenerator.generateKeyPair() } /** * 获取密钥对 */ @TargetApi(Build.VERSION_CODES.M) @Throws(java.lang.Exception::class) fun getKeyPair(keyName: String): KeyPair? { val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) if (keyStore.containsAlias(keyName)) { // Get public key val publicKey: PublicKey = keyStore.getCertificate(keyName).publicKey // Get private key val privateKey: PrivateKey = keyStore.getKey(keyName, null) as PrivateKey // Return a key pair return KeyPair(publicKey, privateKey) } return null } @TargetApi(Build.VERSION_CODES.M) @Nullable @Throws(java.lang.Exception::class) fun initSignature(keyName: String): Signature? { val keyPair = getKeyPair(keyName) if (keyPair != null) { val signature: Signature = Signature.getInstance("SHA256withECDSA") signature.initSign(keyPair.private) return signature } return null } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/HitUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.view.View import android.widget.Toast import androidx.annotation.StringRes import com.google.android.material.snackbar.Snackbar import com.keepassdroid.database.exception.ArcFourException import com.keepassdroid.database.exception.InvalidAlgorithmException import com.keepassdroid.database.exception.InvalidDBException import com.keepassdroid.database.exception.InvalidDBSignatureException import com.keepassdroid.database.exception.InvalidDBVersionException import com.keepassdroid.database.exception.InvalidKeyFileException import com.keepassdroid.database.exception.InvalidPasswordException import com.keepassdroid.database.exception.KeyFileEmptyException import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.thegrizzlylabs.sardineandroid.impl.SardineException import java.io.FileNotFoundException import java.net.HttpURLConnection /** * 提示工具 */ object HitUtil { /** * 打印打开数据库的错误提示 */ fun toaskOpenDbException(e: Exception) { when (e) { is ArcFourException -> { toaskShort(R.string.error_open_db_arcfour_error) } is InvalidAlgorithmException -> { toaskShort(R.string.error_open_db_algorithm_error) } is InvalidDBSignatureException -> { toaskShort(R.string.error_open_db_signature_error) } is InvalidDBVersionException -> { toaskShort(R.string.error_open_db_version_error) } is InvalidKeyFileException -> { toaskShort(R.string.error_open_db_key_invalid) } is InvalidPasswordException -> { toaskShort(R.string.error_open_db_pass_error) } is KeyFileEmptyException -> { toaskShort(R.string.error_open_db_key_empty) } is InvalidDBException -> { toaskShort(R.string.error_open_db) } is FileNotFoundException -> { toaskShort(R.string.db_file_no_exist) } is SardineException -> { when (e.statusCode) { HttpURLConnection.HTTP_UNAUTHORIZED -> { toaskShort(R.string.invalid_auth) } HttpURLConnection.HTTP_NOT_FOUND -> { toaskShort(R.string.db_file_no_exist) } } } } } fun toaskShort(@StringRes strId: Int) { BaseApp.handler.post { Toast.makeText(BaseApp.APP, BaseApp.APP.resources.getString(strId), Toast.LENGTH_SHORT) .show() } } fun toaskShort(text: String) { BaseApp.handler.post { Toast.makeText(BaseApp.APP, text, Toast.LENGTH_SHORT) .show() } } fun toaskLong(text: String) { BaseApp.handler.post { Toast.makeText(BaseApp.APP, text, Toast.LENGTH_LONG) .show() } } fun snackShort( view: View, text: String ) { Snackbar.make(view, text, Snackbar.LENGTH_SHORT) .setAction("OK") {} .show() } fun snackLong( view: View, text: String ) { Snackbar.make(view, text, Snackbar.LENGTH_LONG) .setAction("OK") {} .show() } fun snackLong( view: View, text: String, actionStr: String, action: View.OnClickListener ) { Snackbar.make(view, text, Snackbar.LENGTH_LONG) .setAction(actionStr, action) .show() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/IconUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.content.Context import android.graphics.Bitmap import android.graphics.Bitmap.Config.ARGB_8888 import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.Color import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.graphics.drawable.VectorDrawable import android.os.Build import android.widget.ImageView import androidx.annotation.DrawableRes import androidx.palette.graphics.Palette import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import com.arialyy.frame.util.ResUtil import com.bumptech.glide.Glide import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwEntryV3 import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroup import com.keepassdroid.database.PwGroupV3 import com.keepassdroid.database.PwGroupV4 import com.keepassdroid.database.PwIconCustom import com.lyy.keepassa.R import com.lyy.keepassa.R.dimen import com.lyy.keepassa.widget.toPx import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber object IconUtil { val icons = listOf( R.drawable.cc0_password, R.drawable.cc1_package_network, R.drawable.cc2_messagebox_warning, R.drawable.cc3_server, R.drawable.cc4_klipper, R.drawable.cc5_edu_languages, R.drawable.cc6_kcmdf, R.drawable.cc7_kate, R.drawable.cc8_socket, R.drawable.cc9_identity, R.drawable.cc10_kontact, R.drawable.cc11_camera, R.drawable.cc12_irkick_flash, R.drawable.cc13_kgpg_key3, R.drawable.cc14_laptop_power, R.drawable.cc15_scanner, R.drawable.cc16_mozilla_firebird, R.drawable.cc17_cdrom_unmount, R.drawable.cc18_display, R.drawable.cc19_email_generic, R.drawable.cc20_misc, R.drawable.cc21_korganizer, R.drawable.cc22_ascii, R.drawable.cc23_icons, R.drawable.cc24_connect_established, R.drawable.cc25_folder_mail, R.drawable.cc26_file_save, R.drawable.cc27_nfs_unmount, R.drawable.cc28_quick_time, R.drawable.cc29_kgpg_term, R.drawable.cc30_konsole, R.drawable.cc31_file_print, R.drawable.cc32_fsview, R.drawable.cc33_run, R.drawable.cc34_configure, R.drawable.cc35_krfb, R.drawable.cc36_ark, R.drawable.cc37_kpercentage, R.drawable.cc38_samba_unmount, R.drawable.cc39_history, R.drawable.cc40_mail_find, R.drawable.cc41_vectorgfx, R.drawable.cc42_kcmemory, R.drawable.cc43_edit_trash, R.drawable.cc44_knotes, R.drawable.cc45_cancel, R.drawable.cc46_help, R.drawable.cc47_kpackage, R.drawable.cc48_folder, R.drawable.cc49_folder_blue_open, R.drawable.cc50_folder_tar, R.drawable.cc51_decrypted, R.drawable.cc52_encrypted, R.drawable.cc53_apply, R.drawable.cc54_signature, R.drawable.cc55_thumbnail, R.drawable.cc56_kaddress_book, R.drawable.cc57_view_text, R.drawable.cc58_kgpg, R.drawable.cc59_package_development, R.drawable.cc60_kfm_home, R.drawable.cc61_services, R.drawable.cc62_tux, R.drawable.cc63_feather, R.drawable.cc64_apple, R.drawable.cc65_w, R.drawable.cc66_money, R.drawable.cc67_certificate, R.drawable.cc68_blackberry ) fun getIconById(kpaIconId: Int): Int { return icons[kpaIconId] } /** * 获取图标 bitmap * @param context */ fun getAppIcon( context: Context, apkPkgName: String ): Bitmap? { try { val d = context.packageManager.getApplicationIcon(apkPkgName) return getBitmapFromDrawable(context, d) } catch (e: Exception) { Timber.e(e) } return null } /** * 将自定义图标转换为drawable,如果自定义图标为空,则需要返回默认图标 * @param defIcon 默认图标 */ fun convertCustomIcon2Drawable( context: Context, customIcon: PwIconCustom, @DrawableRes defIcon: Int = R.drawable.ic_image_blue_24px ): Drawable { if (customIconIsNull(customIcon)) { return context.resources.getDrawable(defIcon) } val bd = BitmapDrawable( context.resources, BitmapFactory.decodeByteArray( customIcon.imageData, 0, customIcon.imageData.size ) ) return zoomDrawable(context, bd) } /** * 设置组的icon */ fun setGroupIcon( context: Context, group: PwGroup, img: ImageView ) { if (group is PwGroupV3) { Glide.with(context) .load(getIconById(group.icon.iconId)) .into(img) return } if (customIconIsNull((group as PwGroupV4).customIcon)) { Glide.with(context) .load(getIconById(group.icon.iconId)) .into(img) return } Glide.with(context) .load(group.customIcon.imageData) .into(img) } /** * 获取group的drawable */ fun getGroupIconDrawable( context: Context, group: PwGroup, zoomIcon: Boolean = false ): Drawable? { if (group is PwGroupV3) { return context.getDrawable(getIconById(group.icon.iconId)) } else { val v4Group = group as PwGroupV4 return if (!customIconIsNull(group.customIcon)) { val dr = BitmapDrawable( context.resources, BitmapFactory.decodeByteArray( group.customIcon.imageData, 0, group.customIcon.imageData.size ) ) if (zoomIcon) zoomDrawable(context, dr) else dr } else { context.getDrawable(getIconById(group.icon.iconId)) } } } /** * 获取自定义图片 */ fun getCustomBitmap(entry: PwEntryV4): Bitmap { return BitmapFactory.decodeByteArray( entry.customIcon.imageData, 0, entry.customIcon.imageData.size ) } /** * 获取entry的drawable */ fun getEntryIconDrawable( context: Context, entry: PwEntry, zoomIcon: Boolean = false ): Drawable? { if (entry is PwEntryV3) { return context.getDrawable(getIconById(entry.icon.iconId)) } else { val v4Entry = entry as PwEntryV4 return if (!customIconIsNull(entry.customIcon)) { val dr = BitmapDrawable( context.resources, BitmapFactory.decodeByteArray( entry.customIcon.imageData, 0, entry.customIcon.imageData.size ) ) if (zoomIcon) zoomDrawable(context, dr) else dr } else { context.getDrawable(getIconById(entry.icon.iconId)) } } } /** * 设置项目的icon */ fun setEntryIcon( entry: PwEntry, icon: ImageView ) { if (entry.icon == null) { return } if (entry is PwEntryV3) { icon.loadImg(getIconById(entry.icon.iconId)) return } if (entry is PwEntryV4) { if (!customIconIsNull(entry.customIcon)) { icon.loadImg(entry.customIcon.imageData) return } icon.loadImg(getIconById(entry.icon.iconId)) } } /** * 检查自定义图标是否为空 * @return true 自定义图标为空 */ private fun customIconIsNull(customIcon: PwIconCustom?): Boolean { return (customIcon?.imageData == null) || customIcon.imageData.isEmpty() || customIcon == PwIconCustom.ZERO } /** * 调整drawable大小 */ fun zoomDrawable( context: Context, drawable: BitmapDrawable ): BitmapDrawable { val iconSize = context.resources.getDimension(R.dimen.icon_size).toInt() val newbmp = Bitmap.createScaledBitmap(drawable.bitmap, iconSize, iconSize, true) drawable.bitmap.recycle() val dr = BitmapDrawable(context.resources, newbmp) dr.setBounds(0, 0, iconSize, iconSize) return dr } /** * drawable 转bitmap * @param iconSize 默认28dp,如果设置-1,则为图片本身大小 */ fun getBitmapFromDrawable( context: Context, @DrawableRes drawableId: Int, iconSize: Int = context.resources.getDimension(dimen.icon_size) .toInt() ): Bitmap? { val drawable: Drawable? = context.getDrawable(drawableId) return getBitmapFromDrawable(context, drawable, iconSize) } fun getBitmapFromDrawable( context: Context, drawable: Drawable?, iconSize: Int = context.resources.getDimension(dimen.icon_size) .toInt() ): Bitmap? { return if (drawable is BitmapDrawable) { drawable.bitmap } else if (drawable is VectorDrawable || drawable is VectorDrawableCompat || drawable is LayerDrawable) { val bitmap = Bitmap.createBitmap( drawable.intrinsicWidth, drawable.intrinsicHeight, ARGB_8888 ) val canvas = Canvas(bitmap) if (iconSize == -1) { drawable.setBounds(0, 0, canvas.width, canvas.height) } else { drawable.setBounds(0, 0, iconSize, iconSize) } drawable.draw(canvas) bitmap } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) { val drr = arrayOfNulls(2) drr[0] = drawable.background drr[1] = drawable.foreground val layerDrawable = LayerDrawable(drr) return getBitmapFromDrawable(context, layerDrawable, layerDrawable.intrinsicWidth) } else { throw IllegalArgumentException("unsupported drawable type") } } /** * get highlight color */ fun getIconMdColor( context: Context, icon: Drawable? ): Int { val temp = getBitmapFromDrawable(context, icon, 40.toPx()) if (temp == null || temp.isRecycled) { return Color.WHITE } val sw = Palette.from(temp) .maximumColorCount(12) .generate() return when { sw.mutedSwatch != null -> sw.mutedSwatch!!.rgb sw.darkMutedSwatch != null -> sw.darkMutedSwatch!!.rgb sw.lightMutedSwatch != null -> sw.lightMutedSwatch!!.rgb sw.darkVibrantSwatch != null -> sw.darkVibrantSwatch!!.rgb sw.lightVibrantSwatch != null -> sw.lightVibrantSwatch!!.rgb sw.vibrantSwatch != null -> sw.vibrantSwatch!!.rgb else -> ResUtil.getColor(R.color.colorPrimary) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/ImageExtensions.kt ================================================ package com.lyy.keepassa.util import android.app.Activity import android.content.Context import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.widget.Button import android.widget.ImageView import androidx.annotation.DrawableRes import androidx.appcompat.widget.AppCompatImageView import com.bumptech.glide.Glide import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 10:42 上午 2022/1/27 **/ /** * check bitmap is invalid * @return true is effect */ fun Bitmap?.isInvalid():Boolean{ if (this == null) return true if (this.isRecycled) return true return false } /** * @return true 有效 */ private fun checkoutContextEffective(context: Context): Boolean { if (context is Activity) { if (context.isFinishing || context.isDestroyed) { Timber.w("activity已经被销毁,不加载图片") return false } } return true } fun Button.loadBackground(imgUrl: String?) { if (checkoutContextEffective(context)) { Glide.with(context).asDrawable().load(imgUrl).into(object : SimpleTarget() { override fun onResourceReady(resource: Drawable, transition: Transition?) { this@loadBackground.background = resource } }) } } fun ImageView.loadImg(@DrawableRes resId: Int) { if (checkoutContextEffective(context)) { Glide.with(context).load(resId).into(this) } } fun ImageView.loadImg(imgUrl: String?) { if (checkoutContextEffective(context)) { Glide.with(context).load(imgUrl).into(this) } } fun ImageView.loadImg(imgBm: Bitmap?) { if (checkoutContextEffective(context) && imgBm != null && !imgBm.isRecycled) { Glide.with(context).load(imgBm).into(this) } } fun ImageView.loadImg(byteArray: ByteArray?) { if (checkoutContextEffective(context) && byteArray != null && byteArray.isNotEmpty()) { Glide.with(context).load(byteArray).into(this) } } fun AppCompatImageView.loadImg(@DrawableRes resId: Int) { if (checkoutContextEffective(context)) { Glide.with(context).load(resId).into(this) } } fun AppCompatImageView.loadImg(imgUrl: String?) { if (checkoutContextEffective(context)) { Glide.with(context).load(imgUrl).into(this) } } fun AppCompatImageView.loadImg(imgBm: Bitmap?) { if (checkoutContextEffective(context) && imgBm != null && !imgBm.isRecycled) { Glide.with(context).load(imgBm).into(this) } } fun AppCompatImageView.loadImg(byteArray: ByteArray?) { if (checkoutContextEffective(context) && byteArray != null && byteArray.isNotEmpty()) { Glide.with(context).load(byteArray).into(this) } } fun AppCompatImageView.loadImg(drawable: Drawable?) { if (checkoutContextEffective(context) && drawable != null) { Glide.with(context).load(drawable).into(this) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/KLog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.os.Bundle import android.text.TextUtils import android.util.Log import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import java.util.Arrays import kotlin.collections.Map.Entry /** * Created by Aria.Lao on 2017/10/25. * Aria日志工具 */ @Deprecated("please use timber") object KLog { const val DEBUG = true const val LOG_LEVEL_VERBOSE = 2 const val LOG_LEVEL_DEBUG = 3 const val LOG_LEVEL_INFO = 4 const val LOG_LEVEL_WARN = 5 const val LOG_LEVEL_ERROR = 6 const val LOG_LEVEL_ASSERT = 7 const val LOG_CLOSE = 8 const val LOG_DEFAULT = LOG_LEVEL_DEBUG var LOG_LEVEL = LOG_DEFAULT fun v( tag: String, msg: String ): Int { return println(Log.VERBOSE, tag, msg) } fun d( tag: String, msg: String ): Int { return println(Log.DEBUG, tag, msg) } fun i( tag: String, msg: String ): Int { return println(Log.INFO, tag, msg) } fun w( tag: String, msg: String ): Int { return println(Log.WARN, tag, msg) } fun e( tag: String, msg: String ): Int { return println(Log.ERROR, tag, msg) } fun e( tag: String?, msg: String?, e: Throwable? ) { Log.e(tag, msg, e) } /** * 打印MAp,debug级别日志 */ fun m( tag: String, map: Map<*, *> ) { if (LOG_LEVEL <= Log.DEBUG) { val set: Set<*> = map.entries if (set.size < 1) { d(tag, "[]") return } val s = arrayOfNulls(set.size) for ((i, aSet) in set.withIndex()) { val entry = aSet as Entry<*, *> s[i] = "${entry.key.toString()} = ${entry.value}," } println(Log.DEBUG, tag, s.contentToString()) } } /** * 打印JSON,debug级别日志 */ fun j( tag: String, jsonStr: String ) { if (LOG_LEVEL <= Log.DEBUG) { val message: String = try { when { jsonStr.startsWith("{") -> { val jsonObject = JSONObject(jsonStr) jsonObject.toString(4) //这个是核心方法 } jsonStr.startsWith("[") -> { val jsonArray = JSONArray(jsonStr) jsonArray.toString(4) } else -> { jsonStr } } } catch (e: JSONException) { jsonStr } println(Log.DEBUG, tag, "\n\r$message") } } private fun bundleToString( builder: StringBuilder, data: Bundle ) { val keySet = data.keySet() builder.append("[Bundle with ") .append(keySet.size) .append(" keys:") for (key in keySet) { builder.append(' ') .append(key) .append('=') val value = data.get(key) if (value is Bundle) { bundleToString(builder, value) } else { val string = if (value is Array<*>) Arrays.toString(value) else value builder.append(string) } } builder.append(']') } fun b(data: Bundle?): String { if (data == null) { return "N/A" } val builder = StringBuilder() bundleToString(builder, data) return builder.toString() } /** * 将异常信息转换为字符串 */ fun getExceptionString(ex: Throwable?): String { if (ex == null) { return "" } val err = StringBuilder() err.append("ExceptionDetailed:\n") err.append("====================Exception Info====================\n") err.append(ex.toString()) err.append("\n") val stack = ex.stackTrace for (stackTraceElement in stack) { err.append(stackTraceElement.toString()) .append("\n") } val cause = ex.cause if (cause != null) { err.append("【Caused by】: ") err.append(cause.toString()) err.append("\n") val stackTrace = cause.stackTrace for (stackTraceElement in stackTrace) { err.append(stackTraceElement.toString()) .append("\n") } } err.append("===================================================") return err.toString() } private fun println( level: Int, tag: String, msg: String ): Int { return if (LOG_LEVEL <= level) { Log.println(level, tag, if (TextUtils.isEmpty(msg)) "null" else msg) } else { -1 } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/KVStorage.kt ================================================ package com.lyy.keepassa.util import android.os.Parcelable /** * K>V持久化 */ interface KVStorage { fun put(key: String, value: String): Boolean fun getString(key: String, defaultValue: String = ""): String fun put(key: String, value: Boolean): Boolean fun getBoolean(key: String, defaultValue: Boolean = false): Boolean fun put(key: String, value: Int): Boolean fun getInt(key: String, defaultValue: Int = 0): Int fun put(key: String, value: Long): Boolean fun getLong(key: String, defaultValue: Long = 0L): Long fun put(key: String, value: Float): Boolean fun getFloat(key: String, defaultValue: Float = 0F): Float fun put(key: String, value: Double): Boolean fun getDouble(key: String, defaultValue: Double = 0.0): Double fun put(key: String, value: Set): Boolean fun getStringSet(key: String, defaultValue: Set = setOf()): Set fun put(key: String, value: Parcelable): Boolean fun get(key: String?, tClass: Class?, defaultValue: T?): T? fun containsKey(key: String): Boolean fun remove(key: String) fun clean() } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/KdbUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.widget.TextView import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.FileUtil import com.arialyy.frame.util.RegularRule import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.Utils import com.keepassdroid.Database import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroup import com.keepassdroid.database.PwGroupIdV3 import com.keepassdroid.database.PwGroupIdV4 import com.keepassdroid.database.security.ProtectedBinary import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.totp.OtpUtil import com.lyy.keepassa.widget.pb.RoundProgressBarWidthNumber import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File import java.io.FileOutputStream import java.nio.channels.Channels import java.util.UUID object KdbUtil { val scope = MainScope() val txtArray = arrayListOf("txt", "md") val imgArray = arrayListOf("png", "jpg", "jpeg", "webp") val DATE_FORMAT = "yyyy/MM/dd HH:mm" /** * 定时自动获取otp密码 */ fun startAutoGetOtp(entryV4: PwEntryV4, rPb: RoundProgressBarWidthNumber, tvValue: TextView) { val p = OtpUtil.getOtpPass(entryV4) if (p.second.isNullOrEmpty()) { Timber.e("无法自动获取otp密码") return } rPb.setCountdown(true) scope.launch(Dispatchers.Main) { Timber.d(p.toString()) val time = p.first rPb.max = time tvValue.text = p.second for (i in time downTo 1) { rPb.progress = i withContext(Dispatchers.IO) { delay(1000) } } startAutoGetOtp(entryV4, rPb, tvValue) } } fun openFile(fileName: String, file: ProtectedBinary) { txtArray.forEach { if (fileName.endsWith(it, true)) { Routerfit.create(DialogRouter::class.java).showMsgDialog( msgTitle = ResUtil.getString(R.string.txt_viewer), msgContent = String(file.data.readBytes()), showCancelBt = false ) return } } imgArray.forEach { if (fileName.endsWith(it, true)) { val bytes = file.data.readBytes() if (bytes.isNotEmpty()) { Routerfit.create(DialogRouter::class.java).showImgViewerDialog(bytes) } return } } openFileBySystem(fileName, file) } private fun openFileBySystem(fileName: String, file: ProtectedBinary) { KpaUtil.scope.launch { val context = Utils.getApp() val targetFile = File(context.cacheDir, fileName) withContext(Dispatchers.IO) { val fic = Channels.newChannel(file.data) val foc = FileOutputStream(targetFile).channel foc.transferFrom(fic, 0, Int.MAX_VALUE.toLong()) fic.close() foc.close() } FileUtil.openFile(context, targetFile) } } fun Database?.isNull(): Boolean { return this == null || this.pm == null } suspend fun getAllTags(): Set { val tagList = hashSetOf() withContext(Dispatchers.IO) { BaseApp.KDB.pm.entries.forEach { val entry = it.value as PwEntryV4 if (entry.tags.isNullOrEmpty()) { return@forEach } tagList.addAll(getEntryTag(entry)) } } return tagList } fun getEntryTag(entryV4: PwEntryV4): Set { val tagSet = hashSetOf() entryV4.tags.split(",").forEach { tagSet.add(it) } return tagSet } /** * 获取用户名,如果是引用其它条目的,解析其引用 */ @Deprecated( message = "please use pwEntry.getRealUserName()", ReplaceWith("getRealUserName()", "com.lyy.keepassa.util.getRealUserName") ) fun getUserName(entry: PwEntry): String { return entry.getRealUserName() } /** * 获取密码,如果是引用其它条目的,解析其引用 */ @Deprecated( message = "please use pwEntry.getRealPass()", ReplaceWith("getRealPass()", "com.lyy.keepassa.util.getRealPass") ) fun getPassword(entry: PwEntry): String { return entry.getRealPass() } /** * 通过搜索条目 * @param domain 域名 * @param listStorage 搜索结果 */ fun searchEntriesByDomain( domain: String?, listStorage: MutableList ) { if (domain.isNullOrEmpty()) { return } val topDomain = Regex(RegularRule.DOMAIN_TOP, RegexOption.IGNORE_CASE).find(domain)?.value.toString() Timber.d("topDomain = $topDomain") for (entry in BaseApp.KDB.pm.entries.values) { val pe4 = entry as PwEntryV4 if (pe4.url.contains(topDomain, true) || pe4.strings["URL"]?.toString()?.contains(topDomain) == true ) { listStorage.add(pe4) } } } /** * 通过包名搜索条目 * * @param pkgName 包名 * @param listStorage 搜索结果 */ fun searchEntriesByPackageName( pkgName: String, listStorage: MutableList ) { for (entry in BaseApp.KDB.pm.entries.values) { val pe4 = entry as PwEntryV4 if (pe4.strings == null || pe4.strings.isEmpty()) { continue } for (ps in pe4.strings.values) { if (ps.toString() .equals("androidapp://$pkgName", ignoreCase = true) ) { listStorage.add(pe4) break } } } } fun getGroupEntryNum(pwGroup: PwGroup): Int { return pwGroup.childEntries.size + pwGroup.childGroups.size } /** * 获取组中的条目数 */ fun getGroupAllEntryNum(pwGroup: PwGroup): Int { var num = 0 if (pwGroup.childEntries.isEmpty() && pwGroup.childGroups.isEmpty()) { return 0 } if (pwGroup.childGroups.isNotEmpty()) { for (group in pwGroup.childGroups) { num += getGroupAllEntryNum(group) } num += pwGroup.childEntries.size } else { num += pwGroup.childEntries.size } return num } fun findEntry(uuid: UUID): PwEntry? { val enties = BaseApp.KDB.pm.entries return enties[uuid] } /** * 通过id 获取v3的group信息 */ fun findV3GroupById(groupId: Int): PwGroup? { val groups = BaseApp.KDB.pm.groups return groups[PwGroupIdV3(groupId)] } /** * 通过id 获取v4的group信息 */ fun findV4GroupById(groupId: UUID): PwGroup? { val groups = BaseApp.KDB.pm.groups return groups[PwGroupIdV4(groupId)] } fun filterCustomStr(map: Map): Map { val remap = HashMap() // var addOTPPass = false for (str in map) { if (str.key.equals(PwEntryV4.STR_NOTES, true) || str.key.equals(PwEntryV4.STR_PASSWORD, true) || str.key.equals(PwEntryV4.STR_TITLE, true) || str.key.equals(PwEntryV4.STR_URL, true) || str.key.equals(PwEntryV4.STR_USERNAME, true) ) { continue } remap[str.key] = str.value // // 增加TOP密码字段 // if (!addOTPPass && (str.key.startsWith("TOTP", ignoreCase = true) // || str.key.startsWith("OTP", ignoreCase = true)) // ) { // addOTPPass = true // val totpPass = OtpUtil.getOtpPass(entryV4) // if (TextUtils.isEmpty(totpPass.second)) { // continue // } // val totpPassStr = ProtectedString(true, totpPass.second) // totpPassStr.isOtpPass = true // map["TOTP"] = totpPassStr // } } return remap.toList() .sortedBy { it.first } .toMap() } /** * 过滤并排序自定义字段和自定义数据 */ fun filterCustomStr( entryV4: PwEntryV4, ): Map { return filterCustomStr(entryV4.strings) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/KeepassAUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import KDBAutoFillRepository import android.annotation.SuppressLint import android.annotation.TargetApi import android.app.Activity import android.app.assist.AssistStructure import android.content.ComponentName import android.content.ContentUris import android.content.Context import android.content.Intent import android.content.pm.PackageManager.NameNotFoundException import android.content.res.Configuration import android.database.Cursor import android.net.Uri import android.os.Build import android.os.Build.VERSION import android.os.Environment import android.provider.DocumentsContract import android.provider.MediaStore.Audio import android.provider.MediaStore.Images.Media import android.provider.MediaStore.Video import android.provider.OpenableColumns import android.text.TextUtils import android.view.View import android.view.autofill.AutofillManager import android.view.inputmethod.InputMethodManager import androidx.annotation.ColorInt import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat.getSystemService import androidx.core.graphics.ColorUtils import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceManager import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.core.AbsFrame import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.ResUtil import com.arialyy.frame.util.StringUtil import com.blankj.utilcode.util.AppUtils import com.keepassdroid.database.PwDataInf import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwGroup import com.keepassdroid.utils.UriUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.DbHistoryRecord import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.router.ActivityRouter import com.lyy.keepassa.router.ServiceRouter import com.lyy.keepassa.service.autofill.AutoFillHelper import com.lyy.keepassa.service.autofill.StructureParser import com.lyy.keepassa.view.create.CreateDbActivity import com.lyy.keepassa.view.launcher.LauncherActivity import com.lyy.keepassa.view.launcher.OpenDbHistoryActivity import com.lyy.keepassa.view.main.QuickUnlockActivity import com.lyy.keepassa.view.search.AutoFillEntrySearchActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber import java.io.BufferedReader import java.io.File import java.io.FileReader import java.io.IOException import java.math.BigDecimal import java.text.SimpleDateFormat import java.util.Date import java.util.GregorianCalendar class KeepassAUtil private constructor() { companion object { val instance: KeepassAUtil by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { KeepassAUtil() } } private var LAST_CLICK_TIME = System.currentTimeMillis() fun getAppVersionCode(context: Context): Int { val manager = context.packageManager var code = 0 try { val info = manager.getPackageInfo(context.packageName, 0) code = info.versionCode } catch (e: NameNotFoundException) { Timber.e(e) } return code } fun getAppVersionName(context: Context): String { val manager = context.packageManager var name: String? = null try { val info = manager.getPackageInfo(context.packageName, 0) name = info.versionName } catch (e: NameNotFoundException) { Timber.e(e) } return name!! } /** * 获取进程号对应的进程名 * * @param pid 进程号 * @return 进程名 */ fun getProcessName(pid: Int): String? { var reader: BufferedReader? = null try { reader = BufferedReader(FileReader("/proc/$pid/cmdline")) var processName: String = reader.readLine() if (!TextUtils.isEmpty(processName)) { processName = processName.trim { it <= ' ' } } return processName } catch (throwable: Throwable) { Timber.e(throwable) } finally { try { reader?.close() } catch (exception: IOException) { Timber.e(exception) } } return null } /** * is auto lock the database * @return true auto lock the database */ fun isAutoLockDb(): Boolean { return PreferenceManager.getDefaultSharedPreferences(BaseApp.APP) .getBoolean(BaseApp.APP.getString(R.string.set_key_auto_lock_database), true) } /** * is display loading anim * @return true display loading anim */ fun isDisplayLoadingAnim(): Boolean { return PreferenceManager.getDefaultSharedPreferences(BaseApp.APP) .getBoolean(BaseApp.APP.getString(R.string.set_key_loading_anim), true) } /** * start lock timer * @param obj fragment or activity */ fun startLockTimer(obj: Any?) { KpaUtil.scope.launch(Dispatchers.IO) { // delay 1s, in order to wait for the configuration file to take effect delay(1000) if (!isAutoLockDb()) { return@launch } if (isNeedStartLockActivity(obj)) { if (BaseApp.isLocked) { AutoLockDbUtil.get() .startLockWorkerNow() return@launch } if (AppUtils.isAppForeground()) { AutoLockDbUtil.get().resetTimer() return@launch } } } } /** * lock the db */ fun lock() { Timber.d("锁定数据库") BaseApp.isLocked = true val isOpenQuickLock = BaseApp.APP.isCanOpenQuickLock() // 只有应用在前台才会跳转到锁屏页面 if (AppUtils.isAppForeground() && BaseApp.KDB != null) { // 开启快速解锁则跳转到快速解锁页面 if (isOpenQuickLock) { NotificationUtil.startQuickUnlockNotify(BaseApp.APP) val cActivity = AbsFrame.getInstance().currentActivity // if (cActivity != null && cActivity is QuickUnlockActivity) { // Timber.w("快速解锁已启动,不再启动快速解锁") // return // } Timber.d("启动快速解锁") BaseApp.APP.startActivity(Intent(Intent.ACTION_MAIN).also { it.component = ComponentName( BaseApp.APP.packageName, "${BaseApp.APP.packageName}.view.main.QuickUnlockActivity" ) it.flags = Intent.FLAG_ACTIVITY_NEW_TASK }) return } // 没有开启快速解锁,则回到启动页 NotificationUtil.startDbLocked(BaseApp.APP) val cActivity = AbsFrame.getInstance().currentActivity if (cActivity != null && cActivity is LauncherActivity) { Timber.w("解锁页面已启动,不再启动快速解锁") return } Timber.d("快速解锁没有启动,进入解锁界面") Routerfit.create(ServiceRouter::class.java).getDbSaveService().clearDb() BaseApp.APP.startActivity(Intent(Intent.ACTION_MAIN).also { it.component = ComponentName( BaseApp.APP.packageName, "${BaseApp.APP.packageName}.view.launcher.LauncherActivity" ) it.flags = Intent.FLAG_ACTIVITY_NEW_TASK }) for (ac in AbsFrame.getInstance().activityStack) { if (isHomeActivity(ac)) { continue } ac.finish() } return } // 处理处于后台的情况 if (isOpenQuickLock) { NotificationUtil.startQuickUnlockNotify(BaseApp.APP) return } Routerfit.create(ServiceRouter::class.java).getDbSaveService().clearDb() NotificationUtil.startDbLocked(BaseApp.APP) } fun isHomeActivity(ac: Activity): Boolean { val clazz = ac.javaClass return (clazz == LauncherActivity::class.java || clazz == CreateDbActivity::class.java || clazz == OpenDbHistoryActivity::class.java || clazz == QuickUnlockActivity::class.java ) } /** * 判断颜色是不是亮色 * * @param color * @return * @from https://stackoverflow.com/questions/24260853/check-if-color-is-dark-or-light-in-android */ fun isLightColor(@ColorInt color: Int): Boolean { return ColorUtils.calculateLuminance(color) >= 0.5 } /** * is night mode * @return true yes, false no */ fun isNightMode(): Boolean { return BaseApp.APP.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES } /** * 是否需要启动快速解锁 * @return true 启动快速解锁 */ private fun isNeedStartLockActivity(obj: Any?): Boolean { if (obj == null) { return false } if (obj is Fragment && obj.activity == null) { return false } val clazz = if (obj is Fragment) { obj.requireActivity().javaClass } else { obj.javaClass } return ( clazz != LauncherActivity::class.java && clazz != QuickUnlockActivity::class.java && clazz != CreateDbActivity::class.java && clazz != AutoFillEntrySearchActivity::class.java && clazz != OpenDbHistoryActivity::class.java ) } /** * 重新打开数据库 * 如果数据库没有打开或者没有启动快速解锁 跳转到数据库打开页面 * 否则跳转到快速启动页 */ fun reOpenDb(context: Context) { if (!BaseApp.APP.isCanOpenQuickLock()) { ARouter.getInstance() .build("/launcher/activity") .withInt(LauncherActivity.KEY_OPEN_TYPE, LauncherActivity.OPEN_TYPE_OPEN_DB) .navigation() for (ac in AbsFrame.getInstance().activityStack) { if (ac is LauncherActivity) { continue } ac.finish() } return } QuickUnlockActivity.startQuickUnlockActivity(context) } /** * 回到启动页 * @param turnFragment [LauncherActivity.OPEN_TYPE_CHANGE_DB]选择数据库页面, * [LauncherActivity.OPEN_TYPE_OPEN_DB] 打开数据库 */ fun turnLauncher(turnFragment: Int = LauncherActivity.OPEN_TYPE_CHANGE_DB) { BaseApp.isLocked = true ARouter.getInstance() .build("/launcher/activity") .withInt(LauncherActivity.KEY_OPEN_TYPE, turnFragment) .navigation() for (ac in AbsFrame.getInstance().activityStack) { if (ac is LauncherActivity) { continue } ac.finish() } } /** * 检查http地址是否有效 * @return false 无效,true 有效 */ fun checkUrlIsValid(url: String?): Boolean { if (url.isNullOrBlank()) { return false } val rs = "^(http|https)://[^\\s]*" // "^(((file|gopher|news|nntp|telnet|http|ftp|https|ftps|sftp)://)|(www\\.))+(([a-zA-Z0-9\\._-]+\\.[a-zA-Z]{2,6})|([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}))(/[a-zA-Z0-9\\&%_\\./-~-]*)?\$" val r1 = Regex(rs, RegexOption.IGNORE_CASE) return r1.matches(url) } /** * 将pwentry 转换为列表实体 */ fun convertPwEntry2Item(entry: PwEntry): SimpleItemEntity { val item = SimpleItemEntity() item.title = entry.title item.subTitle = if (entry.isRef()) { val refStr = "${BaseApp.APP.resources.getString(R.string.ref_entry)}: " val tempStr = "${refStr}${KdbUtil.getUserName(entry)}" StringUtil.highLightStr(tempStr, refStr, ResUtil.getColor(R.color.colorPrimary), true) } else { entry.username } item.obj = entry return item } /** * 将pwGroup 转换为列表实体 */ fun convertPwGroup2Item(pwGroup: PwGroup): SimpleItemEntity { val item = SimpleItemEntity() item.title = pwGroup.name item.subTitle = ResUtil.getString( R.string.hint_group_desc, KdbUtil.getGroupEntryNum(pwGroup) .toString() ) item.obj = pwGroup return item } /** * 保存上一次打开的数据库记录 */ fun saveLastOpenDbHistory(record: DbHistoryRecord?) { if (record == null) { return } KpaUtil.scope.launch(Dispatchers.IO) { val dao = BaseApp.appDatabase.dbRecordDao() val his = dao.findRecord(record.localDbUri) if (his == null || his.localDbUri.isEmpty()) { record.uid = 0 // 保证uid能自增且不冲突 record.time = System.currentTimeMillis() dao.saveRecord(record) BaseApp.dbRecord = record Timber.d("保存数据库打开记录成功") return@launch } his.keyUri = record.keyUri his.cloudDiskPath = record.cloudDiskPath his.type = record.type his.time = record.time dao.updateRecord(his) BaseApp.dbRecord = his Timber.d("更新数据库打开记录成功") } } /** * 自动填充的response * @param intent 自动填充服务床进来的intentc * @param apkPkgName 第三方包名 */ @TargetApi(Build.VERSION_CODES.O) fun getFillResponse( context: Context, intent: Intent, apkPkgName: String ): Intent { val autoFillStructure = intent.getParcelableExtra( AutofillManager.EXTRA_ASSIST_STRUCTURE ) if (autoFillStructure == null) { Timber.e("autoFillStructure is null") return Intent() } val parser = StructureParser(autoFillStructure) parser.parseForFill(true, apkPkgName) val autofillFields = parser.autoFillFields val datas = KDBAutoFillRepository.getAutoFillDataByPackageName(apkPkgName) val response = AutoFillHelper.newResponse( context, true, autofillFields, datas, apkPkgName, autoFillStructure ) val data = Intent() data.putExtra(LauncherActivity.KEY_PKG_NAME, apkPkgName) data.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, response) return data } /** * 自动填充的response * @param intent 自动填充服务床进来的intent * @param pwEntry u数据条目 */ @TargetApi(Build.VERSION_CODES.O) fun getFillResponse( context: Context, intent: Intent, pwEntry: PwEntry, apkPkgName: String ): Intent { val autoFillStructure = intent.getParcelableExtra( AutofillManager.EXTRA_ASSIST_STRUCTURE ) if (autoFillStructure == null) { Timber.e("autoFillStructure is null") return Intent() } val parser = StructureParser(autoFillStructure) parser.parseForFill(true, apkPkgName) val autofillFields = parser.autoFillFields val response = AutoFillHelper.newResponse( context, true, autofillFields, arrayListOf(pwEntry), apkPkgName, autoFillStructure ) val data = Intent() data.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, response) return data } /** * 转换uri */ fun convertUri(uriString: String?): Uri? { if (uriString == null) { return null } if (uriString.equals("null", ignoreCase = true)) { return null } val temp = Uri.parse(uriString) // val ub = Uri.Builder() // ub.authority("com.android.externalstorage.documents") // 必须设置,否则8.0上会出现崩溃的问题 // val uri = ub.scheme(temp.scheme) // .path(temp.path) // .appendPath(temp.path) // .encodedPath(temp.encodedPath) // .fragment(temp.fragment) // .encodedFragment(temp.encodedFragment) // .query(temp.query) // .encodedQuery(temp.encodedQuery) // .build() // Log.d(TAG, "new uri = $uri") // Log.d(TAG, "temp = $temp") // Log.d(TAG, "uriString = $uriString") return temp } fun isFastClick(): Boolean { val isFast = System.currentTimeMillis() - LAST_CLICK_TIME < 400 LAST_CLICK_TIME = System.currentTimeMillis() return isFast } /** * 截取并保存短密码 */ fun subShortPass() { val sh = PreferenceManager.getDefaultSharedPreferences(BaseApp.APP) val subType = sh.getString(BaseApp.APP.getString(R.string.set_quick_pass_type), "1")!! .toString() .toInt() val passLen = sh.getString(BaseApp.APP.getString(R.string.set_quick_pass_len), "3")!! .toString() .toInt() val masterPass = QuickUnLockUtil.decryption(BaseApp.dbPass) var shortPass = "" Timber.i("截取短密码,长度:$passLen,截取类型:$subType") when (subType) { // 前面位 1 -> { shortPass = if (masterPass.length <= passLen) masterPass else masterPass.substring(0, passLen) } // 末尾 2 -> { shortPass = if (masterPass.length <= passLen) masterPass else masterPass.substring( masterPass.length - passLen, masterPass.length ) } } Timber.d("shortPass = $shortPass") BaseApp.shortPass = QuickUnLockUtil.encryptStr(shortPass) } /** * 格式化时间 */ @SuppressLint("SimpleDateFormat") fun formatTime(time: Date?): String { val format = SimpleDateFormat(" yyyy/MM/dd HH:mm") if (time == null) { return format.format(GregorianCalendar(1970, 1, 1, 0, 0, 0)) } return format.format(time) } @SuppressLint("SimpleDateFormat") fun formatTime( time: Date, format: String ): String { return SimpleDateFormat(format).format(time) } /** * 跳转群组详情或项目详情 */ fun turnEntryDetail( activity: FragmentActivity, entry: PwDataInf, showElement: View? = null ) { if (entry is PwGroup) { Routerfit.create(ActivityRouter::class.java, activity).toGroupDetailActivity( groupName = entry.name, groupId = entry.id, opt = ActivityOptionsCompat.makeSceneTransitionAnimation(activity) ) return } if (entry is PwEntry) { val opt = if (showElement != null) { val pair = androidx.core.util.Pair( showElement, activity.getString(R.string.transition_entry_icon) ) ActivityOptionsCompat.makeSceneTransitionAnimation(activity, pair) } else { ActivityOptionsCompat.makeSceneTransitionAnimation(activity) } Routerfit.create(ActivityRouter::class.java, activity).toEntryDetailActivity( entryId = entry.uuid, opt = opt ) } } /** * @param requestCode 请求码 * @param obj activity 或 fragment * @param type mime * * @see mime */ fun openSysFileManager( obj: Any, type: String, requestCode: Int ) { try { Intent.ACTION_GET_CONTENT val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { this.type = type addCategory(Intent.CATEGORY_OPENABLE) addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } if (obj is Activity) { obj.startActivityForResult(intent, requestCode) } else if (obj is Fragment) { obj.startActivityForResult(intent, requestCode) } } catch (e: Exception) { Timber.e("打开文件失败") Timber.e(e) } } /** * 使用ASF创建文件 * @param obj activity 或 fragment * @param mimeType mime * @see 创建文档 */ @Deprecated("请使用registerForActivityResult(ActivityResultContracts.CreateDocument())") fun createFile( obj: Any, mimeType: String, fileName: String, requestCode: Int ) { try { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.type = mimeType intent.putExtra(Intent.EXTRA_TITLE, fileName) if (obj is Activity) { obj.startActivityForResult(intent, requestCode) } else if (obj is Fragment) { obj.startActivityForResult(intent, requestCode) } } catch (e: Exception) { Timber.e(e) } } /** * 打开或隔壁键盘 */ fun toggleKeyBord(context: Context) { val imm = getSystemService(context, InputMethodManager::class.java) imm!!.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0) } /** * 从uri中获取文件长度 */ fun getFileSizeFormUri( context: Context, uri: Uri ): Long { if ("content".equals(uri.scheme, false)) { val df = DocumentFile.fromSingleUri(context, uri) if (df != null) { return df.length() } if (!UriUtil.checkPermissions(context, uri)) { Timber.e("uri没有授权:$uri") return 0 } val cursor = context.contentResolver.query(uri, null, null, null, null, null) if (cursor != null && cursor.moveToFirst()) { val size = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE)) cursor.close() return size } } else if (uri.scheme.equals("file", false)) { return File(uri.path).length() } return 0 } /** * 从uri 中获取文件的路径,没用!! * 该方法的拷贝地址:https://stackoverflow.com/questions/13209494/how-to-get-the-full-file-path-from-uri * 如果只是想使用流,可以使用 contentResolver.openOutputStream(uri) 获取流 */ @SuppressLint("ObsoleteSdkInt") fun getFilePathFormUri( context: Context, uri: Uri ): String? { var tempUri = uri val needToCheckUri = VERSION.SDK_INT >= 19 var selection: String? = null var selectionArgs: Array? = null Timber.d("uri = $uri") if (needToCheckUri && DocumentsContract.isDocumentUri(context.applicationContext, tempUri)) { when { isExternalStorageDocument(tempUri) -> { val docId = DocumentsContract.getDocumentId(tempUri) val split = docId.split(":") .toTypedArray() return Environment.getExternalStorageDirectory() .toString() + "/" + split[1] } isDownloadsDocument(tempUri) -> { val id = DocumentsContract.getDocumentId(tempUri) if (id.startsWith("raw", ignoreCase = true)) { val temp = Uri.parse(id) // val path = temp.path // val s = temp.scheme return temp.path } else if (isNumeric(id)) { tempUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), id.toLong() ) } else if (id.startsWith("msf", ignoreCase = true)) { // android 10 的问题,一样有问题!! tempUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), id.split(":")[1].toLong() ) Timber.d("msf Uri = $tempUri") } } isMediaDocument(tempUri) -> { val docId = DocumentsContract.getDocumentId(tempUri) val split = docId.split(":") .toTypedArray() val type = split[0] if ("image" == type) { tempUri = Media.EXTERNAL_CONTENT_URI } else if ("video" == type) { tempUri = Video.Media.EXTERNAL_CONTENT_URI } else if ("audio" == type) { tempUri = Audio.Media.EXTERNAL_CONTENT_URI } } } } if ("content".equals(tempUri.scheme, ignoreCase = true)) { val projection = arrayOf(Media.DATA) var url = "" val cursor: Cursor? try { cursor = context.contentResolver .query(uri, projection, selection, selectionArgs, null) if (cursor == null) { return null } val columnIndex: Int = cursor.getColumnIndexOrThrow(Media.DATA) if (cursor.moveToFirst()) { url = cursor.getString(columnIndex) } cursor.close() } catch (e: Exception) { } return url } else if ("file".equals(tempUri.scheme, ignoreCase = true)) { return tempUri.path } return null } fun isNumeric(str: String?): Boolean { val bigStr: String = try { BigDecimal(str).toString() } catch (e: java.lang.Exception) { return false //异常 说明包含非数字。 } return true } /** * @param uri The Uri to check. * @return Whether the Uri authority is ExternalStorageProvider. */ private fun isExternalStorageDocument(uri: Uri): Boolean { return "com.android.externalstorage.documents" == uri.authority } /** * @param uri The Uri to check. * @return Whether the Uri authority is DownloadsProvider. */ private fun isDownloadsDocument(uri: Uri): Boolean { return "com.android.providers.downloads.documents" == uri.authority } /** * @param uri The Uri to check. * @return Whether the Uri authority is MediaProvider. */ private fun isMediaDocument(uri: Uri): Boolean { return "com.android.providers.media.documents" == uri.authority } } fun Uri.getFileInfo( context: Context ): Pair { return Pair( UriUtil.getFileNameFromUri(context, this), KeepassAUtil.instance.getFileSizeFormUri(context, this) ) } fun PwEntry.isRef(): Boolean { return (!username.isNullOrEmpty() && username.startsWith("{REF:", ignoreCase = true)) || (!password.isNullOrEmpty() && password.startsWith("{REF:", ignoreCase = true)) } /** * uri 授权 */ fun Uri.takePermission() { try { val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION BaseApp.APP.contentResolver.takePersistableUriPermission(this, takeFlags) } catch (e: Exception) { HitUtil.toaskShort(BaseApp.APP.getString(R.string.error_uri_grant_permission)) Timber.e(e) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/KpaExtensions.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.annotation.SuppressLint import android.app.Activity import android.net.Uri import android.text.TextUtils import android.view.MenuInflater import android.view.MenuItem import android.view.MotionEvent import android.view.View import androidx.appcompat.view.menu.MenuPopupHelper import androidx.appcompat.widget.PopupMenu import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener import com.arialyy.frame.util.ReflectionUtil import com.arialyy.frame.util.ResUtil import com.arialyy.frame.util.adapter.RvItemClickSupport import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.Constance import com.lyy.keepassa.entity.HmacOtpBean import com.lyy.keepassa.entity.KeepassBean import com.lyy.keepassa.entity.KeepassXcBean import com.lyy.keepassa.entity.TimeOtp2Bean import com.lyy.keepassa.entity.TrayTotpBean import com.lyy.keepassa.util.totp.ComposeKeeTrayTotp import com.lyy.keepassa.util.totp.ComposeKeepass import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Secret import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Secret import com.lyy.keepassa.util.totp.ComposeKeepassxc import com.lyy.keepassa.util.totp.ComposeKeepassxc.KEY_STEAM import com.lyy.keepassa.util.totp.OtpUtil import com.lyy.keepassa.util.totp.SecretHexType import com.lyy.keepassa.util.totp.TokenCalculator import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm import timber.log.Timber val charRegex = Regex("[^a-zA-Z0-9]") enum class ClickScope { /** * 仅在该View中有效 */ VIEW, /** * 全局的 */ SYS } private var lastClickTime = -1L /** * 时间间隔 * @param clickScope 时间间隔,[ClickScope.VIEW],[ClickScope.SYS] */ fun View.doClick( intervalTime: Long = 1000, clickScope: ClickScope = ClickScope.VIEW, body: (View) -> Unit ) { var curTime = -1L fun viewScopeClick(it: View) { if (curTime == -1L || kotlin.math.abs(System.currentTimeMillis() - curTime) > intervalTime) { curTime = System.currentTimeMillis() body.invoke(it) return } Timber.d("间隔太短") } fun sysScopeClick(it: View) { if (kotlin.math.abs(System.currentTimeMillis() - lastClickTime) > intervalTime) { lastClickTime = System.currentTimeMillis() body.invoke(it) return } Timber.d("间隔太短") } setOnClickListener { if (intervalTime == 0L) { body.invoke(it) return@setOnClickListener } if (clickScope == ClickScope.VIEW) { viewScopeClick(it) return@setOnClickListener } if (clickScope == ClickScope.SYS) { sysScopeClick(it) return@setOnClickListener } Timber.d("间隔太短") } } @SuppressLint("RestrictedApi") fun PopupMenu.init(menuId: Int, onItemCLick: (MenuItem) -> Unit): PopupMenu { val inflater: MenuInflater = menuInflater inflater.inflate(menuId, this.menu) // 以下代码为强制显示icon val mPopup = ReflectionUtil.getField(PopupMenu::class.java, "mPopup") mPopup.isAccessible = true val help = mPopup.get(this) as MenuPopupHelper help.setForceShowIcon(true) setOnMenuItemClickListener { onItemCLick.invoke(it) return@setOnMenuItemClickListener true } return this } fun Activity.isDestroy() = isDestroyed || isFinishing /** * @return true has special char */ fun CharSequence.hasSpecialChar(): Boolean { return charRegex.containsMatchIn(this) } /** * isOpenQuickLock * @return true already open quick lock */ fun BaseApp.isCanOpenQuickLock(): Boolean { return PreferenceManager.getDefaultSharedPreferences(this) .getBoolean(applicationContext.getString(R.string.set_quick_unlock), false) && BaseApp.dbRecord != null && !KpaUtil.isEmptyPass() } fun PwEntryV4.hasNote(): Boolean { for (str in this.strings) { if (str.key.equals(PwEntryV4.STR_NOTES, true) && !TextUtils.isEmpty(str.value.toString())) { return true } } return false } fun Map.hasTOTP(): Boolean { for (str in this) { if (str.key.equals(PwEntryV4.STR_NOTES, true) || str.key.equals(PwEntryV4.STR_PASSWORD, true) || str.key.equals(PwEntryV4.STR_TITLE, true) || str.key.equals(PwEntryV4.STR_URL, true) || str.key.equals(PwEntryV4.STR_USERNAME, true) ) { continue } // 增加TOP密码字段 if (str.key.startsWith("TOTP", ignoreCase = true) || str.key.startsWith("OTP", ignoreCase = true) || str.key.startsWith(ComposeKeepass.HmacOtp, ignoreCase = true) || str.key.startsWith(ComposeKeepass.TimeOtp, ignoreCase = true) ) { return true } } return false } fun PwEntryV4.hasTOTP(): Boolean { return strings.hasTOTP() } fun PwEntryV4.isCollectioned(): Boolean { val value = strings[Constance.KPA_IS_COLLECTION] return value != null && value.toString().equals("true", true) } fun PwEntryV4.setCollection(isCollection: Boolean) { this.strings[Constance.KPA_IS_COLLECTION] = ProtectedString(false, isCollection.toString()) } fun PwEntry.copyUserName() { val userName = KdbUtil.getUserName(this) ClipboardUtil.get() .copyDataToClip(userName) HitUtil.toaskShort(ResUtil.getString(R.string.hint_copy_user)) } fun PwEntry.copyPassword() { val pass = KdbUtil.getPassword(this) ClipboardUtil.get() .copyDataToClip(pass) HitUtil.toaskShort(ResUtil.getString(R.string.hint_copy_pass)) } fun PwEntryV4.copyTotp() { val pass = OtpUtil.getOtpPass(this).second if (pass.isNullOrBlank()) { HitUtil.toaskShort(ResUtil.getString(R.string.totp_key_error)) return } ClipboardUtil.get() .copyDataToClip(pass) HitUtil.toaskShort(ResUtil.getString(R.string.hint_copy_totp)) } inline fun RecyclerView.doOnItemClickListener( crossinline action: ( rv: RecyclerView, position: Int, v: View ) -> Unit ) { RvItemClickSupport.addTo(this) .setOnItemClickListener { rv, position, v -> return@setOnItemClickListener action.invoke(rv, position, v) } } inline fun RecyclerView.doOnItemLongClickListener( crossinline action: ( rv: RecyclerView, position: Int, v: View ) -> Boolean ) { RvItemClickSupport.addTo(this) .setOnItemLongClickListener { rv, position, v -> return@setOnItemLongClickListener action.invoke(rv, position, v) } } inline fun RecyclerView.doOnTouchEvent( crossinline action: ( rv: RecyclerView, e: MotionEvent ) -> Unit ) = addOnItemTouchListener(onTouchEvent = action) inline fun RecyclerView.doOnInterceptTouchEvent( crossinline action: (rv: RecyclerView, e: MotionEvent) -> Boolean ) = addOnItemTouchListener(onInterceptTouchEvent = action) inline fun RecyclerView.doOnRequestDisallowInterceptTouchEvent( crossinline action: (disallowIntercept: Boolean) -> Unit ) = addOnItemTouchListener(onRequestDisallowInterceptTouchEvent = action) inline fun RecyclerView.addOnItemTouchListener( crossinline onTouchEvent: ( rv: RecyclerView, e: MotionEvent ) -> Unit = { _, _ -> }, crossinline onInterceptTouchEvent: ( rv: RecyclerView, e: MotionEvent ) -> Boolean = { _, _ -> false }, crossinline onRequestDisallowInterceptTouchEvent: ( disallowIntercept: Boolean ) -> Unit = { _ -> } ): OnItemTouchListener { val touchListener = object : OnItemTouchListener { override fun onTouchEvent( rv: RecyclerView, e: MotionEvent ) { onTouchEvent.invoke(rv, e) } override fun onInterceptTouchEvent( rv: RecyclerView, e: MotionEvent ): Boolean { return onInterceptTouchEvent.invoke(rv, e) } override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { onRequestDisallowInterceptTouchEvent.invoke(disallowIntercept) } } addOnItemTouchListener(touchListener) return touchListener } fun PwEntryV4.removeAttrFile(key: String) { binaries.remove(key) } fun PwEntryV4.removeAttrStr(str: String) { strings.remove(str) } fun PwEntryV4.otpIsKeeTrayTotp(): Boolean { return strings[ComposeKeeTrayTotp.KEY_SETTING] != null } fun PwEntryV4.otpIsKeeTraySteam(): Boolean { val otpSettings = strings[ComposeKeeTrayTotp.KEY_SETTING] if (otpSettings != null) { val tempArray = otpSettings.toString() .split(";") return tempArray[1] == "S" } return false } fun PwEntryV4.getKeeTrayBean(): TrayTotpBean { if (!otpIsKeeTrayTotp()) { throw IllegalAccessException("not kray otp") } val totpSetting = strings[ComposeKeeTrayTotp.KEY_SETTING] val array = totpSetting.toString() .split(";") return TrayTotpBean( secret = strings[ComposeKeeTrayTotp.KEY_SEED].toString(), period = array[0].toInt(), isSteam = array[1] == "s" ) } fun PwEntryV4.otpIsKeepassXcSteam(): Boolean { val seed = strings[ComposeKeepassxc.KEY_SEED]?.toString() val uri = Uri.parse(seed) val encoder = uri.getQueryParameter(ComposeKeepassxc.KEY_ENCODER) if (encoder != null && encoder.equals(KEY_STEAM, ignoreCase = true)) { return true } return false } fun PwEntryV4.otpKeepassXC(): Boolean { return strings[ComposeKeepassxc.KEY_SEED]?.toString() ?.startsWith("otpauth", ignoreCase = true) == true } fun PwEntryV4.getKeepassXcBean(): KeepassXcBean { if (!otpKeepassXC()) { throw IllegalAccessException("not keepassxc otp") } val seed = strings[ComposeKeepassxc.KEY_SEED]?.toString() val uri = Uri.parse(seed) val algorithm = uri.getQueryParameter(ComposeKeepassxc.KEY_ALGORITHM) return KeepassXcBean( host = uri.host ?: "totp", title = getRealTitle(), userName = getRealUserName(), isSteam = otpIsKeepassXcSteam(), encoder = uri.getQueryParameter(ComposeKeepassxc.KEY_ENCODER) ?: "", secret = uri.getQueryParameter(ComposeKeepassxc.KEY_SECRET) ?: "", issuer = uri.getQueryParameter(ComposeKeepassxc.KEY_ISSUER) ?: "", period = uri.getQueryParameter(ComposeKeepassxc.KEY_PERIOD)?.toInt() ?: TokenCalculator.TOTP_DEFAULT_PERIOD, digits = uri.getQueryParameter(ComposeKeepassxc.KEY_DIGITS)?.toInt() ?: TokenCalculator.TOTP_DEFAULT_DIGITS, algorithm = when (algorithm) { "SHA256" -> HashAlgorithm.SHA256 "SHA512" -> HashAlgorithm.SHA512 else -> HashAlgorithm.SHA1 }, counter = uri.getQueryParameter(ComposeKeepassxc.KEY_COUNTER) ?: "", ) } fun PwEntryV4.otpKeepass(): Boolean { for (str in strings) { val key = str.toString() if (key.startsWith(TimeOtp_Secret) || key.startsWith(HmacOtp_Secret)) { return true } } return false } fun PwEntryV4.getKeepassBean(): KeepassBean { var otpBean: TimeOtp2Bean? = null var hmacBean: HmacOtpBean? = null fun isHmacOtp(): Boolean { strings.forEach { if (it.toString().startsWith(ComposeKeepass.HmacOtp)) { return true } } return false } fun isTotp(): Boolean { strings.forEach { if (it.toString().startsWith(ComposeKeepass.TimeOtp)) { return true } } return false } if (isHmacOtp()) { val secretType: SecretHexType val secret = when { strings[ComposeKeepass.HmacOtp_Secret_Base32] != null -> { secretType = SecretHexType.BASE_32 strings[ComposeKeepass.HmacOtp_Secret_Base32].toString() } strings[ComposeKeepass.HmacOtp_Secret_Base64] != null -> { secretType = SecretHexType.BASE_64 strings[ComposeKeepass.HmacOtp_Secret_Base64].toString() } strings[ComposeKeepass.HmacOtp_Secret_Hex] != null -> { secretType = SecretHexType.HEX strings[ComposeKeepass.HmacOtp_Secret_Hex].toString() } else -> { secretType = SecretHexType.UTF_8 strings[HmacOtp_Secret].toString() } } hmacBean = HmacOtpBean( secretType = secretType, secret = secret, algorithm = HashAlgorithm.SHA1, counter = strings[ComposeKeepass.HmacOtp_Counter]?.toString()?.toInt() ?: TokenCalculator.HOTP_INITIAL_COUNTER, len = TokenCalculator.TOTP_DEFAULT_DIGITS ) } if (isTotp()) { val secretType: SecretHexType val secret = when { strings[ComposeKeepass.TimeOtp_Secret_Base32] != null -> { secretType = SecretHexType.BASE_32 strings[ComposeKeepass.TimeOtp_Secret_Base32].toString() } strings[ComposeKeepass.TimeOtp_Secret_Base64] != null -> { secretType = SecretHexType.BASE_64 strings[ComposeKeepass.TimeOtp_Secret_Base64].toString() } strings[ComposeKeepass.TimeOtp_Secret_Hex] != null -> { secretType = SecretHexType.HEX strings[ComposeKeepass.TimeOtp_Secret_Hex].toString() } else -> { secretType = SecretHexType.UTF_8 strings[TimeOtp_Secret].toString() } } val algorithm = when (strings[ComposeKeepass.TimeOtp_Algorithm].toString()) { ComposeKeepass.HMAC_SHA_256 -> HashAlgorithm.SHA256 ComposeKeepass.HMAC_SHA_512 -> HashAlgorithm.SHA512 else -> HashAlgorithm.SHA1 } otpBean = TimeOtp2Bean( secretType = secretType, secret = secret, digits = strings[ComposeKeepass.TimeOtp_Length]?.toString()?.toInt() ?: TokenCalculator.TOTP_DEFAULT_DIGITS, algorithm = algorithm, period = strings[ComposeKeepass.TimeOtp_Period]?.toString()?.toInt() ?: TokenCalculator.TOTP_DEFAULT_PERIOD ) } return KeepassBean( otpBean, hmacBean ) } fun PwEntryV4.otpIsKeepOtp(): Boolean { val seed = strings["otp"]?.toString() if (seed?.startsWith("key") == true) { return true } return false } /** * 判断是否是KeeOtp2插件 */ fun PwEntryV4.otpIsKeeOtp2(): Boolean { for (str in strings) { val key = str.toString() if (key.startsWith("TimeOtp") || key.startsWith("HmacOtp")) { return true } } return false } fun PwEntry.getRealTitle(): String { if (BaseApp.KDB?.pm == null) { return "" } return if (isRef()) getTitle(true, BaseApp.KDB!!.pm) else title } fun PwEntry.getRealUserName(): String { if (BaseApp.KDB?.pm == null) { return "" } return if (isRef()) getUsername(true, BaseApp.KDB!!.pm) else username } fun PwEntry.getRealPass(): String { if (BaseApp.KDB?.pm == null) { return "" } return if (isRef()) getPassword(true, BaseApp.KDB!!.pm) else password } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/KpaListEntryExtensions.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import com.arialyy.frame.util.ResUtil import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroupV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.view.SimpleEntryAdapter import timber.log.Timber /** * Check whether the entry is in the follow group */ fun PwEntryV4.checkGroupIsParent(group: PwGroupV4?): Boolean { if (group == null){ return false } return this.parent == group } /** * Check whether the entry is in the same group leve */ fun PwEntryV4.checkInGroupLeve(group: PwGroupV4): Boolean { group.childGroups.forEach { if (this.parent == it) { return true } } return false } private fun updateEntryModify(itemEntity: SimpleItemEntity){ KpaUtil.updateEntryItemInfo(itemEntity) } /** * update the status of deleted items */ fun SimpleEntryAdapter.deleteEntry( entryList: MutableList, entry: PwEntryV4, oldParentGroup: PwGroupV4, dirGroup: PwGroupV4? ) { if (oldParentGroup == dirGroup || dirGroup == null) { val entryItem = entryList.find { (it.obj is PwEntryV4) && (it.obj as PwEntryV4).uuid == entry.uuid } if (entryItem != null) { val index = entryList.indexOf(entryItem) if (index != -1) { entryList.removeAt(index) notifyItemRemoved(index) } } } if (oldParentGroup == BaseApp.KDB.pm.rootGroup) { val recycleBinItem = entryList.find { it.obj == BaseApp.KDB.pm.recycleBin } if (recycleBinItem != null) { val index = entryList.indexOf(recycleBinItem) if (index != -1) { recycleBinItem.subTitle = ResUtil.getString( R.string.hint_group_desc, KdbUtil.getGroupEntryNum(BaseApp.KDB.pm.recycleBin) ) notifyItemChanged(index) } } } if (dirGroup?.childGroups?.contains(oldParentGroup) == true) { entryList.forEachIndexed { index, it -> if (it.obj == oldParentGroup) { it.subTitle = ResUtil.getString(R.string.hint_group_desc, KdbUtil.getGroupEntryNum(oldParentGroup)) notifyItemChanged(index) return } } } Timber.d("The entry is not from the home page, title = ${entry.title}") } /** * update the status of modified items */ fun SimpleEntryAdapter.updateModifyEntry( entryList: MutableList, pwEntryV4: PwEntryV4, dirGroup: PwGroupV4 ) { if (pwEntryV4.checkGroupIsParent(dirGroup)) { entryList.forEachIndexed { index, item -> if (item.obj !is PwEntryV4){ return@forEachIndexed } if ((item.obj as PwEntryV4).uuid == pwEntryV4.uuid) { updateEntryModify(item) notifyItemChanged(index) return } } return } Timber.d("The entry is not from the root page, title = ${pwEntryV4.title}") } /** * update root list state */ fun SimpleEntryAdapter.createNewEntry( entryList: MutableList, pwEntryV4: PwEntryV4, dirGroup: PwGroupV4 ) { if (pwEntryV4.checkGroupIsParent(dirGroup)) { val index = entryList.size entryList.add(KeepassAUtil.instance.convertPwEntry2Item(pwEntryV4)) notifyItemInserted(index) // notifyDataSetChanged() return } if (pwEntryV4.checkInGroupLeve(dirGroup)) { entryList.forEachIndexed { index, simpleItemEntity -> if (simpleItemEntity.obj == pwEntryV4.parent) { simpleItemEntity.subTitle = ResUtil.getString(R.string.hint_group_desc, KdbUtil.getGroupEntryNum(pwEntryV4.parent)) notifyItemChanged(index) return } } } Timber.d("The entry is not from the home page, title = ${pwEntryV4.title}") } /** * move entry from other group * @param oldParentGroup Group before the item is moved * @param dirGroup The group directory currently displayed */ fun SimpleEntryAdapter.moveEntry( entryList: MutableList, entry: PwEntryV4, oldParentGroup: PwGroupV4, dirGroup: PwGroupV4 ) { // 检查是否是当前目录下 if (entry.parent == dirGroup) { val isInDir = entryList.find { (it.obj is PwEntryV4) && (it.obj as PwEntryV4).uuid == entry.uuid } != null if (!isInDir) { val index = entryList.size entryList.add(KeepassAUtil.instance.convertPwEntry2Item(entry)) notifyItemInserted(index) } } // 检查是否在当前目录的子目录下,不能return 因为有可能是移动到同级目录下 if (entry.checkInGroupLeve(dirGroup)) { kotlin.run breaking@{ entryList.forEachIndexed { index, simpleItemEntity -> if (simpleItemEntity.obj == entry.parent) { simpleItemEntity.subTitle = ResUtil.getString(R.string.hint_group_desc, KdbUtil.getGroupEntryNum(entry.parent)) notifyItemChanged(index) return@breaking } } } } // 检查是否是从当前目录下被移走 if (oldParentGroup == dirGroup) { val tempIndex = kotlin.run braking@{ entryList.forEachIndexed { index, simpleItemEntity -> if (simpleItemEntity.obj !is PwEntryV4) { return@forEachIndexed } if ((simpleItemEntity.obj as PwEntryV4).uuid == entry.uuid) { return@braking index } } return@braking -1 } if (tempIndex != -1) { entryList.removeAt(tempIndex) notifyItemRemoved(tempIndex) } return } // 检查是否是从当前目录的子目录下被移走 if (dirGroup.childGroups.contains(oldParentGroup)) { kotlin.run braking@{ entryList.forEachIndexed { index, simpleItemEntity -> if (simpleItemEntity.obj !is PwGroupV4) { return@forEachIndexed } if (simpleItemEntity.obj == oldParentGroup) { simpleItemEntity.subTitle = ResUtil.getString(R.string.hint_group_desc, KdbUtil.getGroupEntryNum(oldParentGroup)) notifyItemChanged(index) return@braking } } } return } Timber.d("The entry is not from the home page, title = ${entry.title}") } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/KpaListGroupExtensions.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import com.arialyy.frame.util.ResUtil import com.keepassdroid.database.PwGroupV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.view.SimpleEntryAdapter import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 2:38 下午 2022/4/2 **/ /** * Check whether the entry is in the follow group */ fun PwGroupV4.checkGroupIsParent(group: PwGroupV4?): Boolean { if (group == null) { return false } return this.parent == group } fun SimpleEntryAdapter.createGroup( entryList: MutableList, groupV4: PwGroupV4, dirGroup: PwGroupV4? ) { if (dirGroup == null) { Timber.w("parent group is null") return } if (groupV4.checkGroupIsParent(dirGroup)) { var lastGroupIndex = -1 entryList.forEachIndexed { index, item -> if (item.obj is PwGroupV4) { lastGroupIndex = index } } if (lastGroupIndex == -1) { val lastIndex = entryList.size entryList.add(KeepassAUtil.instance.convertPwGroup2Item(groupV4)) notifyItemInserted(lastIndex) return } entryList.add(lastGroupIndex + 1, KeepassAUtil.instance.convertPwGroup2Item(groupV4)) notifyItemInserted(lastGroupIndex + 1) // notifyDataSetChanged() return } Timber.d("The entry is not from the home page, title = ${groupV4.name}") } fun SimpleEntryAdapter.updateModifyGroup( entryList: MutableList, groupV4: PwGroupV4, curDirGroup: PwGroupV4? ) { if (curDirGroup == null) { Timber.w("parent group is null") return } if (!groupV4.checkGroupIsParent(curDirGroup)) { Timber.d("The entry is not from the home page, title = ${groupV4.name}") return } entryList.forEachIndexed { index, item -> if (item.obj !is PwGroupV4) { return@forEachIndexed } if ((item.obj as PwGroupV4).uuid == groupV4.uuid) { KpaUtil.updateGroupItemInfo(item) notifyItemChanged(index) return } } } fun SimpleEntryAdapter.deleteGroup( entryList: MutableList, groupV4: PwGroupV4, oldParentGroup: PwGroupV4, curDirGroup: PwGroupV4? ) { if (curDirGroup == null) { Timber.w("parent group is null") return } if (oldParentGroup != curDirGroup) { Timber.d("The entry is not from the home page, title = ${groupV4.name}, curDirGroup = ${curDirGroup.name}") return } // remove group form entryList val delIndex = kotlin.run breaking@{ entryList.forEachIndexed { index, item -> if (item.obj !is PwGroupV4) { return@forEachIndexed } if ((item.obj as PwGroupV4).uuid == groupV4.uuid) { return@breaking index } } return@breaking -1 } entryList.removeAt(delIndex) notifyItemRemoved(delIndex) // update recycling bin if (curDirGroup == BaseApp.KDB.pm.rootGroup) { entryList.forEachIndexed { index, item -> if (item.obj == BaseApp.KDB.pm.recycleBin) { item.subTitle = ResUtil.getString( R.string.hint_group_desc, KdbUtil.getGroupEntryNum(BaseApp.KDB.pm.recycleBin) ) notifyItemChanged(index) return } } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/KpaUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.content.Intent import android.graphics.Paint import android.net.Uri import android.text.InputType import android.widget.TextView import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.ResUtil import com.arialyy.frame.util.StringUtil import com.blankj.utilcode.util.ActivityUtils import com.blankj.utilcode.util.ToastUtils import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroupV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.EntryRecord import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.router.ServiceRouter import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.MutableSharedFlow import timber.log.Timber import java.util.Date import java.util.Locale /** * @Author laoyuyu * @Description * @Date 2022/3/22 **/ object KpaUtil { var scope = MainScope() private var isEmptyPass = false val kdbHandlerService by lazy { Routerfit.create(ServiceRouter::class.java).getDbSaveService() } val kdbOpenService by lazy { Routerfit.create(ServiceRouter::class.java).getDbOpenService() } val openEntryRecordFlow = MutableSharedFlow() fun isEmptyPass(): Boolean { return isEmptyPass } fun setEmptyPass(isEmptyPass: Boolean) { this.isEmptyPass = isEmptyPass } fun isChina(): Boolean { return LanguageUtil.getSysCurrentLan().country == Locale.CHINA.country } fun updateEntryItemInfo(item: SimpleItemEntity) { val entry = (item.obj as PwEntryV4) item.title = entry.title item.subTitle = if (entry.isRef()) { val refStr = "${BaseApp.APP.resources.getString(R.string.ref_entry)}: " val tempStr = "${refStr}${KdbUtil.getUserName(entry)}" StringUtil.highLightStr(tempStr, refStr, ResUtil.getColor(R.color.colorPrimary), true) } else { entry.username } } fun updateGroupItemInfo(item: SimpleItemEntity) { val pwGroup = (item.obj as PwGroupV4) item.title = pwGroup.name item.subTitle = ResUtil.getString( R.string.hint_group_desc, KdbUtil.getGroupEntryNum(pwGroup) .toString() ) item.obj = pwGroup } /** * open url with browser */ fun openUrlWithBrowser(url: String) { try { ActivityUtils.getTopActivity().startActivity(Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(url) }) } catch (e: Exception) { ToastUtils.showLong("${ResUtil.getString(R.string.invalid)}${ResUtil.getString(R.string.url)}") Timber.e(e) } } /** * 处理密码的显示 */ fun handleShowPass(tv: TextView, show: Boolean) { tv.inputType = if (show) { InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD } else { InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD } } /** * 处理过期的view,并加上中横线 */ fun handleExpire(tv: TextView, pwEntryV4: PwEntryV4) { if (pwEntryV4.expires() && pwEntryV4.expiryTime != null && pwEntryV4.expiryTime.before(Date(System.currentTimeMillis())) ) { val paint = tv.paint paint.flags = Paint.STRIKE_THRU_TEXT_FLAG paint.isAntiAlias = true } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/LanguageUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.content.Context import android.content.res.Configuration import android.content.res.Resources import android.os.Build import android.os.LocaleList import android.text.TextUtils import com.lyy.keepassa.base.Constance import java.util.Locale /** * 语言工具 */ object LanguageUtil { private val LOCALE_KEY_LANG = "LOCALE_KEY_LANG" private val LOCALE_KEY_COUNTRY = "LOCALE_KEY_COUNTRY" @JvmField val SUPPORT_LAN = arrayListOf( Locale.ENGLISH, Locale.SIMPLIFIED_CHINESE, Locale.TRADITIONAL_CHINESE, Locale.CANADA_FRENCH ) /** * 设置app语言 * @return context, activity 需要使用attachBaseContext()来更新该context */ // @CheckResult(suggest = "activity需要使用attachBaseContext()来更新该context") fun setLanguage( context: Context, locale: Locale ): Context { val res: Resources = context.resources val conf = res.configuration conf.setLocale(locale) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { conf.setLocales(LocaleList(locale)) } res.updateConfiguration(conf, res.displayMetrics) val cx = context.createConfigurationContext(conf) return cx } fun getLanguageConfig(context: Context, locale: Locale): Configuration { val res: Resources = context.resources val conf = res.configuration conf.setLocale(locale) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { conf.setLocales(LocaleList(locale)) } return conf } /** * 获取当前系统语言 * https://mp.weixin.qq.com/s/A27dvFV3glX26Ur0WsdJ9g */ fun getSysCurrentLan(): Locale { return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { Locale.getDefault() } else { LocaleList.getDefault().get(0) } } /** * 保存语言到配置文件 */ fun saveLanguage( context: Context, locale: Locale ) { val pre = context.getSharedPreferences(Constance.PRE_FILE_NAME, Context.MODE_PRIVATE) val editor = pre.edit() editor.putString(LOCALE_KEY_LANG, locale.language) editor.putString(LOCALE_KEY_COUNTRY, locale.country) editor.apply() } /** * 读取保存的语言 * @return 如果没有,返回null */ fun getDefLanguage(context: Context): Locale? { val pre = context.getSharedPreferences(Constance.PRE_FILE_NAME, Context.MODE_PRIVATE) val lang = pre.getString(LOCALE_KEY_LANG, "") val country = pre.getString(LOCALE_KEY_COUNTRY, "") return if (TextUtils.isEmpty(lang) && TextUtils.isEmpty(country)) { null } else { Locale(lang, country) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/NotificationUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.content.Context import android.content.Intent import android.os.Build import android.text.TextUtils import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.service.DbOpenNotificationService /** * 通知工具 */ object NotificationUtil { /** * 打开数据库通知 */ fun startDbOpenNotify(context: Context) { val intent = Intent(context, DbOpenNotificationService::class.java).apply { putExtra( DbOpenNotificationService.KEY_NOTIFY_TYPE, DbOpenNotificationService.NOTIFY_TYPE_OPEN_DB ) } startService(context, intent) } /** * 数据库已锁定,启动快速解锁 */ fun startQuickUnlockNotify(context: Context) { val intent = Intent(context, DbOpenNotificationService::class.java).apply { putExtra( DbOpenNotificationService.KEY_NOTIFY_TYPE, DbOpenNotificationService.NOTIFY_TYPE_QUICK_UNLOCK_DB ) } startService(context, intent) } /** * 数据库已锁定通知 */ fun startDbLocked(context: Context) { val intent = Intent(context, DbOpenNotificationService::class.java).apply { putExtra( DbOpenNotificationService.KEY_NOTIFY_TYPE, DbOpenNotificationService.NOTIFY_TYPE_DB_LOCKED ) } startService(context, intent) } private fun startService( context: Context, intent: Intent ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { /* * Android 在 8.0 限制了后台服务这些,启动后台服务需要设置通知栏,使服务变成前台服务。 * 但是在 9.0 上,就会出现Permission Denial: startForeground requires */ context.startForegroundService(intent) } else { context.startService(intent) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/PasswordBuildUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import timber.log.Timber import kotlin.random.Random /** * 创建密码工具 */ class PasswordBuildUtil private constructor() { companion object { @Volatile private var INSTANCE: PasswordBuildUtil? = null private const val LOWER_CHAR = "lowerChar" private const val UP_CHAR = "upChar" private const val NUM_CHAR = "numChar" private const val SYMBOL_CHAR = "symbolChar" private const val BRACKET_CHAR = "bracketChar" private const val SPACE_CHAR = "spaceChar" private const val UNDERLINE = "underline" private const val MINUS = "minus" @Synchronized fun getInstance(): PasswordBuildUtil { if (INSTANCE == null) { synchronized(PasswordBuildUtil::class.java) { INSTANCE = PasswordBuildUtil() } } return INSTANCE!! } } private val lowerChar = "abcdefghijklmnopqrstuvwxyz" private val upChar = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" private val numChar = "1234567890" // 特殊符号 private val symbolChar = "`~!@#$%^&*+=;:'\",.?/|\\" // ~ // 括号 private val bracketChar = "()<>{}[]" // 空格 private val spaceChar = " " // 下划线 private val underline = "_" // 减号 private val minus = "-" private val charList = arrayListOf() // 默认字符串 // private var defStr: StringBuffer = StringBuffer() /** * 清空字符 */ public fun clear(): PasswordBuildUtil { charList.clear() return this } /** * 使用小写字符 */ public fun addLowerChar(): PasswordBuildUtil { charList.add(lowerChar) return this } /** * 使用大写字符 */ public fun addUpChar(): PasswordBuildUtil { charList.add(upChar) return this } /** * 使用数字 */ public fun addNumChar(): PasswordBuildUtil { charList.add(numChar) return this } /** * 使用特殊字符 */ public fun addSymbolChar(): PasswordBuildUtil { charList.add(symbolChar) return this } /** * 使用括号 */ public fun addBracketChar(): PasswordBuildUtil { charList.add(bracketChar) return this } /** * 使用空格 */ public fun addSpaceChar(): PasswordBuildUtil { charList.add(spaceChar) return this } /** * 使用下划线 */ public fun addUnderline(): PasswordBuildUtil { charList.add(underline) return this } /** * 使用减号 */ public fun addMinus(): PasswordBuildUtil { charList.add(minus) return this } /** * 构建密码,如果密码长度小于选择的[charList]长度,则修改其长度为[charList]的长度 * @param len 密码长度 * @return 如果[种子][charList]没有设置,或长度小于1 返回"" */ public fun builder(len: Int): String { if (charList.isEmpty()) { return "" } if (len < 1) { return "" } val realLen = if (len < charList.size) { Timber.w("密码长度小于选择的特殊符号种类,将生成和特殊符号种类数量一致长度的密码") charList.size } else { len } val cloneList = arrayListOf() cloneList.addAll(charList) val random = Random.Default val sb = StringBuilder() for (i in 1..realLen) { // 确保每种类型都有 if (cloneList.isNotEmpty()) { val charIndex = random.nextInt(cloneList.size) val char = cloneList[charIndex] sb.append(char[random.nextInt(char.length)]) cloneList.remove(char) continue } val charIndex = random.nextInt(charList.size) val char = charList[charIndex] sb.append(char[random.nextInt(char.length)]) } return sb.toString() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/PermissionPageManagement.java ================================================ package com.lyy.keepassa.util; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.provider.Settings; import com.blankj.utilcode.util.RomUtils; import timber.log.Timber; public class PermissionPageManagement { /** * 此函数可以自己定义 */ public static void goToSetting(Context activity) { if (RomUtils.isHuawei()) { Huawei(activity); return; } if (RomUtils.isMeizu()) { Meizu(activity); return; } if (RomUtils.isXiaomi()) { Xiaomi(activity); return; } if (RomUtils.isVivo()) { VIVO(activity); return; } if (RomUtils.isOppo()) { OPPO(activity); return; } if (RomUtils.isSony()) { Sony(activity); return; } if (RomUtils.isLg()) { LG(activity); return; } ApplicationInfo(activity); Timber.e("目前暂不支持此系统"); } public static void Huawei(Context activity) { try { Intent intent = new Intent(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("packageName", activity.getApplicationInfo().packageName); ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity"); intent.setComponent(comp); activity.startActivity(intent); } catch (Exception e) { Timber.e(e); goIntentSetting(activity); } } public static void Meizu(Context activity) { try { Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC"); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.putExtra("packageName", activity.getPackageName()); activity.startActivity(intent); } catch (Exception e) { goIntentSetting(activity); Timber.e(e); } } public static void Xiaomi(Context activity) { try { Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR"); intent.putExtra("extra_pkgname", activity.getPackageName()); //ComponentName componentName = new ComponentName("com.miui.securitycenter", // "com.miui.permcenter.permissions.PermissionsEditorActivity"); //intent.setComponent(componentName); intent.addCategory(Intent.CATEGORY_DEFAULT); activity.startActivity(intent); } catch (Exception e) { goIntentSetting(activity); Timber.e(e); } } public static void Sony(Context activity) { try { Intent intent = new Intent(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("packageName", activity.getPackageName()); ComponentName comp = new ComponentName("com.sonymobile.cta", "com.sonymobile.cta.SomcCTAMainActivity"); intent.setComponent(comp); activity.startActivity(intent); } catch (Exception e) { goIntentSetting(activity); Timber.e(e); } } public static void OPPO(Context activity) { try { Intent intent = new Intent(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("packageName", activity.getPackageName()); // ComponentName comp = new ComponentName("com.color.safecenter", "com.color.safecenter.permission.PermissionManagerActivity"); ComponentName comp = new ComponentName("com.coloros.securitypermission", "com.coloros.securitypermission.permission.PermissionAppAllPermissionActivity");//R11t 7.1.1 os-v3.2 intent.setComponent(comp); activity.startActivity(intent); } catch (Exception e) { goIntentSetting(activity); Timber.e(e); } } public static void VIVO(Context activity) { Intent localIntent; if (((Build.MODEL.contains("Y85")) && (!Build.MODEL.contains("Y85A"))) || (Build.MODEL.contains( "vivo Y53L"))) { localIntent = new Intent(); localIntent.setClassName("com.vivo.permissionmanager", "com.vivo.permissionmanager.activity.PurviewTabActivity"); localIntent.putExtra("packagename", activity.getPackageName()); localIntent.putExtra("tabId", "1"); activity.startActivity(localIntent); } else { localIntent = new Intent(); localIntent.setClassName("com.vivo.permissionmanager", "com.vivo.permissionmanager.activity.SoftPermissionDetailActivity"); localIntent.setAction("secure.intent.action.softPermissionDetail"); localIntent.putExtra("packagename", activity.getPackageName()); activity.startActivity(localIntent); } } public static void LG(Context activity) { try { Intent intent = new Intent("android.intent.action.MAIN"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("packageName", activity.getPackageName()); ComponentName comp = new ComponentName("com.android.settings", "com.android.settings.Settings$AccessLockSummaryActivity"); intent.setComponent(comp); activity.startActivity(intent); } catch (Exception e) { goIntentSetting(activity); Timber.e(e); } } /** * 只能打开到自带安全软件 */ public static void _360(Context activity) { Intent intent = new Intent("android.intent.action.MAIN"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("packageName", activity.getPackageName()); ComponentName comp = new ComponentName("com.qihoo360.mobilesafe", "com.qihoo360.mobilesafe.ui.index.AppEnterActivity"); intent.setComponent(comp); activity.startActivity(intent); } /** * 应用信息界面 */ public static void ApplicationInfo(Context activity) { Intent localIntent = new Intent(); localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS"); localIntent.setData(Uri.fromParts("package", activity.getPackageName(), null)); activity.startActivity(localIntent); } /** * 系统设置界面 */ public static void SystemConfig(Context activity) { Intent intent = new Intent(Settings.ACTION_SETTINGS); activity.startActivity(intent); } /** * 默认打开应用详细页 */ private static void goIntentSetting(Context pActivity) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", pActivity.getPackageName(), null); intent.setData(uri); try { pActivity.startActivity(intent); } catch (Exception e) { Timber.e(e); } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/PermissionsUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util; import android.app.AppOpsManager import android.content.Context import android.net.Uri import android.os.Build import android.os.Process import android.provider.Settings import android.text.Html import android.view.autofill.AutofillManager import android.widget.Button import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.RomUtils import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.view.dialog.OnMsgBtClickListener import timber.log.Timber import java.lang.reflect.Method object PermissionsUtil { /** * 是否需要弹出后台启动提示弹窗 */ fun needShowBackgroundStartDialog(context: Context): Boolean { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return false } val am = context.getSystemService(AutofillManager::class.java) if (!am.isAutofillSupported) { Timber.i("不支持自动填充") return false } if (am.hasEnabledAutofillServices() && !isCanBackgroundStart()) { Timber.i("已经打开了自动填充") return true } return false } /** * 显示弹出框提示用户打开后台启动界面的权限 */ fun showAutoFillMsgDialog(context: Context, msg: String) { // val IS_HOWED_AUTO_FILL_HINT_DIALOG = "IS_HOWED_AUTO_FILL_HINT_DIALOG" // val isShowed = // SharePreUtil.getBoolean( // Constance.PRE_FILE_NAME, // context, // IS_HOWED_AUTO_FILL_HINT_DIALOG // ) // // if (!isShowed) { Routerfit.create(DialogRouter::class.java).showMsgDialog( msgContent = Html.fromHtml(BaseApp.APP.getString(R.string.hint_background_start, msg)), showCancelBt = true, cancelText = ResUtil.getString(R.string.cancel), enterText = ResUtil.getString(R.string.open_setting), btnClickListener = object : OnMsgBtClickListener { override fun onEnter(v: Button) { PermissionPageManagement.goToSetting(context) } override fun onCancel(v: Button) { } } ) // SharePreUtil.putBoolean( // Constance.PRE_FILE_NAME, // context, // IS_HOWED_AUTO_FILL_HINT_DIALOG, // true // ) // } else { // Timber.i("已显示过自动填充对话框,不再重复显示") // } } fun isCanBackgroundStart(): Boolean { if (RomUtils.isXiaomi()) { return miuiCanBackgroundStart() } if (RomUtils.isVivo()) { return vivoBackgroundStartAllowed() } if (RomUtils.isHuawei()) { return hwBackgroundStartAllowed(BaseApp.APP) } return true } /** * 检查miui 是否被允许后台启动 * * @return true 允许后台启动 */ fun miuiCanBackgroundStart(): Boolean { val ops = BaseApp.APP.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager try { val op = 10021 val method = ops.javaClass.getMethod( "checkOpNoThrow", Int::class.java, Int::class.java, String::class.java ) val result = method.invoke(ops, op, Process.myUid(), BaseApp.APP.packageName) as Int return result == AppOpsManager.MODE_ALLOWED } catch (e: Exception) { Timber.e(e, "not support") } return false } /** * 判断vivo后台弹出界面 1未开启 0开启 */ fun vivoBackgroundStartAllowed(): Boolean { val packageName = BaseApp.APP.packageName val uri2 = Uri.parse("content://com.vivo.permissionmanager.provider.permission/start_bg_activity") val selection = "pkgname = ?" val selectionArgs = arrayOf(packageName) try { val cursor = BaseApp.APP .contentResolver .query(uri2, null, selection, selectionArgs, null); if (cursor != null) { return if (cursor.moveToFirst()) { val index = cursor.getColumnIndex("currentstate") if (index == -1) { false } else { val currentmode = cursor.getInt(index) cursor.close() currentmode == 0 } } else { cursor.close() false } } } catch (throwable: Throwable) { Timber.d(throwable) } return false } fun hwBackgroundStartAllowed(context: Context): Boolean { try { val c = Class.forName("com.huawei.android.app.AppOpsManagerEx") val ops = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager val m: Method = c.getDeclaredMethod( "checkHwOpNoThrow", AppOpsManager::class.java, Int::class.java, Int::class.java, String::class.java ) return m.invoke( c.newInstance(), ops, 100000, Process.myUid(), context.packageName ) as Int == AppOpsManager.MODE_ALLOWED } catch (e: Exception) { return false } } /** * 检查oppo 是否被允许后台启动 * * @return true 允许后台启动 */ fun oppoBackgroundStartAllowed(): Boolean { return Settings.canDrawOverlays(BaseApp.APP) } // fun testCheckHwOp() { // val c = Class.forName("com.huawei.android.app.AppOpsManagerEx") // val m: Method = c.getDeclaredMethod( // "checkHwOpNoThrow", // AppOpsManager::class.java, // Int::class.javaPrimitiveType, // Int::class.javaPrimitiveType, // String::class.java // ) // val bundle: Bundle = getNoteParamInt() // val op = bundle.getInt(KEY_OP_CODES) // val packageName = bundle.getString(KEY_PKG_NAME) // val uid = bundle.getInt(KEY_UID) // val checkResult = m.invoke( // c.newInstance(), arrayOf( // context.getSystemService( // Context.APP_OPS_SERVICE // ) as AppOpsManager, op, uid, packageName // ) // ) as Int // com.sun.corba.se.impl.activation.ServerMain.printResult("check result:$checkResult op:$op uid:$uid packageName:$packageName") // } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/PlayUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.app.Activity import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability object PlayUtil { fun playServiceExist(context: Activity):Boolean { // 验证是否已在此设备上安装并启用Google Play服务,以及此设备上安装的旧版本是否为此客户端所需的版本 val code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) if (code == ConnectionResult.SUCCESS) { // 支持Google服务 return true } else { /** * 依靠 Play 服务 SDK 运行的应用在访问 Google Play 服务功能之前,应始终检查设备是否拥有兼容的 Google Play 服务 APK。 * 我们建议您在以下两个位置进行检查:主 Activity 的 onCreate() 方法中,及其 onResume() 方法中。 * onCreate() 中的检查可确保该应用在检查成功之前无法使用。 * onResume() 中的检查可确保当用户通过一些其他方式返回正在运行的应用(比如通过返回按钮)时,检查仍将继续进行。 * 如果设备没有兼容的 Google Play 服务版本,您的应用可以调用以下方法,以便让用户从 Play 商店下载 Google Play 服务。 * 它将尝试在此设备上提供Google Play服务。如果Play服务已经可用,则Task可以立即完成返回。 */ GoogleApiAvailability.getInstance().makeGooglePlayServicesAvailable(context) // 或者使用以下代码 /** * 通过isUserResolvableError来确定是否可以通过用户操作解决错误 */ if (GoogleApiAvailability.getInstance().isUserResolvableError(code)) { /** * 返回一个对话框,用于解决提供的errorCode。 * @param activity 用于创建对话框的父活动 * @param code 通过调用返回的错误代码 * @param activity 调用startActivityForResult时给出的requestCode */ GoogleApiAvailability.getInstance().getErrorDialog(context, code, 200)?.show() } } return false } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/QuickUnLockUtil.java ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util; /** * 密码工具 */ public class QuickUnLockUtil { /** * 快速解锁保存的信息 */ private static final String QUICK_UN_LOCK_FILE_NAME = "91b7b54da7f5154020a528"; static { System.loadLibrary("keepassA"); } /** * 加密字符串 * * @param str 明文 * @return 密文 */ //@Deprecated(message = "不安全") public native static String encryptStr(String str); /** * 解密字符串 * * @param str 密文 * @return 明文 */ public native static String decryption(String str); /** * 获取数据库密码 */ public native static String getDbPass(); } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/RealPathUtil.java ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util; import android.annotation.SuppressLint; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.DocumentsContract; import android.provider.MediaStore; import android.provider.OpenableColumns; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import timber.log.Timber; public class RealPathUtil { public static @Nullable Uri compatUriFromFile(@NonNull final Context context, @NonNull final File file) { Uri result = null; if (Build.VERSION.SDK_INT < 21) { result = Uri.fromFile(file); } else { final String packageName = context.getApplicationContext().getPackageName(); final String authority = new StringBuilder(packageName).append(".provider").toString(); try { result = FileProvider.getUriForFile(context, authority, file); } catch (IllegalArgumentException e) { Timber.e(e); } } return result; } @SuppressLint("NewApi") public static @Nullable String getRealPathFromURI(@NonNull final Context context, @NonNull final Uri uri) { final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; // DocumentProvider if (isKitKat && 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); // Handle raw file urls differently, we can just return the current string minus // the raw: prefix. https://github.com/Yalantis/uCrop/issues/318 if (id != null && id.startsWith("raw:")) { return id.substring(4); } final Uri contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), Long.valueOf(id.split(":")[1])); try { return getDataColumn(context, contentUri, null, null); } catch (Exception e) { Timber.e(e); } // File couldn't be loaded so we have to create a temporary file and then stream // the contents into that file, some of this code has been implemented from // https://github.com/coltoscosmin/FileUtils/blob/master/FileUtils.java String fileName = getFileName(context, uri); File cacheDir = new File(context.getCacheDir(), "documents"); if (!cacheDir.exists()) cacheDir.mkdirs(); File file = new File(cacheDir, fileName); try { file.createNewFile(); } catch (Exception e) { Timber.e(e); } String destinationPath = file.getAbsolutePath(); Timber.d("Destination path: %s", destinationPath); destinationPath = file.getAbsolutePath(); saveFileFromUri(context, uri, destinationPath); return destinationPath; } // 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 the remote address if (isGooglePhotosUri(uri)) { return uri.getLastPathSegment(); } if (isFileProviderUri(context, uri)) { return getFileProviderPath(context, uri); } 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. */ public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = "_data"; final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { final int index = cursor.getColumnIndexOrThrow(column); return cursor.getString(index); } } finally { if (cursor != null) { cursor.close(); } } return null; } /** * @param uri The Uri to check. * @return Whether the Uri authority is ExternalStorageProvider. */ public 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. */ public 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. */ public static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is Google Photos. */ public static boolean isGooglePhotosUri(@NonNull final Uri uri) { return "com.google.android.apps.photos.content".equals(uri.getAuthority()); } /** * @param context The Application context * @param uri The Uri is checked by functions * @return Whether the Uri authority is FileProvider */ public static boolean isFileProviderUri(@NonNull final Context context, @NonNull final Uri uri) { final String packageName = context.getPackageName(); final String authority = new StringBuilder(packageName).append(".provider").toString(); return authority.equals(uri.getAuthority()); } /** * @param context The Application context * @param uri The Uri is checked by functions * @return File path or null if file is missing */ public static @Nullable String getFileProviderPath(@NonNull final Context context, @NonNull final Uri uri) { final File appDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); final File file = new File(appDir, uri.getLastPathSegment()); return file.exists() ? file.toString() : null; } private static void saveFileFromUri(Context context, Uri uri, String destinationPath) { InputStream is = null; BufferedOutputStream bos = null; try { is = context.getContentResolver().openInputStream(uri); bos = new BufferedOutputStream(new FileOutputStream(destinationPath, false)); byte[] buf = new byte[1024]; is.read(buf); do { bos.write(buf); } while (is.read(buf) != -1); } catch (IOException e) { Timber.e(e); } finally { try { if (is != null) is.close(); if (bos != null) bos.close(); } catch (IOException e) { Timber.e(e); } } } public static String getFileName(@NonNull Context context, Uri uri) { String mimeType = context.getContentResolver().getType(uri); String filename = null; if (mimeType == null && context != null) { String path = getPath(context, uri); if (path == null) { filename = getName(uri.toString()); } else { File file = new File(path); filename = file.getName(); } } else { Cursor returnCursor = context.getContentResolver().query(uri, null, null, null, null); if (returnCursor != null) { int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); returnCursor.moveToFirst(); filename = returnCursor.getString(nameIndex); returnCursor.close(); } } return filename; } public static String getName(String filename) { if (filename == null) { return null; } int index = filename.lastIndexOf('/'); return filename.substring(index + 1); } public static String getPath(final Context context, final Uri uri) { String absolutePath = getRealPathFromURI(context, uri); // String absolutePath = getLocalPath(context, uri); return absolutePath != null ? absolutePath : uri.toString(); } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/VibratorUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util import android.app.Service import android.os.Build import android.os.VibrationEffect import android.os.Vibrator import com.lyy.keepassa.base.BaseApp /** * 震动工具 */ object VibratorUtil { private val vb: Vibrator = BaseApp.APP.getSystemService(Service.VIBRATOR_SERVICE) as Vibrator /** * 震动一次 */ fun vibrator(time: Long) { if (!vb.hasVibrator()) { return } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { vb.vibrate(VibrationEffect.createOneShot(time, VibrationEffect.DEFAULT_AMPLITUDE)) } else { vb.vibrate(time) } } /** * 指定手机以pattern指定的模式振动 * @param pattern pattern为new int[200,400,600,800],就是让他在200,400,600,800这个时间交替启动与关闭振动器! * @param repeat 而第二个则是重复次数,如果是-1的只振动一次,如果是0的话则一直振动 */ fun vibrate( pattern: LongArray, repeat: Int ) { if (!vb.hasVibrator()) { return } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { vb.vibrate(VibrationEffect.createWaveform(pattern, repeat)) } else { vb.vibrate(pattern, repeat) } } fun cancel() { vb.cancel() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/CloudFileInfo.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud import java.util.Date /** * 云端文件信息 */ data class CloudFileInfo( val fileKey: String, // webDav/dropbox中为云端路径,onedrive为id val fileName: String, // 文件名 val serviceModifyDate: Date, // 该文件在云端的修改时间 val size: Long, // 文件大小 val isDir: Boolean, val contentHash: String? = null, // hash ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/CloudUtilFactory.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud import com.lyy.keepassa.view.StorageType import com.lyy.keepassa.view.StorageType.DROPBOX import com.lyy.keepassa.view.StorageType.ONE_DRIVE import com.lyy.keepassa.view.StorageType.WEBDAV /** * 云端文件工具工厂 */ object CloudUtilFactory { fun getCloudUtil(storageType: StorageType): ICloudUtil { return when (storageType) { DROPBOX -> DropboxUtil WEBDAV -> WebDavUtil ONE_DRIVE -> OneDriveUtil else -> throw IllegalArgumentException("不识别的工具类型:${storageType}") } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/DbSynUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud import android.content.Context import android.net.Uri import android.text.TextUtils import com.arialyy.frame.util.SharePreUtil import com.arialyy.frame.util.StringUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.Constance import com.lyy.keepassa.entity.DbHistoryRecord import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.cloud.interceptor.DbSyncCheckInterceptor import com.lyy.keepassa.util.cloud.interceptor.DbSyncCompareInterceptor import com.lyy.keepassa.util.cloud.interceptor.DbSyncRequest import com.lyy.keepassa.util.cloud.interceptor.DbSyncResponse import com.lyy.keepassa.util.cloud.interceptor.DbSyncUploadInterceptor import com.lyy.keepassa.util.cloud.interceptor.IDbSyncInterceptor import com.lyy.keepassa.view.StorageType import com.lyy.keepassa.view.StorageType.AFS import timber.log.Timber import java.io.File import java.util.Date /** * 数据库同步工具 */ object DbSynUtil : SynStateCode { private val TAG = StringUtil.getClassName(this) private const val KEY_SERVICE_MODIFY_TIME = "KEY_SERVICE_MODIFY_TIME" /** * 打开数据库时,记录的云盘时间,上传完成需要重新更新 */ var serviceModifyTime: Date = Date(System.currentTimeMillis()) init { serviceModifyTime = Date(SharePreUtil.getLong(Constance.PRE_FILE_NAME, BaseApp.APP, KEY_SERVICE_MODIFY_TIME)) } private val interceptors = arrayListOf().apply { add(DbSyncCheckInterceptor()) add(DbSyncCompareInterceptor()) add(DbSyncUploadInterceptor()) } /** * 或去该记录在云端的修改时间 */ suspend fun getFileServiceModifyTime(record: DbHistoryRecord): Date { return CloudUtilFactory.getCloudUtil(record.getDbPathType()) .getFileServiceModifyTime(record.cloudDiskPath!!) } suspend fun fileExists(record: DbHistoryRecord): Boolean { return CloudUtilFactory.getCloudUtil(record.getDbPathType()).fileExists(record.cloudDiskPath!!) } suspend fun getFileInfo(record: DbHistoryRecord): CloudFileInfo? { return CloudUtilFactory.getCloudUtil(record.getDbPathType()).getFileInfo(record.cloudDiskPath!!) } /** * 更新服务器端文件的修改时间 */ suspend fun updateServiceModifyTime( record: DbHistoryRecord ) { serviceModifyTime = CloudUtilFactory.getCloudUtil(record.getDbPathType()) .getFileServiceModifyTime(record.cloudDiskPath!!) SharePreUtil.putLong( Constance.PRE_FILE_NAME, BaseApp.APP, KEY_SERVICE_MODIFY_TIME, serviceModifyTime.time ) Timber.d( TAG, "更新云端文件修改时间:${KeepassAUtil.instance.formatTime(serviceModifyTime)}" ) } /** * 从云端下载的文件缓存路径 * @param cloudTypeName 云端网盘名 */ fun getCloudDbTempPath( cloudTypeName: String, dbName: String ): Uri { val file = File("${BaseApp.APP.cacheDir.path}/$cloudTypeName/${dbName}") if (file.parentFile != null && !file.parentFile!!.exists()) { file.parentFile!!.mkdirs() } return Uri.fromFile(file) } /** * 上传同步 */ suspend fun uploadSyn(record: DbHistoryRecord, isCreate: Boolean = false): DbSyncResponse { val storageType = record.getDbPathType() if (storageType == AFS) { return DbSyncResponse(STATE_SUCCEED, "") } val util = CloudUtilFactory.getCloudUtil(storageType) if (isCreate) { val ins = arrayListOf().apply { add(DbSyncUploadInterceptor()) } return ins[0].intercept(DbSyncRequest(record, util, ins)) } return interceptors[0].intercept( DbSyncRequest( record, util, interceptors ) ) } /** * 只用于下载,如果文件存在,先会删除文件,再执行下载 */ suspend fun downloadOnly( context: Context, dbRecord: DbHistoryRecord, filePath: Uri ): String? { Timber.i("开始下载文件,云端路径:${dbRecord.cloudDiskPath},文件保存路径:${filePath}") val util = CloudUtilFactory.getCloudUtil(StorageType.valueOf(dbRecord.type)) val path = util.downloadFile(context, dbRecord, filePath) if (!TextUtils.isEmpty(path)) { updateServiceModifyTime(dbRecord) } return path } fun toask( msg: String, success: Boolean, des: String ) { BaseApp.handler.post { HitUtil.toaskShort( "$msg ${ if (success) BaseApp.APP.getString(R.string.success) else BaseApp.APP.getString( R.string.fail ) } $des" ) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/DropboxContentHasher.java ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud; import java.nio.ByteBuffer; import java.security.DigestException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * Computes a hash using the same algorithm that the Dropbox API uses for the * the "content_hash" metadata field. * *

* The {@link #digest()} method returns a raw binary representation of the hash. * The "content_hash" field in the Dropbox API is a hexadecimal-encoded version * of the digest. *

* *

* Example: *

* *
 * MessageDigest hasher = new DropboxContentHasher();
 * byte[] buf = new byte[1024];
 * InputStream in = new FileInputStream("some-file");
 * try {
 *     while (true) {
 *         int n = in.read(buf);
 *         if (n < 0) break;  // EOF
 *         hasher.update(buf, 0, n);
 *     }
 * }
 * finally {
 *     in.close();
 * }
 *
 * byte[] rawHash = hasher.digest();
 * System.out.println(hex(rawHash));
 *     // Assuming 'hex' is a method that converts a byte[] to
 *     // a hexadecimal-encoded String
 * 
* *

* If you need to hash something as it passes through a stream, you can use the * {@link java.security.DigestInputStream} or {@code java.security.DigestOutputStream} helpers. *

* *
 * MessageDigest hasher = new DropboxContentHasher();
 * InputStream in = new FileInputStream("some-file");
 * UploadResponse r;
 * try {
 *     r = someApiClient.upload(new DigestInputStream(in, hasher)));
 * }
 * finally {
 *     in.close();
 * }
 *
 * String locallyComputed = hex(hasher.digest());
 * assert r.contentHash.equals(locallyComputed);
 * 
*/ public final class DropboxContentHasher extends MessageDigest implements Cloneable { private MessageDigest overallHasher; private MessageDigest blockHasher; private int blockPos = 0; public static final int BLOCK_SIZE = 4 * 1024 * 1024; public DropboxContentHasher() { this(newSha256Hasher(), newSha256Hasher(), 0); } private DropboxContentHasher(MessageDigest overallHasher, MessageDigest blockHasher, int blockPos) { super("Dropbox-Content-Hash"); this.overallHasher = overallHasher; this.blockHasher = blockHasher; this.blockPos = blockPos; } @Override protected void engineUpdate(byte input) { finishBlockIfFull(); blockHasher.update(input); blockPos += 1; } @Override protected int engineGetDigestLength() { return overallHasher.getDigestLength(); } @Override protected void engineUpdate(byte[] input, int offset, int len) { int inputEnd = offset + len; while (offset < inputEnd) { finishBlockIfFull(); int spaceInBlock = BLOCK_SIZE - this.blockPos; int inputPartEnd = Math.min(inputEnd, offset + spaceInBlock); int inputPartLength = inputPartEnd - offset; blockHasher.update(input, offset, inputPartLength); blockPos += inputPartLength; offset += inputPartLength; } } @Override protected void engineUpdate(ByteBuffer input) { int inputEnd = input.limit(); while (input.position() < inputEnd) { finishBlockIfFull(); int spaceInBlock = BLOCK_SIZE - this.blockPos; int inputPartEnd = Math.min(inputEnd, input.position() + spaceInBlock); int inputPartLength = inputPartEnd - input.position(); input.limit(inputPartEnd); blockHasher.update(input); blockPos += inputPartLength; input.position(inputPartEnd); } } @Override protected byte[] engineDigest() { finishBlockIfNonEmpty(); return overallHasher.digest(); } @Override protected int engineDigest(byte[] buf, int offset, int len) throws DigestException { finishBlockIfNonEmpty(); return overallHasher.digest(buf, offset, len); } @Override protected void engineReset() { this.overallHasher.reset(); this.blockHasher.reset(); this.blockPos = 0; } @Override public DropboxContentHasher clone() throws CloneNotSupportedException { DropboxContentHasher clone = (DropboxContentHasher) super.clone(); clone.overallHasher = (MessageDigest) clone.overallHasher.clone(); clone.blockHasher = (MessageDigest) clone.blockHasher.clone(); return clone; } private void finishBlock() { overallHasher.update(blockHasher.digest()); blockPos = 0; } private void finishBlockIfFull() { if (blockPos == BLOCK_SIZE) { finishBlock(); } } private void finishBlockIfNonEmpty() { if (blockPos > 0) { finishBlock(); } } static MessageDigest newSha256Hasher() { try { return MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException ex) { throw new AssertionError("Couldn't create SHA-256 hasher"); } } static final char[] HEX_DIGITS = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; public static String hex(byte[] data) { char[] buf = new char[2 * data.length]; int i = 0; for (byte b : data) { buf[i++] = HEX_DIGITS[(b & 0xf0) >>> 4]; buf[i++] = HEX_DIGITS[b & 0x0f]; } return new String(buf); } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/DropboxUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud import android.content.Context import android.net.Uri import android.text.TextUtils import com.arialyy.frame.util.SharePreUtil import com.dropbox.core.DbxRequestConfig import com.dropbox.core.android.Auth import com.dropbox.core.http.OkHttp3Requestor import com.dropbox.core.oauth.DbxCredential import com.dropbox.core.v2.DbxClientV2 import com.dropbox.core.v2.files.DeletedMetadata import com.dropbox.core.v2.files.FileMetadata import com.keepassdroid.utils.UriUtil import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.Constance import com.lyy.keepassa.entity.DbHistoryRecord import com.lyy.keepassa.util.QuickUnLockUtil import timber.log.Timber import java.util.Date /** * dropbox 工具 */ object DropboxUtil : ICloudUtil { val APP_KEY = "ib45r6jnfz3oakq" private val DROPBOX_KEY_TOKEN = "DROPBOX_KEY_TOKEN" /** * 是否申请短期令牌 * {@code true} 使用短期令牌 */ private val USE_SLT = false private var sDbxClient: DbxClientV2? = null private var sDbxRequestConfig: DbxRequestConfig? = null /** * 配置dropbox请求信息 */ private fun getRequestConfig(): DbxRequestConfig? { if (sDbxRequestConfig == null) { sDbxRequestConfig = DbxRequestConfig.newBuilder("keepassA") .withHttpRequestor(OkHttp3Requestor(OkHttp3Requestor.defaultOkHttpClient())) .build() } return sDbxRequestConfig } /** * 通过token 初始化客户端 */ @Synchronized private fun init(accessToken: String) { if (sDbxClient == null) { sDbxClient = DbxClientV2( getRequestConfig(), accessToken ) } } /** * 通过令牌初始化客户端 */ @Synchronized private fun init(credential: DbxCredential) { val temp = DbxCredential( credential.accessToken, -1L, credential.refreshToken, credential.appKey ) if (sDbxClient == null) { sDbxClient = DbxClientV2( getRequestConfig(), temp ) } } /** * 获取dropbox 客户端 * @retun 如果没有授权,返null,需要进入授权页面[Auth.startOAuth2Authentication] */ fun getClient(): DbxClientV2? { var token = getLocalToken() if (TextUtils.isEmpty(token)) { token = Auth.getOAuth2Token() if (!TextUtils.isEmpty(token)) { saveToken(token) } else { return null } } if (token != null) { init(token) } return sDbxClient } override suspend fun fileExists(fileKey: String): Boolean { return getFileInfo(fileKey) != null } /** * https://github.com/dropbox/dropbox-sdk-obj-c/issues/32 * dropbox 的根路径是:"" */ override fun getRootPath(): String { return "" } override suspend fun getFileList(path: String): List? { Timber.d("开始获取文件列表,path = $path") val client = getClient() ?: return null val entries = client.files() .listFolder(path).entries if (entries.isEmpty()) { return null } val list = ArrayList() val root = if (TextUtils.isEmpty(path)) "" else path for (e in entries) { if (e is FileMetadata) { list.add( CloudFileInfo("$root/${e.name}", e.name, e.serverModified, e.size, false, e.contentHash) ) } else { list.add(CloudFileInfo("$root/${e.name}", e.name, Date(), 0, true)) } } return list } /** * [hash 算法](https://www.dropbox.com/developers/reference/content-hash) * */ override suspend fun checkContentHash( cloudFileHash: String?, localFileUri: Uri ): Boolean { if (cloudFileHash == null){ return false } val hasher = DropboxContentHasher() val buf = ByteArray(1024) val ips = UriUtil.getUriInputStream(BaseApp.APP, localFileUri) while (true) { val n = ips.read(buf) if (n < 0) break hasher.update(buf, 0, n) } val localHash = DropboxContentHasher.hex(hasher.digest()) ips.close() Timber.i("本地文件hash: $localHash") return cloudFileHash.equals(localHash, ignoreCase = true) } override suspend fun getFileInfo(fileKey: String): CloudFileInfo? { val client = getClient() ?: return null try { val entries = client.files() .listRevisions(fileKey).entries val entry = entries[0] return CloudFileInfo( fileKey, entry.name, entry.serverModified, entry.size, true, entry.contentHash ) } catch (e: Exception) { Timber.e(e) } return null } override suspend fun delFile(fileKey: String): Boolean { Timber.d("删除云端文件: $fileKey") val d = getClient() ?.files() ?.deleteV2(fileKey) if (d == null || d.metadata == null || d.metadata is DeletedMetadata) { return false } return true } /** * 获取服务器端文件的修改时间 */ override suspend fun getFileServiceModifyTime(fileKey: String): Date { val client = getClient() ?: return Date(System.currentTimeMillis()) val entries = client.files() .listRevisions(fileKey).entries return entries[0].serverModified } /** * 上传文件 */ override suspend fun uploadFile( context: Context, dbRecord: DbHistoryRecord ): Boolean { val dbUri = Uri.parse(dbRecord.localDbUri) val cloudDiskPath = dbRecord.cloudDiskPath val ips = BaseApp.APP.contentResolver.openInputStream(dbUri) val fd = getClient() ?.files() ?.uploadBuilder(cloudDiskPath) ?.uploadAndFinish(ips) if (fd != null) { DbSynUtil.serviceModifyTime = fd.serverModified } ips?.close() return true } override suspend fun downloadFile( context: Context, dbRecord: DbHistoryRecord, filePath: Uri ): String? { val client = getClient() ?: return null val os = context.contentResolver.openOutputStream(filePath) client.files() .download(dbRecord.cloudDiskPath) .download(os) os?.let { it.flush() it.close() } return filePath.toString() } /** * 保存token */ fun saveToken(token: String) { SharePreUtil.putString( Constance.PRE_FILE_NAME, BaseApp.APP, DROPBOX_KEY_TOKEN, QuickUnLockUtil.encryptStr(token) ) } /** * 获取本地保存的token * @return 没有保存的token,返回null */ private fun getLocalToken(): String? { val token = SharePreUtil.getString( Constance.PRE_FILE_NAME, BaseApp.APP, DROPBOX_KEY_TOKEN ) return if (!TextUtils.isEmpty(token)) { QuickUnLockUtil.decryption(token) } else { null } } /** * 判断是否登录成功,如果没有授权,请使用[Auth.startOAuth2Authentication]启动授权界面 * @return false 没有登录 */ fun isAuthorized(): Boolean { val token = SharePreUtil.getString( Constance.PRE_FILE_NAME, BaseApp.APP, DROPBOX_KEY_TOKEN ) return !TextUtils.isEmpty(token) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/ICloudUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud import android.content.Context import android.net.Uri import com.lyy.keepassa.entity.DbHistoryRecord import java.util.Date /** * 云盘工具 */ interface ICloudUtil { /** * 文件是否存在 * @param fileKey webDav/dropbox中为云端路径,onedrive为id * @return false 文件不存在 */ suspend fun fileExists(fileKey: String): Boolean /** * 获取云盘根路径 */ fun getRootPath(): String /** * 获取文件列表 * @return 如果获取不到文件列表,返回null */ suspend fun getFileList(dirPath: String): List? /** * 检查云端文件的hash和本地文件的hash是否一致 * @param cloudFileHash 云端文件的hash * @param localFileUri 本地文件的Uri * @return true 两端文件一致 */ suspend fun checkContentHash( cloudFileHash: String?, localFileUri: Uri ): Boolean /** * 获取云端文件信息 * @param fileKey webDav/dropbox中为云端路径,onedrive为id * @return null 云端文件不存在 */ suspend fun getFileInfo(fileKey: String): CloudFileInfo? /** * 删除文件 * @param fileKey webDav/dropbox中为云端路径,onedrive为id * @return true 删除成功 */ suspend fun delFile(fileKey: String): Boolean /** * 云端文件的修改时间 * @param fileKey 云端文件路径 */ suspend fun getFileServiceModifyTime(fileKey: String): Date /** * 上传文件,上传完成需要更新[DbSynUtil.serviceModifyTime] * @param dbRecord 文件打开记录 * @return true 上传成功 */ suspend fun uploadFile( context: Context, dbRecord: DbHistoryRecord ): Boolean /** * 下载文件 * @param filePath 文件保存路径 * @return 文件保存路径,null 表示下载失败 */ suspend fun downloadFile( context: Context, dbRecord: DbHistoryRecord, filePath: Uri ): String? } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/OneDriveUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud import android.app.Activity import android.content.Context import android.net.Uri import com.arialyy.frame.base.net.NetManager1 import com.arialyy.frame.core.AbsFrame import com.arialyy.frame.util.FileUtil import com.blankj.utilcode.util.EncryptUtils import com.blankj.utilcode.util.ToastUtils import com.google.gson.Gson import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.DbHistoryRecord import com.lyy.keepassa.ondrive.MsalApi import com.lyy.keepassa.ondrive.MsalSourceItem import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KLog import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.getBytes import com.microsoft.identity.client.AuthenticationCallback import com.microsoft.identity.client.IAccount import com.microsoft.identity.client.IAuthenticationResult import com.microsoft.identity.client.IPublicClientApplication.ISingleAccountApplicationCreatedListener import com.microsoft.identity.client.ISingleAccountPublicClientApplication import com.microsoft.identity.client.ISingleAccountPublicClientApplication.CurrentAccountCallback import com.microsoft.identity.client.ISingleAccountPublicClientApplication.SignOutCallback import com.microsoft.identity.client.PublicClientApplication import com.microsoft.identity.client.SilentAuthenticationCallback import com.microsoft.identity.client.exception.MsalException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.Headers import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.asRequestBody import org.joda.time.format.DateTimeFormat import timber.log.Timber import java.io.File import java.io.FileOutputStream import java.net.HttpURLConnection import java.nio.charset.Charset import java.util.Date /** * Onedrive util */ object OneDriveUtil : ICloudUtil { /** * 应用根目录 */ const val APP_ROOT_DIR = "approot" /** * token key */ const val TOKEN_KEY = "Authorization" private const val BASE_URL = "https://graph.microsoft.com/v1.0/" private lateinit var oneDriveApp: ISingleAccountPublicClientApplication private var authInfo: IAuthenticationResult? = null private val netManager by lazy { NetManager1().builderManager(BASE_URL, arrayListOf()) } private val dateFormat = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ") var loginCallback: OnLoginCallback? = null private var getTokenFailNum = 0 private val MAX_FAIL_NUM = 1 private val okClient by lazy { OkHttpClient() } fun isInitialized(): Boolean = this::oneDriveApp.isInitialized interface OnLoginCallback { fun callback(success: Boolean) } private fun getContext(): Context { return BaseApp.APP } private fun getCurActivity(): Activity { return AbsFrame.getInstance().currentActivity!! } private fun getAuthInfo() = authInfo!! private fun getUserId() = authInfo?.account?.id ?: "" /** * check login * @return true login success */ private fun checkLogin(): Boolean { if (authInfo == null || !this::oneDriveApp.isInitialized) { Timber.e("登陆失败,sdk没有初始化,或登陆失败") HitUtil.toaskShort("${getContext().getString(R.string.login)}${getContext().getString(R.string.fail)}") return false } return true } /** * 初始化 */ fun initOneDrive(callback: (Boolean) -> Unit) { PublicClientApplication.createSingleAccountPublicClientApplication( getContext(), R.raw.auth_config_single_account_release, object : ISingleAccountApplicationCreatedListener { override fun onCreated(application: ISingleAccountPublicClientApplication) { /** * This test app assumes that the app is only going to support one account. * This requires "account_mode" : "SINGLE" in the config json file. */ oneDriveApp = application Timber.d("初始化成功") callback.invoke(true) } override fun onError(exception: MsalException) { exception.printStackTrace() callback.invoke(false) } }) } /** * 加载用户,没有登陆过,则需要重新登陆 */ fun loadAccount() { if (!this::oneDriveApp.isInitialized) { Timber.e("还没有初始化sdk") return } oneDriveApp.getCurrentAccountAsync(object : CurrentAccountCallback { override fun onAccountLoaded(activeAccount: IAccount?) { if (activeAccount == null) { Timber.w("用户还没有登陆") login() return } Timber.w("已经登陆过,自动登陆,开始获取token") getTokenByAccountInfo(activeAccount) } override fun onAccountChanged( priorAccount: IAccount?, currentAccount: IAccount? ) { Timber.w("账号已却换,重新获取token") if (currentAccount == null) { Timber.e("当前账户为空") return } getTokenByAccountInfo(currentAccount) } override fun onError(exception: MsalException) { exception.printStackTrace() ToastUtils.showLong(R.string.one_drive_load_user_failure) } }) } /** * 根据用户信息获取token */ private fun getTokenByAccountInfo(account: IAccount) { oneDriveApp.acquireTokenSilentAsync(getScopes(), account.authority, object : SilentAuthenticationCallback { override fun onSuccess(authenticationResult: IAuthenticationResult?) { Timber.d("获取token成功") authInfo = authenticationResult loginCallback?.callback(true) } override fun onError(exception: MsalException) { Timber.d("获取token失败,重新启动登陆流程") exception.printStackTrace() if (getTokenFailNum >= MAX_FAIL_NUM) { HitUtil.toaskShort(R.string.get_token_fail) loginCallback?.callback(false) return } oneDriveApp.signOut(object : SignOutCallback { override fun onSignOut() { Timber.d("登出成功,重新开始登陆流程") login() getTokenFailNum += 1 } override fun onError(exception: MsalException) { Timber.e("登出失败") exception.printStackTrace() loginCallback?.callback(false) } }) } }) } private fun login() { oneDriveApp.signIn(getCurActivity(), "", getScopes(), object : AuthenticationCallback { override fun onSuccess(authenticationResult: IAuthenticationResult?) { authInfo = authenticationResult HitUtil.toaskShort("${getContext().getString(R.string.login)}${getContext().getString(R.string.success)}") Timber.d("登陆成功") loginCallback?.callback(true) } override fun onError(exception: MsalException?) { HitUtil.toaskShort("${getContext().getString(R.string.login)}${getContext().getString(R.string.fail)}") Timber.d("登陆失败") exception?.printStackTrace() loginCallback?.callback(false) } override fun onCancel() { Timber.d("取消登陆") HitUtil.toaskShort("${getContext().getString(R.string.login)}${getContext().getString(R.string.cancel)}") loginCallback?.callback(false) } }) } private fun getScopes(): Array { return arrayOf("User.Read", "Files.ReadWrite.AppFolder") } private fun msalItem2CloudItem(item: MsalSourceItem): CloudFileInfo { return CloudFileInfo( fileKey = item.id, fileName = item.name, serviceModifyDate = dateFormat.parseDateTime(item.lastModifiedDateTime) .toDate(), size = item.size, isDir = item.isFolder(), contentHash = if (item.isFolder()) null else item.file?.hashes?.sha256Hash ) } override suspend fun fileExists(fileKey: String): Boolean { return getFileInfo(fileKey) != null } override fun getRootPath(): String { return "/" } override suspend fun getFileList(path: String): List? { if (!checkLogin()) { return null } Timber.d("获取文件列表,path = $path") val response = if (path == "/") { netManager.request(MsalApi::class.java) .getAppFolderList(getAuthInfo().accessToken, getUserId()) } else { netManager.request(MsalApi::class.java) .getFolderListById(getAuthInfo().accessToken, getUserId(), path) } if (response.value == null) { return null } val fileList = arrayListOf() response.value?.forEach { fileList.add(msalItem2CloudItem(it)) } return fileList } override suspend fun checkContentHash( cloudFileHash: String?, localFileUri: Uri ): Boolean { if (cloudFileHash == null) { return false } Timber.d("localFileUri = $localFileUri") val bytes = localFileUri.getBytes() if (bytes == null) { Timber.e("localFileUri get bytes null") return false } val localHash = EncryptUtils.encryptSHA256ToString(bytes) Timber.d("cloudFileHash = $cloudFileHash,localHash = $localHash") return localHash == cloudFileHash } override suspend fun getFileInfo(fileKey: String): CloudFileInfo? { val userId = getUserId() Timber.d("getFileInfo, userId = ${userId}, fileKey = $fileKey") try { val response: MsalSourceItem? = if (fileKey.startsWith("/")) { netManager.request(MsalApi::class.java) .getFileInfoByPath( getAuthInfo().accessToken, userId, fileKey.substring(1, fileKey.length) ) } else { netManager.request(MsalApi::class.java) .getFileInfoById(getAuthInfo().accessToken, userId, fileKey) } if (response == null) { Timber.e("获取文件信息失败") return null } return msalItem2CloudItem(response) } catch (e: Exception) { Timber.e(e) return null } } /** * 如果成功,此调用将返回 204 No Content 响应,以指明资源已被删除,没有可返回的内容。 */ override suspend fun delFile(fileKey: String): Boolean { Timber.d("删除文件,fileKey = $fileKey") val response = netManager.request(MsalApi::class.java) .deleteFile(getAuthInfo().accessToken, getUserId(), fileKey) return response.code() == HttpURLConnection.HTTP_NO_CONTENT } override suspend fun getFileServiceModifyTime(fileKey: String): Date { val fileInfo = getFileInfo(fileKey) ?: return Date(System.currentTimeMillis()) return fileInfo.serviceModifyDate } override suspend fun uploadFile( context: Context, dbRecord: DbHistoryRecord ): Boolean { val file = File(Uri.parse(dbRecord.localDbUri).path!!) try { // 创建上传session val uploadSession = netManager.request(MsalApi::class.java) .createUploadSession( authorization = getAuthInfo().accessToken, userId = getUserId(), itemPath = file.name ) Timber.d("获取session成功,上传地址:${uploadSession.uploadUrl}") // 取消上传的临时文件会话 cancelUploadSession(uploadSession.uploadUrl) // 开始上传文件 val desc = file.asRequestBody("multipart/form-data".toMediaType()) val body = MultipartBody.Part.createFormData("file", file.name, desc) val fileSize = file.length() val request = Request.Builder() .header("Content-Length", fileSize.toString()) .header("Content-Range", "bytes 0-${fileSize - 1}/${fileSize}") .url(uploadSession.uploadUrl) .put(body = body.body) .build() val response = okClient.newCall(request) .execute() if (response.code != HttpURLConnection.HTTP_OK && response.code != HttpURLConnection.HTTP_CREATED) { Timber.e("上传失败,code = ${response.code}, msg = ${response.message}") return false } val responseBytes = response.body?.bytes() if (responseBytes == null) { Timber.e("body为null") return false } val responseContent = String(responseBytes, Charset.forName("UTF-8")) Timber.d("上传成功,响应内容") KLog.j("TAG", responseContent) val obj = Gson().fromJson(responseContent, MsalSourceItem::class.java) dbRecord.cloudDiskPath = obj.id KeepassAUtil.instance.saveLastOpenDbHistory(dbRecord) return true } catch (e: Exception) { Timber.e(e) return false } } /** * 取消上传会话 */ private fun cancelUploadSession(uploadUrl: String) { try { Timber.d("如果有的话,取消临时文件的上传对话") val request = Request.Builder() .url(uploadUrl) .delete() .build() val response = okClient.newCall(request) .execute() Timber.d("code = ${response.code}, body = ${response.body?.string()}") } catch (e: Exception) { Timber.e(e) } } override suspend fun downloadFile( context: Context, dbRecord: DbHistoryRecord, filePath: Uri ): String? { return withContext(Dispatchers.IO) { val hb = Headers.Builder() .add(TOKEN_KEY, getAuthInfo().accessToken) .build() val request: Request = Request.Builder() .url("$BASE_URL/users/${getUserId()}/drive/items/${dbRecord.cloudDiskPath}/content") .headers(hb) .build() val call = netManager.getClient() .newCall(request) try { val response = call.execute() if (!response.isSuccessful) { return@withContext null } val byteSystem = response.body?.byteStream() ?: return@withContext null val outF = File(filePath.path) val fr = FileUtil.createFile(outF) if (!fr) { Timber.e("创建文件失败,path = $filePath") return@withContext null } var len = 0 val buf = ByteArray(1024) val fos = FileOutputStream(outF) do { len = byteSystem.read(buf) if (len != -1) { fos.write(buf, 0, len) } } while (len != -1) return@withContext filePath.toString() } catch (e: Exception) { Timber.e(e) } return@withContext null } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/PwDataMap.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud import com.keepassdroid.database.PwDataInf data class PwDataMap( val cloudPwData: PwDataInf, val localPwData: PwDataInf ) ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/SynStateCode.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud interface SynStateCode { val STATE_SUCCEED: Int get() = 0 val STATE_FAIL: Int get() = 1 val STATE_DEL_FILE_FAIL: Int get() = 2 val STATE_DOWNLOAD_FILE_FAIL: Int get() = 3 val STATE_SAVE_DB_FAIL: Int get() = 4 val STATE_CANCEL: Int get() = 100 } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/WebDavUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud import android.content.Context import android.net.Uri import androidx.core.net.toFile import com.arialyy.frame.util.FileUtil import com.lyy.keepassa.entity.DbHistoryRecord import com.lyy.keepassa.util.hasSpecialChar import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine import timber.log.Timber import java.io.FileOutputStream import java.net.URI import java.net.URLEncoder import java.nio.channels.Channels import java.nio.channels.FileChannel import java.nio.channels.ReadableByteChannel import java.util.Date /** * webdav工具 */ object WebDavUtil : ICloudUtil { val SUPPORTED_WEBDAV_URLS = mutableListOf().apply { add("https://dav.jianguoyun.com/") add("https://dav.box.com") add("https://webdav.4shared.com") add("nextcloud") // https://docs.nextcloud.com/server/24/user_manual/en/files/access_webdav.html // add("https://aki.teracloud.jp/dav") // 需要在https://teracloud.jp/en/modules/mypage/usage/ 中 勾选 Turn on Apps Connection // add("https://my.powerfolder.com/webdav") // 只能用他家的client登录?电脑qspase可以 // add("https://dav.dropdav.com") // 需要注册:https://app.dropdav.com/users/sign_in // add("https://webdav.yandex.com") 需要使用sdk, htts://yandex.com/dev/id/ add("other") } val REMOVE_PARENT_URLS = mutableListOf().apply { add("https://dav.jianguoyun.com") add("https://dav.box.com") add("https://webdav.4shared.com") } var sardine: OkHttpSardine? = null var fileName: String = "" private var hostUri: String = "" var userName: String = "" var password: String = "" /** * key: 原始路径,value:转换后的路径 */ private val urlPathMap = hashMapOf() /** * 是否登录 * @return false 未登录 */ fun isLogin(): Boolean { return sardine != null } fun createDir(path: String): Boolean { return try { sardine?.createDirectory(path) true } catch (e: Exception) { Timber.e(e) false } } fun setHostUri(hostUri: String) { this.hostUri = if (hostUri.endsWith("/")) hostUri.substring(0, hostUri.length - 1) else hostUri } fun getHostUri() = hostUri /** * 检查登录 * @return true 登录成功,false 登录失败 */ fun checkLogin( uri: String, userName: String, password: String, isPreemptive: Boolean ): Boolean { Timber.d("checkLogin, uri = ${uri}, userName = ${userName}, password = ${password}") this.userName = userName this.password = password setHostUri(uri) sardine = OkHttpSardine() sardine?.setCredentials(userName, password, isPreemptive) try { val list = sardine?.list(uri) return !list.isNullOrEmpty() } catch (e: Exception) { Timber.e(e) } return false } /** * 进行登录 * 创建一个小文件上传成成功后并删除 */ fun login( uri: String, userName: String, password: String ): OkHttpSardine { this.userName = userName this.password = password setHostUri(uri) sardine = OkHttpSardine() sardine?.setCredentials(userName, password, true) return sardine as OkHttpSardine } override suspend fun fileExists(fileKey: String): Boolean { sardine ?: return false Timber.d("fileExists, fileKey = $fileKey") try { return sardine!!.exists(fileKey) } catch (e: Exception) { Timber.e(e) } return false } override fun getRootPath(): String { return "/" } /** *@param dirPath 相对路径,如:/dav/ */ override suspend fun getFileList(dirPath: String): List? { sardine ?: return null Timber.d("getFileList, host = ${hostUri}, fileKey = $dirPath") val list = ArrayList() try { val resources = sardine!!.list(convertUrl("${hostUri}${dirPath}")) if (resources == null || resources.isEmpty()) { return null } for (file in resources) { list.add( CloudFileInfo(file.path, file.name, file.modified, file.contentLength, file.isDirectory) ) } if (hostUri in REMOVE_PARENT_URLS) { // 坚果云移除第一个item list.removeAt(0) } } catch (e: Exception) { Timber.e(e, "获取文件列表失败") } return list } /** * 不支持比对 */ override suspend fun checkContentHash( cloudFileHash: String?, localFileUri: Uri ): Boolean { return false } override suspend fun getFileInfo(fileKey: String): CloudFileInfo? { Timber.i("获取文件信息,cloudPath:$fileKey") try { sardine ?: return null val resources = sardine!!.list(convertUrl(fileKey)) if (resources == null || resources.isEmpty()) { return null } val file = resources[0] return CloudFileInfo( file.path, file.name, file.modified, file.contentLength, file.isDirectory ) } catch (e: Exception) { Timber.e(e) } return null } override suspend fun delFile(fileKey: String): Boolean { sardine ?: return false try { sardine!!.delete(convertUrl(fileKey)) } catch (e: Exception) { Timber.e(e, "删除文件失败") return false } return true } override suspend fun getFileServiceModifyTime(fileKey: String): Date { sardine ?: return Date() val cloudInfo = getFileInfo(convertUrl(fileKey)) cloudInfo ?: return Date() return cloudInfo.serviceModifyDate } override suspend fun uploadFile( context: Context, dbRecord: DbHistoryRecord ): Boolean { Timber.d("uploadFile, cloudPath = ${dbRecord.cloudDiskPath}, localPath = ${dbRecord.localDbUri}") sardine ?: return false var localToken: String? = null try { // delFile(dbRecord.cloudDiskPath!!) // 不能删除,否则如果上传失败,文件就丢失了 localToken = sardine!!.lock(getConvertedCloudPath(dbRecord), 5) Timber.d("localToken = $localToken") sardine?.put( getConvertedCloudPath(dbRecord), Uri.parse(dbRecord.localDbUri).toFile(), "application/binary", false, localToken ) Timber.d("上传完成,重新获取文件信息") val info = getFileInfo(getConvertedCloudPath(dbRecord)) if (info != null) { DbSynUtil.serviceModifyTime = info.serviceModifyDate } } catch (e: Exception) { Timber.e(e, "上传文件失败") return false } finally { localToken?.let { sardine?.unlock(getConvertedCloudPath(dbRecord), it) } } return true } /** * 检查上传路径是否有特殊字符,如果有特殊字符,转换特殊字符 */ private fun getConvertedCloudPath(dbRecord: DbHistoryRecord): String { val tempUrl = urlPathMap[dbRecord.cloudDiskPath!!] if (tempUrl != null) { return tempUrl } val realUrl = if (dbRecord.cloudDiskPath!!.hasSpecialChar()) { Timber.d("url中有特殊,需要对特殊字符进行编码处理") val path = URI.create(dbRecord.cloudDiskPath).path val encodedPath = path.split("/") .joinToString("/") { URLEncoder.encode(it, "UTF-8") } "${dbRecord.cloudDiskPath!!.substringBefore(path)}${encodedPath}" } else { dbRecord.cloudDiskPath!! } urlPathMap[dbRecord.cloudDiskPath!!] = realUrl return realUrl } override suspend fun downloadFile( context: Context, dbRecord: DbHistoryRecord, filePath: Uri ): String? { sardine ?: return null Timber.d("start download file, save path: $filePath") val cloudPath = convertUrl(dbRecord.cloudDiskPath.toString()) val fp = filePath.toFile() if (!fp.exists()) { FileUtil.createFile(fp) } sardine?.let { var token = "" var fic: ReadableByteChannel? = null var foc: FileChannel? = null try { token = it.lock(cloudPath) val ips = it.get(cloudPath) val fileInfo = getFileInfo(cloudPath) fic = Channels.newChannel(ips) foc = FileOutputStream(fp).channel foc.transferFrom(fic, 0, fileInfo!!.size) } catch (e: Exception) { Timber.e(e, "下载文件失败") return null } finally { try { fic?.close() foc?.close() } catch (e: Exception) { Timber.e(e) } it.unlock(cloudPath, token) } } return filePath.toString() } /** * 将含有中文的url进行格式化 */ private fun convertUrl(url: String): String { return url } private fun getRelativePath() { } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/interceptor/DbMergeDelegate.kt ================================================ package com.lyy.keepassa.util.cloud.interceptor import android.util.Pair import android.widget.Button import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.ResUtil import com.keepassdroid.database.PwDataInf import com.keepassdroid.database.PwDatabase import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroup import com.keepassdroid.database.PwGroupV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.DbHistoryRecord import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.cloud.DbSynUtil import com.lyy.keepassa.util.cloud.PwDataMap import com.lyy.keepassa.view.dialog.OnMsgBtClickListener import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.MainScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 5:42 下午 2021/12/24 **/ object DbMergeDelegate { private val scope = MainScope() const val COVER_LOCAL = 999 const val COVER_CLOUD = 998 /** * 对比云端和本地的数据库,并进行合并 * @param isUpload 是否是上传 * @return [COVER_CLOUD]、[COVER_LOCAL]、[VS] */ @ExperimentalCoroutinesApi suspend fun compareDb( record: DbHistoryRecord, cloudDb: PwDatabase, localDb: PwDatabase, isUpload: Boolean ): Int { val modifyList = ArrayList>() // 有改动的条目,first 为云端的条目,second 为本地的条目 val delList = ArrayList() // 云端没有的条目 val newList = ArrayList() // 本地没有的条目 val moveList = ArrayList() // 被移动的条目,first 为云端的条目,second 为本地的条目 for (cloudEntry in cloudDb.entries.values) { val localEntry = localDb.entries[cloudEntry.uuid] when { localEntry == null -> { newList.add(cloudEntry) } cloudEntry.parent.id != localEntry.parent.id -> { moveList.add(PwDataMap(cloudEntry, localEntry)) } localDb.entries[cloudEntry.uuid] == null -> { delList.add(cloudEntry) } cloudEntry != localEntry -> { Timber.d("修改的条目:${cloudEntry.title}") modifyList.add(Pair(cloudEntry, localEntry)) } } } for (cloudGroup in cloudDb.groups.values) { val localGroup = localDb.groups[cloudGroup.id] when { localGroup == null -> { newList.add(cloudGroup) } cloudGroup.parent == null -> { Timber.w("云端数据库的群组的parent为空,群组名:${cloudGroup.name}") } cloudGroup.parent.id != localGroup.parent.id -> { moveList.add(PwDataMap(cloudGroup, localGroup)) } localDb.groups[cloudGroup.id] == null -> { delList.add(cloudGroup) } cloudGroup != localGroup -> { Timber.d("修改的群组:${cloudGroup.name}") modifyList.add(Pair(cloudGroup, localGroup)) } } } Timber.i( "比对数据完成,newListSize = ${newList.size},moveListSize = ${moveList.size},delListSize = ${delList.size},modifyListSize = ${modifyList.size}" ) if (newList.size == 0 && moveList.size == 0 && delList.size == 0 && modifyList.size == 0) { Timber.i("对比结果:无新增,无删除,无移动,无修改,忽略该次上传,并更新缓存的云端文件修改时间") DbSynUtil.updateServiceModifyTime(record) return DbSynUtil.STATE_SUCCEED } if (newList.size > 0) { // 本地需要新增的条目 Timber.i("本地需要新增条目") localAddNewEntry(newList, localDb) } if (moveList.size > 0) { // 本地需要移动的条目 Timber.i("本地需要移动条目") moveLocalEntry(moveList, localDb) } if (modifyList.size <= 0) { KpaUtil.kdbHandlerService.saveDbByBackground() return DbSynUtil.STATE_SUCCEED } // 有改动提示用户合并数据 Timber.i("有改动提示用户合并数据") var code = DbSynUtil.STATE_FAIL val channel = Channel() if (isUpload) { showUploadCoverDialog(modifyList, channel) } else { showDownloadCoverDialog(modifyList, channel) } val job = scope.launch { code = channel.receive() } // 等待直到子协程执行结束,完美替换wait single job.join() job.cancel() channel.cancel() Timber.d("compareDb end point, code = $code") return code } /** * 显示下载文件时冲突的对话框 */ @ExperimentalCoroutinesApi private fun showDownloadCoverDialog( modifyList: ArrayList>, channel: Channel ) { val sb = StringBuilder() for (p in modifyList) { if (p.second is PwEntry) { sb.append((p.second as PwEntry).title) } else { sb.append((p.second as PwGroup).name) } sb.append("\n") } val res = BaseApp.APP.resources Routerfit.create(DialogRouter::class.java).showMsgDialog( msgTitle = ResUtil.getString(R.string.warning), msgContent = res.getString(R.string.file_conflict_msg, sb.toString()), showCoverBt = false, showCancelBt = false, interceptBackKey = true, enterText = ResUtil.getString(R.string.cover_local), btnClickListener = object : OnMsgBtClickListener { override fun onCover(v: Button) { } override fun onEnter(v: Button) { scope.launch { // 覆盖本地数据 coverModifyEntry(modifyList) channel.send(COVER_LOCAL) } } override fun onCancel(v: Button) { } } ) } /** * 显示上传文件冲突对话框 * @param modifyList 有改动的条目,first 为云端的条目,second 为本地的条目 */ @ExperimentalCoroutinesApi private fun showUploadCoverDialog( modifyList: ArrayList>, channel: Channel ) { val sb = StringBuilder() for (p in modifyList) { if (p.second is PwEntry) { sb.append((p.second as PwEntry).title) } else { sb.append((p.second as PwGroup).name) } sb.append("\n") } val res = BaseApp.APP.resources Routerfit.create(DialogRouter::class.java).showMsgDialog( msgTitle = ResUtil.getString(R.string.warning), msgContent = res.getString(R.string.file_conflict_msg, sb.toString()), showCancelBt = false, showCoverBt = true, interceptBackKey = true, enterText = ResUtil.getString(R.string.cover_local), coverText = ResUtil.getString(R.string.cover_cloud), btnClickListener = object : OnMsgBtClickListener { override fun onCover(v: Button) { // 覆盖云端数据 scope.launch { channel.send(COVER_CLOUD) } } override fun onEnter(v: Button) { scope.launch { // 覆盖本地数据 coverModifyEntry(modifyList) channel.send(COVER_LOCAL) } } override fun onCancel(v: Button) { } } ) Timber.d("showUploadCoverDialog endPoint") } /** * 覆盖本地数据库有修改冲突的条目和群组 * @param modifyList 有改动的条目,first 为云端的条目,second 为本地的条目 */ private fun coverModifyEntry(modifyList: ArrayList>): Int { for (p in modifyList) { if (p.first is PwEntry) { (p.second as PwEntry).assign(p.first as PwEntry) } else { (p.second as PwGroup).assign(p.first as PwGroup) } } KpaUtil.kdbHandlerService.saveDbByBackground() return DbSynUtil.STATE_SUCCEED } /** * 移动数据 * @param needMoveList 本地需要移动的数据,first 为云端的条目,second 为本地的条目 */ private fun moveLocalEntry( needMoveList: ArrayList, localDb: PwDatabase ) { for (p in needMoveList) { if (p.cloudPwData is PwEntry) { localDb.moveEntry(p.localPwData as PwEntry, getParentByCloudPwData(p.cloudPwData, localDb)) continue } // 处理群组的移动 localDb.moveGroup(p.localPwData as PwGroup, getParentByCloudPwData(p.cloudPwData, localDb)) } } /** * 本地新增云端有而本地没的条目 * @param newList 云端服务器新增加的条目列表 */ private suspend fun localAddNewEntry( newList: ArrayList, localDb: PwDatabase ) { // 需要先增加群组 for (pwData in newList) { if (pwData is PwGroup) { val newGroup = pwData.clone() newGroup.childGroups?.clear() newGroup.childEntries?.clear() newGroup.parent = getParentByCloudPwData(pwData, localDb) KpaUtil.kdbHandlerService.addGroup(newGroup as PwGroupV4) } } // 再增加条目 for (pwData in newList) { if (pwData is PwEntry) { val newEntry = pwData.clone(true) newEntry.parent = getParentByCloudPwData(pwData, localDb) KpaUtil.kdbHandlerService.createEntry(newEntry as PwEntryV4) } } } /** * 通过云端条目获取本地条目 */ private fun getParentByCloudPwData( cloudPwDataInf: PwDataInf, localDb: PwDatabase ): PwGroup { var parent = localDb.rootGroup if (cloudPwDataInf.parent != null) { val temp = localDb.groups[cloudPwDataInf.parent.id] if (temp != null) { parent = temp } } return parent } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/interceptor/DbSyncCheckInterceptor.kt ================================================ package com.lyy.keepassa.util.cloud.interceptor import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.util.cloud.DbSynUtil import timber.log.Timber /** * cloud file check interceptor * @Author laoyuyu * @Description * @Date 4:54 下午 2021/12/24 **/ class DbSyncCheckInterceptor : IDbSyncInterceptor { override suspend fun intercept(request: DbSyncRequest): DbSyncResponse { if (BaseApp.isAFS()) { return normal(DbSynUtil.STATE_SUCCEED, "AFS 不需要上传") } val nextInterceptor = request.nextInterceptor() ?: throw IllegalArgumentException("没有上传拦截器") val util = request.syncUtil val record = request.record val cloudFileInfo = util.getFileInfo(record.cloudDiskPath!!) Timber.i("获取文件信息成功:${cloudFileInfo.toString()}") if (cloudFileInfo == null) { Timber.i("云端文件不存在,开始上传文件") val uploadInterceptor = request.interceptors[request.index + 2] return uploadInterceptor.intercept( DbSyncRequest( record = record, syncUtil = util, interceptors = request.interceptors, index = request.index + 1 ) ) } // val st = util.getFileServiceModifyTime(record.cloudDiskPath!!) // if (st == DbSynUtil.serviceModifyTime) { // return normal(DbSynUtil.STATE_SUCCEED, "云端文件和本地文件的修改时间一致,忽略该上传") // } if (cloudFileInfo.contentHash != null && util.checkContentHash(cloudFileInfo.contentHash, record.getDbUri()) ) { return normal(DbSynUtil.STATE_SUCCEED, "云端文件和本地文件的hash一致,忽略该上传") } Timber.i("云端文件存在,开始同步数据") return nextInterceptor.intercept( DbSyncRequest( record = record, syncUtil = util, interceptors = request.interceptors, index = request.index + 1 ) ) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/interceptor/DbSyncCompareInterceptor.kt ================================================ package com.lyy.keepassa.util.cloud.interceptor import android.content.Context import android.net.Uri import android.text.TextUtils import com.arialyy.frame.util.ResUtil import com.arialyy.frame.util.StringUtil import com.keepassdroid.database.PwDatabase import com.keepassdroid.database.helper.KDBHandlerHelper import com.lyy.keepassa.R.string import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.QuickUnLockUtil import com.lyy.keepassa.util.cloud.DbSynUtil import com.lyy.keepassa.view.StorageType.DROPBOX import com.lyy.keepassa.view.StorageType.WEBDAV import timber.log.Timber import java.io.File /** * db compare * @Author laoyuyu * @Description * @Date 5:10 下午 2021/12/24 **/ class DbSyncCompareInterceptor : IDbSyncInterceptor { private var nextRequest: DbSyncRequest? = null override suspend fun intercept(request: DbSyncRequest): DbSyncResponse { val util = request.syncUtil val record = request.record val st = util.getFileServiceModifyTime(record.cloudDiskPath!!) if (st == DbSynUtil.serviceModifyTime) { Timber.i( "云端文件修改时间:${KeepassAUtil.instance.formatTime(st)} 和本地缓存的云端文件时间:${ KeepassAUtil.instance.formatTime( DbSynUtil.serviceModifyTime ) } 一致,开始覆盖云端文件" ) return coverFile(request) } Timber.i( "云端文件修改时间:${KeepassAUtil.instance.formatTime(st)} 和本地缓存的云端文件时间:${ KeepassAUtil.instance.formatTime( DbSynUtil.serviceModifyTime ) } 不一致,开始下载云端文件" ) // 下载临时文件 val filePath = DbSynUtil.getCloudDbTempPath( record.type, "kpa_${StringUtil.keyToHashKey(record.cloudDiskPath)}.kdbx" ) val path = util.downloadFile(BaseApp.APP, record, filePath) if (path.isNullOrEmpty()) { DbSynUtil.toask(ResUtil.getString(string.sync_db), false, ResUtil.getString(string.net_error)) return error(DbSynUtil.STATE_DOWNLOAD_FILE_FAIL, "下载文件失败,${record.cloudDiskPath}") } val db = File(path) Timber.i("云端文件下载成功,开始打开数据库,filePath = ${db.path},fileSize = ${db.length()}") val kdb = openDb(BaseApp.APP, dbPath = path) if (kdb == null) { Timber.e("打开云端数据库失败,将覆盖云端数据库") return coverFile(request) } if (BaseApp.KDB?.pm == null) { return error(DbSynUtil.STATE_FAIL, "synUploadFile, local db is null") } Timber.i("打开云端数据库成功,开始比对数据") val code = DbMergeDelegate.compareDb(record, kdb, BaseApp.KDB!!.pm, true) if (code == DbSynUtil.STATE_FAIL) { return error(code, "save db fail") } return coverFile(request) } @Synchronized private fun getNextRequest(request: DbSyncRequest): DbSyncRequest { if (nextRequest == null) { nextRequest = DbSyncRequest( record = request.record, syncUtil = request.syncUtil, interceptors = request.interceptors, index = request.index + 1 ) } return nextRequest!! } /** * 覆盖文件,webdav 不需要删除 */ private suspend fun coverFile(request: DbSyncRequest): DbSyncResponse { val record = request.record val util = request.syncUtil val needDelFile = when (record.getDbPathType()) { DROPBOX -> true WEBDAV -> false else -> false } // 处理需要删除文件的情况 if (needDelFile) { if (util.delFile(record.cloudDiskPath!!)) { Timber.i("删除云端文件成功:${record.cloudDiskPath}") return request.nextInterceptor()!!.intercept(getNextRequest(request)) } return error(DbSynUtil.STATE_DEL_FILE_FAIL, "删除云端文件失败:${record.cloudDiskPath}") } return request.nextInterceptor()!!.intercept(getNextRequest(request)) } /** * 打开数据库 */ private fun openDb( context: Context, dbPath: String ): PwDatabase? { val uri = Uri.parse(dbPath) Timber.i("dbUri = $uri") val temp = KDBHandlerHelper.getInstance(context) .openDb( uri, QuickUnLockUtil.decryption(BaseApp.dbPass), if (TextUtils.isEmpty(BaseApp.dbKeyPath)) null else Uri.parse( QuickUnLockUtil.decryption(BaseApp.dbKeyPath) ) ) if (temp?.pm == null) { return null } return temp.pm } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/interceptor/DbSyncRequest.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud.interceptor import com.lyy.keepassa.entity.DbHistoryRecord import com.lyy.keepassa.util.cloud.ICloudUtil /** * @Author laoyuyu * @Description * @Date 4:41 下午 2021/12/24 **/ class DbSyncRequest constructor( var record: DbHistoryRecord, var syncUtil: ICloudUtil, val interceptors: List, val index: Int = 0 ) { fun nextInterceptor(): IDbSyncInterceptor? { if (index == interceptors.size) { return null } return interceptors[index + 1] } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/interceptor/DbSyncResponse.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud.interceptor /** * @Author laoyuyu * @Description * @Date 4:38 下午 2021/12/24 **/ class DbSyncResponse constructor(var code: Int, var msg: String) { } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/interceptor/DbSyncUploadInterceptor.kt ================================================ package com.lyy.keepassa.util.cloud.interceptor import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.util.cloud.DbSynUtil import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 5:08 下午 2021/12/24 **/ class DbSyncUploadInterceptor : IDbSyncInterceptor { override suspend fun intercept(request: DbSyncRequest): DbSyncResponse { val util = request.syncUtil val record = request.record val b = util.uploadFile(BaseApp.APP, record) val msg = "上传文件${if (b) "成功" else "失败"}, fileKey = ${record.cloudDiskPath}" Timber.d(msg) return DbSyncResponse(if (b) DbSynUtil.STATE_SUCCEED else DbSynUtil.STATE_FAIL, msg) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/cloud/interceptor/IDbSyncInterceptor.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.cloud.interceptor import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 4:34 下午 2021/12/24 **/ interface IDbSyncInterceptor { suspend fun intercept(request: DbSyncRequest): DbSyncResponse fun error(code: Int, msg: String): DbSyncResponse { Timber.e(msg) return DbSyncResponse(code, msg) } fun normal(code: Int, msg: String): DbSyncResponse { Timber.i(msg) return DbSyncResponse(code, msg) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/totp/Base32String.java ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.totp; import java.util.HashMap; import java.util.Locale; import java.util.Map; /** * Encodes arbitrary byte arrays as case-insensitive base-32 strings. * *

The implementation is slightly different than in RFC 4648. During encoding, padding is not * added, and during decoding the last incomplete chunk is not taken into account. The result is * that multiple strings decode to the same byte array, for example, string of sixteen 7s ("7...7") * and seventeen 7s both decode to the same byte array. * *

TODO: Revisit this encoding and whether this ambiguity needs fixing. */ public class Base32String { private static final String SEPARATOR = "-"; private static final char[] DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray(); private static final int MASK = DIGITS.length - 1; private static final int SHIFT = Integer.numberOfTrailingZeros(DIGITS.length); private static final Map CHAR_MAP = new HashMap<>(); static { for (int i = 0; i < DIGITS.length; i++) { CHAR_MAP.put(DIGITS[i], i); } } public static byte[] decode(String encoded) throws DecodingException { // Remove whitespace and separators encoded = encoded.trim().replaceAll(SEPARATOR, "").replaceAll(" ", ""); // Remove padding. Note: the padding is used as hint to determine how many // bits to decode from the last incomplete chunk (which is commented out // below, so this may have been wrong to start with). encoded = encoded.replaceFirst("[=]*$", ""); // Canonicalize to all upper case encoded = encoded.toUpperCase(Locale.US); if (encoded.length() == 0) { return new byte[0]; } int encodedLength = encoded.length(); int outLength = encodedLength * SHIFT / 8; byte[] result = new byte[outLength]; int buffer = 0; int next = 0; int bitsLeft = 0; for (char c : encoded.toCharArray()) { if (!CHAR_MAP.containsKey(c)) { throw new DecodingException("Illegal character: " + c); } buffer <<= SHIFT; buffer |= CHAR_MAP.get(c) & MASK; bitsLeft += SHIFT; if (bitsLeft >= 8) { result[next++] = (byte) (buffer >> (bitsLeft - 8)); bitsLeft -= 8; } } // We'll ignore leftover bits for now. // // if (next != outLength || bitsLeft >= SHIFT) { // throw new DecodingException("Bits left: " + bitsLeft); // } return result; } public static String encode(byte[] data) { int dataLength = data.length; if (dataLength == 0) { return ""; } // SHIFT is the number of bits per output character, so the length of the // output is the length of the input multiplied by 8/SHIFT, rounded up. if (dataLength >= (1 << 28)) { // The computation below will fail, so don't do it. throw new IllegalArgumentException(); } int outputLength = (dataLength * 8 + SHIFT - 1) / SHIFT; StringBuilder result = new StringBuilder(outputLength); int buffer = data[0]; int next = 1; int bitsLeft = 8; while (bitsLeft > 0 || next < dataLength) { if (bitsLeft < SHIFT) { if (next < dataLength) { buffer <<= 8; buffer |= (data[next++] & 0xff); bitsLeft += 8; } else { int pad = SHIFT - bitsLeft; buffer <<= pad; bitsLeft += pad; } } int index = MASK & (buffer >> (bitsLeft - SHIFT)); bitsLeft -= SHIFT; result.append(DIGITS[index]); } return result.toString(); } /** Exception thrown when decoding fails */ public static class DecodingException extends Exception { public DecodingException(String message) { super(message); } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/totp/ComposeKeeOtp.kt ================================================ package com.lyy.keepassa.util.totp import android.net.Uri import com.blankj.utilcode.util.ConvertUtils import com.blankj.utilcode.util.EncodeUtils import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm.SHA256 import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm.SHA512 object ComposeKeeOtp : IOtpCompose { override fun getOtpPass(entry: PwEntryV4): Pair { // keeotp val keepOtp = entry.strings["otp"] if (keepOtp != null) { val uri = Uri.parse("otp://laoyuyu.me/?$keepOtp") val key = uri.getQueryParameter("key") val type = uri.getQueryParameter("type") val len = uri.getQueryParameter("size") ?: TokenCalculator.TOTP_DEFAULT_DIGITS.toString() val hashMode = uri.getQueryParameter("otpHashMode") val encoding = uri.getQueryParameter("encoding") val counter = uri.getQueryParameter("counter") ?: "0" val period = uri.getQueryParameter("step") ?: TokenCalculator.TOTP_DEFAULT_PERIOD.toString() val algorithm = when (hashMode) { "Sha256" -> SHA256 "Sha512" -> SHA512 else -> HashAlgorithm.SHA1 } val seedByte = when (encoding) { "Base64" -> { EncodeUtils.base64Decode(key) } "UTF8" -> { key!!.toByteArray(Charsets.UTF_8) } "Hex" -> { // hex ConvertUtils.hexString2Bytes(key) } else -> { // base32 Base32String.decode(key) } } val token = when (type) { "Hotp" -> { TokenCalculator.HOTP(seedByte, counter.toLong(), len.toInt(), algorithm) } "Steam" -> { TokenCalculator.TOTP_Steam(seedByte, period.toInt(), len.toInt(), algorithm) } else -> { // totp TokenCalculator.TOTP_RFC6238(seedByte, period.toInt(), len.toInt(), algorithm) } } return Pair(if (type == "Hotp") counter.toInt() else period.toInt(), token) } return Pair(TokenCalculator.TOTP_DEFAULT_PERIOD, null) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/totp/ComposeKeeOtp2.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.totp import com.blankj.utilcode.util.ConvertUtils import com.blankj.utilcode.util.EncodeUtils import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.util.totp.ComposeKeepass.HMAC_SHA_256 import com.lyy.keepassa.util.totp.ComposeKeepass.HMAC_SHA_512 import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Algorithm import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Counter import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Secret import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Secret_Base32 import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Secret_Base64 import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Secret_Hex import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Algorithm import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Length import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Period import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Secret import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Secret_Base32 import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Secret_Base64 import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Secret_Hex import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm.SHA256 import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm.SHA512 /** * 兼容KeeOtp2插件的totp获取 * @Author laoyuyu * @Description * @Date 4:12 PM 2023/9/20 **/ @Deprecated("有异常,未实现") object ComposeKeeOtp2 : IOtpCompose { override fun getOtpPass(entry: PwEntryV4): Pair { return getKeeOtp2Totp(entry) } /** * 兼容KeeOtp2插件的totp获取 */ private fun getKeeOtp2Totp(entry: PwEntryV4): Pair { // 默认的32位 val def32 = entry.strings[TimeOtp_Secret_Base32] if (def32.toString().isNotEmpty()) { return Pair( TokenCalculator.TOTP_DEFAULT_PERIOD, getTotpPass( def32.toString(), TokenCalculator.TOTP_DEFAULT_PERIOD, TokenCalculator.TOTP_DEFAULT_DIGITS, false ) ) } // 自定义的totp val secretTotp = entry.strings.keys.find { it.startsWith(TimeOtp_Secret) } if (secretTotp != null) { val lenStr = entry.strings[TimeOtp_Length] val algorithmStr = entry.strings[TimeOtp_Algorithm] val periodStr = entry.strings[TimeOtp_Period] val len = lenStr?.toString()?.toInt() ?: TokenCalculator.TOTP_DEFAULT_PERIOD var algorithm = HashAlgorithm.SHA1 if (algorithmStr != null) { algorithm = when (algorithmStr.toString()) { HMAC_SHA_256 -> SHA256 HMAC_SHA_512 -> SHA512 else -> HashAlgorithm.SHA1 } } val period = periodStr?.toString()?.toInt() ?: TokenCalculator.TOTP_DEFAULT_PERIOD val seedByte = when { entry.strings[TimeOtp_Secret_Base64] != null -> { EncodeUtils.base64Decode(secretTotp) } entry.strings[TimeOtp_Secret_Base32] != null -> { Base32String.decode(secretTotp) } entry.strings[TimeOtp_Secret_Hex] != null -> { // hex ConvertUtils.hexString2Bytes(secretTotp) } else -> { // utf-8 secretTotp.toByteArray(Charsets.UTF_8) } } return Pair( period, TokenCalculator.TOTP_RFC6238(seedByte, period, len, algorithm) ) } return Pair(-1, null) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/totp/ComposeKeeTrayTotp.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.totp import com.arialyy.frame.router.Routerfit import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.entity.TrayTotpBean import com.lyy.keepassa.router.ServiceRouter import com.lyy.keepassa.util.otpIsKeeTraySteam /** * 兼容KeeTrayTOTP插件的totp获取 */ object ComposeKeeTrayTotp : IOtpCompose { const val KEY_SETTING = "TOTP Settings" const val KEY_SEED = "TOTP Seed" private val kdbService by lazy { Routerfit.create(ServiceRouter::class.java).getDbSaveService() } override fun getOtpPass(entry: PwEntryV4): Pair { // 修复1.7之前的bug if (isSteamEntry(entry)) { fix1_7bug(entry) } return getKeeTrayTotp(entry) } /** * 判断是否是steam的条目,1.7之前的版本创建totp时,会将 TOTP Settings 字段设置为 30;S,而S表示的是Steam */ private fun isSteamEntry(entry: PwEntryV4): Boolean { return entry.url .contains("steampowered", ignoreCase = true) || entry.customData.any { it.value.equals( "androidapp://com.valvesoftware.android.steam.community", true ) } } /** * 1.7之前的版本创建totp时,会将 TOTP Settings 字段设置为 30;S,而S表示的是Steam */ private fun fix1_7bug(entry: PwEntryV4) { val totpSetting = entry.strings["TOTP Settings"] if (totpSetting != null) { val tempArray = totpSetting.toString() .split(";") if (tempArray.isNotEmpty() && tempArray.size == 2 && !tempArray[1].equals("S", true)) { entry.strings["TOTP Settings"] = ProtectedString(false, "${tempArray[0]};S") kdbService.saveDbByBackground() } } } /** * 兼容KeeTrayTOTP插件的totp获取 */ private fun getKeeTrayTotp(entry: PwEntryV4): Pair { val totpSetting = entry.strings[KEY_SETTING] val isSteam = entry.otpIsKeeTraySteam() var period = TokenCalculator.TOTP_DEFAULT_PERIOD var digits = TokenCalculator.TOTP_DEFAULT_DIGITS val s = totpSetting.toString() .split(";") if (s.isNotEmpty() && s.size == 2) { period = s[0].toInt() digits = if (isSteam) TokenCalculator.STEAM_DEFAULT_DIGITS else s[1].toInt() } return Pair( period, getTotpPass(entry.strings[KEY_SEED].toString(), period, digits, isSteam) ) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/totp/ComposeKeepass.kt ================================================ package com.lyy.keepassa.util.totp import com.blankj.utilcode.util.ConvertUtils import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.entity.HmacOtpBean import com.lyy.keepassa.entity.TimeOtp2Bean import com.lyy.keepassa.util.getKeepassBean object ComposeKeepass : IOtpCompose { const val HmacOtp = "HmacOtp" const val TimeOtp = "TimeOtp" const val TimeOtp_Secret = "TimeOtp-Secret" const val TimeOtp_Length = "TimeOtp-Length" const val TimeOtp_Algorithm = "TimeOtp-Algorithm" const val TimeOtp_Period = "TimeOtp-Period" const val TimeOtp_Secret_Hex = "TimeOtp-Secret-Hex" const val TimeOtp_Secret_Base32 = "TimeOtp-Secret-Base32" const val TimeOtp_Secret_Base64 = "TimeOtp-Secret-Base64" const val HMAC_SHA_1 = "HMAC-SHA-1" const val HMAC_SHA_256 = "HMAC-SHA-256" const val HMAC_SHA_512 = "HMAC-SHA-512" //hOtp const val HmacOtp_Counter = "HmacOtp-Counter" const val HmacOtp_Secret = "HmacOtp-Secret" const val HmacOtp_Algorithm = "HmacOtp-Algorithm" const val HmacOtp_Secret_Hex = "HmacOtp-Secret-Hex" const val HmacOtp_Secret_Base32 = "HmacOtp-Secret-Base32" const val HmacOtp_Secret_Base64 = "HmacOtp-Secret-Base64" const val HmacOtp_Length = "HmacOtp-Length" override fun getOtpPass(entry: PwEntryV4): Pair { val bean = entry.getKeepassBean() if (bean.hmac != null) return handleHotp(bean.hmac) if (bean.otpBean != null) return handleTotp(bean.otpBean) return Pair(-1, null) } private fun handleHotp(hmacBean: HmacOtpBean): Pair { val secret = when (hmacBean.secretType) { SecretHexType.BASE_32 -> { Base32String.decode(hmacBean.secret) } SecretHexType.BASE_64 -> { Base32String.decode(hmacBean.secret) } SecretHexType.HEX -> { ConvertUtils.hexString2Bytes(hmacBean.secret) } else -> { hmacBean.secret.toByteArray(Charsets.UTF_8) } } return Pair( TokenCalculator.TOTP_DEFAULT_PERIOD, TokenCalculator.HOTP(secret, hmacBean.counter.toLong(), hmacBean.len, hmacBean.algorithm) ) } private fun handleTotp(otpBean: TimeOtp2Bean): Pair { val secret = when (otpBean.secretType) { SecretHexType.BASE_32 -> { Base32String.decode(otpBean.secret) } SecretHexType.BASE_64 -> { Base32String.decode(otpBean.secret) } SecretHexType.HEX -> { ConvertUtils.hexString2Bytes(otpBean.secret) } else -> { otpBean.secret.toByteArray(Charsets.UTF_8) } } val token = TokenCalculator.TOTP_RFC6238( secret, otpBean.period, otpBean.digits, otpBean.algorithm ) return Pair(otpBean.period, token) } fun getSecretType(secretType: SecretHexType): String { return when (secretType) { SecretHexType.UTF_8 -> TimeOtp_Secret SecretHexType.HEX -> TimeOtp_Secret_Hex SecretHexType.BASE_32 -> TimeOtp_Secret_Base32 SecretHexType.BASE_64 -> TimeOtp_Secret_Base64 } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/totp/ComposeKeepassxc.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.totp import android.text.TextUtils import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.R.string import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.GoogleOtpBean import com.lyy.keepassa.entity.KeepassXcBean import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.getKeepassXcBean import com.lyy.keepassa.util.otpIsKeepassXcSteam import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm import timber.log.Timber import java.util.Locale object ComposeKeepassxc : IOtpCompose { const val KEY_SEED = "otp" const val KEY_ENCODER = "encoder" const val KEY_STEAM = "steam" const val KEY_SECRET = "secret" const val KEY_COUNTER = "counter" const val KEY_ISSUER = "issuer" const val KEY_PERIOD = "period" const val KEY_DIGITS = "digits" const val KEY_ALGORITHM = "algorithm" override fun getOtpPass(entry: PwEntryV4): Pair { return getPass(entry) } private fun getPass(entry: PwEntryV4): Pair { val bean = entry.getKeepassXcBean() if (TextUtils.isEmpty(bean.secret)) { return Pair(0, null) } try { val b = Base32String.decode(bean.secret) if (entry.otpIsKeepassXcSteam()) { val pass = TokenCalculator.TOTP_Steam( b, TokenCalculator.TOTP_DEFAULT_PERIOD, TokenCalculator.STEAM_DEFAULT_DIGITS, TokenCalculator.DEFAULT_ALGORITHM ) return Pair(bean.period, pass) } val arithmetic = when (bean.algorithm) { HashAlgorithm.SHA256 -> "SHA256" HashAlgorithm.SHA512 -> "SHA512" else -> "SHA1" } val pass = when (bean.host) { "totp", "TOTP" -> { TokenCalculator.TOTP_RFC6238( b, bean.period, bean.digits, HashAlgorithm.valueOf(arithmetic.toUpperCase(Locale.ROOT)) ) } "hotp", "HOTP" -> { TokenCalculator.HOTP( b, bean.counter?.toLong() ?: TokenCalculator.HOTP_INITIAL_COUNTER.toLong(), bean.digits, HashAlgorithm.valueOf(arithmetic.toUpperCase(Locale.ROOT)) ) } else -> { Timber.e("不识别的类型:${bean.host}") null } } return Pair(bean.period, pass) } catch (e: Exception) { HitUtil.toaskShort(BaseApp.APP.getString(string.totp_key_error)) Timber.e(e) } return Pair(bean.period, null) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/totp/IOtpCompose.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.totp import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.R.string import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.util.HitUtil import timber.log.Timber /** * otp * @Author laoyuyu * @Description * @Date 4:02 PM 2023/9/20 **/ interface IOtpCompose { fun getTotpPass( seed: String?, period: Int, digits: Int = TokenCalculator.TOTP_DEFAULT_DIGITS, isSteam: Boolean ): String? { // 适配keepass totp插件的密码 try { val b = Base32String.decode(seed) return if (isSteam) { TokenCalculator.TOTP_Steam( b, TokenCalculator.TOTP_DEFAULT_PERIOD, TokenCalculator.STEAM_DEFAULT_DIGITS, TokenCalculator.DEFAULT_ALGORITHM ) } else { TokenCalculator.TOTP_RFC6238(b, period, digits, TokenCalculator.DEFAULT_ALGORITHM) } } catch (e: Exception) { HitUtil.toaskShort(BaseApp.APP.getString(string.totp_key_error)) Timber.e(e) } return null } /** * 获取totp密码 * @return first period, second 密码 */ fun getOtpPass(entry: PwEntryV4): Pair // fun toOtpStringMap(bean: OtpBean): Map { // return hashMapOf() // } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/totp/OtpEnum.kt ================================================ package com.lyy.keepassa.util.totp /** * @Author laoyuyu * @Description * @Date 5:24 PM 2024/1/11 **/ enum class OtpEnum { GOOGLE_OTP, KEEPASSXC, TRAY_TOTP, KEEPASS } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/totp/OtpUtil.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.totp import android.annotation.SuppressLint import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.util.otpIsKeeOtp2 import com.lyy.keepassa.util.otpIsKeeTrayTotp import com.lyy.keepassa.util.otpIsKeepOtp import com.lyy.keepassa.util.otpKeepass import com.lyy.keepassa.util.otpKeepassXC object OtpUtil { /** * 获取totp密码 * @return first period, second 密码 */ @SuppressLint("DefaultLocale") fun getOtpPass(entry: PwEntryV4): Pair { val otpCompose = when { entry.otpIsKeeTrayTotp() -> { ComposeKeeTrayTotp } entry.otpIsKeepOtp() -> { ComposeKeeOtp } entry.otpKeepassXC() -> { ComposeKeepassxc } entry.otpKeepass() -> { ComposeKeepass } entry.otpIsKeeOtp2() -> { ComposeKeeOtp2 } else -> null } return otpCompose?.getOtpPass(entry) ?: Pair(-1, null) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/totp/SecretHexType.kt ================================================ package com.lyy.keepassa.util.totp enum class SecretHexType { UTF_8, HEX, BASE_32, BASE_64 } ================================================ FILE: app/src/main/java/com/lyy/keepassa/util/totp/TokenCalculator.java ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.util.totp; import java.nio.ByteBuffer; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.NumberFormat; import java.util.Locale; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import timber.log.Timber; /** * 地址:https://github.com/andOTP/andOTP.git */ public class TokenCalculator { public static final int TOTP_DEFAULT_PERIOD = 30; public static final int TOTP_DEFAULT_DIGITS = 6; public static final int HOTP_INITIAL_COUNTER = 1; public static final int STEAM_DEFAULT_DIGITS = 5; private static final char[] STEAMCHARS = new char[] { '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'T', 'V', 'W', 'X', 'Y' }; public enum HashAlgorithm { SHA1, SHA256, SHA512 } public static final HashAlgorithm DEFAULT_ALGORITHM = HashAlgorithm.SHA1; private static byte[] generateHash(HashAlgorithm algorithm, byte[] key, byte[] data) throws NoSuchAlgorithmException, InvalidKeyException { String algo = "Hmac" + algorithm.toString(); Mac mac = Mac.getInstance(algo); mac.init(new SecretKeySpec(key, algo)); return mac.doFinal(data); } public static int TOTP_RFC6238(byte[] secret, int period, long time, int digits, HashAlgorithm algorithm) { int fullToken = TOTP(secret, period, time, algorithm); int div = (int) Math.pow(10, digits); return fullToken % div; } /** * TOTP_RFC6238 协议的TOTP * * @param secret seed * @param period {@link #TOTP_DEFAULT_PERIOD}、 * @param digits {@link #TOTP_DEFAULT_DIGITS}、{@link #STEAM_DEFAULT_DIGITS} * @param algorithm {@link HashAlgorithm} */ public static String TOTP_RFC6238(byte[] secret, int period, int digits, HashAlgorithm algorithm) { return formatTokenString( TOTP_RFC6238(secret, period, System.currentTimeMillis() / 1000, digits, algorithm), digits); } /** * TOTP_RFC6238 协议的TOTP * * @param secret seed * @param period {@link #TOTP_DEFAULT_PERIOD}、 * @param digits {@link #TOTP_DEFAULT_DIGITS}、{@link #STEAM_DEFAULT_DIGITS} * @param algorithm {@link HashAlgorithm} */ public static String TOTP_Steam(byte[] secret, int period, int digits, HashAlgorithm algorithm) { int fullToken = TOTP(secret, period, System.currentTimeMillis() / 1000, algorithm); StringBuilder tokenBuilder = new StringBuilder(); for (int i = 0; i < digits; i++) { tokenBuilder.append(STEAMCHARS[fullToken % STEAMCHARS.length]); fullToken /= STEAMCHARS.length; } return tokenBuilder.toString(); } /** * TOTP_RFC6238 协议的HOTP * * @param secret seed * @param counter {@link #HOTP_INITIAL_COUNTER} * @param digits {@link #TOTP_DEFAULT_DIGITS}、{@link #STEAM_DEFAULT_DIGITS} * @param algorithm {@link HashAlgorithm} */ public static String HOTP(byte[] secret, long counter, int digits, HashAlgorithm algorithm) { int fullToken = HOTP(secret, counter, algorithm); int div = (int) Math.pow(10, digits); return formatTokenString(fullToken % div, digits); } private static int TOTP(byte[] key, int period, long time, HashAlgorithm algorithm) { return HOTP(key, time / period, algorithm); } private static int HOTP(byte[] key, long counter, HashAlgorithm algorithm) { int r = 0; try { byte[] data = ByteBuffer.allocate(8).putLong(counter).array(); byte[] hash = generateHash(algorithm, key, data); int offset = hash[hash.length - 1] & 0xF; int binary = (hash[offset] & 0x7F) << 0x18; binary |= (hash[offset + 1] & 0xFF) << 0x10; binary |= (hash[offset + 2] & 0xFF) << 0x08; binary |= (hash[offset + 3] & 0xFF); r = binary; } catch (Exception e) { Timber.e(e); } return r; } private static String formatTokenString(int token, int digits) { NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH); numberFormat.setMinimumIntegerDigits(digits); numberFormat.setGroupingUsed(false); return numberFormat.format(token); } public static String formatToken(String s, int chunkSize) { if (chunkSize == 0 || s == null) { return s; } StringBuilder ret = new StringBuilder(""); int index = s.length(); while (index > 0) { ret.insert(0, s.substring(Math.max(index - chunkSize, 0), index)); ret.insert(0, " "); index = index - chunkSize; } return ret.toString().trim(); } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/ChoseDirModule.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view import androidx.fragment.app.FragmentActivity import com.keepassdroid.database.PwDatabaseV4 import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroup import com.keepassdroid.database.PwGroupId import com.keepassdroid.database.PwGroupV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.BaseModule import com.lyy.keepassa.event.MoveEvent import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KpaUtil import org.greenrobot.eventbus.EventBus import java.util.UUID class ChoseDirModule : BaseModule() { /** * 恢复群组 * @param groupId 需要恢复的群组 * @param curGroup 当前群组 */ fun moveGroup( ac: FragmentActivity, groupId: PwGroupId, curGroup: PwGroup ) { val group = BaseApp.KDB.pm.groups[groupId] as PwGroupV4 if (group.parent == BaseApp.KDB.pm.recycleBin) { (BaseApp.KDB.pm as PwDatabaseV4).undoRecycle(group, curGroup) } else { (BaseApp.KDB.pm as PwDatabaseV4).moveGroup(group, curGroup) } KpaUtil.kdbHandlerService.saveDbByBackground() EventBus.getDefault().post(MoveEvent(MoveEvent.MOVE_TYPE_GROUP, null, group)) HitUtil.toaskShort(ac.getString(R.string.undo_grouped)) ac.finishAfterTransition() } /** * 恢复条目 */ fun moveEntry( ac: FragmentActivity, entryId: UUID, curGroup: PwGroupV4 ) { val entry = BaseApp.KDB.pm.entries[entryId] ?: return val entryV4 = entry as PwEntryV4 KpaUtil.kdbHandlerService.moveEntry(entryV4, curGroup) HitUtil.toaskShort(ac.getString(R.string.undo_entryed)) ac.finishAfterTransition() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/KpaCaptureManager.java ================================================ package com.lyy.keepassa.view; import android.Manifest; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.view.Display; import android.view.KeyEvent; import android.view.Surface; import android.view.Window; import android.view.WindowManager; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import com.google.zxing.BarcodeFormat; import com.google.zxing.DecodeHintType; import com.google.zxing.MultiFormatReader; import com.google.zxing.ResultMetadataType; import com.google.zxing.ResultPoint; import com.google.zxing.client.android.BeepManager; import com.google.zxing.client.android.DecodeFormatManager; import com.google.zxing.client.android.DecodeHintManager; import com.google.zxing.client.android.InactivityTimer; import com.google.zxing.client.android.Intents; import com.google.zxing.client.android.R; import com.journeyapps.barcodescanner.BarcodeCallback; import com.journeyapps.barcodescanner.BarcodeResult; import com.journeyapps.barcodescanner.BarcodeView; import com.journeyapps.barcodescanner.CameraPreview; import com.journeyapps.barcodescanner.DecoratedBarcodeView; import com.journeyapps.barcodescanner.DefaultDecoderFactory; import com.journeyapps.barcodescanner.camera.CameraSettings; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Set; import timber.log.Timber; /** * Manages barcode scanning for a CaptureActivity. This class may be used to have a custom Activity * (e.g. with a customized look and feel, or a different superclass), but not the barcode scanning * process itself. * * This is intended for an Activity that is dedicated to capturing a single barcode and returning * it via setResult(). For other use cases, use DefaultBarcodeScannerView or BarcodeView directly. * * The following is managed by this class: * - Orientation lock * - InactivityTimer * - BeepManager * - Initializing from an Intent (via IntentIntegrator) * - Setting the result and finishing the Activity when a barcode is scanned * - Displaying camera errors */ public class KpaCaptureManager { private static int cameraPermissionReqCode = 250; private final Activity activity; private int orientationLock = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; private static final String SAVED_ORIENTATION_LOCK = "SAVED_ORIENTATION_LOCK"; private boolean returnBarcodeImagePath = false; private boolean showDialogIfMissingCameraPermission = true; private String missingCameraPermissionDialogMessage = ""; private boolean destroyed = false; private final InactivityTimer inactivityTimer; private final BeepManager beepManager; private final Handler handler; private final BarcodeView barcodeView; /** * The instance of @link TorchListener to send events callback. */ private DecoratedBarcodeView.TorchListener torchListener; private boolean finishWhenClosed = false; private final BarcodeCallback callback = new BarcodeCallback() { @Override public void barcodeResult(final BarcodeResult result) { barcodeView.pause(); beepManager.playBeepSoundAndVibrate(); handler.post(() -> returnResult(result)); } @Override public void possibleResultPoints(List resultPoints) { } }; public KpaCaptureManager(Activity activity, BarcodeView barcodeView) { this.activity = activity; this.barcodeView = barcodeView; CameraPreview.StateListener stateListener = new CameraPreview.StateListener() { @Override public void previewSized() { } @Override public void previewStarted() { } @Override public void previewStopped() { } @Override public void cameraError(Exception error) { displayFrameworkBugMessageAndExit( activity.getString(R.string.zxing_msg_camera_framework_bug) ); } @Override public void cameraClosed() { if (finishWhenClosed) { Timber.d("Camera closed; finishing activity"); finish(); } } }; barcodeView.addStateListener(stateListener); handler = new Handler(); inactivityTimer = new InactivityTimer(activity, () -> { Timber.d("Finishing due to inactivity"); finish(); }); beepManager = new BeepManager(activity); } /** * Perform initialization, according to preferences set in the intent. * * @param intent the intent containing the scanning preferences * @param savedInstanceState saved state, containing orientation lock */ public void initializeFromIntent(Intent intent, Bundle savedInstanceState) { Window window = activity.getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); if (savedInstanceState != null) { // If the screen was locked and unlocked again, we may start in a different orientation // (even one not allowed by the manifest). In this case we restore the orientation we were // previously locked to. this.orientationLock = savedInstanceState.getInt(SAVED_ORIENTATION_LOCK, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } if (intent != null) { // Only lock the orientation if it's not locked to something else yet boolean orientationLocked = intent.getBooleanExtra(Intents.Scan.ORIENTATION_LOCKED, true); if (orientationLocked) { lockOrientation(); } if (Intents.Scan.ACTION.equals(intent.getAction())) { initializeFromIntent(intent); } if (!intent.getBooleanExtra(Intents.Scan.BEEP_ENABLED, true)) { beepManager.setBeepEnabled(false); } if (intent.hasExtra(Intents.Scan.SHOW_MISSING_CAMERA_PERMISSION_DIALOG)) { setShowMissingCameraPermissionDialog( intent.getBooleanExtra(Intents.Scan.SHOW_MISSING_CAMERA_PERMISSION_DIALOG, true), intent.getStringExtra(Intents.Scan.MISSING_CAMERA_PERMISSION_DIALOG_MESSAGE) ); } if (intent.hasExtra(Intents.Scan.TIMEOUT)) { handler.postDelayed(this::returnResultTimeout, intent.getLongExtra(Intents.Scan.TIMEOUT, 0L)); } if (intent.getBooleanExtra(Intents.Scan.BARCODE_IMAGE_ENABLED, false)) { returnBarcodeImagePath = true; } } } public void initializeFromIntent(Intent intent) { // Scan the formats the intent requested, and return the result to the calling activity. Set decodeFormats = DecodeFormatManager.parseDecodeFormats(intent); Map decodeHints = DecodeHintManager.parseDecodeHints(intent); CameraSettings settings = new CameraSettings(); if (intent.hasExtra(Intents.Scan.CAMERA_ID)) { int cameraId = intent.getIntExtra(Intents.Scan.CAMERA_ID, -1); if (cameraId >= 0) { settings.setRequestedCameraId(cameraId); } } if (intent.hasExtra(Intents.Scan.TORCH_ENABLED)) { if (intent.getBooleanExtra(Intents.Scan.TORCH_ENABLED, false)) { this.setTorchOn(); } } // Check what type of scan. Default: normal scan int scanType = intent.getIntExtra(Intents.Scan.SCAN_TYPE, 0); String characterSet = intent.getStringExtra(Intents.Scan.CHARACTER_SET); MultiFormatReader reader = new MultiFormatReader(); reader.setHints(decodeHints); barcodeView.setCameraSettings(settings); barcodeView.setDecoderFactory( new DefaultDecoderFactory(decodeFormats, decodeHints, characterSet, scanType)); } public void setTorchOn() { barcodeView.setTorch(true); if (torchListener != null) { torchListener.onTorchOn(); } } /** * Lock display to current orientation. */ protected void lockOrientation() { // Only get the orientation if it's not locked to one yet. if (this.orientationLock == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { // Adapted from http://stackoverflow.com/a/14565436 Display display = activity.getWindowManager().getDefaultDisplay(); int rotation = display.getRotation(); int baseOrientation = activity.getResources().getConfiguration().orientation; int orientation = 0; if (baseOrientation == Configuration.ORIENTATION_LANDSCAPE) { if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; } else { orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; } } else if (baseOrientation == Configuration.ORIENTATION_PORTRAIT) { if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) { orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; } else { orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; } } this.orientationLock = orientation; } //noinspection ResourceType activity.setRequestedOrientation(this.orientationLock); } /** * Start decoding. */ public void decode() { barcodeView.decodeSingle(callback); } /** * Call from Activity#onResume(). */ public void onResume() { if (Build.VERSION.SDK_INT >= 23) { openCameraWithPermission(); } else { barcodeView.resume(); } inactivityTimer.start(); } private boolean askedPermission = false; @TargetApi(23) private void openCameraWithPermission() { if (ContextCompat.checkSelfPermission(this.activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { barcodeView.resume(); } else if (!askedPermission) { ActivityCompat.requestPermissions(this.activity, new String[] { Manifest.permission.CAMERA }, cameraPermissionReqCode); askedPermission = true; } // else wait for permission result } /** * Call from Activity#onRequestPermissionsResult * * @param requestCode The request code passed in {@link androidx.core.app.ActivityCompat#requestPermissions(Activity, * String[], int)}. * @param permissions The requested permissions. * @param grantResults The grant results for the corresponding permissions * which is either {@link android.content.pm.PackageManager#PERMISSION_GRANTED} * or {@link android.content.pm.PackageManager#PERMISSION_DENIED}. Never null. */ public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { if (requestCode == cameraPermissionReqCode) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // permission was granted barcodeView.resume(); } else { setMissingCameraPermissionResult(); if (showDialogIfMissingCameraPermission) { displayFrameworkBugMessageAndExit(missingCameraPermissionDialogMessage); } else { closeAndFinish(); } } } } /** * Call from Activity#onPause(). */ public void onPause() { inactivityTimer.cancel(); barcodeView.pauseAndWait(); } /** * Call from Activity#onDestroy(). */ public void onDestroy() { destroyed = true; inactivityTimer.cancel(); handler.removeCallbacksAndMessages(null); } /** * Call from Activity#onSaveInstanceState(). */ public void onSaveInstanceState(Bundle outState) { outState.putInt(SAVED_ORIENTATION_LOCK, this.orientationLock); } /** * Create a intent to return as the Activity result. * * @param rawResult the BarcodeResult, must not be null. * @param barcodeImagePath a path to an exported file of the Barcode Image, can be null. * @return the Intent */ public static Intent resultIntent(BarcodeResult rawResult, String barcodeImagePath) { Intent intent = new Intent(Intents.Scan.ACTION); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); intent.putExtra(Intents.Scan.RESULT, rawResult.toString()); intent.putExtra(Intents.Scan.RESULT_FORMAT, rawResult.getBarcodeFormat().toString()); byte[] rawBytes = rawResult.getRawBytes(); if (rawBytes != null && rawBytes.length > 0) { intent.putExtra(Intents.Scan.RESULT_BYTES, rawBytes); } Map metadata = rawResult.getResultMetadata(); if (metadata != null) { if (metadata.containsKey(ResultMetadataType.UPC_EAN_EXTENSION)) { intent.putExtra(Intents.Scan.RESULT_UPC_EAN_EXTENSION, metadata.get(ResultMetadataType.UPC_EAN_EXTENSION).toString()); } Number orientation = (Number) metadata.get(ResultMetadataType.ORIENTATION); if (orientation != null) { intent.putExtra(Intents.Scan.RESULT_ORIENTATION, orientation.intValue()); } String ecLevel = (String) metadata.get(ResultMetadataType.ERROR_CORRECTION_LEVEL); if (ecLevel != null) { intent.putExtra(Intents.Scan.RESULT_ERROR_CORRECTION_LEVEL, ecLevel); } @SuppressWarnings("unchecked") Iterable byteSegments = (Iterable) metadata.get(ResultMetadataType.BYTE_SEGMENTS); if (byteSegments != null) { int i = 0; for (byte[] byteSegment : byteSegments) { intent.putExtra(Intents.Scan.RESULT_BYTE_SEGMENTS_PREFIX + i, byteSegment); i++; } } } if (barcodeImagePath != null) { intent.putExtra(Intents.Scan.RESULT_BARCODE_IMAGE_PATH, barcodeImagePath); } return intent; } /** * Save the barcode image to a temporary file stored in the application's cache, and return its * path. * Only does so if returnBarcodeImagePath is enabled. * * @param rawResult the BarcodeResult, must not be null * @return the path or null */ private String getBarcodeImagePath(BarcodeResult rawResult) { String barcodeImagePath = null; if (returnBarcodeImagePath) { Bitmap bmp = rawResult.getBitmap(); try { File bitmapFile = File.createTempFile("barcodeimage", ".jpg", activity.getCacheDir()); FileOutputStream outputStream = new FileOutputStream(bitmapFile); bmp.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); outputStream.close(); barcodeImagePath = bitmapFile.getAbsolutePath(); } catch (IOException e) { Timber.w("Unable to create temporary file and store bitmap! %s", e); } } return barcodeImagePath; } private void finish() { activity.finish(); } protected void closeAndFinish() { if (barcodeView.isCameraClosed()) { finish(); } else { finishWhenClosed = true; } barcodeView.pause(); inactivityTimer.cancel(); } private void setMissingCameraPermissionResult() { Intent intent = new Intent(Intents.Scan.ACTION); intent.putExtra(Intents.Scan.MISSING_CAMERA_PERMISSION, true); activity.setResult(Activity.RESULT_CANCELED, intent); } protected void returnResultTimeout() { Intent intent = new Intent(Intents.Scan.ACTION); intent.putExtra(Intents.Scan.TIMEOUT, true); activity.setResult(Activity.RESULT_CANCELED, intent); closeAndFinish(); } protected void returnResult(BarcodeResult rawResult) { Intent intent = resultIntent(rawResult, getBarcodeImagePath(rawResult)); activity.setResult(Activity.RESULT_OK, intent); closeAndFinish(); } protected void displayFrameworkBugMessageAndExit(String message) { if (activity.isFinishing() || this.destroyed || finishWhenClosed) { return; } if (message.isEmpty()) { message = activity.getString(R.string.zxing_msg_camera_framework_bug); } AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(activity.getString(R.string.zxing_app_name)); builder.setMessage(message); builder.setPositiveButton(R.string.zxing_button_ok, (dialog, which) -> finish()); builder.setOnCancelListener(dialog -> finish()); builder.show(); } public static int getCameraPermissionReqCode() { return cameraPermissionReqCode; } public static void setCameraPermissionReqCode(int cameraPermissionReqCode) { KpaCaptureManager.cameraPermissionReqCode = cameraPermissionReqCode; } /** * If set to true, shows the default error dialog if camera permission is missing. *

* If set to false, instead the capture manager just finishes. *

* In both cases, the activity result is set to {@link Intents.Scan#MISSING_CAMERA_PERMISSION} * and cancelled */ public void setShowMissingCameraPermissionDialog(boolean visible) { setShowMissingCameraPermissionDialog(visible, ""); } /** * If set to true, shows the specified error dialog message if camera permission is missing. *

* If set to false, instead the capture manager just finishes. *

* In both cases, the activity result is set to {@link Intents.Scan#MISSING_CAMERA_PERMISSION} * and cancelled */ public void setShowMissingCameraPermissionDialog(boolean visible, String message) { showDialogIfMissingCameraPermission = visible; missingCameraPermissionDialogMessage = message != null ? message : ""; } public void setTorchListener(DecoratedBarcodeView.TorchListener listener) { this.torchListener = listener; } /** * Turn off the device's flashlight. */ public void setTorchOff() { barcodeView.setTorch(false); if (torchListener != null) { torchListener.onTorchOff(); } } public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_FOCUS: case KeyEvent.KEYCODE_CAMERA: // Handle these events so they don't launch the Camera app return true; // Use volume up/down to turn on light case KeyEvent.KEYCODE_VOLUME_DOWN: setTorchOff(); return true; case KeyEvent.KEYCODE_VOLUME_UP: setTorchOn(); return true; } return false; } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/MarkDownEditorActivity.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view import android.app.Activity import android.app.ActivityOptions import android.content.Context import android.content.Intent import android.os.Bundle import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseActivity import com.lyy.keepassa.databinding.ActivityMarkdownEditorBinding import com.lyy.keepassa.event.EditorEvent import com.lyy.keepassa.widget.editor.MarkDownEditor import org.greenrobot.eventbus.EventBus import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 2020/12/2 **/ class MarkDownEditorActivity : BaseActivity() { private var reqCode: Int = 0 private var content: CharSequence? = null companion object { private val KEY_REQUESTOIN_CODE = "KEY_REQUESTOIN_CODE" private var KEY_CONTENT = "KEY_CONTENT" fun turnMarkDownEditor( context: Context, requestCode: Int, content: CharSequence? ) { val intent = Intent(context, MarkDownEditorActivity::class.java) intent.putExtra(KEY_REQUESTOIN_CODE, requestCode) intent.putExtra(KEY_CONTENT, content) if (context is Activity) { context.startActivity( intent, ActivityOptions.makeSceneTransitionAnimation(context) .toBundle() ) return } context.startActivity(intent) } } override fun setLayoutId(): Int { return R.layout.activity_markdown_editor } override fun initData(savedInstanceState: Bundle?) { super.initData(savedInstanceState) reqCode = intent.getIntExtra(KEY_REQUESTOIN_CODE, -1) content = intent.getCharSequenceExtra(KEY_CONTENT) if (reqCode == -1) { Timber.e( "没有设置请求码") finishAfterTransition() return } binding.mdeEditor.setText(content) binding.mdeEditor.setOnSaveListener(object : MarkDownEditor.OnSaveListener { override fun onSave(content: CharSequence?) { EventBus.getDefault() .post(EditorEvent(reqCode, content)) finishAfterTransition() } }) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/QrCodeScannerActivity.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view import android.os.Bundle import android.view.KeyEvent import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseActivity import com.lyy.keepassa.databinding.ActivityQrCodeScannerBinding /** * @Author laoyuyu * @Description * @Date 2022/1/11 **/ internal class QrCodeScannerActivity : BaseActivity() { private lateinit var capture: KpaCaptureManager override fun setLayoutId(): Int { return R.layout.activity_qr_code_scanner } override fun initData(savedInstanceState: Bundle?) { super.initData(savedInstanceState) capture = KpaCaptureManager(this, binding.barcodeView) capture.initializeFromIntent(this.intent, savedInstanceState) capture.decode() } override fun onResume() { super.onResume() capture.onResume() } override fun onPause() { super.onPause() capture.onPause() } override fun onDestroy() { super.onDestroy() capture.onDestroy() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) capture.onSaveInstanceState(outState) } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) capture.onRequestPermissionsResult(requestCode, permissions, grantResults) } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { return capture.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/SimpleAdapter.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view import android.content.Context import android.view.View import android.widget.TextView import androidx.appcompat.widget.AppCompatImageView import androidx.preference.PreferenceManager import com.arialyy.frame.util.adapter.AbsHolder import com.arialyy.frame.util.adapter.AbsRVAdapter import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.ShapeAppearanceModel import com.keepassdroid.database.PwGroup import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.view.SimpleAdapter.Holder import com.lyy.keepassa.widget.toPx /** * list适配器 */ class SimpleAdapter( context: Context, data: List ) : AbsRVAdapter(context, data) { private val useRoundedCorners by lazy { PreferenceManager.getDefaultSharedPreferences(BaseApp.APP) .getBoolean(BaseApp.APP.getString(R.string.set_key_fillet_bg_icon), true) } private val shapeMode by lazy { ShapeAppearanceModel.Builder() .setAllCorners( CornerFamily.ROUNDED, 8.toPx() .toFloat() ) .build() } override fun getViewHolder( convertView: View?, viewType: Int ): Holder { return Holder(convertView!!) } override fun setLayoutId(type: Int): Int { return R.layout.item_path_type } override fun bindData( holder: Holder, position: Int, item: SimpleItemEntity ) { // if (useRoundedCorners) { // holder.icon.shapeAppearanceModel = shapeMode // } holder.icon.setImageResource(item.icon) holder.title.text = item.title holder.des.text = item.subTitle } class Holder(view: View) : AbsHolder(view) { val icon: ShapeableImageView = view.findViewById(R.id.icon) val title: TextView = view.findViewById(R.id.title) val des: TextView = view.findViewById(R.id.des) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/SimpleEntryAdapter.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view import android.content.Context import android.graphics.Paint import android.view.View import android.widget.TextView import androidx.appcompat.widget.AppCompatImageView import com.arialyy.frame.util.adapter.AbsHolder import com.arialyy.frame.util.adapter.AbsRVAdapter import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwGroup import com.lyy.keepassa.R import com.lyy.keepassa.entity.EntryType import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.util.IconUtil import com.lyy.keepassa.util.loadImg import com.lyy.keepassa.view.SimpleEntryAdapter.Holder import java.util.Date /** * list适配器 */ class SimpleEntryAdapter( context: Context, data: List ) : AbsRVAdapter(context, data) { override fun getViewHolder( convertView: View?, viewType: Int ): Holder { return Holder(convertView!!) } override fun setLayoutId(type: Int): Int { return R.layout.item_entry } override fun bindData( holder: Holder, position: Int, item: SimpleItemEntity ) { if (item.obj is PwGroup) { IconUtil.setGroupIcon(context, item.obj as PwGroup, holder.icon) } else if (item.obj is PwEntry) { IconUtil.setEntryIcon(item.obj as PwEntry, holder.icon) val paint = holder.title.paint if ((item.obj as PwEntry).expires() && (item.obj as PwEntry).expiryTime != null && (item.obj as PwEntry).expiryTime.before(Date(System.currentTimeMillis())) ) { paint.flags = Paint.STRIKE_THRU_TEXT_FLAG paint.isAntiAlias = true } else { paint.flags = 0 } } else if (item.obj == EntryType.TYPE_COLLECTION) { holder.icon.loadImg(item.icon) } holder.title.text = item.title holder.des.text = item.subTitle holder.des.visibility = if (item.subTitle.isBlank()) View.GONE else View.VISIBLE } class Holder(view: View) : AbsHolder(view) { val icon: AppCompatImageView = view.findViewById(R.id.icon) val title: TextView = view.findViewById(R.id.title) val des: TextView = view.findViewById(R.id.des) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/StorageType.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view import androidx.annotation.DrawableRes import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp enum class StorageType( var type: Int, @DrawableRes var icon: Int, var lable: String ) { AFS(0, R.drawable.ic_android, BaseApp.APP.getString(R.string.afs)), DROPBOX(1, R.drawable.ic_dropbox, "Dropbox"), ONE_DRIVE(2, R.drawable.ic_onedrive, "OneDrive"), GOOGLE_DRIVE(3, R.drawable.ic_google_drive, "GoogleDrive"), WEBDAV(4, R.drawable.ic_http, "WebDav"), FTP(5, R.drawable.ic_ftp, "Ftp"), UNKNOWN(-1, R.drawable.ic_android, "Unknown") } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/UpgradeLogDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view import android.content.Context import android.content.Intent import android.net.Uri import android.provider.Settings import android.text.TextUtils import androidx.core.app.ActivityOptionsCompat import androidx.core.content.edit import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.AndroidUtils import com.arialyy.frame.util.ResUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.base.Constance import com.lyy.keepassa.databinding.DialogUpgradeBinding import com.lyy.keepassa.router.ActivityRouter import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.FingerprintUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.LanguageUtil import com.lyy.keepassa.view.dialog.DonateDialog import com.lyy.keepassa.view.fingerprint.FingerprintActivity import com.lyy.keepassa.widget.DrawableTextView import com.lyy.keepassa.widget.toPx import com.zzhoujay.richtext.RichText import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.io.InputStream /** * 版本升级对话框 */ class UpgradeLogDialog : BaseDialog() { private val scope = MainScope() override fun setLayoutId(): Int { return R.layout.dialog_upgrade } override fun initData() { super.initData() scope.launch { var context = "" // val fileName = "version_log/version_log_${getVersionSuffix()}.md" val fileName = "version_log/version_log_${if (KpaUtil.isChina()) "zh_CN" else "en"}.md" withContext(Dispatchers.IO) { // val ins = requireContext().assets.open(fileName) // context = String(ins.readBytes()) // ins.close() var ins: InputStream? = null try { ins = requireContext().assets.open(fileName) } catch (e: Exception) { ins = requireContext().assets.open("version_log/version_log_en.md") Timber.e(e) } ins?.let { context = String(it.readBytes()) it.close() } } RichText.fromMarkdown(context) .urlClick { url -> if (handlerUrlClick(url)) { dismiss() } return@urlClick true } .into(binding.tvContent) } binding.btEnter.setOnClickListener { dismiss() } binding.btDonate.setDrawable( DrawableTextView.LEFT, ResUtil.getSvgIcon(R.drawable.ic_favorite_24px, R.color.text_blue_color), 16.toPx(), 16.toPx() ) binding.btDonate.setOnClickListener { DonateDialog().show() } } override fun onStart() { super.onStart() dialog?.window?.setLayout(360.toPx(), 600.toPx()) } override fun dismiss() { super.dismiss() requireContext().getSharedPreferences(Constance.PRE_FILE_NAME, Context.MODE_PRIVATE) .edit { putInt(Constance.VERSION_CODE, AndroidUtils.getVersionCode(requireContext())) } } /** * 根据语言获取版本日志后缀名 */ private fun getVersionSuffix(): String { var defLocal = LanguageUtil.getDefLanguage(requireContext()) if (defLocal == null) { defLocal = LanguageUtil.getSysCurrentLan() } return if (TextUtils.isEmpty(defLocal.country)) { defLocal.language } else { "${defLocal.language}_${defLocal.country}" } } /** * 除了url点击 * @return true 已处理 */ private fun handlerUrlClick(url: String): Boolean { val uri = Uri.parse(url) if (uri.scheme == "route") { val activity = uri.getQueryParameter("activity") if (!activity.isNullOrEmpty()) { when (activity) { "FingerprintActivity" -> { if (FingerprintUtil.hasBiometricPrompt(requireContext())) { FingerprintActivity.toFingerprintActivity(requireActivity()) return true } } "WebDavLoginDialog" -> { Routerfit.create(DialogRouter::class.java).showWebDavLoginDialog() return true } "SettingActivity" -> { val type = uri.getQueryParameter("type") when (type) { "db" -> { Routerfit.create(ActivityRouter::class.java, requireActivity()).toAppSetting( opt = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity()) ) } "app" -> { val scrollKey = uri.getQueryParameter("scrollKey") Routerfit.create(ActivityRouter::class.java, requireActivity()).toAppSetting( opt = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity()), scrollKey = scrollKey ) } } return true } "ime" -> { startActivity(Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)) return true } } } } else { Timber.d("url = $url") startActivity(Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(url) }) } return false } override fun onDestroy() { super.onDestroy() scope.cancel() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/collection/CollectionActivity.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.collection import android.annotation.SuppressLint import android.os.Bundle import android.view.View import androidx.appcompat.widget.AppCompatImageView import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.ResUtil import com.keepassdroid.database.PwEntry import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseActivity import com.lyy.keepassa.databinding.ActivityCollectionBinding import com.lyy.keepassa.event.CollectionEventType.COLLECTION_STATE_ADD import com.lyy.keepassa.event.CollectionEventType.COLLECTION_STATE_REMOVE import com.lyy.keepassa.event.CollectionEventType.COLLECTION_STATE_TOTAL import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.doOnItemClickListener import com.lyy.keepassa.view.SimpleEntryAdapter import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch /** * @Author laoyuyu * @Description * @Date 19:43 上午 2022/3/29 **/ @Route(path = "/collection/ac") internal class CollectionActivity : BaseActivity() { private lateinit var module: CollectionModule private lateinit var adapter: SimpleEntryAdapter override fun setLayoutId(): Int { return R.layout.activity_collection } override fun initData(savedInstanceState: Bundle?) { super.initData(savedInstanceState) toolbar.title = ResUtil.getString(R.string.my_collection) module = ViewModelProvider(this)[CollectionModule::class.java] adapter = SimpleEntryAdapter(this, module.itemDataList) binding.rvList.let { it.layoutManager = LinearLayoutManager(this) it.setHasFixedSize(true) it.adapter = adapter } binding.rvList.doOnItemClickListener { _, position, v -> val item = module.itemDataList[position] val icon = v.findViewById(R.id.icon) KeepassAUtil.instance.turnEntryDetail(this, item.obj as PwEntry, icon) } binding.emptyView.setText(ResUtil.getString(R.string.no_collection)) listenerCollection() listenerGetData() module.getData() } @SuppressLint("NotifyDataSetChanged") private fun listenerGetData() { lifecycleScope.launch { module.itemDataFlow.collectLatest { if (it.isNullOrEmpty()) { binding.emptyView.visibility = View.VISIBLE return@collectLatest } binding.emptyView.visibility = View.GONE adapter.notifyDataSetChanged() } } } @SuppressLint("NotifyDataSetChanged") private fun listenerCollection() { lifecycleScope.launch { KpaUtil.kdbHandlerService.collectionStateFlow.collectLatest { if (it.collectionNum == 0) { binding.emptyView.visibility = View.VISIBLE return@collectLatest } binding.emptyView.visibility = View.GONE when (it.state) { COLLECTION_STATE_TOTAL -> { adapter.notifyDataSetChanged() } COLLECTION_STATE_ADD -> { module.addNewItem(adapter, it.pwEntryV4) } COLLECTION_STATE_REMOVE -> { module.removeItem(adapter, it.pwEntryV4) } } } } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/collection/CollectionModule.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.collection import androidx.lifecycle.viewModelScope import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.base.BaseModule import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.view.SimpleEntryAdapter import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 19:48 上午 2022/3/29 **/ internal class CollectionModule : BaseModule() { val itemDataList = arrayListOf() val itemDataFlow = MutableStateFlow?>(null) fun removeItem(adapter: SimpleEntryAdapter, newEntry: PwEntryV4?) { if (newEntry == null) { Timber.d("entry is null") return } val newItem = KeepassAUtil.instance.convertPwEntry2Item(newEntry) var removePosition = -1 itemDataList.forEachIndexed { index, simpleItemEntity -> if (simpleItemEntity.obj == newItem.obj) { removePosition = index return@forEachIndexed } } if (removePosition == -1) { Timber.d("the entry is not in the list, title = ${newEntry.title}") return } itemDataList.removeAt(removePosition) adapter.notifyItemRemoved(removePosition) } /** * add new collection */ fun addNewItem(adapter: SimpleEntryAdapter, newEntry: PwEntryV4?) { if (newEntry == null) { Timber.d("entry is null") return } val newItem = KeepassAUtil.instance.convertPwEntry2Item(newEntry) val temp = itemDataList.find { it.obj == newItem.obj } if (temp != null) { Timber.d("already has the entry, title = ${newEntry.title}") return } val newPosition = itemDataList.size itemDataList.add(newItem) adapter.notifyItemInserted(newPosition) } fun getData() { itemDataList.clear() KpaUtil.kdbHandlerService.getCollectionEntries().forEach { itemDataList.add(KeepassAUtil.instance.convertPwEntry2Item(it)) } viewModelScope.launch { itemDataFlow.emit(itemDataList) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/CreateCustomStrDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create import android.view.View import androidx.lifecycle.lifecycleScope import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogAddAttrStrBinding import com.lyy.keepassa.entity.CommonState import com.lyy.keepassa.event.AttrStrEvent import com.lyy.keepassa.util.HitUtil import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch /** * 创建自定义字段的对话框 */ @Route(path = "/dialog/customStrDialog") class CreateCustomStrDialog : BaseDialog(), View.OnClickListener { companion object { val CustomStrFlow = MutableSharedFlow(0) } @Autowired(name = "key") @JvmField var key: String? = null @Autowired(name = "value") @JvmField var value: ProtectedString? = null @Autowired(name = "position") @JvmField var position: Int = 0 override fun setLayoutId(): Int { return R.layout.dialog_add_attr_str } override fun initData() { super.initData() ARouter.getInstance().inject(this) binding.cancel.setOnClickListener(this) binding.enter.setOnClickListener(this) if (key != null) { binding.strKey.setText(key) } if (value != null) { binding.strValue.setText(value.toString()) binding.cb.isChecked = value!!.isProtected } } override fun onClick(v: View?) { if (v!!.id == R.id.enter) { if (binding.strKey.text.toString().trim().isEmpty()) { HitUtil.toaskShort(getString(R.string.error_attr_str_null)) return } lifecycleScope.launch { CustomStrFlow.emit( AttrStrEvent( if (key != null) CommonState.MODIFY else CommonState.CREATE, binding.strKey.text.toString(), ProtectedString(binding.cb.isChecked, binding.strValue.text.toString()), position ) ) } } dismiss() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/CreateDbActivity.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create import android.os.Bundle import android.text.TextUtils import android.view.View import android.view.ViewAnimationUtils import androidx.appcompat.widget.Toolbar import androidx.core.app.ActivityOptionsCompat import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.transition.Transition import androidx.transition.TransitionInflater import com.alibaba.android.arouter.facade.annotation.Route import com.arialyy.frame.router.Routerfit import com.blankj.utilcode.util.ToastUtils import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseActivity import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.databinding.ActivityCreateDbBinding import com.lyy.keepassa.router.ActivityRouter import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.KpaUtil import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import timber.log.Timber /** * 创建见数据库页面 */ @Route(path = "/launcher/createDb") class CreateDbActivity : BaseActivity(), View.OnClickListener { private var curSetup = 1 private lateinit var firstFragment: CreateDbFirstFragment private var secondFragment: CreateDbSecondFragment? = null private lateinit var module: CreateDbModule override fun initData(savedInstanceState: Bundle?) { super.initData(savedInstanceState) module = ViewModelProvider(this).get(CreateDbModule::class.java) toolbar.setTitle(R.string.create_db) binding.next.setOnClickListener(this) binding.up.setOnClickListener(this) firstFragment = CreateDbFirstFragment() val transaction = supportFragmentManager.beginTransaction() transaction.replace(R.id.content, firstFragment) transaction.commitNow() listenerOpenDb() } private fun listenerOpenDb() { lifecycleScope.launch { KpaUtil.kdbOpenService.openDbFlow.collectLatest { if (it == null) { ToastUtils.showShort("${getString(R.string.open_db)}${getString(R.string.fail)}") return@collectLatest } Timber.d("创建数据库成功") HitUtil.toaskShort(getString(R.string.hint_db_create_success, module.dbName)) Routerfit.create(ActivityRouter::class.java, this@CreateDbActivity).toMainActivity( opt = ActivityOptionsCompat.makeSceneTransitionAnimation(this@CreateDbActivity) ) KeepassAUtil.instance.saveLastOpenDbHistory(BaseApp.dbRecord) finishAfterTransition() } } } /** * 右 -> 左 */ private fun getRlAnim(): Transition { return TransitionInflater.from(this) .inflateTransition(R.transition.slide_enter) } /** * 左 -> 右 */ private fun getLrAnim(): Transition { return TransitionInflater.from(this) .inflateTransition(R.transition.slide_exit) } override fun setLayoutId(): Int { return R.layout.activity_create_db } override fun onBackPressed() { if (curSetup == 2) { upFragment() } else { finishAfterTransition() } } override fun onClick(v: View?) { if (KeepassAUtil.instance.isFastClick()) { return } when (v!!.id) { R.id.next -> { if (curSetup == 1) { // startNextFragment() firstFragment.startNext() } else { // 完成 done() } } R.id.up -> upFragment() } } /** * 完成信息输入,并创建数据库 */ private fun done() { // 密码需要重新获取,将密码设置到module中 secondFragment?.getPass() module.createAndOpenDb() } /** * 开始设置密码 */ fun startNextFragment() { if (TextUtils.isEmpty(firstFragment.getDbName())) { firstFragment.handleDbNameNull() return } else if (module.localDbUri == null) { firstFragment.showSaveTypeDialog() return } curSetup = 2 binding.next.setText(R.string.done) binding.up.visibility = View.VISIBLE if (secondFragment == null) { secondFragment = CreateDbSecondFragment() } /* * 重新设置动画: * fragment1 (进入)左 -> 右;(退出)左 -> 右 * fragment2 (进入)右 -> 左;(退出)左 -> 右 */ firstFragment.exitTransition = getLrAnim() secondFragment!!.enterTransition = getRlAnim() val changeBoundsTransition = TransitionInflater.from(this) // .inflateTransition(R.transition.changebounds_with_arcmotion) .inflateTransition(android.R.transition.move) secondFragment!!.sharedElementEnterTransition = changeBoundsTransition supportFragmentManager.beginTransaction() .replace(R.id.content, secondFragment!!) .addSharedElement(firstFragment.getShareElement(), getString(R.string.transition_db_name)) .commit() // changeBg(true) } /** * 返回设置数据库路径 */ private fun upFragment() { curSetup = 1 binding.next.setText(R.string.next) binding.up.visibility = View.GONE /* * 重新设置动画: * fragment1 (进入)左 -> 右;(退出)右 -> 左 * fragment2 (进入)右 -> 左;(退出)右 -> 左 */ firstFragment.enterTransition = getLrAnim() firstFragment.exitTransition = getRlAnim() secondFragment!!.exitTransition = getRlAnim() val changeBoundsTransition = TransitionInflater.from(this) // .inflateTransition(R.transition.changebounds_with_arcmotion) .inflateTransition(android.R.transition.move) firstFragment.sharedElementEnterTransition = changeBoundsTransition supportFragmentManager.beginTransaction() .replace(R.id.content, firstFragment) .addSharedElement( secondFragment!!.getShareElement(), getString(R.string.transition_db_name) ) .commitNow() // changeBg(false) } /** * 切换fragment改变背景 */ private fun changeBg(toSecondFragment: Boolean) { val view = findViewById(R.id.kpa_toolbar) val finalRadius = view.width.coerceAtLeast(view.height) val anim = ViewAnimationUtils.createCircularReveal( view, if (toSecondFragment) view.right else 0, 0, 0f, finalRadius.toFloat() ) view.setBackgroundResource( if (toSecondFragment) R.color.colorPrimary else R.color.white ) anim.duration = resources.getInteger(R.integer.anim_duration_long) .toLong() // anim.interpolator = AccelerateInterpolator() view.visibility = View.VISIBLE anim.start() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/CreateDbFirstFragment.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create import android.content.Intent import android.text.TextUtils import android.view.View import android.view.inputmethod.EditorInfo import androidx.collection.arrayMapOf import androidx.lifecycle.ViewModelProvider import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.ResUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseFragment import com.lyy.keepassa.databinding.FragmentCreateDbFirstBinding import com.lyy.keepassa.event.DbPathEvent import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.cloud.DbSynUtil import com.lyy.keepassa.view.StorageType import com.lyy.keepassa.view.StorageType.AFS import com.lyy.keepassa.view.StorageType.DROPBOX import com.lyy.keepassa.view.StorageType.ONE_DRIVE import com.lyy.keepassa.view.StorageType.UNKNOWN import com.lyy.keepassa.view.StorageType.WEBDAV import com.lyy.keepassa.view.create.auth.AuthFlowFactory import com.lyy.keepassa.view.create.auth.IAuthCallback import com.lyy.keepassa.view.create.auth.IAuthFlow import com.lyy.keepassa.view.create.auth.OnNextFinishCallback import com.lyy.keepassa.widget.BubbleTextView import com.lyy.keepassa.widget.BubbleTextView.OnIconClickListener import timber.log.Timber /** * 创建数据库的第一步 * 1、设置数据库保存类型 * 2、设置数据库名 */ class CreateDbFirstFragment : BaseFragment() { private lateinit var module: CreateDbModule private lateinit var pathTypeDialog: PathTypeDialog private var authFlow: IAuthFlow? = null private var isAuthorized: Boolean = false private val flowMap = arrayMapOf() override fun initData() { module = ViewModelProvider(requireActivity()).get(CreateDbModule::class.java) initView() } private fun initView() { setPathTypeInfo() showSaveTypeDialog() binding.pathType.setOnIconClickListener(object : OnIconClickListener { override fun onClick( view: BubbleTextView, index: Int ) { if (index == 2) { Routerfit.create(DialogRouter::class.java) .showMsgDialog( msgContent = ResUtil.getString(R.string.help_create_db_path), showCancelBt = false ) } } }) // 设置键盘确定按钮属性 binding.dbName.setOnEditorActionListener { _, actionId, _ -> if (!isAdded) { return@setOnEditorActionListener false } // actionId 和android:imeOptions 属性要保持一致 if (actionId == EditorInfo.IME_ACTION_DONE && !TextUtils.isEmpty(binding.dbName.text)) { KeepassAUtil.instance.toggleKeyBord(requireContext()) // showPathDialog() startNext() true } else { false } } } /** * 处理数据库名没有设置的情况 */ fun handleDbNameNull() { val hint = getString(R.string.error_db_name_null) binding.dbNameLayout.error = hint binding.dbName.requestFocus() HitUtil.toaskShort(hint) KeepassAUtil.instance.toggleKeyBord(requireContext()) } /** * 和其它fragment共享的元素 */ fun getShareElement(): View { return binding.dbName } /** * 获取数据库名 */ fun getDbName(): String { module.dbName = binding.dbName.text.toString() .trim() return module.dbName } fun showSaveTypeDialog() { pathTypeDialog = PathTypeDialog( binding.dbName.text.toString() .trim() ) pathTypeDialog.showNow(childFragmentManager, "PathDialog") pathTypeDialog.setOnDismissListener { if (module.storageType == UNKNOWN) { requireActivity().finishAfterTransition() return@setOnDismissListener } setPathTypeInfo() authFlow = flowMap[module.storageType] if (authFlow == null) { authFlow = AuthFlowFactory.getAuthFlow(module.storageType) flowMap[module.storageType] = authFlow lifecycle.addObserver(authFlow!!) } authFlow?.let { it.initContent(requireContext(), object : IAuthCallback { override fun callback(success: Boolean) { isAuthorized = success binding.dbName.requestFocus() } }) it.startFlow() } } } /** * 流程结束 */ private fun finishFlow(event: DbPathEvent) { if (event.fileUri == null && event.storageType == AFS) { Timber.e("uri 获取失败") return } // 直接启动下一界面 val startNextFragment = true when (event.storageType) { AFS -> { binding.dbNameLayout.visibility = View.VISIBLE module.localDbUri = event.fileUri!! module.dbName = event.dbName } DROPBOX -> { binding.dbNameLayout.visibility = View.VISIBLE module.localDbUri = DbSynUtil.getCloudDbTempPath(DROPBOX.name, event.dbName) module.cloudPath = event.cloudDiskPath!! module.dbName = event.dbName } WEBDAV -> { binding.dbNameLayout.visibility = View.GONE module.dbName = event.dbName module.localDbUri = DbSynUtil.getCloudDbTempPath(WEBDAV.name, event.dbName) module.cloudPath = event.cloudDiskPath!! } ONE_DRIVE -> { binding.dbNameLayout.visibility = View.VISIBLE module.localDbUri = DbSynUtil.getCloudDbTempPath(ONE_DRIVE.name, event.dbName) module.cloudPath = event.cloudDiskPath!! module.dbName = event.dbName } else -> { throw IllegalArgumentException("不支持的类型: ${event.storageType.lable}") } } binding.dbName.setText(module.dbName) setPathTypeInfo() if (startNextFragment) { (activity as CreateDbActivity).startNextFragment() } } /** * 检查是否可以进入下一步 */ fun startNext(): Boolean { val temp = binding.dbName.text.toString() .trim() if (TextUtils.isEmpty(temp)) { HitUtil.toaskShort(getString(R.string.error_db_name_null)) return false } authFlow?.doNext(this, temp, object : OnNextFinishCallback { override fun onFinish(event: DbPathEvent) { finishFlow(event) } }) return true } /** * 设置文件路径类型提示 */ private fun setPathTypeInfo() { binding.pathType.text = module.storageType.lable binding.pathType.setLeftIcon(module.storageType.icon) setDbNameHint(module.storageType) } /** * 设置数据名输入提示 */ private fun setDbNameHint(storageType: StorageType) { binding.dbNameLayout.helperText = getString(R.string.help_create_db) binding.dbNameLayout.hint = getString(R.string.db_name) } override fun setLayoutId(): Int { return R.layout.fragment_create_db_first } override fun onActivityResult( requestCode: Int, resultCode: Int, data: Intent? ) { super.onActivityResult(requestCode, resultCode, data) authFlow?.onActivityResult(requestCode, resultCode, data) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/CreateDbModule.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create import android.content.Context import android.net.Uri import androidx.core.app.ActivityOptionsCompat import androidx.fragment.app.FragmentActivity import androidx.lifecycle.liveData import com.arialyy.frame.router.Routerfit import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.BaseModule import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.router.ActivityRouter import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.NotificationUtil import com.lyy.keepassa.view.StorageType import com.lyy.keepassa.view.StorageType.UNKNOWN import timber.log.Timber class CreateDbModule : BaseModule() { /** * 设置的数据库密码 */ var dbPass: String = "" /** * 数据库名,包含.kdbx */ var dbName: String = "" /** * 数据库uri */ var localDbUri: Uri? = null /** * key uri */ var keyUri: Uri? = null /** * key 的名字 */ var keyName: String = "" /** * 数据库类型 */ var storageType: StorageType = UNKNOWN /** * 云盘路径 */ var cloudPath: String = "" /** * 创建并打开数据库 */ fun createAndOpenDb() { KpaUtil.kdbOpenService.createDb(dbName, localDbUri, dbPass, keyUri, cloudPath, storageType) } /** * 数据库打开方式 */ fun getDbOpenTypeData(context: Context) = liveData { val titles = context.resources.getStringArray(R.array.cloud_names) val icons = context.resources.obtainTypedArray(R.array.path_type_img) val items = ArrayList() for ((index, title) in titles.withIndex()) { val item = SimpleItemEntity() item.title = title item.subTitle = titles[index] item.id = index item.icon = icons.getResourceId(index, 0) items.add(item) } icons.recycle() emit(items) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/CreateDbSecondFragment.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.annotation.SuppressLint import android.text.InputType import android.text.TextUtils import android.view.View import android.view.animation.LinearInterpolator import android.widget.RadioButton import androidx.lifecycle.ViewModelProvider import com.arialyy.frame.router.Routerfit import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseFragment import com.lyy.keepassa.databinding.FragmentCreateDbSecondBinding import com.lyy.keepassa.event.KeyPathEvent import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.EventBusHelper import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.widget.BubbleTextView import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode.MAIN /** * 设置密码、密钥等信息 */ class CreateDbSecondFragment : BaseFragment(), BubbleTextView.OnIconClickListener { private var keyPassLayoutH: Int = 0 private var isShowPass = false private lateinit var module: CreateDbModule override fun setLayoutId(): Int { return R.layout.fragment_create_db_second } @SuppressLint("RestrictedApi") override fun initData() { EventBusHelper.reg(this) module = ViewModelProvider(requireActivity()).get(CreateDbModule::class.java) binding.dbName.setText(module.dbName) val leftDrawable = resources.getDrawable(module.storageType.icon, requireContext().theme) val iconSize = resources.getDimension(R.dimen.icon_size) leftDrawable.setBounds(0, 0, iconSize.toInt(), iconSize.toInt()) binding.dbHint.setCompoundDrawables(leftDrawable, null, null, null) binding.encryptGroup.setOnCheckedChangeListener { group, checkedId -> val rb = group.findViewById(checkedId) if (rb.tag == "1") { // binding.passKeyLayout.visibility = View.GONE hintPassLayout() } else { // binding.passKeyLayout.visibility = View.VISIBLE showPassLayout() } } binding.encryptType.setOnIconClickListener(this) binding.passKey.setOnIconClickListener(this) (binding.encryptGroup.getChildAt(0) as RadioButton).isChecked = true binding.passKeyLayout.post { keyPassLayoutH = binding.passKeyLayout.height } binding.chooseBt.setOnClickListener { val dialog = CreatePassKeyDialog() dialog.show(childFragmentManager, "passKeyDialog") } KeepassAUtil.instance.toggleKeyBord(requireContext()) binding.password.requestFocus() binding.passwordLayout.endIconDrawable = resources.getDrawable(R.drawable.ic_view_off) // binding.password.imeOptions = EditorInfo.IME_ACTION_NEXT binding.passwordLayout.setEndIconOnClickListener { isShowPass = !isShowPass if (isShowPass) { binding.passwordLayout.endIconDrawable = resources.getDrawable(R.drawable.ic_view) binding.enterPasswordLayout.visibility = View.GONE binding.password.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD // 重新修改确认按钮 // binding.password.imeOptions = EditorInfo.IME_ACTION_NEXT } else { binding.passwordLayout.endIconDrawable = resources.getDrawable(R.drawable.ic_view_off) binding.enterPasswordLayout.visibility = View.VISIBLE binding.password.setRawInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT) // 重新修改确认按钮 // binding.password.imeOptions = EditorInfo.IME_ACTION_DONE } // 将光标移动到最后 binding.password.setSelection(binding.password.text!!.length) binding.password.requestFocus() } } private fun showPassLayout() { val h = resources.getDimension(R.dimen.create_pass_key_h) .toInt() binding.passKeyLayoutWrap.visibility = View.VISIBLE binding.passKeyLayout.layoutParams.height = 0 binding.passKeyLayout.visibility = View.VISIBLE val anim = ValueAnimator.ofInt(0, h) anim.addUpdateListener { animation -> binding.passKeyLayout.layoutParams.height = animation.animatedValue as Int binding.passKeyLayout.requestLayout() } anim.interpolator = LinearInterpolator() anim.duration = 400 anim.start() } private fun hintPassLayout() { val h = resources.getDimension(R.dimen.create_pass_key_h) .toInt() binding.passKeyLayoutWrap.visibility = View.VISIBLE binding.passKeyLayout.layoutParams.height = 0 binding.passKeyLayout.visibility = View.VISIBLE module.keyUri = null val anim = ValueAnimator.ofInt(h, 0) anim.addUpdateListener { animation -> binding.passKeyLayout.layoutParams.height = animation.animatedValue as Int binding.passKeyLayout.requestLayout() } anim.interpolator = LinearInterpolator() anim.duration = 400 anim.start() anim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) binding.passKeyLayout.visibility = View.GONE binding.passKeyLayoutWrap.visibility = View.GONE } }) } /** * 获取密码,如果两次密码不一致,返回null */ fun getPass(): String? { val pass = binding.password.text.toString() .trim() val enterPass = binding.enterPassword.text.toString() .trim() if (TextUtils.isEmpty(pass)) { HitUtil.toaskShort(getString(R.string.error_pass_null)) return null } // 如果没有显示密码,需要判断两次输入的密码是否一致 if (!isShowPass) { if (TextUtils.isEmpty(enterPass)) { HitUtil.toaskShort(getString(R.string.error_enter_pass_null)) binding.enterPassword.requestFocus() KeepassAUtil.instance.toggleKeyBord(requireContext()) return null } if (!pass.equals(enterPass, false)) { HitUtil.toaskShort(getString(R.string.error_pass_unfit)) return null } } module.dbPass = pass return module.dbPass } /** * 获取key的路径 */ @Subscribe(threadMode = MAIN) fun onKeyEvent(event: KeyPathEvent) { module.keyUri = event.keyUri module.keyName = event.keyName binding.passKeyName.setText(module.keyName) } fun getShareElement(): View { return binding.dbName } override fun onClick( view: BubbleTextView, index: Int ) { var msg = "" when (view.id) { R.id.encrypt_type -> { msg = getString(R.string.help_pass_type) } R.id.pass_key -> { msg = getString(R.string.help_pass_key) } } Routerfit.create(DialogRouter::class.java).showMsgDialog(msgContent = msg, showCancelBt = false) } override fun onDestroy() { super.onDestroy() EventBusHelper.unReg(this) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/CreateGroupDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create import android.view.View import androidx.lifecycle.ViewModelProvider import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.keepassdroid.database.PwGroupV4 import com.keepassdroid.database.PwIconCustom import com.keepassdroid.database.PwIconStandard import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogAddGroupBinding import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.IconUtil import com.lyy.keepassa.view.create.entry.CreateEntryModule import com.lyy.keepassa.view.icon.IconBottomSheetDialog import com.lyy.keepassa.view.icon.IconItemCallback /** * 创建或编辑群组dialog */ @Route(path = "/dialog/createGroup") class CreateGroupDialog : BaseDialog(), View.OnClickListener { private var icon = PwIconStandard(48) private var csIcon: PwIconCustom? = null private lateinit var module: CreateEntryModule @Autowired(name = "parentGroup") @JvmField var parentGroup: PwGroupV4 = BaseApp.KDB!!.pm.rootGroup as PwGroupV4 override fun setLayoutId(): Int { return R.layout.dialog_add_group } override fun initData() { super.initData() ARouter.getInstance().inject(this) module = ViewModelProvider(this).get(CreateEntryModule::class.java) binding.groupNameLayout.setEndIconOnClickListener { showIconDialog() } binding.enter.setOnClickListener(this) binding.cancel.setOnClickListener(this) } private fun showIconDialog() { val iconDialog = IconBottomSheetDialog() iconDialog.setCallback(object : IconItemCallback { override fun onDefaultIcon(defIcon: PwIconStandard) { icon = defIcon binding.groupNameLayout.endIconDrawable = resources.getDrawable(IconUtil.getIconById(icon.iconId), requireContext().theme) csIcon = PwIconCustom.ZERO } override fun onCustomIcon(customIcon: PwIconCustom) { csIcon = customIcon binding.groupNameLayout.endIconDrawable = IconUtil.convertCustomIcon2Drawable(requireContext(), csIcon!!) } }) iconDialog.show(childFragmentManager, IconBottomSheetDialog::class.java.simpleName) } override fun onClick(v: View?) { when (v!!.id) { R.id.enter -> { val title = binding.groupName.text.toString() .trim() if (title.isEmpty()) { HitUtil.toaskShort(getString(R.string.error_group_name_null)) return } if (title.length > 16) { HitUtil.toaskShort(requireContext().getString(R.string.title_too_long)) return } createGroup() } R.id.cancel -> { dismiss() } } } /** * 创建群组 */ private fun createGroup() { module.createGroup( binding.groupName.text.toString(), parentGroup, icon, csIcon ) { HitUtil.toaskShort( "${BaseApp.APP.getString(R.string.create_group)}${ BaseApp.APP.getString( R.string.success ) }" ) dismiss() } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/CreatePassKeyDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.View import android.widget.Toast import com.arialyy.frame.util.StringUtil import com.google.android.material.bottomsheet.BottomSheetBehavior import com.keepassdroid.utils.UriUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseBottomSheetDialogFragment import com.lyy.keepassa.databinding.DialogPassKeyBinding import com.lyy.keepassa.event.KeyPathEvent import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.PasswordBuildUtil import com.lyy.keepassa.util.takePermission import org.greenrobot.eventbus.EventBus import timber.log.Timber import java.io.FileOutputStream import java.io.IOException /** * 创建key对话框 */ class CreatePassKeyDialog : BaseBottomSheetDialogFragment(), View.OnClickListener { private lateinit var behavior: BottomSheetBehavior<*> private val openFileReqCode = 0xB1 private val createFileReqCode = 0xB2 override fun setLayoutId(): Int { return R.layout.dialog_pass_key } override fun init(savedInstanceState: Bundle?) { super.init(savedInstanceState) behavior = BottomSheetBehavior.from(binding.content) binding.close.setOnClickListener(this) binding.item1.setOnClickListener(this) binding.item2.setOnClickListener(this) behavior.state = BottomSheetBehavior.STATE_EXPANDED } override fun onClick(v: View?) { when (v!!.id) { R.id.close -> dismiss() R.id.item_1 -> { KeepassAUtil.instance.openSysFileManager(this, "*/*", openFileReqCode) } R.id.item_2 -> { KeepassAUtil.instance.createFile( this, "*/*", "${getString(R.string.app_name)}.passkey", createFileReqCode ) } } } /** * 将一个随机字符串写入密钥文件中 */ private fun writeData(uri: Uri?) { val fos = requireContext().contentResolver.openOutputStream(uri!!) as FileOutputStream try { val str = PasswordBuildUtil.getInstance() .addLowerChar() .addNumChar() .addMinus() .addSymbolChar() .builder(128) fos.write( StringUtil.keyToHashKey(str) .toByteArray() ) fos.flush() } catch (e: IOException) { Timber.e(e) } finally { fos.close() } } override fun onActivityResult( requestCode: Int, resultCode: Int, data: Intent? ) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK && data != null && data.data != null) { // 申请长期的uri权限 data.data?.takePermission() when (requestCode) { openFileReqCode -> { EventBus.getDefault() .post( KeyPathEvent( keyName = UriUtil.getFileNameFromUri(requireContext(), data.data), keyUri = data.data!! ) ) } createFileReqCode -> { writeData(data.data) Toast.makeText( context, getString( R.string.create_pass_key_success, UriUtil.getFileNameFromUri(requireContext(), data.data!!) ), Toast.LENGTH_SHORT ) .show() EventBus.getDefault() .post( KeyPathEvent( keyName = UriUtil.getFileNameFromUri(requireContext(), data.data), keyUri = data.data!! ) ) } else -> { Timber.e("未知请求码:$requestCode") } } dismiss() } else { HitUtil.toaskShort("${getString(R.string.invalid)} ${getString(R.string.key)}") Timber.e("选择密钥文件失败,data为空") } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/GeneratePassActivity.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create import android.app.Activity import android.content.Intent import android.os.Bundle import android.text.TextUtils import android.widget.CompoundButton import android.widget.CompoundButton.OnCheckedChangeListener import androidx.core.widget.doAfterTextChanged import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider.OnSliderTouchListener import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseActivity import com.lyy.keepassa.databinding.ActivityGeneratePassNewBinding import com.lyy.keepassa.util.ClipboardUtil import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.PasswordBuildUtil import com.lyy.keepassa.util.doClick /** * 密码生成器 */ class GeneratePassActivity : BaseActivity(), OnCheckedChangeListener { private lateinit var generater: PasswordBuildUtil private var passLen = 16 private var isUserInputPass = false companion object { const val DATA_PASS_WORD = "DATA_PASS_WORD" } override fun setLayoutId(): Int { return R.layout.activity_generate_pass_new } override fun initData(savedInstanceState: Bundle?) { super.initData(savedInstanceState) toolbar.title = getString(R.string.pass_generater) // binding.cancel.setOnClickListener { // finishAfterTransition() // } binding.edPassLen.setText("$passLen") binding.slider.addOnSliderTouchListener(object : OnSliderTouchListener { override fun onStartTrackingTouch(slider: Slider) { } override fun onStopTrackingTouch(slider: Slider) { isUserInputPass = false passLen = slider.value.toInt() binding.edPassLen.setText("$passLen") } }) generater = PasswordBuildUtil.getInstance() binding.scUAZ.setOnCheckedChangeListener(this) binding.scLAZ.setOnCheckedChangeListener(this) binding.scNum.setOnCheckedChangeListener(this) binding.scCh.setOnCheckedChangeListener(this) binding.scBracketChar.setOnCheckedChangeListener(this) binding.scSpace.setOnCheckedChangeListener(this) binding.scUAZ.isChecked = true binding.scLAZ.isChecked = true binding.scNum.isChecked = true binding.scCh.isChecked = true generatePass(passLen) binding.ivRefresh.doClick { if (checkParamsIsInvalid()) { HitUtil.toaskShort(getString(R.string.error_genera_params)) return@doClick } generatePass(passLen) } binding.ivCopy.doClick { if (checkParamsIsInvalid()) { HitUtil.toaskShort(getString(R.string.error_genera_params)) return@doClick } ClipboardUtil.get() .copyDataToClip(binding.edPass.text.toString()) } binding.edPassLen.doAfterTextChanged { text -> if (!TextUtils.isEmpty(text)) { passLen = text.toString() .toInt() generatePass(passLen) } } binding.slider.setLabelFormatter { "${it.toInt()}" } } override fun finishAfterTransition() { val intent = Intent() intent.putExtra(DATA_PASS_WORD, binding.edPass.text.toString().trim()) setResult(Activity.RESULT_OK, intent) super.finishAfterTransition() } /** * 检查密码生成条件 * @return true: 条件无效 */ private fun checkParamsIsInvalid(): Boolean { return !binding.scUAZ.isChecked && !binding.scLAZ.isChecked && !binding.scNum.isChecked && !binding.scCh.isChecked && !binding.scBracketChar.isChecked && !binding.scSpace.isChecked } /** * 生产密码 * @param len 密码长度 */ private fun generatePass(len: Int): String { if (checkParamsIsInvalid()) { binding.edPass.setText("") return "" } generater.clear() if (binding.scUAZ.isChecked) { generater.addUpChar() } if (binding.scLAZ.isChecked) { generater.addLowerChar() } if (binding.scNum.isChecked) { generater.addNumChar() } if (binding.scCh.isChecked) { generater.addMinus() generater.addUnderline() generater.addSymbolChar() } if (binding.scSpace.isChecked) { generater.addSpaceChar() } if (binding.scBracketChar.isChecked) { generater.addBracketChar() } val pass = generater.builder(len) binding.edPass.setText(pass) return pass } override fun onCheckedChanged( buttonView: CompoundButton?, isChecked: Boolean ) { generatePass(passLen) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/PathTypeDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create import android.os.Bundle import android.view.View import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.arialyy.frame.util.adapter.RvItemClickSupport import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseBottomSheetDialogFragment import com.lyy.keepassa.databinding.DialogPathTypeBinding import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.view.StorageType.AFS import com.lyy.keepassa.view.StorageType.DROPBOX import com.lyy.keepassa.view.StorageType.ONE_DRIVE import com.lyy.keepassa.view.StorageType.WEBDAV import com.lyy.keepassa.view.SimpleAdapter /** * 数据库路径选择 * @param dbName 如果是webdav,该字段为文件的http url */ class PathTypeDialog( private val dbName: String ) : BaseBottomSheetDialogFragment(), View.OnClickListener { private lateinit var module: CreateDbModule override fun setLayoutId(): Int { return R.layout.dialog_path_type } override fun init(savedInstanceState: Bundle?) { super.init(savedInstanceState) module = ViewModelProvider(requireActivity()) .get(CreateDbModule::class.java) val data: ArrayList = ArrayList() val adapter = SimpleAdapter(requireContext(), data) binding.list.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) binding.list.adapter = adapter binding.content.post { module.getDbOpenTypeData(requireContext()) .observe(this, Observer { items -> data.addAll(items) adapter.notifyDataSetChanged() }) } mRootView.setOnClickListener(this) binding.close.setOnClickListener(this) RvItemClickSupport.addTo(binding.list) .setOnItemClickListener { _, position, _ -> val item = data[position] when (item.icon) { R.drawable.ic_android -> {//使用系统文件管理器 module.storageType = AFS } R.drawable.ic_dropbox -> { // dropbox module.storageType = DROPBOX } R.drawable.ic_http -> { // webDav module.storageType = WEBDAV } R.drawable.ic_onedrive -> { // onedrive module.storageType = ONE_DRIVE } } dismiss() } } override fun onClick(v: View?) { when (v!!.id) { mRootView.id, R.id.close -> dismiss() } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/auth/AFSAuthFlow.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.auth import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri import com.keepassdroid.utils.UriUtil import com.lyy.keepassa.event.DbPathEvent import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.takePermission import com.lyy.keepassa.view.StorageType.AFS import com.lyy.keepassa.view.create.CreateDbFirstFragment /** * @Author laoyuyu * @Description * @Date 2021/2/25 **/ class AFSAuthFlow : IAuthFlow { private val PATH_REQUEST_CODE = 0xA1 private var context: Context? = null private lateinit var authCallback: IAuthCallback private lateinit var nextCallback: OnNextFinishCallback private var dbUri: Uri? = null override fun initContent( context: Context, callback: IAuthCallback ) { this.context = context this.authCallback = callback } override fun startFlow() { authCallback.callback(true) } override fun onResume() { } override fun doNext( fragment: CreateDbFirstFragment, dbName: String, callback: OnNextFinishCallback ) { nextCallback = callback if (dbUri == null) { KeepassAUtil.instance.createFile( fragment, "*/*", "$dbName.kdbx", PATH_REQUEST_CODE ) return } nextCallback.onFinish( DbPathEvent( dbName = UriUtil.getFileNameFromUri(context, dbUri), fileUri = dbUri, storageType = AFS ) ) } override fun onDestroy() { } override fun onActivityResult( requestCode: Int, resultCode: Int, data: Intent? ) { dbUri = data?.data if (resultCode == Activity.RESULT_OK && requestCode == PATH_REQUEST_CODE && data != null && data.data != null && context != null ) { // 申请长期的uri权限 // 防止一个不可思议的空指针,data.data 有可能还是为空 data.data?.apply { takePermission() nextCallback.onFinish( DbPathEvent( dbName = UriUtil.getFileNameFromUri(context, this), fileUri = this, storageType = AFS ) ) } } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/auth/DropboxAuthFlow.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.auth import android.content.Context import android.content.Intent import android.text.Html import android.text.TextUtils import android.widget.Button import androidx.lifecycle.Lifecycle import androidx.lifecycle.OnLifecycleEvent import com.arialyy.frame.router.Routerfit import com.dropbox.core.android.Auth import com.lyy.keepassa.R import com.lyy.keepassa.event.DbPathEvent import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.cloud.DbSynUtil import com.lyy.keepassa.util.cloud.DropboxUtil import com.lyy.keepassa.view.StorageType.DROPBOX import com.lyy.keepassa.view.create.CreateDbFirstFragment import com.lyy.keepassa.view.dialog.OnMsgBtClickListener import timber.log.Timber /** * @Author laoyuyu * @Description default save db to root path (eg: "/") * @Date 2021/2/25 **/ class DropboxAuthFlow : IAuthFlow { private val TAG = javaClass.simpleName private lateinit var context: Context private var isNeedAuth = false private lateinit var callback: IAuthCallback override fun initContent( context: Context, callback: IAuthCallback ) { this.context = context this.callback = callback } override fun onResume() { Timber.d("onResume") if (!isNeedAuth || DropboxUtil.isAuthorized()) { return } val token = Auth.getOAuth2Token() if (!TextUtils.isEmpty(token)) { DropboxUtil.saveToken(token) HitUtil.toaskShort("dropbox ${context.getString(R.string.auth)}${context.getString(R.string.success)}") callback.callback(true) return } HitUtil.toaskShort("dropbox ${context.getString(R.string.auth)}${context.getString(R.string.fail)}") callback.callback(false) } override fun doNext( fragment: CreateDbFirstFragment, dbName: String, callback: OnNextFinishCallback ) { if (!DropboxUtil.isAuthorized()) { authDropbox() return } val name = "$dbName.kdbx" callback.onFinish( DbPathEvent( dbName = name, fileUri = DbSynUtil.getCloudDbTempPath(DROPBOX.name, name), storageType = DROPBOX, cloudDiskPath = "/$name" ) ) } override fun startFlow() { isNeedAuth = true if (!DropboxUtil.isAuthorized()) { isNeedAuth = true authDropbox() return } } /** * 选择dropbox路径 * 只有dropbox为授权才显示该对话框 */ private fun authDropbox() { Routerfit.create(DialogRouter::class.java) .showMsgDialog( msgContent = Html.fromHtml(context.getString(R.string.dropbox_msg)), showCancelBt = false, btnClickListener = object : OnMsgBtClickListener { override fun onCover(v: Button) { } override fun onEnter(v: Button) { Auth.startOAuth2Authentication(context, DropboxUtil.APP_KEY) } override fun onCancel(v: Button) { } } ) } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) override fun onDestroy() { Timber.d("onDestroy") } override fun onActivityResult( requestCode: Int, resultCode: Int, data: Intent? ) { } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/auth/IAuthFlow.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.auth import android.content.Context import android.content.Intent import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import com.lyy.keepassa.event.DbPathEvent import com.lyy.keepassa.view.StorageType import com.lyy.keepassa.view.StorageType.AFS import com.lyy.keepassa.view.StorageType.DROPBOX import com.lyy.keepassa.view.StorageType.ONE_DRIVE import com.lyy.keepassa.view.StorageType.WEBDAV import com.lyy.keepassa.view.create.CreateDbFirstFragment /** * @Author laoyuyu * @Description cloud file create * @Date 2021/2/25 **/ interface IAuthFlow : LifecycleObserver { fun initContent( context: Context, callback: IAuthCallback ) fun startFlow() @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() /** * 点下一步的处理事件 */ fun doNext( fragment: CreateDbFirstFragment, dbName: String, callback: OnNextFinishCallback ) fun onDestroy() fun onActivityResult( requestCode: Int, resultCode: Int, data: Intent? ) } /** * 验证回调 */ interface IAuthCallback { fun callback(success: Boolean) } /** * 完成选择云服务的回调 */ interface OnNextFinishCallback { fun onFinish(event: DbPathEvent) } object AuthFlowFactory { fun getAuthFlow(type: StorageType): IAuthFlow? = when (type) { DROPBOX -> DropboxAuthFlow() AFS -> AFSAuthFlow() WEBDAV -> WebDavAuthFlow() ONE_DRIVE -> OneDriveAuthFlow() else -> null } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/auth/OneDriveAuthFlow.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.auth import android.content.Context import android.content.Intent import com.arialyy.frame.router.Routerfit import com.lyy.keepassa.event.DbPathEvent import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.cloud.DbSynUtil import com.lyy.keepassa.util.cloud.OneDriveUtil import com.lyy.keepassa.view.StorageType.ONE_DRIVE import com.lyy.keepassa.view.create.CreateDbFirstFragment import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 2021/4/26 **/ class OneDriveAuthFlow : IAuthFlow { private lateinit var context: Context private lateinit var callback: IAuthCallback private var loginCallback: OneDriveUtil.OnLoginCallback? = null private var isAuthid = false private val loadingDialog by lazy { Routerfit.create(DialogRouter::class.java).getLoadingDialog() } override fun initContent( context: Context, callback: IAuthCallback ) { this.context = context this.callback = callback } override fun startFlow() { auth() } override fun onResume() { } override fun doNext( fragment: CreateDbFirstFragment, dbName: String, callback: OnNextFinishCallback ) { if (!isAuthid) { auth() return } val name = "$dbName.kdbx" callback.onFinish( DbPathEvent( dbName = name, fileUri = DbSynUtil.getCloudDbTempPath(ONE_DRIVE.name, name), storageType = ONE_DRIVE, cloudDiskPath = "/$name" ) ) } private fun auth() { if (isAuthid) { Timber.d("已经完成授权") return } loadingDialog.show() OneDriveUtil.initOneDrive { if (it) { OneDriveUtil.loadAccount() return@initOneDrive } this.callback.callback(false) loadingDialog.dismiss() } OneDriveUtil.loginCallback = object : OneDriveUtil.OnLoginCallback { override fun callback(success: Boolean) { isAuthid = success this@OneDriveAuthFlow.callback.callback(success) loadingDialog.dismiss() } } } override fun onDestroy() { loginCallback = null } override fun onActivityResult( requestCode: Int, resultCode: Int, data: Intent? ) { } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/auth/WebDavAuthFlow.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.auth import android.content.Context import android.content.Intent import android.graphics.Color import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan import android.widget.Button import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.ResUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.entity.CloudServiceInfo import com.lyy.keepassa.event.DbPathEvent import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.QuickUnLockUtil import com.lyy.keepassa.util.cloud.DbSynUtil import com.lyy.keepassa.util.cloud.WebDavUtil import com.lyy.keepassa.view.StorageType.WEBDAV import com.lyy.keepassa.view.create.CreateDbFirstFragment import com.lyy.keepassa.view.dialog.OnMsgBtClickListener import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber /** * @Author laoyuyu * @Description webdav auth flow * @Date 2021/2/25 **/ class WebDavAuthFlow : IAuthFlow { private var context: Context? = null private lateinit var callback: IAuthCallback private var webDavUri: String? = null private var nextCallback: OnNextFinishCallback? = null private var dbName: String? = null private val scope = MainScope() private var isLogin = false private val loginDialog by lazy { Routerfit.create(DialogRouter::class.java).getWebDavLoginDialog() } private val fileSelectDialog by lazy { Routerfit.create(DialogRouter::class.java).getCloudFileListDialog(WEBDAV, true) } override fun initContent( context: Context, callback: IAuthCallback ) { this.context = context this.callback = callback } override fun startFlow() { // changeWebDav(false) scope.launch { loginDialog.webDavLoginFlow.collectLatest { isLogin = it.loginSuccess if (it.loginSuccess) { fileSelectDialog.show() } } } loginDialog.show() scope.launch { fileSelectDialog.cloudFileSelectFlow.collectLatest { if (it.storageType == WEBDAV) { webDavUri = it.fileFullPath callback.callback(true) } } } } override fun onResume() { } override fun doNext( fragment: CreateDbFirstFragment, dbName: String, callback: OnNextFinishCallback ) { this.dbName = "${dbName}.kdbx" if (!isLogin) { loginDialog.show() return } if (webDavUri == null) { fileSelectDialog.show() return } nextCallback = callback scope.launch { val fileExist = withContext(Dispatchers.IO) { return@withContext WebDavUtil.fileExists("${webDavUri!!}${this@WebDavAuthFlow.dbName}") } if (fileExist) { val content = ResUtil.getString( R.string.hint_cloud_file_already_exist, this@WebDavAuthFlow.dbName!! ) val color = ResUtil.getColor(R.color.red) val ss = SpannableString(content) ss.setSpan( ForegroundColorSpan(color), 0, this@WebDavAuthFlow.dbName!!.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) Routerfit.create(DialogRouter::class.java).showMsgDialog( msgTitle = ResUtil.getString(R.string.hint), msgContent = ss, btnClickListener = object : OnMsgBtClickListener{ override fun onCover(v: Button) { } override fun onEnter(v: Button) { sendFinishEvent() } override fun onCancel(v: Button) { } } ) return@launch } sendFinishEvent() } } private fun sendFinishEvent() { if (dbName == null) { return } scope.launch { saveWebDavServiceInfo("${webDavUri!!}${dbName}", WebDavUtil.userName, WebDavUtil.password) nextCallback?.onFinish( DbPathEvent( dbName = dbName!!, storageType = WEBDAV, fileUri = DbSynUtil.getCloudDbTempPath(WEBDAV.name, dbName!!), cloudDiskPath = "${webDavUri}${dbName}" ) ) } } private suspend fun saveWebDavServiceInfo( uri: String, userName: String, pass: String ) { withContext(Dispatchers.IO) { Timber.d("开始保存webDav登陆记录,uri = $uri") val dao = BaseApp.appDatabase.cloudServiceInfoDao() var data = dao.queryServiceInfo(uri) if (data == null) { data = CloudServiceInfo( userName = QuickUnLockUtil.encryptStr(userName), password = QuickUnLockUtil.encryptStr(pass), cloudPath = uri ) dao.saveServiceInfo(data) } else { data.userName = QuickUnLockUtil.encryptStr(userName) data.password = QuickUnLockUtil.encryptStr(pass) dao.updateServiceInfo(data) } } } override fun onDestroy() { context = null } override fun onActivityResult( requestCode: Int, resultCode: Int, data: Intent? ) { } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CardListHelper.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.entry import android.animation.ObjectAnimator import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.animation.doOnEnd import androidx.recyclerview.widget.LinearLayoutManager import com.lyy.keepassa.databinding.LayoutEntryCreateStrCardBinding import com.lyy.keepassa.util.doClick /** * @Author laoyuyu * @Description * @Date 3:27 PM 2023/10/25 **/ internal class CardListHelper(val binding: LayoutEntryCreateStrCardBinding) { companion object { private val ARROW_ANIM_DURATION = 100L } private var isExpand = false fun handleList() { binding.rvList.apply { setHasFixedSize(true) layoutManager = object : LinearLayoutManager(context) { override fun canScrollVertically(): Boolean { return false } } isNestedScrollingEnabled = false } handleArrow() } private fun handleArrow() { binding.vClick.doClick { if (!isExpand) { expand() return@doClick } hind() } } private fun hind() { val anim = ObjectAnimator.ofFloat(binding.ivArrow, "rotation", 180f, 0f) anim.duration = ARROW_ANIM_DURATION anim.doOnEnd { isExpand = false binding.rvList.visibility = ConstraintLayout.GONE } anim.start() } /** * 展开列表 */ private fun expand() { val anim = ObjectAnimator.ofFloat(binding.ivArrow, "rotation", 0f, 180f) anim.duration = ARROW_ANIM_DURATION anim.doOnEnd { isExpand = true binding.rvList.visibility = ConstraintLayout.VISIBLE } anim.start() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CreateEntryActivity.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.entry import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.IntentSender import android.os.Bundle import android.text.InputType import android.view.View import android.widget.ArrayAdapter import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import androidx.core.app.ActivityOptionsCompat import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.ResUtil import com.keepassdroid.database.PwGroupId import com.keepassdroid.database.PwIconCustom import com.keepassdroid.database.PwIconStandard import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseActivity import com.lyy.keepassa.databinding.ActivityEntryEditNewBinding import com.lyy.keepassa.entity.AutoFillParam import com.lyy.keepassa.entity.CommonState.DELETE import com.lyy.keepassa.entity.GoogleOtpBean import com.lyy.keepassa.entity.KeepassBean import com.lyy.keepassa.entity.KeepassXcBean import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.entity.TagBean import com.lyy.keepassa.entity.TrayTotpBean import com.lyy.keepassa.entity.toOtpStringMap import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.IconUtil import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.doClick import com.lyy.keepassa.util.hasTOTP import com.lyy.keepassa.util.loadImg import com.lyy.keepassa.util.takePermission import com.lyy.keepassa.util.totp.OtpEnum import com.lyy.keepassa.view.create.CreateCustomStrDialog import com.lyy.keepassa.view.create.GeneratePassActivity import com.lyy.keepassa.view.create.entry.CreateEnum.CREATE import com.lyy.keepassa.view.create.entry.CreateEnum.MODIFY import com.lyy.keepassa.view.dialog.AddMoreDialog import com.lyy.keepassa.view.dialog.ChooseTagDialog import com.lyy.keepassa.view.dialog.CreateTagDialog import com.lyy.keepassa.view.dialog.TimeChangeDialog import com.lyy.keepassa.view.dialog.otp.CreateOtpModule import com.lyy.keepassa.view.dir.ChooseGroupActivity import com.lyy.keepassa.view.icon.IconBottomSheetDialog import com.lyy.keepassa.view.icon.IconItemCallback import com.lyy.keepassa.view.launcher.LauncherActivity import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import timber.log.Timber import kotlin.math.abs /** * @Author laoyuyu * @Description * @Date 7:24 PM 2023/10/13 **/ @Route(path = "/entry/create") class CreateEntryActivity : BaseActivity() { companion object { const val KEY_ENTRY = "KEY_ENTRY" /** * 类型,1:新建条目,2:利用模版新建条目,3:编辑条目 */ const val KEY_TYPE = "KEY_IS_TYPE" const val IS_SHORTCUTS = "isShortcuts" const val PARENT_GROUP_ID = "PARENT_GROUP_ID" /** * 数据库未解锁,保存数据时打开数据库,并保存 */ internal fun authAndSaveDb( context: Context, autoFillParam: AutoFillParam, ): IntentSender { val intent = Intent(context, CreateEntryActivity::class.java).also { it.putExtra(LauncherActivity.KEY_AUTO_FILL_PARAM, autoFillParam) it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } return PendingIntent.getActivity( context, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE ) .intentSender } } @Autowired(name = IS_SHORTCUTS) @JvmField var isShortcuts: Boolean = false @Autowired(name = KEY_TYPE) @JvmField var createEnum: CreateEnum = CREATE internal lateinit var module: CreateEntryModule private lateinit var createHandler: ICreateHandler private var isShowPass = false private var addMoreDialog: AddMoreDialog? = null private lateinit var addMoreData: ArrayList private val getFileLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> uri?.let { it.takePermission() module.addAttrFile(this, it) } } /** * 密码创建器 */ private val passGenerateLauncher = registerForActivityResult(object : ActivityResultContract() { override fun createIntent(context: Context, input: String?): Intent { return Intent(context, GeneratePassActivity::class.java) } override fun parseResult(resultCode: Int, intent: Intent?): String { return intent?.getStringExtra(GeneratePassActivity.DATA_PASS_WORD) ?: "" } }) { if (it.isEmpty()) { return@registerForActivityResult } binding.edPassword.setText(it) binding.tvConfirm.setText(it) } /** * 选择群组 */ private val chooseGroupLauncher = registerForActivityResult(object : ActivityResultContract() { override fun createIntent(context: Context, input: String?): Intent { return Intent(context, ChooseGroupActivity::class.java).apply { putExtra(ChooseGroupActivity.KEY_TYPE, ChooseGroupActivity.DATA_SELECT_GROUP) } } override fun parseResult(resultCode: Int, intent: Intent?): PwGroupId? { return intent?.getSerializableExtra(ChooseGroupActivity.DATA_PARENT) as PwGroupId? } }) { if (it == null) { Timber.d("pwGroupId is null") return@registerForActivityResult } module.updateEntryGroupIdAndSave(this, it) } fun launchGroupChoose() { chooseGroupLauncher.launch(null, ActivityOptionsCompat.makeSceneTransitionAnimation(this)) } override fun initData(savedInstanceState: Bundle?) { super.initData(savedInstanceState) ARouter.getInstance().inject(this) module = ViewModelProvider(this)[CreateEntryModule::class.java] createHandler = if (createEnum == MODIFY) { ModifyEntryHandler(this) } else { CreateEntryHandler(this) } createHandler.bindData() handleTopBarLayout() handlePassLayout() handleIconClick() handlerAddMore() handlerUserLayout() handleTimeLayout() handleTagLayout() handleTotpLayout() handleStr() handleAttrFile() } private fun handleAttrFile() { lifecycleScope.launch { CreateEntryModule.attrFlow.collectLatest { bean -> if (bean.state != DELETE) { module.fileCacheMap[bean.key] = bean.file binding.cardFile.isVisible = true binding.cardFile.bindData(module.fileCacheMap) return@collectLatest } module.fileCacheMap.remove(bean.key) binding.cardFile.removeItem(bean.key) } } } private fun handleStr() { lifecycleScope.launch { CreateCustomStrDialog.CustomStrFlow.collectLatest { bean -> if (bean == null) { Timber.d("attr is null") return@collectLatest } checkAddMoreBtn() if (bean.state != DELETE) { module.strCacheMap[bean.key] = bean.str binding.cardStr.isVisible = true binding.cardStr.bindDate(module.strCacheMap) return@collectLatest } module.strCacheMap.remove(bean.key) if (!module.strCacheMap.hasTOTP()) { binding.groupOtp.isVisible = false } binding.cardStr.removeItem(bean.key) } } } private fun handleTotpLayout() { fun startOtp() { binding.groupOtp.isVisible = true KdbUtil.startAutoGetOtp(module.pwEntry, binding.pbRound, binding.edOtp) } lifecycleScope.launch { CreateOtpModule.otpFlow.collectLatest { val map = when (it.first) { OtpEnum.TRAY_TOTP -> (it.second as? TrayTotpBean)?.toOtpStringMap() OtpEnum.KEEPASSXC -> (it.second as? KeepassXcBean)?.toOtpStringMap() OtpEnum.GOOGLE_OTP -> (it.second as? GoogleOtpBean)?.toOtpStringMap() OtpEnum.KEEPASS -> (it.second as? KeepassBean)?.toOtpStringMap() } map?.let { strs -> strs.forEach { kv -> module.strCacheMap[kv.key] = kv.value } startOtp() binding.cardStr.bindDate(module.strCacheMap) } checkAddMoreBtn() } } if (module.strCacheMap.hasTOTP()) { startOtp() } binding.edOtp.doClick { if (module.strCacheMap.hasTOTP()) { Routerfit.create(DialogRouter::class.java).showModifyOtpDialog(module.pwEntry.uuid) return@doClick } Routerfit.create(DialogRouter::class.java) .showCreateOtpDialog(module.pwEntry.title, module.pwEntry.username) } } private fun handlerUserLayout() { binding.edUser.threshold = 1 // 设置输入几个字符后开始出现提示 默认是2 binding.edUser.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) { binding.edUser.showDropDown() } } lifecycleScope.launch { CreateEntryModule.userNameFlow.collectLatest { if (it.isNullOrEmpty()) { return@collectLatest } binding.edUser.setAdapter( ArrayAdapter( this@CreateEntryActivity, R.layout.android_simple_dropdown_item_1line, it ) ) } } lifecycleScope.launch { module.getUserNameCache() } } private fun handleTimeLayout() { binding.edLoseTime.doClick { Routerfit.create(DialogRouter::class.java).showTimeChangeDialog() } lifecycleScope.launch { TimeChangeDialog.timeFlow.collectLatest { event -> if (event == null) { return@collectLatest } val time = "${event.year}/${event.month}/${event.dayOfMonth} ${event.hour}:${event.minute}" binding.edLoseTime.setText(time) binding.tlLoseTime.visibility = View.VISIBLE checkAddMoreBtn() } } } private fun handleTagLayout() { binding.edTag.doClick { Routerfit.create(DialogRouter::class.java).showChooseTagDialog(module.pwEntry) } lifecycleScope.launch { ChooseTagDialog.chooseTagFlow.collectLatest { tagBeanList -> val tagStrList = arrayListOf() tagBeanList.forEach { tagStrList.add(it.tag) } val tags = tagStrList.joinToString(separator = ",") binding.edTag.setText(tags) binding.tlTag.visibility = View.VISIBLE checkAddMoreBtn() } } lifecycleScope.launch { CreateTagDialog.createTagFlow.collectLatest { Routerfit.create(DialogRouter::class.java) .showChooseTagDialog(module.pwEntry, if (it.isNullOrEmpty()) null else TagBean(it, true)) } } } private fun handlerAddMore() { binding.btnAddMore.doClick { if (addMoreDialog == null) { addMoreData = module.getMoreItem(this) addMoreDialog = AddMoreDialog(addMoreData) addMoreDialog!!.setOnItemClickListener(object : AddMoreDialog.OnItemClickListener { override fun onItemClick( position: Int, item: SimpleItemEntity, view: View ) { when (item.icon) { R.drawable.ic_attr_str -> { // 自定义字段 Routerfit.create(DialogRouter::class.java).showCreateCustomDialog() } R.drawable.ic_attr_file -> { // file changeFile() } R.drawable.ic_token_grey -> { // totp Routerfit.create(DialogRouter::class.java) .showCreateOtpDialog(module.pwEntry.title, module.pwEntry.username) } R.drawable.ic_notice -> { // notice binding.tlNote.visibility = View.VISIBLE binding.tlNote.requestFocus() } R.drawable.ic_net -> { //url binding.tlUrl.visibility = View.VISIBLE binding.tlUrl.requestFocus() } R.drawable.ic_tag -> { Routerfit.create(DialogRouter::class.java).showChooseTagDialog(module.pwEntry) } R.drawable.ic_lose_time -> { Routerfit.create(DialogRouter::class.java).showTimeChangeDialog() } } checkAddMoreBtn() addMoreDialog!!.dismiss() } }) } if (binding.tlLoseTime.isVisible) { addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_lose_time }) } if (binding.cardStr.isVisible) { addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_attr_str }) } if (binding.cardFile.isVisible) { addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_attr_file }) } if (module.pwEntry.hasTOTP()) { addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_token_grey }) } if (binding.tlTag.isVisible) { addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_tag }) } if (binding.tlNote.isVisible) { addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_notice }) } if (binding.tlUrl.isVisible) { addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_net }) } addMoreDialog!!.notifyData() addMoreDialog!!.show(supportFragmentManager, "add_more_dialog") } checkAddMoreBtn() } private fun checkAddMoreBtn() { if (binding.tlLoseTime.isVisible && binding.cardStr.isVisible && binding.cardFile.isVisible && module.pwEntry.hasTOTP() && binding.tlTag.isVisible && binding.tlNote.isVisible && binding.tlUrl.isVisible ) { binding.btnAddMore.visibility = View.GONE } else { binding.btnAddMore.visibility = View.VISIBLE } } fun changeFile() { getFileLauncher.launch(arrayOf("*/*")) } /** * 标题栏 */ private fun handleTopBarLayout() { binding.topAppBar.title = createHandler.getTitle() toolbar = binding.topAppBar toolbar.setNavigationOnClickListener { finishAfterTransition() } toolbar.inflateMenu(R.menu.menu_entry_edit) toolbar.setOnMenuItemClickListener { item -> if (KeepassAUtil.instance.isFastClick()) { return@setOnMenuItemClickListener true } when (item.itemId) { R.id.save -> { createHandler.saveDb(module.pwEntry) } R.id.cancel -> { finishAfterTransition() } } true } binding.appBarLayout.addOnOffsetChangedListener { _, verticalOffset -> if (verticalOffset == 0) { binding.topAppBar.title = "" return@addOnOffsetChangedListener } if (abs(verticalOffset) >= binding.appBarLayout.totalScrollRange) { binding.topAppBar.title = createHandler.getTitle() return@addOnOffsetChangedListener } } } private fun handleIconClick() { binding.ivIcon.doClick { val iconDialog = IconBottomSheetDialog() iconDialog.setCallback(object : IconItemCallback { override fun onDefaultIcon(defIcon: PwIconStandard) { module.icon = defIcon binding.ivIcon.loadImg(ResUtil.getDrawable(IconUtil.getIconById(module.icon.iconId))) module.customIcon = PwIconCustom.ZERO } override fun onCustomIcon(customIcon: PwIconCustom) { module.customIcon = customIcon binding.ivIcon.loadImg( IconUtil.convertCustomIcon2Drawable( this@CreateEntryActivity, module.customIcon!! ) ) } }) iconDialog.show(supportFragmentManager, IconBottomSheetDialog::class.java.simpleName) } } /** * 处理密码 */ private fun handlePassLayout() { binding.tlPass.endIconDrawable = ResUtil.getDrawable(R.drawable.ic_view_off) binding.tlPass.setEndIconOnClickListener { isShowPass = !isShowPass if (isShowPass) { binding.tlPass.endIconDrawable = ResUtil.getDrawable(R.drawable.ic_view) binding.tlConfirm.visibility = View.GONE binding.edPassword.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD } else { binding.tlPass.endIconDrawable = ResUtil.getDrawable(R.drawable.ic_view_off) binding.tlConfirm.visibility = View.VISIBLE binding.edPassword.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT } // 将光标移动到最后 binding.edPassword.setSelection(binding.edPassword.text?.length ?: 0) binding.edPassword.requestFocus() } binding.ivGeneratePw.setOnClickListener { passGenerateLauncher.launch(null, ActivityOptionsCompat.makeSceneTransitionAnimation(this)) } } override fun setLayoutId(): Int { return R.layout.activity_entry_edit_new } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CreateEntryHandler.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.entry import android.view.View import androidx.core.view.isVisible import com.arialyy.frame.util.ResUtil import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroupId import com.keepassdroid.database.PwGroupV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp /** * @Author laoyuyu * @Description * @Date 7:34 PM 2023/10/13 **/ internal class CreateEntryHandler(val context: CreateEntryActivity) : ICreateHandler { override fun bindData() { val groupId = context.intent.getSerializableExtra(CreateEntryActivity.PARENT_GROUP_ID) as? PwGroupId val binding = context.binding val group = (if (groupId != null) BaseApp.KDB.pm.groups[groupId] else BaseApp.KDB.pm.rootGroup) as PwGroupV4 val entry = PwEntryV4(group, true, true) context.module.pwEntry = entry context.module.initCache() binding.cardStr.visibility = View.GONE binding.cardFile.visibility = View.GONE binding.tlLoseTime.visibility = View.GONE binding.tlUrl.visibility = View.GONE binding.tlNote.visibility = View.GONE binding.tlTag.visibility = View.GONE binding.groupOtp.isVisible = false } override fun getTitle(): String { return ResUtil.getString(R.string.create_entry) } override fun saveDb(pwEntryV4: PwEntryV4) { checkAttr(context, pwEntryV4) context.launchGroupChoose() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CreateEntryModule.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.entry import KDBAutoFillRepository import android.content.Context import android.graphics.Bitmap.CompressFormat.PNG import android.net.Uri import android.text.TextUtils import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewModelScope import com.arialyy.frame.util.ResUtil import com.keepassdroid.database.PwDatabaseV4 import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroupId import com.keepassdroid.database.PwGroupV4 import com.keepassdroid.database.PwIconCustom import com.keepassdroid.database.PwIconStandard import com.keepassdroid.database.security.ProtectedBinary import com.keepassdroid.database.security.ProtectedString import com.keepassdroid.utils.UriUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.BaseModule import com.lyy.keepassa.entity.AutoFillParam import com.lyy.keepassa.entity.CommonState.CREATE import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.entity.TagBean import com.lyy.keepassa.event.AttrFileEvent import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.IconUtil import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.getFileInfo import com.lyy.keepassa.util.getRealUserName import com.lyy.keepassa.util.hasNote import com.lyy.keepassa.util.hasTOTP import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.io.ByteArrayOutputStream import java.util.UUID /** * 创建条目、群组的module */ class CreateEntryModule : BaseModule() { companion object { val attrFlow = MutableSharedFlow(0) val userNameFlow = MutableStateFlow?>(null) private val userNameCache = arrayListOf() } /** * 已经选中的标签 */ var selectedTagBeanCache = mutableListOf() var customIcon: PwIconCustom? = null var icon = PwIconStandard(0) var autoFillParam: AutoFillParam? = null var strCacheMap = hashMapOf() var fileCacheMap = hashMapOf() lateinit var pwEntry: PwEntryV4 fun updateEntryGroupIdAndSave(context: CreateEntryActivity, groupId: PwGroupId) { viewModelScope.launch { KpaUtil.kdbHandlerService.createEntry(pwEntry) KpaUtil.kdbHandlerService.saveOnly(true) { context.finishAfterTransition() } } } fun initCache() { pwEntry.strings.forEach { strCacheMap[it.key] = it.value } pwEntry.binaries.forEach { fileCacheMap[it.key] = it.value } customIcon = pwEntry.customIcon ?: PwIconCustom.ZERO icon = pwEntry.icon } fun cacheTag(tagList: List) { selectedTagBeanCache.clear() selectedTagBeanCache.addAll(tagList.filter { it.isSet }.map { it.tag }) } /** * 添加附件 */ fun addAttrFile(context: CreateEntryActivity, uri: Uri?) { val rootView = context.rootView if (uri == null) { Timber.e("附件uri为空") HitUtil.snackShort( rootView, "${ResUtil.getString(R.string.add_attr_file)}${ResUtil.getString(R.string.fail)}" ) return } val fileInfo = uri.getFileInfo(context) if (TextUtils.isEmpty(fileInfo.first) || fileInfo.second == null) { Timber.e("获取文件名失败") HitUtil.snackShort( rootView, "${ResUtil.getString(R.string.add_attr_file)}${ResUtil.getString(R.string.fail)}" ) return } val fileName = fileInfo.first!! val fileSize = fileInfo.second!! if (fileSize >= 1024 * 1024 * 10) { HitUtil.snackShort(rootView, ResUtil.getString(R.string.error_attr_file_too_large)) return } val pbf = ProtectedBinary( false, UriUtil.getUriInputStream(context, uri) .readBytes() ) (BaseApp.KDB.pm as PwDatabaseV4).binPool.poolAdd(pbf) context.lifecycleScope.launch { attrFlow.emit(AttrFileEvent(CREATE, fileName, pbf)) } } /** * Traverse database and get all userName */ suspend fun getUserNameCache() { if (userNameCache.isNotEmpty()) { userNameFlow.emit(userNameCache) return } val temp = hashSetOf() withContext(Dispatchers.IO) { for (map in BaseApp.KDB.pm.entries) { if (map.value.username.isNullOrEmpty()) { continue } temp.add(map.value.getRealUserName()) } } userNameCache.addAll(temp) userNameFlow.emit(userNameCache) } /** * 自动填充进行保存数据时,搜索条目信息,如果条目不存在,新建条目 */ fun getEntryFromAutoFillSave( context: Context, apkPkgName: String, userName: String?, pass: String? ): PwEntryV4 { val listStorage = ArrayList() KdbUtil.searchEntriesByPackageName(apkPkgName, listStorage) val entry: PwEntryV4 if (listStorage.isEmpty()) { entry = PwEntryV4(BaseApp.KDB.pm.rootGroup as PwGroupV4) val icon = IconUtil.getAppIcon(context, apkPkgName) if (icon != null) { val baos = ByteArrayOutputStream() icon.compress(PNG, 100, baos) val datas: ByteArray = baos.toByteArray() val customIcon = PwIconCustom(UUID.randomUUID(), datas) entry.customIcon = customIcon (BaseApp.KDB.pm as PwDatabaseV4).putCustomIcons(customIcon) entry.strings["KP2A_URL_1"] = ProtectedString(false, "androidapp://$apkPkgName") } val appName = KDBAutoFillRepository.getAppName(context, apkPkgName) entry.setTitle(appName ?: "newEntry", BaseApp.KDB.pm) entry.icon = PwIconStandard(0) } else { entry = listStorage[0] as PwEntryV4 Timber.w("已存在含有【$apkPkgName】的条目,将更新条目") } if (!userName.isNullOrEmpty()) { entry.setUsername(userName, BaseApp.KDB.pm) } if (!pass.isNullOrEmpty()) { entry.setPassword(pass, BaseApp.KDB.pm) } return entry } /** * 创建群组 * @param groupName 群组名 * @param parentGroup 父群组 * @param icon 标准图标 * @param customIcon 自定义图标 */ fun createGroup( groupName: String, parentGroup: PwGroupV4, icon: PwIconStandard, customIcon: PwIconCustom?, callback: (PwGroupV4) -> Unit ) { KpaUtil.kdbHandlerService.createGroup(groupName, icon, customIcon, parentGroup, callback) } /** * 构建的更多选择项目 */ fun getMoreItem(context: Context): ArrayList { val list = ArrayList() val titles = context.resources.getStringArray(R.array.v4_add_mor_item) val icons = context.resources.obtainTypedArray(R.array.v4_add_more_icon) val len = titles.size - 1 for (i in 0..len) { val item = SimpleItemEntity() item.title = titles[i] item.icon = icons.getResourceId(i, 0) if (item.icon == R.drawable.ic_token_grey && pwEntry.hasTOTP()) { Timber.d("Already used totp") continue } if (item.icon == R.drawable.ic_notice && pwEntry.hasNote()) { Timber.d("Already used note") continue } list.add(item) } icons.recycle() return list } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CreateEnum.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.entry /** * @Author laoyuyu * @Description * @Date 7:28 PM 2023/10/13 **/ enum class CreateEnum { MODIFY, CREATE } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CreateFileCard.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.entry import android.content.Context import android.util.AttributeSet import android.view.Gravity import android.view.LayoutInflater import androidx.appcompat.widget.PopupMenu import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import com.arialyy.frame.util.ResUtil import com.keepassdroid.database.security.ProtectedBinary import com.lyy.keepassa.R import com.lyy.keepassa.base.AbsViewBindingAdapter import com.lyy.keepassa.databinding.LayoutEntryAttachmentBinding import com.lyy.keepassa.databinding.LayoutEntryCreateStrCardBinding import com.lyy.keepassa.entity.CommonState.DELETE import com.lyy.keepassa.event.AttrFileEvent import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.doOnItemClickListener import com.lyy.keepassa.util.init import kotlinx.coroutines.launch /** * @Author laoyuyu * @Description * @Date 3:26 PM 2023/10/25 **/ class CreateFileCard(context: Context, attributeSet: AttributeSet) : ConstraintLayout(context, attributeSet) { companion object { val ADD_MORE_DATA = Pair("addMore", ProtectedBinary(false, null)) } private val binding = LayoutEntryCreateStrCardBinding.inflate(LayoutInflater.from(context), this, true) private val fileList = mutableListOf>() private val fileAdapter = FileAdapter() private val helper = CardListHelper(binding) init { binding.tvTitle.text = ResUtil.getString(R.string.attachment) } fun bindData(fileMap: HashMap) { fileList.clear() fileMap.entries.forEach { fileList.add(Pair(it.key, it.value)) } fileList.add(ADD_MORE_DATA) binding.rvList.apply { this.adapter = this@CreateFileCard.fileAdapter this@CreateFileCard.fileAdapter.setData(fileList) } binding.rvList.doOnItemClickListener { _, position, v -> val data = fileList[position] if (data == ADD_MORE_DATA) { (context as CreateEntryActivity).changeFile() return@doOnItemClickListener } PopupMenu(context, v, Gravity.END).init(R.menu.entry_modify_file_summary) { when (it.itemId) { R.id.remove_file -> { KpaUtil.scope.launch { CreateEntryModule.attrFlow.emit(AttrFileEvent(DELETE, data.first, data.second)) } } R.id.open_file -> { KdbUtil.openFile(data.first, data.second) } } }.show() } helper.handleList() } fun removeItem(key: String) { fileList.find { it.first == key }?.let { val pos = fileList.indexOf(it) if (pos >= 0) { fileList.removeAt(pos) fileAdapter.notifyItemRemoved(pos) } } if (fileList.size == 1) { visibility = GONE } } private class FileAdapter : AbsViewBindingAdapter, LayoutEntryAttachmentBinding>() { override fun bindData( binding: LayoutEntryAttachmentBinding, item: Pair ) { if (item == ADD_MORE_DATA) { binding.value.isVisible = false binding.addMore.isVisible = true binding.addMore.text = ResUtil.getString(R.string.add_attr_file) return } binding.value.isVisible = true binding.addMore.isVisible = false binding.value.text = item.first } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CreateStrCard.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.entry import android.content.Context import android.os.Build import android.util.AttributeSet import android.view.Gravity import android.view.LayoutInflater import androidx.appcompat.widget.PopupMenu import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isGone import com.arialyy.frame.router.Routerfit import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.R import com.lyy.keepassa.base.AbsViewBindingAdapter import com.lyy.keepassa.databinding.LayoutEntryCreateStrCardBinding import com.lyy.keepassa.databinding.LayoutEntryStrBinding import com.lyy.keepassa.entity.CommonState.DELETE import com.lyy.keepassa.event.AttrStrEvent import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.doOnItemClickListener import com.lyy.keepassa.util.init import com.lyy.keepassa.view.create.CreateCustomStrDialog import kotlinx.coroutines.launch /** * @Author laoyuyu * @Description * @Date 3:12 PM 2023/10/12 **/ class CreateStrCard(context: Context, attributeSet: AttributeSet) : ConstraintLayout(context, attributeSet) { private val binding = LayoutEntryCreateStrCardBinding.inflate(LayoutInflater.from(context), this, true) private val strList = mutableListOf>() companion object { val ADD_MORE_DATA = Pair("addMore", ProtectedString(false, "addMore")) } private val helper = CardListHelper(binding) private val adapter = StrAdapter() fun bindDate(strMap: Map) { handleList(strMap) } private fun handleList(strMap: Map) { strList.clear() visibility = VISIBLE KdbUtil.filterCustomStr(strMap).entries.forEach { strList.add(Pair(it.key, it.value)) } strList.add(ADD_MORE_DATA) binding.rvList.adapter = adapter adapter.setData(strList) binding.rvList.doOnItemClickListener { _, position, v -> val data = strList[position] if (data == ADD_MORE_DATA) { Routerfit.create(DialogRouter::class.java).showCreateCustomDialog() return@doOnItemClickListener } PopupMenu(context, v, Gravity.END).init(R.menu.entry_modify_str_summary) { when (it.itemId) { R.id.remove_str -> { KpaUtil.scope.launch { CreateCustomStrDialog.CustomStrFlow.emit( AttrStrEvent( DELETE, data.first, data.second ) ) } } R.id.modify_text -> { Routerfit.create(DialogRouter::class.java) .showCreateCustomDialog(position, data.first, data.second) } } }.show() } helper.handleList() } fun removeItem(key: String) { strList.find { it.first == key }?.let { val pos = strList.indexOf(it) if (pos >= 0) { strList.removeAt(pos) adapter.notifyItemRemoved(pos) } } if (strList.size == 1) { visibility = GONE } } private class StrAdapter : AbsViewBindingAdapter, LayoutEntryStrBinding>() { private fun showContent(binding: LayoutEntryStrBinding, show: Boolean) { binding.title.isGone = !show binding.value.isGone = !show binding.addMore.isGone = show } override fun bindData(binding: LayoutEntryStrBinding, item: Pair) { if (item == ADD_MORE_DATA) { showContent(binding, false) return } showContent(binding, true) binding.title.text = item.first val tvValue = binding.value KpaUtil.handleShowPass(tvValue, !item.second.isProtected) if (item.second.toString().isEmpty()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { tvValue.typeface = context.resources.getFont(R.font.roboto_thinitalic) } tvValue.text = "null" return } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { tvValue.typeface = context.resources.getFont(R.font.roboto_regular) } tvValue.text = item.second.toString() } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/ICreateHandler.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.entry import android.text.Html import androidx.core.view.isVisible import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.ToastUtils import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.KeyConstance import com.lyy.keepassa.util.KdbUtil import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat /** * @Author laoyuyu * @Description * @Date 7:32 PM 2023/10/13 **/ interface ICreateHandler { companion object { private val PROTECTED_KEY = arrayListOf().apply { add(PwEntryV4.STR_TITLE) add(PwEntryV4.STR_USERNAME) add(PwEntryV4.STR_PASSWORD) add(PwEntryV4.STR_URL) add(PwEntryV4.STR_NOTES) } } fun bindData() fun getTitle(): String fun saveDb(pwEntryV4: PwEntryV4) fun checkAttr(context: CreateEntryActivity, pwEntryV4: PwEntryV4) { val binding = context.binding val title = binding.title.text.toString() val db = BaseApp.KDB.pm pwEntryV4.setTitle(title.ifEmpty { "unknown" }, db) pwEntryV4.setUsername(binding.edUser.text.toString(), db) pwEntryV4.setPassword(binding.edPassword.text.toString(), db) pwEntryV4.setUrl(binding.edUrl.text.toString(), db) pwEntryV4.tags = binding.edTag.text.toString() val loseTime = binding.edLoseTime.text.toString() if (loseTime.isNotEmpty() && binding.tlLoseTime.isVisible) { pwEntryV4.expiryTime = DateTime.parse(loseTime, DateTimeFormat.forPattern(KdbUtil.DATE_FORMAT)).toDate() } if (binding.tlNote.isVisible) { pwEntryV4.setNotes(binding.edNote.text.toString(), db) } if (binding.cardStr.isVisible) { val temp = arrayListOf() pwEntryV4.strings.forEach { if (it.key in PROTECTED_KEY) { return@forEach } temp.add(it.key) } temp.forEach { pwEntryV4.strings.remove(it) } context.module.strCacheMap.filter { it.key != KeyConstance.TOTP && it.key !in PROTECTED_KEY } .forEach { pwEntryV4.strings[it.key] = it.value } } if (binding.cardFile.isVisible && checkEntry(pwEntryV4)) { pwEntryV4.binaries.clear() context.module.fileCacheMap.forEach { pwEntryV4.binaries[it.key] = it.value } } pwEntryV4.customIcon = context.module.customIcon pwEntryV4.icon = context.module.icon } private fun checkEntry(pwEntry: PwEntryV4): Boolean { pwEntry.binaries.forEach { if (it.value == null || it.value.length() == 0) { ToastUtils.showLong(Html.fromHtml(ResUtil.getString(R.string.error_file, it.key))) return false } } return true } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/ModifyEntryHandler.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.create.entry import android.view.View import androidx.lifecycle.lifecycleScope import com.arialyy.frame.util.ResUtil import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.util.IconUtil import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.getRealPass import com.lyy.keepassa.util.getRealTitle import com.lyy.keepassa.util.getRealUserName import com.lyy.keepassa.util.loadImg import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.joda.time.DateTime import java.util.UUID /** * @Author laoyuyu * @Description * @Date 7:34 PM 2023/10/13 **/ internal class ModifyEntryHandler(val context: CreateEntryActivity) : ICreateHandler { override fun bindData() { val entryId = context.intent.getSerializableExtra(CreateEntryActivity.KEY_ENTRY) as UUID context.module.pwEntry = BaseApp.KDB!!.pm.entries[entryId] as PwEntryV4 context.module.initCache() val entry = context.module.pwEntry val binding = context.binding context.lifecycleScope.launch(Dispatchers.IO) { val name = entry.getRealUserName() val pass = entry.getRealPass() val title = entry.getRealTitle() withContext(Dispatchers.Main) { binding.title.setText(title) binding.edUser.setText(name) binding.edPassword.setText(pass) binding.tvConfirm.setText(pass) } } if (entry.notes.isNotEmpty()) { binding.tlNote.visibility = View.VISIBLE binding.edNote.setText(entry.notes.toString()) } if (entry.url.isNotEmpty()) { binding.tlUrl.visibility = View.VISIBLE binding.edUrl.setText(entry.url) } handleIcon(context, entry) binding.cardStr.apply { visibility = if (entry.strings.isNotEmpty()) View.VISIBLE else View.GONE } binding.cardStr.bindDate(context.module.strCacheMap) binding.cardFile.apply { visibility = if (entry.binaries.isNotEmpty()) View.VISIBLE else View.GONE } binding.cardFile.bindData(context.module.fileCacheMap) binding.tlTag.apply { visibility = if (entry.tags.isNotEmpty()) View.VISIBLE else View.GONE binding.edTag.setText(entry.tags) } if (entry.expiryTime != null) { binding.edLoseTime.setText(DateTime(entry.expiryTime).toString(KdbUtil.DATE_FORMAT)) binding.tlLoseTime.visibility = View.VISIBLE } } private fun handleIcon(ac: CreateEntryActivity, pwEntry: PwEntryV4) { ac.module.icon = pwEntry.icon ac.binding.ivIcon.loadImg(IconUtil.getEntryIconDrawable(ac, pwEntry, zoomIcon = true)) } override fun getTitle(): String { return ResUtil.getString(R.string.edit) } override fun saveDb(pwEntryV4: PwEntryV4) { checkAttr(context, pwEntryV4) context.lifecycleScope.launch { KpaUtil.kdbHandlerService.saveOnly(true) { context.finishAfterTransition() } } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/detail/AppIconAdapter.kt ================================================ package com.lyy.keepassa.view.detail import com.blankj.utilcode.util.AppUtils import com.lyy.keepassa.base.AbsViewBindingAdapter import com.lyy.keepassa.databinding.ItemAppIconBinding import com.lyy.keepassa.util.loadImg class AppIconAdapter : AbsViewBindingAdapter() { override fun bindData(binding: ItemAppIconBinding, item: String) { binding.ivIcon.apply { // val drawable = ResUtil.getDrawable(R.drawable.ic_app) val drawable = AppUtils.getAppIcon(item) loadImg(drawable) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/detail/AppIconLayoutManager.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.detail import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.LayoutManager import androidx.recyclerview.widget.RecyclerView.LayoutParams class AppIconLayoutManager(private val offset: Int) : LayoutManager() { override fun generateDefaultLayoutParams(): LayoutParams { return LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT ) } override fun onMeasure( recycler: RecyclerView.Recycler, state: RecyclerView.State, widthSpec: Int, heightSpec: Int ) { if (state.itemCount == 0) { super.onMeasure(recycler, state, widthSpec, heightSpec) return } if (state.isPreLayout) return //假定每个item的宽高一直,所以用第一个view计算宽高, //这种方式可能不太好 val itemView = recycler.getViewForPosition(0) addView(itemView) //这里不能用measureChild方法,具体看内部源码实现,内部getWidth默认为0 // measureChildWithMargins(itemView, 0, 0) itemView.measure(widthSpec, heightSpec) val mItemWidth = getDecoratedMeasuredWidth(itemView) val mItemHeight = getDecoratedMeasuredHeight(itemView) //回收这个View detachAndScrapView(itemView, recycler) //设置宽高 setMeasuredDimension(mItemWidth, mItemHeight) } override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) { //轻量级的将view移除屏幕 detachAndScrapAttachedViews(recycler) //开始填充view var totalSpace = width - paddingRight var currentPosition = 0 var left = 0 val top = 0 var right = 0 var bottom = 0 //模仿LinearLayoutManager的写法,当可用距离足够和要填充 //的itemView的position在合法范围内才填充View while (totalSpace > 0 && currentPosition < state.itemCount) { val view = recycler.getViewForPosition(currentPosition) addView(view) measureChild(view, 0, 0) right = left + getDecoratedMeasuredWidth(view) bottom = top + getDecoratedMeasuredHeight(view) layoutDecorated(view, left, top, right, bottom) currentPosition++ left += getDecoratedMeasuredWidth(view) - offset //关键点 totalSpace -= getDecoratedMeasuredWidth(view) - offset } //layout完成后输出相关信息 } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/detail/EntryDetailActivityNew.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.detail import android.graphics.drawable.BitmapDrawable import android.os.Bundle import android.view.View import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.ToastUtils import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.security.ProtectedBinary import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseActivity import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.databinding.ActivityEntryDetailNewBinding import com.lyy.keepassa.router.ActivityRouter import com.lyy.keepassa.util.IconUtil import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.copyPassword import com.lyy.keepassa.util.copyTotp import com.lyy.keepassa.util.copyUserName import com.lyy.keepassa.util.doClick import com.lyy.keepassa.util.hasTOTP import com.lyy.keepassa.util.isCollectioned import com.lyy.keepassa.util.takePermission import com.lyy.keepassa.view.detail.card.EntryFileCard import com.lyy.keepassa.widget.toPx import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import timber.log.Timber import java.nio.ByteBuffer import java.nio.channels.Channels import java.util.Locale import java.util.UUID import kotlin.math.abs /** * @Author laoyuyu * @Description * @Date 5:24 PM 2023/9/26 **/ @Route(path = "/entry/detail") class EntryDetailActivityNew : BaseActivity() { companion object { const val KEY_ENTRY_ID = "KEY_ENTRY_ID" } private lateinit var module: EntryDetailModule private lateinit var pwEntry: PwEntryV4 private var isInRecycleBin = false private var fileSaveCache: ProtectedBinary? = null private val saveFile = registerForActivityResult(CreateDocument("*/*")) { if (it == null) { Timber.e("uri为空") return@registerForActivityResult } if (fileSaveCache == null) { Timber.e("文件为空") return@registerForActivityResult } it.takePermission() lifecycleScope.launch(Dispatchers.IO) { contentResolver.openOutputStream(it).use { out -> val iChannel = Channels.newChannel(fileSaveCache!!.data) val oChannel = Channels.newChannel(out) val buffer = ByteBuffer.allocateDirect(16 * 1024) while (iChannel.read(buffer) != -1) { // 切换为读状态 buffer.flip() // 保证缓冲区的数据全部写入 while (buffer.hasRemaining()) { oChannel.write(buffer) } buffer.clear() } iChannel.close() oChannel.close() ToastUtils.showLong(ResUtil.getString(R.string.file_save_success)) } } } @Autowired(name = KEY_ENTRY_ID) lateinit var uuid: UUID override fun setLayoutId(): Int { return R.layout.activity_entry_detail_new } override fun initData(savedInstanceState: Bundle?) { super.initData(savedInstanceState) ARouter.getInstance().inject(this) module = ViewModelProvider(this)[EntryDetailModule::class.java] pwEntry = (BaseApp.KDB!!.pm.entries[uuid] as PwEntryV4?)!! module.initEntry(pwEntry) if (BaseApp.isV4 && pwEntry.parent == BaseApp.KDB!!.pm.recycleBin) { isInRecycleBin = true } setTopBar() listenerSaveFile() module.saveRecord() } private fun listenerSaveFile() { lifecycleScope.launch { EntryFileCard.SAVE_FILE_FLOW.collectLatest { saveFile.launch(it.first) fileSaveCache = it.second } } } override fun onStart() { super.onStart() bindData() } private fun bindData() { setIcon() // 处理过期 KpaUtil.handleExpire(binding.tvTitle, pwEntry) binding.tvTitle.text = pwEntry.title binding.topAppBar.title = pwEntry.title binding.cardBaseInfo.bindData(pwEntry) binding.cardNote.bindData(pwEntry) binding.cardStr.bindData(pwEntry) binding.cardAtta.bindData(pwEntry) binding.cardTag.bindData(pwEntry) } /** * 标题栏 */ private fun setTopBar() { toolbar = binding.topAppBar toolbar.setNavigationOnClickListener { finishAfterTransition() } toolbar.inflateMenu(R.menu.menu_entry_detail) toolbar.menu.findItem(R.id.collect) .setIcon(if (!pwEntry.isCollectioned()) R.drawable.ic_star_outline else R.drawable.ic_star) toolbar.setOnMenuItemClickListener { item -> if (KeepassAUtil.instance.isFastClick()) { return@setOnMenuItemClickListener true } when (item.itemId) { R.id.edit -> { Routerfit.create(ActivityRouter::class.java, this).toEditEntryActivity(pwEntry.uuid) } R.id.collect -> { KpaUtil.kdbHandlerService.collection(pwEntry, !pwEntry.isCollectioned()) item.setIcon(if (!pwEntry.isCollectioned()) R.drawable.ic_star_outline else R.drawable.ic_star) } } true } binding.appBarLayout.addOnOffsetChangedListener { _, verticalOffset -> // Timber.d("offset: $verticalOffset, ${binding.appBarLayout.totalScrollRange}") if (verticalOffset == 0) { binding.topAppBar.title = "" return@addOnOffsetChangedListener } if (abs(verticalOffset) >= binding.appBarLayout.totalScrollRange) { binding.topAppBar.title = pwEntry.title return@addOnOffsetChangedListener } } handleMenuBar() } private fun handleMenuBar() { binding.btnUserName.doClick { pwEntry.copyUserName() } binding.btnUserPass.doClick { pwEntry.copyPassword() } binding.btnTotp.visibility = if (!pwEntry.hasTOTP()) View.GONE else View.VISIBLE binding.btnTotp.doClick { pwEntry.copyTotp() } } /** * 设置图标 */ private fun setIcon() { val color = if (pwEntry.getCustomIcon()?.imageData?.isNotEmpty() == true) { module.getColor(this, BitmapDrawable(IconUtil.getCustomBitmap(pwEntry))) } else { ResUtil.getColor(R.color.color_444E85DB) } binding.tvChar.visibility = View.VISIBLE if (pwEntry.title.isEmpty()){ binding.tvChar.text = "#" }else{ binding.tvChar.text = pwEntry.title.substring(0, 1).uppercase(Locale.getDefault()) } binding.ivIcon.setBackgroundColor(color) } private fun setAppIcon() { val adapter = AppIconAdapter() binding.rvAppIcon.apply { this.adapter = adapter setChildDrawingOrderCallback { childCount, i -> if (childCount <= 1) { return@setChildDrawingOrderCallback i } return@setChildDrawingOrderCallback childCount - i - 1 } layoutManager = AppIconLayoutManager(15.toPx()) } adapter.setData(arrayListOf().apply { add("tv.danmaku.bili") }) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/detail/EntryDetailModule.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.detail import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.content.Context import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.net.Uri import android.view.View import android.view.ViewAnimationUtils import android.widget.ImageView import androidx.fragment.app.FragmentActivity import androidx.lifecycle.viewModelScope import androidx.palette.graphics.Palette import com.arialyy.frame.module.SingleLiveEvent import com.arialyy.frame.util.ResUtil import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.security.ProtectedBinary import com.keepassdroid.database.security.ProtectedString import com.keepassdroid.utils.Types import com.keepassdroid.utils.UriUtil import com.lyy.keepassa.R import com.lyy.keepassa.R.color import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.BaseModule import com.lyy.keepassa.entity.EntryRecord import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.IconUtil import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.VibratorUtil import com.lyy.keepassa.widget.toPx import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber /** * 条目详情 */ class EntryDetailModule : BaseModule() { private lateinit var pwEntry: PwEntry private val finishAnimEvent = SingleLiveEvent() private val startAnimEvent = SingleLiveEvent() fun initEntry(pwEntry: PwEntry) { this.pwEntry = pwEntry } /** * 结束动画 */ fun finishAnim( context: Context, rootView: View, icon: ImageView ): SingleLiveEvent { viewModelScope.launch { val rgb = getColor(context, icon.drawable) val x = icon.x + 20.toPx() val y = icon.y + 60.toPx() val anim = ViewAnimationUtils.createCircularReveal( rootView, x.toInt(), y.toInt(), rootView.height.toFloat(), 0f, ) anim.duration = 400 anim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator) { super.onAnimationStart(animation) rootView.background = ColorDrawable(rgb) } override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) rootView.background = ColorDrawable(ResUtil.getColor(R.color.background_color)) finishAnimEvent.postValue(true) } }) anim.start() } return finishAnimEvent } /** * 启动动画 */ fun startAnim( context: Context, rootView: View, icon: ImageView ): SingleLiveEvent { viewModelScope.launch { val rgb = getColor(context, icon.drawable) val x = icon.x + 20.toPx() val y = icon.y + 60.toPx() val anim = ViewAnimationUtils.createCircularReveal( rootView, x.toInt(), y.toInt(), 40.toPx() .toFloat(), rootView.height.toFloat() ) anim.duration = 400 anim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator) { super.onAnimationStart(animation) rootView.background = ColorDrawable(rgb) } override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) rootView.background = ColorDrawable(ResUtil.getColor(color.background_color)) startAnimEvent.postValue(true) } }) anim.start() } return startAnimEvent } /** * get highlight color */ fun getColor( context: Context, icon: Drawable ): Int { return with(Dispatchers.IO) { val temp = IconUtil.getBitmapFromDrawable(context, icon, 40.toPx()) if (temp == null || temp.isRecycled) { return@with Color.WHITE } val sw = Palette.from(temp) .maximumColorCount(12) .generate() return@with when { sw.mutedSwatch != null -> sw.mutedSwatch!!.rgb sw.darkMutedSwatch != null -> sw.darkMutedSwatch!!.rgb sw.lightMutedSwatch != null -> sw.lightMutedSwatch!!.rgb sw.darkVibrantSwatch != null -> sw.darkVibrantSwatch!!.rgb sw.lightVibrantSwatch != null -> sw.lightVibrantSwatch!!.rgb sw.vibrantSwatch != null -> sw.vibrantSwatch!!.rgb else -> ResUtil.getColor(R.color.colorPrimary) } } } /** * 保存附件到sd卡 * @param saveUri 保存路径 * @param source 需要保存的文件 */ fun saveAttachment( context: Context, saveUri: Uri, source: ProtectedBinary ) { viewModelScope.launch(Dispatchers.IO) { try { val byte = source.data.readBytes() val os = context.contentResolver.openOutputStream(saveUri) if (os != null) { os.write(byte, 0, byte.size) os.flush() os.close() } withContext(Dispatchers.Main) { val fileName = UriUtil.getFileNameFromUri(context, saveUri) HitUtil.toaskShort(context.getString(R.string.save_file_success, fileName)) } } catch (e: Exception) { Timber.e(e) } } } /** * 回收项目 * @param pwEntry 需要回收的条目 */ fun recycleEntry(ac: FragmentActivity, pwEntry: PwEntryV4) { KpaUtil.kdbHandlerService.deleteEntry(pwEntry) { HitUtil.toaskShort( "${ac.getString(R.string.del_entry)}${ac.getString(R.string.success)}" ) VibratorUtil.vibrator(300) ac.finishAfterTransition() } } /** * 保存打开记录 */ fun saveRecord() { if (BaseApp.dbRecord == null) { return } KpaUtil.scope.launch(Dispatchers.IO) { val dao = BaseApp.appDatabase.entryRecordDao() var record = dao.getRecord(Types.UUIDtoBytes(pwEntry.uuid), BaseApp.dbRecord!!.localDbUri) if (record == null) { record = EntryRecord( userName = pwEntry.username, title = pwEntry.title, uuid = Types.UUIDtoBytes(pwEntry.uuid), time = System.currentTimeMillis(), dbFileUri = BaseApp.dbRecord!!.localDbUri ) dao.saveRecord(record) } else { record.title = pwEntry.title record.userName = pwEntry.username record.time = System.currentTimeMillis() dao.updateRecord(record) } KpaUtil.openEntryRecordFlow.emit(record) } } /** * 获取项目的属性字段,只有v4版本才有自定义属性字段 */ fun getV4EntryStr(entryV4: PwEntryV4): Map { return KdbUtil.filterCustomStr(entryV4) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/detail/GroupDetailActivity.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.detail import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.os.Bundle import android.transition.TransitionInflater import android.view.MotionEvent import android.view.View import androidx.appcompat.widget.AppCompatImageView import androidx.core.app.ActivityOptionsCompat import androidx.core.transition.addListener import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.router.Routerfit import com.blankj.utilcode.util.ActivityUtils import com.keepassdroid.database.PwEntry import com.keepassdroid.database.PwGroup import com.keepassdroid.database.PwGroupId import com.keepassdroid.database.PwGroupV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.AnimState import com.lyy.keepassa.base.AnimState.NOT_ANIM import com.lyy.keepassa.base.BaseActivity import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.common.SortType.CHAR_ASC import com.lyy.keepassa.common.SortType.CHAR_DESC import com.lyy.keepassa.common.SortType.NONE import com.lyy.keepassa.common.SortType.TIME_ASC import com.lyy.keepassa.common.SortType.TIME_DESC import com.lyy.keepassa.databinding.ActivityGroupDetailBinding import com.lyy.keepassa.entity.showPopMenu import com.lyy.keepassa.event.EntryState.CREATE import com.lyy.keepassa.event.EntryState.DELETE import com.lyy.keepassa.event.EntryState.MODIFY import com.lyy.keepassa.event.EntryState.MOVE import com.lyy.keepassa.event.EntryState.SAVE import com.lyy.keepassa.event.EntryState.UNKNOWN import com.lyy.keepassa.event.MoveEvent import com.lyy.keepassa.router.ActivityRouter import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.EventBusHelper import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.checkGroupIsParent import com.lyy.keepassa.util.createGroup import com.lyy.keepassa.util.deleteGroup import com.lyy.keepassa.util.doOnInterceptTouchEvent import com.lyy.keepassa.util.doOnItemClickListener import com.lyy.keepassa.util.doOnItemLongClickListener import com.lyy.keepassa.util.updateModifyGroup import com.lyy.keepassa.view.SimpleEntryAdapter import com.lyy.keepassa.widget.MainExpandFloatActionButton import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode.MAIN import timber.log.Timber /** * 群组详情、回收站详情 */ @Route(path = "/group/detail") class GroupDetailActivity : BaseActivity() { companion object { const val KEY_TITLE = "KEY_TITLE" const val KEY_GROUP_ID = "KEY_V3_GROUP_ID" const val KEY_IS_IN_RECYCLE_BIN = "KEY_IS_IN_RECYCLE_BIN" } private lateinit var module: GroupDetailModule private lateinit var adapter: SimpleEntryAdapter private var curx = 0 @JvmField @Autowired(name = KEY_IS_IN_RECYCLE_BIN) var isRecycleBin = false @Autowired(name = KEY_GROUP_ID) lateinit var groupId: PwGroupId @Autowired(name = KEY_TITLE) lateinit var groupTitle: String override fun setLayoutId(): Int { return R.layout.activity_group_detail } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (window.enterTransition == null || !KeepassAUtil.instance.isDisplayLoadingAnim()) { loadData() return } window.enterTransition = null window.exitTransition = null binding.laAnim.speed = 2.5f binding.laAnim.playAnimation() binding.laAnim.addAnimatorListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) loadData() } }) } override fun useAnim(): AnimState { return NOT_ANIM } override fun initData(savedInstanceState: Bundle?) { super.initData(savedInstanceState) ARouter.getInstance().inject(this) EventBusHelper.reg(this) // 检查是否是在回收站中 if (!isRecycleBin) { isRecycleBin = BaseApp.isV4 && BaseApp.KDB!!.pm.recycleBin != null && BaseApp.KDB!!.pm.recycleBin.id == groupId } if (isRecycleBin) { binding.fab.visibility = View.GONE } binding.ctlCollapsingLayout.title = groupTitle binding.kpaToolbar.title = groupTitle binding.kpaToolbar.setNavigationOnClickListener { finishAfterTransition() } } override fun finishAfterTransition() { window.returnTransition = TransitionInflater.from(this) .inflateTransition(R.transition.slide_return) super.finishAfterTransition() } private fun loadData() { binding.kpaToolbar.inflateMenu(R.menu.menu_group_detail) module = ViewModelProvider(this)[GroupDetailModule::class.java] initList() initFab() initMenu() listenerGetGroupData() listenerEntryStateChange() listenerGroupStateChange() module.getGroupData(this, groupId) } /** * listener the group status change, there are states: create, delete, modify, move */ private fun listenerGroupStateChange() { lifecycleScope.launch { KpaUtil.kdbHandlerService.groupStateChangeFlow.collectLatest { if (it.groupV4 == null || module.curGroupV4 == null) { return@collectLatest } when (it.state) { CREATE -> { adapter.createGroup(module.entryData, it.groupV4, module.curGroupV4) if (it.groupV4.checkGroupIsParent(module.curGroupV4)) { showList(true) } } MODIFY -> { adapter.updateModifyGroup(module.entryData, it.groupV4, module.curGroupV4) } MOVE -> { // module.moveEntry(adapter, it.pwEntryV4, it.oldParent!!) } DELETE -> { adapter.deleteGroup( module.entryData, it.groupV4, it.oldParent!!, module.curGroupV4 ) if (module.entryData.isEmpty()) { showList(false) } } SAVE -> { showList(true) adapter.notifyDataSetChanged() } UNKNOWN -> { Timber.d("un known status") } } } } } /** * listener the entry status change, there are three states: create, delete, and modify. */ private fun listenerEntryStateChange() { lifecycleScope.launch { KpaUtil.kdbHandlerService.entryStateChangeFlow.collectLatest { it.pwEntryV4?.let { entry -> when (it.state) { CREATE -> { module.createNewEntry(adapter, entry) if (it.pwEntryV4.checkGroupIsParent(module.curGroupV4)) { showList(true) } } MODIFY -> { module.updateModifyEntry(adapter, entry) if (module.entryData.isEmpty()) { showList(false) } } MOVE -> { module.moveEntry(adapter, entry, it.oldParent!!) } DELETE -> { module.deleteEntry(adapter, entry, it.oldParent!!) if (module.entryData.isEmpty()) { showList(false) } } SAVE -> { showList(true) adapter.notifyDataSetChanged() } UNKNOWN -> { Timber.d("un known status") } } } } } } private fun listenerGetGroupData() { lifecycleScope.launch { module.getDataFlow.collectLatest { list -> binding.laAnim.cancelAnimation() binding.laAnim.visibility = View.GONE if (list.isNullOrEmpty()) { // 设置appbar为收缩状态 binding.appBar.setExpanded(false, false) showList(false) return@collectLatest } showList(true) adapter.notifyDataSetChanged() } } } private fun showList(showList: Boolean) { if (showList) { binding.list.visibility = View.VISIBLE getEmptyLayout().visibility = View.GONE return } getEmptyLayout().visibility = View.VISIBLE binding.list.visibility = View.GONE } private fun initMenu() { binding.kpaToolbar.setOnMenuItemClickListener { val type = when (it.itemId) { R.id.sort_down_by_char -> { CHAR_DESC } R.id.sort_up_by_char -> { CHAR_ASC } R.id.sort_down_by_time -> { TIME_DESC } R.id.sort_up_by_time -> { TIME_ASC } else -> NONE } if (type != NONE) { module.sortData(adapter, type) } return@setOnMenuItemClickListener true } } /** * fab */ private fun initFab() { if (isRecycleBin) { binding.fab.visibility = View.GONE return } binding.fab.setOnItemClickListener(object : MainExpandFloatActionButton.OnItemClickListener { override fun onKeyClick() { Routerfit.create(ActivityRouter::class.java, this@GroupDetailActivity) .toCreateEntryActivity( groupId, ActivityOptionsCompat.makeSceneTransitionAnimation(this@GroupDetailActivity) ) binding.fab.hintMoreOperate() } override fun onGroupClick() { Routerfit.create(DialogRouter::class.java) .showCreateGroupDialog( (BaseApp.KDB!!.pm.groups[groupId] ?: BaseApp.KDB!!.pm.rootGroup) as PwGroupV4 ) binding.fab.hintMoreOperate() } }) } private fun initList() { adapter = SimpleEntryAdapter(this, module.entryData) binding.list.setHasFixedSize(true) binding.list.layoutManager = LinearLayoutManager(this) binding.list.adapter = adapter binding.list.doOnItemClickListener { _, position, v -> val item = module.entryData[position] if (item.obj is PwGroup) { val group = item.obj as PwGroup Routerfit.create(ActivityRouter::class.java, this).toGroupDetailActivity( groupName = group.name, groupId = group.id, isRecycleBin = isRecycleBin, opt = ActivityOptionsCompat.makeSceneTransitionAnimation(this) ) return@doOnItemClickListener } if (item.obj is PwEntry) { val icon = v.findViewById(R.id.icon) KeepassAUtil.instance.turnEntryDetail(this, item.obj as PwEntry, icon) return@doOnItemClickListener } } binding.list.doOnItemLongClickListener { _, position, v -> module.entryData[position].showPopMenu(this, v, curx, isRecycleBin) return@doOnItemLongClickListener true } // 获取点击位置 binding.list.doOnInterceptTouchEvent { _, e -> if (e.action == MotionEvent.ACTION_DOWN) { curx = e.x.toInt() } return@doOnInterceptTouchEvent false } } private fun getEmptyLayout(): View { if (!binding.vsEmpty.isInflated) { binding.vsEmpty.viewStub?.inflate() } return binding.vsEmpty.root } /** * 有条目移动或有条目从回收站中撤回 */ @Subscribe(threadMode = MAIN) fun onMove(event: MoveEvent) { // getData() } override fun onDestroy() { super.onDestroy() EventBusHelper.unReg(this) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/detail/GroupDetailModule.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.detail import android.content.Context import androidx.lifecycle.viewModelScope import com.arialyy.frame.util.PinyinUtil import com.keepassdroid.database.PwDataInf import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.PwGroup import com.keepassdroid.database.PwGroupId import com.keepassdroid.database.PwGroupV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.BaseModule import com.lyy.keepassa.common.SortType import com.lyy.keepassa.common.SortType.CHAR_ASC import com.lyy.keepassa.common.SortType.CHAR_DESC import com.lyy.keepassa.common.SortType.NONE import com.lyy.keepassa.common.SortType.TIME_ASC import com.lyy.keepassa.common.SortType.TIME_DESC import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.util.createNewEntry import com.lyy.keepassa.util.deleteEntry import com.lyy.keepassa.util.moveEntry import com.lyy.keepassa.util.updateModifyEntry import com.lyy.keepassa.view.SimpleEntryAdapter import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch internal class GroupDetailModule : BaseModule() { val entryData = mutableListOf() val getDataFlow = MutableSharedFlow?>() var curGroupV4: PwGroupV4? = null /** * update the status of deleted items */ fun deleteEntry(adapter: SimpleEntryAdapter, pwEntryV4: PwEntryV4, oldParent: PwGroupV4) { curGroupV4?.let { adapter.deleteEntry(entryData, pwEntryV4, oldParent, it) } } /** * update the status of modified items */ fun updateModifyEntry(adapter: SimpleEntryAdapter, pwEntryV4: PwEntryV4) { curGroupV4?.let { adapter.updateModifyEntry(entryData, pwEntryV4, it) } } /** * move entry from other group */ fun moveEntry(adapter: SimpleEntryAdapter, pwEntryV4: PwEntryV4, oldParent: PwGroupV4) { curGroupV4?.let { adapter.moveEntry(entryData, pwEntryV4, oldParent, it) } } /** * update root list state */ fun createNewEntry(adapter: SimpleEntryAdapter, pwEntryV4: PwEntryV4) { curGroupV4?.let { adapter.createNewEntry(entryData, pwEntryV4, it) } } /** * 获取v3版本的group数据 */ fun getGroupData(context: Context, groupId: PwGroupId) { entryData.clear() viewModelScope.launch { val group = BaseApp.KDB.pm.groups[groupId] curGroupV4 = group as PwGroupV4? if (group == null) { getDataFlow.emit(null) return@launch } entryData.addAll(convertGroup(context, group)) getDataFlow.emit(entryData) return@launch } } /** * 排序 * @param sortType */ fun sortData(adapter: SimpleEntryAdapter, sortType: SortType) { val entryList = arrayListOf() val groupList = arrayListOf() val tempList = arrayListOf() for (item in entryData) { if (item.obj is PwGroup) { groupList.add(item) continue } entryList.add(item) } tempList.addAll(sortEntry(sortType, groupList)) tempList.addAll(sortEntry(sortType, entryList)) entryData.clear() entryData.addAll(tempList) adapter.notifyDataSetChanged() } private fun sortEntry( sortType: SortType, data: List ): Set { val map = hashMapOf() for (item in data) { map[item] = PinyinUtil.getFirstSpellChar(item.title) } return when (sortType) { CHAR_ASC -> { map.toList() .sortedBy { it.second } .toMap().keys } CHAR_DESC -> { map.toList() .sortedByDescending { it.second } .toMap().keys } TIME_ASC -> { map.toList() .sortedBy { (it.first.obj as PwDataInf).creationTime.time } .toMap().keys } TIME_DESC -> { map.toList() .sortedByDescending { (it.first.obj as PwDataInf).creationTime.time } .toMap().keys } NONE -> { emptySet() } } } private fun convertGroup( context: Context, group: PwGroup ): ArrayList { val data = ArrayList() for (cGroup in group.childGroups) { val item = SimpleItemEntity() item.title = cGroup.name item.subTitle = context.getString( R.string.hint_group_desc, KdbUtil.getGroupAllEntryNum(cGroup) .toString() ) item.obj = cGroup data.add(item) } for (entry in group.childEntries) { val item = SimpleItemEntity() item.title = entry.title item.subTitle = KdbUtil.getUserName(entry) item.obj = entry data.add(item) } return data } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/detail/card/EntryBaseInfoCard.kt ================================================ package com.lyy.keepassa.view.detail.card import android.content.Context import android.text.Html import android.util.AttributeSet import android.view.LayoutInflater import com.arialyy.frame.util.ResUtil import com.google.android.material.card.MaterialCardView import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.R import com.lyy.keepassa.databinding.LayoutEntryCardBaseInfoBinding import com.lyy.keepassa.util.ClipboardUtil import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.doClick import java.util.Date /** * @Author laoyuyu * @Description * @Date 10:19 AM 2023/9/26 **/ class EntryBaseInfoCard(context: Context, attributeSet: AttributeSet) : MaterialCardView(context, attributeSet) { private val binding = LayoutEntryCardBaseInfoBinding.inflate(LayoutInflater.from(context), this, true) fun bindData(entry: PwEntryV4) { val userName = KdbUtil.getUserName(entry) binding.tvUserName.text = userName binding.tvUserName.doClick { ClipboardUtil.get() .copyDataToClip(userName) HitUtil.toaskShort(context.getString(R.string.hint_copy_user)) } handlePass(entry) if (entry.url.isBlank()) { binding.tvUrl.visibility = GONE } else { binding.tvUrl.visibility = VISIBLE binding.tvUrl.text = entry.url binding.tvUrl.doClick { KpaUtil.openUrlWithBrowser(entry.url) } } handleExpires(entry) } /** * 处理过期 */ private fun handleExpires(entry: PwEntryV4) { if (!entry.expires()) { binding.time1.visibility = GONE return } binding.time1.visibility = VISIBLE if (entry.expiryTime.after(Date())) { binding.time1.text = ResUtil.getString(R.string.expire_time, KeepassAUtil.instance.formatTime(entry.expiryTime)) return } binding.time1.text = Html.fromHtml( ResUtil.getString( R.string.expire, KeepassAUtil.instance.formatTime(entry.expiryTime, "yyyy/MM/dd") ) ) } private fun handlePass(entry: PwEntryV4) { val pass = KdbUtil.getPassword(entry) binding.tvPass.text = pass binding.ivEye.isSelected = true binding.ivEye.doClick { binding.ivEye.isSelected = !binding.ivEye.isSelected KpaUtil.handleShowPass(binding.tvPass, !binding.ivEye.isSelected) } binding.tvPass.doClick { ClipboardUtil.get() .copyDataToClip(pass) HitUtil.toaskShort(context.getString(R.string.hint_copy_pass)) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/detail/card/EntryFileCard.kt ================================================ package com.lyy.keepassa.view.detail.card import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.LinearLayoutManager import com.arialyy.frame.util.ResUtil import com.google.android.material.card.MaterialCardView import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.security.ProtectedBinary import com.lyy.keepassa.R import com.lyy.keepassa.base.AbsViewBindingAdapter import com.lyy.keepassa.databinding.LayoutEntryAttachmentBinding import com.lyy.keepassa.databinding.LayoutEntryCardListBinding import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.doOnItemClickListener import com.lyy.keepassa.view.menu.EntryDetailFilePopMenu import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlin.collections.MutableMap.MutableEntry /** * @Author laoyuyu * @Description * @Date 11:12 AM 2023/9/26 **/ class EntryFileCard(context: Context, attributeSet: AttributeSet) : MaterialCardView(context, attributeSet) { private val binding = LayoutEntryCardListBinding.inflate(LayoutInflater.from(context), this, true) companion object { val SAVE_FILE_FLOW = MutableSharedFlow>() } fun bindData(entry: PwEntryV4) { binding.tvCardTitle.text = ResUtil.getString(R.string.attachment) val data = entry.binaries.entries.toMutableList() if (data.isEmpty()) { visibility = GONE return } visibility = VISIBLE handleList(data) } private fun handleList(data: MutableList>) { val adapter = AttachmentAdapter() binding.rvList.apply { this.adapter = adapter setHasFixedSize(true) layoutManager = LinearLayoutManager(context) adapter.setData(data) } binding.rvList.doOnItemClickListener { _, position, v -> val entry = data[position] val pop = EntryDetailFilePopMenu(context as FragmentActivity, v, entry.key, entry.value) pop.setOnDownloadClick(object : EntryDetailFilePopMenu.OnDownloadClick { override fun onDownload(key: String, file: ProtectedBinary) { KpaUtil.scope.launch { SAVE_FILE_FLOW.emit(Pair(key, file)) } } }) pop.show() } } private class AttachmentAdapter : AbsViewBindingAdapter, LayoutEntryAttachmentBinding>() { override fun bindData( binding: LayoutEntryAttachmentBinding, item: MutableEntry ) { binding.value.text = item.key } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/detail/card/EntryNoteCard.kt ================================================ package com.lyy.keepassa.view.detail.card import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.TextView import androidx.core.content.res.ResourcesCompat import com.google.android.material.card.MaterialCardView import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.R import com.lyy.keepassa.databinding.LayoutEntryCardNoteBinding /** * @Author laoyuyu * @Description * @Date 11:03 AM 2023/9/26 **/ class EntryNoteCard(context: Context, attributeSet: AttributeSet) : MaterialCardView(context, attributeSet) { private val binding = LayoutEntryCardNoteBinding.inflate(LayoutInflater.from(context), this, true) fun bindData(entryV4: PwEntryV4) { visibility = if (entryV4.notes.isBlank()) GONE else VISIBLE binding.expandTv.text = entryV4.notes binding.expandTv.findViewById(R.id.expandable_text).typeface = ResourcesCompat.getFont(context, R.font.roboto_thinitalic) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/detail/card/EntryStrCard.kt ================================================ package com.lyy.keepassa.view.detail.card import android.annotation.SuppressLint import android.content.Context import android.os.Build import android.text.InputType import android.text.TextUtils import android.util.AttributeSet import android.view.LayoutInflater import android.widget.TextView import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.LinearLayoutManager import com.arialyy.frame.util.ResUtil import com.google.android.material.card.MaterialCardView import com.keepassdroid.database.PwEntryV4 import com.keepassdroid.database.security.ProtectedString import com.lyy.keepassa.R import com.lyy.keepassa.base.AbsViewBindingAdapter import com.lyy.keepassa.base.KeyConstance import com.lyy.keepassa.databinding.LayoutEntryCardListBinding import com.lyy.keepassa.databinding.LayoutEntryStrBinding import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.util.doClick import com.lyy.keepassa.util.doOnItemClickListener import com.lyy.keepassa.util.hasTOTP import com.lyy.keepassa.util.totp.OtpUtil import com.lyy.keepassa.view.menu.EntryDetailStrPopMenu import com.lyy.keepassa.view.menu.EntryDetailStrPopMenu.OnShowPassCallback import timber.log.Timber import kotlin.collections.Map.Entry /** * @Author laoyuyu * @Description * @Date 11:12 AM 2023/9/26 **/ class EntryStrCard(context: Context, attributeSet: AttributeSet) : MaterialCardView(context, attributeSet) { private val binding = LayoutEntryCardListBinding.inflate(LayoutInflater.from(context), this, true) fun bindData(entry: PwEntryV4) { binding.tvCardTitle.text = ResUtil.getString(R.string.hint_attr) val data = KdbUtil.filterCustomStr(entry).entries.toMutableList() if (data.isEmpty()) { visibility = GONE return } if (entry.hasTOTP()) { val totpPass = OtpUtil.getOtpPass(entry) if (!TextUtils.isEmpty(totpPass.second)) { val totpPassStr = ProtectedString(true, totpPass.second) totpPassStr.isOtpPass = true data.add(object : Entry { override val key: String get() = KeyConstance.TOTP override val value: ProtectedString get() = totpPassStr }) } } data.sortBy { it.key } visibility = VISIBLE handleList(entry, data) } private fun handleList(entryV4: PwEntryV4, data: MutableList>) { val adapter = StrAdapter(entryV4) binding.rvList.apply { this.adapter = adapter setHasFixedSize(true) layoutManager = object : LinearLayoutManager(context) { override fun canScrollVertically(): Boolean { return false } } adapter.setData(data) isNestedScrollingEnabled = false } binding.rvList.doOnItemClickListener { _, position, view -> val tvValue = view.findViewById(R.id.value) val entry = data[position] if (entry.value.toString().isEmpty()) { Timber.e("value is null") return@doOnItemClickListener } val pop = EntryDetailStrPopMenu( context as FragmentActivity, view, entry.value, tvValue.inputType == InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD ) pop.setOnShowPassCallback(object : OnShowPassCallback { override fun showPass(showPass: Boolean) { KpaUtil.handleShowPass(tvValue, showPass) } }) pop.show() } } private class StrAdapter(val entryV4: PwEntryV4) : AbsViewBindingAdapter, LayoutEntryStrBinding>() { private fun handleOtp(binding: LayoutEntryStrBinding, tvValue: TextView) { binding.rpbBar.visibility = VISIBLE KdbUtil.startAutoGetOtp(entryV4, binding.rpbBar, tvValue) binding.ivEye.isVisible = true binding.ivEye.isSelected = true binding.ivEye.doClick { binding.ivEye.isSelected = !binding.ivEye.isSelected KpaUtil.handleShowPass(binding.value, !binding.ivEye.isSelected) } } @SuppressLint("SetTextI18n") override fun bindData(binding: LayoutEntryStrBinding, item: Entry) { binding.title.text = item.key val tvValue = binding.value KpaUtil.handleShowPass(tvValue, !item.value.isProtected) if (item.value.isOtpPass) { handleOtp(binding, tvValue) return } binding.ivEye.isVisible = false binding.rpbBar.isVisible = false if (item.value.toString().isEmpty()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { tvValue.typeface = context.resources.getFont(R.font.roboto_thinitalic) } tvValue.text = "null" return } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { tvValue.typeface = context.resources.getFont(R.font.roboto_regular) } tvValue.text = item.value.toString() } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/detail/card/EntryTagCard.kt ================================================ package com.lyy.keepassa.view.detail.card import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import com.google.android.material.card.MaterialCardView import com.google.android.material.chip.Chip import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.R import com.lyy.keepassa.databinding.LayoutEntryCardTagBinding /** * @Author laoyuyu * @Description * @Date 11:26 AM 2023/9/27 **/ class EntryTagCard(context: Context, attributeSet: AttributeSet) : MaterialCardView(context, attributeSet) { private val layoutInflater = LayoutInflater.from(context) private val binding = LayoutEntryCardTagBinding.inflate(layoutInflater, this, true) fun bindData(pwEntryV4: PwEntryV4) { if (pwEntryV4.tags.isBlank()) { visibility = GONE return } visibility = VISIBLE val tagList = pwEntryV4.tags.split(",") if (binding.chipGroup.childCount > 0){ binding.chipGroup.removeAllViews() } tagList.forEachIndexed { index, s -> buildChip(index, s) } } private fun buildChip(index: Int, tag: String) { val chip = layoutInflater.inflate( R.layout.layout_chip_harvest, binding.chipGroup, false ) as Chip chip.id = index chip.stateListAnimator = null chip.text = tag // chip.setOnCheckedChangeListener(this) binding.chipGroup.addView(chip, index) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/AddMoreDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.content.Context import android.os.Bundle import android.view.View import android.widget.TextView import androidx.appcompat.widget.AppCompatImageView import androidx.recyclerview.widget.LinearLayoutManager import com.arialyy.frame.util.adapter.AbsHolder import com.arialyy.frame.util.adapter.AbsRVAdapter import com.arialyy.frame.util.adapter.RvItemClickSupport import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseBottomSheetDialogFragment import com.lyy.keepassa.databinding.DialogAddMoreBinding import com.lyy.keepassa.entity.SimpleItemEntity import com.lyy.keepassa.util.loadImg import com.lyy.keepassa.view.dialog.AddMoreDialog.Adapter.Holder /** * 添加更多 */ class AddMoreDialog(val data: List) : BaseBottomSheetDialogFragment() { private lateinit var adapter: Adapter private var listener: OnItemClickListener? = null override fun setLayoutId(): Int { return R.layout.dialog_add_more } interface OnItemClickListener { fun onItemClick( position: Int, item: SimpleItemEntity, view: View ) } override fun init(savedInstanceState: Bundle?) { super.init(savedInstanceState) adapter = Adapter(requireContext(), data) binding.list.adapter = adapter binding.list.setHasFixedSize(true) binding.list.layoutManager = LinearLayoutManager(context) RvItemClickSupport.addTo(binding.list) .setOnItemClickListener { _, position, v -> listener?.onItemClick(position, data[position], v) } } fun setOnItemClickListener(listener: OnItemClickListener) { this.listener = listener } fun notifyData() { if (this::adapter.isInitialized) { adapter.notifyDataSetChanged() } } /** * 适配器 */ private class Adapter( context: Context, data: List ) : AbsRVAdapter(context, data) { private class Holder(view: View) : AbsHolder(view) { val img: AppCompatImageView = view.findViewById(R.id.img) val text: TextView = view.findViewById(R.id.text) } override fun getViewHolder( convertView: View?, viewType: Int ): Holder { return Holder(convertView!!) } override fun setLayoutId(type: Int): Int { return R.layout.item_simple } override fun bindData( holder: Holder?, position: Int, item: SimpleItemEntity? ) { holder!!.text.text = item!!.title holder.img.loadImg(item.icon) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/ChooseTagDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.view.View import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.ResUtil import com.keepassdroid.database.PwEntryV4 import com.lyy.keepassa.R import com.lyy.keepassa.base.AbsViewBindingAdapter import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogChooseTagBinding import com.lyy.keepassa.databinding.ItemChooseTagBinding import com.lyy.keepassa.entity.TagBean import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.KdbUtil import com.lyy.keepassa.util.doOnItemClickListener import com.lyy.keepassa.util.loadImg import com.lyy.keepassa.view.create.entry.CreateEntryModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** * @Author laoyuyu * @Description * @Date 10:39 AM 2023/10/26 **/ @Route(path = "/dialog/chooseTag") internal class ChooseTagDialog : BaseDialog() { companion object { val chooseTagFlow = MutableSharedFlow>(1) val ADD_MORE = TagBean(ResUtil.getString(R.string.create_tag)) } private lateinit var module: CreateEntryModule @Autowired(name = "entry") @JvmField var entry: PwEntryV4? = null @Autowired(name = "newTag") @JvmField var newTag: TagBean? = null private val tagList = mutableListOf() override fun setLayoutId(): Int { return R.layout.dialog_choose_tag } override fun initData() { super.initData() ARouter.getInstance().inject(this) module = ViewModelProvider(requireActivity())[CreateEntryModule::class.java] binding.msgTitle = ResUtil.getString(R.string.add_tag) val tagAdapter = TagAdapter() binding.enableEnterBt = true lifecycleScope.launch { binding.rvList.apply { setHasFixedSize(true) layoutManager = LinearLayoutManager(context) adapter = tagAdapter } withContext(Dispatchers.IO){ tagList.addAll(getTagData().toMutableList()) } tagAdapter.setData(tagList) } binding.rvList.doOnItemClickListener { _, position, _ -> val tagBean = tagList[position] if (tagBean == ADD_MORE) { module.cacheTag(tagList) dismiss() Routerfit.create(DialogRouter::class.java).showCreateTagDialog() return@doOnItemClickListener } tagBean.isSet = !tagBean.isSet tagAdapter.notifyItemChanged(position, tagBean.isSet) } binding.clicker = object : DialogBtnClicker { override fun onEnter(v: View) { lifecycleScope.launch { chooseTagFlow.emit(tagList.filter { it.isSet }) dismiss() } } override fun onCancel(v: View) { dismiss() } } } private suspend fun getTagData(): List { val tagList = arrayListOf() val allTagList = KdbUtil.getAllTags() entry?.let { val curTagList = KdbUtil.getEntryTag(it) allTagList.forEach { tag -> tagList.add(TagBean(tag, tag in curTagList || tag in module.selectedTagBeanCache)) } } newTag?.let { if (!allTagList.contains(it.tag)){ tagList.add(it) } } tagList.add(ADD_MORE) return tagList } private class TagAdapter : AbsViewBindingAdapter() { override fun bindData( binding: ItemChooseTagBinding, item: TagBean, payloads: MutableList ) { super.bindData(binding, item, payloads) binding.cb.isChecked = item.isSet } override fun bindData(binding: ItemChooseTagBinding, item: TagBean) { binding.cb.isVisible = item != ADD_MORE binding.tvTitle.text = item.tag binding.ivIcon.loadImg(if (item == ADD_MORE) R.drawable.ic_add_24px else R.drawable.ic_baseline_label_24) binding.cb.isChecked = item.isSet } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/CloudFileListModule.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import androidx.lifecycle.viewModelScope import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.BaseModule import com.lyy.keepassa.entity.CloudServiceInfo import com.lyy.keepassa.util.QuickUnLockUtil import com.lyy.keepassa.util.cloud.CloudFileInfo import com.lyy.keepassa.util.cloud.CloudUtilFactory import com.lyy.keepassa.util.cloud.WebDavUtil import com.lyy.keepassa.view.StorageType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Date /** * 云文件列表module */ class CloudFileListModule : BaseModule() { private val cache = HashMap?>() val upEntry = CloudFileInfo("", "..", Date(), 0, true) val fileListFlow = MutableSharedFlow?>() suspend fun saveWebHistory(uri: String) { withContext(Dispatchers.IO) { // 保存记录 val dao = BaseApp.appDatabase.cloudServiceInfoDao() var data = dao.queryServiceInfo(uri) if (data == null) { data = CloudServiceInfo( userName = QuickUnLockUtil.encryptStr(WebDavUtil.userName), password = QuickUnLockUtil.encryptStr(WebDavUtil.password), cloudPath = uri ) dao.saveServiceInfo(data) return@withContext } data.userName = QuickUnLockUtil.encryptStr(WebDavUtil.userName) data.password = QuickUnLockUtil.encryptStr(WebDavUtil.password) dao.updateServiceInfo(data) } } /** * 获取云盘根路径 */ fun getCloudRootPath(storageType: StorageType): String { return CloudUtilFactory.getCloudUtil(storageType) .getRootPath() } /** * 获取云文件指定路径的文件列表 */ fun getFileList( storageType: StorageType, path: String, isOnlyGetDir: Boolean ) { viewModelScope.launch { val temp = cache[path] if (temp != null && temp.isNotEmpty()) { fileListFlow.emit(temp) return@launch } val data = withContext(Dispatchers.IO) { try { val util = CloudUtilFactory.getCloudUtil(storageType) val list = util.getFileList(path) val tempList = if (isOnlyGetDir) { list?.filter { it.isDir } } else { list?.sortedBy { !it.isDir } } cache[path] = tempList return@withContext tempList } catch (e: Exception) { Timber.e(e) } null } fileListFlow.emit(data) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/CloudFileSelectDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.content.Context import android.content.res.AssetManager import android.text.TextUtils import android.view.KeyEvent import android.view.View import android.widget.ImageView import android.widget.RelativeLayout import android.widget.RelativeLayout.CENTER_VERTICAL import android.widget.TextView import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.util.ResUtil import com.arialyy.frame.util.adapter.AbsHolder import com.arialyy.frame.util.adapter.AbsRVAdapter import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogCloudFileListBinding import com.lyy.keepassa.event.ChangeDbEvent import com.lyy.keepassa.event.CloudFileSelectedEvent import com.lyy.keepassa.util.KeepassAUtil import com.lyy.keepassa.util.cloud.CloudFileInfo import com.lyy.keepassa.util.cloud.DbSynUtil import com.lyy.keepassa.util.cloud.WebDavUtil import com.lyy.keepassa.util.doOnItemClickListener import com.lyy.keepassa.view.StorageType import com.lyy.keepassa.view.StorageType.UNKNOWN import com.lyy.keepassa.view.StorageType.WEBDAV import com.lyy.keepassa.view.dialog.CloudFileSelectDialog.Adapter.Holder import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import timber.log.Timber import java.io.IOException import java.util.Stack /** * 云文件列表 */ @Route(path = "/dialog/cloudFileList") class CloudFileSelectDialog : BaseDialog() { private val curDirList = ArrayList() private lateinit var adapter: Adapter private lateinit var module: CloudFileListModule private val pathStack = Stack() private var lastPath: String = "" val cloudFileSelectFlow = MutableSharedFlow() @Autowired(name = "storageType") @JvmField var storageType: StorageType = UNKNOWN @Autowired(name = "onlyShowDir") @JvmField var onlyGetDir = false override fun setLayoutId(): Int { return R.layout.dialog_cloud_file_list } override fun initData() { super.initData() ARouter.getInstance().inject(this) module = ViewModelProvider(this)[CloudFileListModule::class.java] adapter = Adapter(requireContext(), curDirList) binding.list.adapter = adapter binding.list.setHasFixedSize(true) binding.list.layoutManager = LinearLayoutManager(context) if (onlyGetDir){ binding.title.text = ResUtil.getString(R.string.select_save_path) } handleItemClick() listenerGetFileList() BaseApp.handler.postDelayed({ val rootPath = module.getCloudRootPath(storageType) getFileList(rootPath) }, 200) binding.btnSelect.visibility = if (onlyGetDir) View.VISIBLE else View.GONE binding.btnSelect.setOnClickListener { lifecycleScope.launch { val cloudPath = if (storageType == WEBDAV) "${WebDavUtil.getHostUri()}${lastPath}" else lastPath cloudFileSelectFlow.emit(CloudFileSelectedEvent(!onlyGetDir, cloudPath, storageType)) dismiss() } } } private fun listenerGetFileList() { lifecycleScope.launch { module.fileListFlow.collectLatest { list -> hintLoadView() curDirList.clear() curDirList.add(0, module.upEntry) if (!list.isNullOrEmpty()) { curDirList.addAll(list) } adapter.notifyDataSetChanged() } } } private fun handleItemClick() { binding.list.doOnItemClickListener { _, position, _ -> if (position == 0) { if (pathStack.isEmpty()) { Timber.d(ResUtil.getString(R.string.error_is_root)) return@doOnItemClickListener } lastPath = pathStack.pop() getFileList(lastPath) return@doOnItemClickListener } val item = curDirList[position] if (item.isDir) { pathStack.push(lastPath) lastPath = item.fileKey getFileList(item.fileKey) return@doOnItemClickListener } lifecycleScope.launch { val cloudPath = if (storageType == WEBDAV) "${WebDavUtil.getHostUri()}${item.fileKey}" else item.fileKey Timber.d("couldPath = $cloudPath") if (storageType == WEBDAV) { module.saveWebHistory(cloudPath) } cloudFileSelectFlow.emit(CloudFileSelectedEvent(!onlyGetDir, cloudPath, storageType)) // 选择文件 EventBus.getDefault() .post( ChangeDbEvent( dbName = item.fileName, localFileUri = DbSynUtil.getCloudDbTempPath( storageType.name, item.fileName ), cloudPath = cloudPath, uriType = storageType ) ) dismiss() } } dialog!!.setOnKeyListener { _, keyCode, _ -> if (keyCode == KeyEvent.KEYCODE_BACK && pathStack.size > 0) { lastPath = pathStack.pop() getFileList(lastPath) return@setOnKeyListener true } return@setOnKeyListener false } binding.ivClose.setOnClickListener { dismiss() } } /** * 获取文件列表 */ private fun getFileList(path: String) { showLoadView() val realPath = if (TextUtils.isEmpty(path)) module.getCloudRootPath(storageType) else path binding.path.text = realPath module.getFileList(storageType, realPath, onlyGetDir) } private fun showLoadView() { binding.list.visibility = View.INVISIBLE binding.anim.visibility = View.VISIBLE binding.path.visibility = View.GONE } private fun hintLoadView() { binding.list.visibility = View.VISIBLE // binding.anim.cancelAnimation() binding.anim.visibility = View.GONE binding.path.visibility = View.VISIBLE } /** * 列表adapter */ private class Adapter( context: Context, fileList: List ) : AbsRVAdapter(context, fileList) { private class Holder(view: View) : AbsHolder(view) { val icon: ImageView = view.findViewById(R.id.icon) val title: TextView = view.findViewById(R.id.title) val des: TextView = view.findViewById(R.id.des) } override fun getViewHolder( convertView: View, viewType: Int ): Holder { return Holder(convertView) } override fun setLayoutId(type: Int): Int { return R.layout.item_cloud_file_list } override fun bindData( holder: Holder, position: Int, item: CloudFileInfo ) { holder.title.text = item.fileName if (item.isDir) { holder.des.visibility = View.GONE holder.icon.setImageResource(R.drawable.ic_folder_24px) (holder.title.layoutParams as RelativeLayout.LayoutParams).addRule(CENTER_VERTICAL) } else { (holder.title.layoutParams as RelativeLayout.LayoutParams).removeRule(CENTER_VERTICAL) holder.des.visibility = View.VISIBLE holder.des.text = KeepassAUtil.instance.formatTime(item.serviceModifyDate) holder.icon.setImageResource(R.drawable.ic_file_24px) } } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/CreateTagDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.view.View import android.widget.ArrayAdapter import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.lifecycleScope import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.util.ResUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogCreateTagBinding import com.lyy.keepassa.util.KdbUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** * @Author laoyuyu * @Description * @Date 7:53 PM 2023/10/26 **/ @Route(path = "/dialog/createTag") class CreateTagDialog : BaseDialog() { companion object { val createTagFlow = MutableSharedFlow(0) val tagCacheSet = hashSetOf() } override fun setLayoutId(): Int { return R.layout.dialog_create_tag } override fun initData() { super.initData() ARouter.getInstance().inject(this) binding.msgTitle = ResUtil.getString(R.string.create_tag) binding.clicker = object : DialogBtnClicker { override fun onEnter(v: View) { dismiss() lifecycleScope.launch { val tag = binding.edTag.text.toString().trim() createTagFlow.emit(tag) tagCacheSet.add(tag) } } override fun onCancel(v: View) { dismiss() lifecycleScope.launch { createTagFlow.emit(null) } } } binding.edTag.doAfterTextChanged { binding.enableEnterBt = (it?.length ?: 0) > 0 } handleTagList() } private fun handleTagList() { binding.edTag.threshold = 1 // 设置输入几个字符后开始出现提示 默认是2 binding.edTag.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) { binding.edTag.showDropDown() } } lifecycleScope.launch { if (tagCacheSet.isEmpty()) { withContext(Dispatchers.IO) { tagCacheSet.addAll(KdbUtil.getAllTags()) } } binding.edTag.setAdapter( ArrayAdapter( requireContext(), R.layout.android_simple_dropdown_item_1line, tagCacheSet.toArray() ) ) } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/DialogBtnClicker.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.view.View import androidx.annotation.Keep /** * @Author laoyuyu * @Description * @Date 10:35 AM 2023/10/26 **/ @Keep interface DialogBtnClicker { fun onEnter(v: View){} fun onCancel(v: View){} } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/DonateDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Intent import android.net.Uri import android.view.View import com.arialyy.frame.router.Routerfit import com.arialyy.frame.util.ResUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogDonateBinding import com.lyy.keepassa.router.DialogRouter import com.lyy.keepassa.util.PlayUtil import com.lyy.keepassa.widget.DrawableTextView import com.lyy.keepassa.widget.toPx import com.zzhoujay.richtext.RichText /** * 捐赠对话框 */ class DonateDialog : BaseDialog(), View.OnClickListener { override fun setLayoutId(): Int { return R.layout.dialog_donate } override fun initData() { super.initData() binding.rlAliPay.setOnClickListener(this) binding.rlPayPal.setOnClickListener(this) binding.rlPayPal.setOnClickListener(this) binding.ivClose.setOnClickListener(this) RichText.fromMarkdown(getString(R.string.donate_desc)) .urlClick { url -> startActivity(Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(url) }) return@urlClick true } .into(binding.tvDesc) binding.title.setDrawable( DrawableTextView.LEFT, ResUtil.getSvgIcon(R.drawable.ic_favorite_24px, R.color.text_blue_color), 24.toPx(), 24.toPx() ) if (PlayUtil.playServiceExist(requireActivity())){ binding.rlPlay.visibility = View.VISIBLE } } override fun onClick(v: View?) { when (v?.id) { R.id.rlAliPay -> { startAliPay() } R.id.rlPayPal -> { startActivity(Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("https://www.paypal.com/paypalme/arialyy") }) } R.id.rlPlay ->{ Routerfit.create(DialogRouter::class.java).showPlayDonateDialog() } } dismiss() } private fun startAliPay() { val qrCode = "https://qr.alipay.com/fkx19330ftk0okdlwzdk968" if (startAliPayIntentUrl(qrCode)) { return } startActivity(Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(qrCode) }) } /** * 打开 Intent Scheme Url * * @param qrCodeUrl Intent 跳转地址 * @return 是否成功调用 */ private fun startAliPayIntentUrl( qrCodeUrl: String ): Boolean { return try { val cn = ComponentName( "com.eg.android.AlipayGphone", "com.alipay.mobile.quinox.SchemeLauncherActivity" ) val intent = Intent(Intent.ACTION_VIEW).apply { component = cn data = Uri.parse("alipayqr://platformapi/startapp?saId=10000007&clientVersion=3.7.0.0718&qrcode=${qrCodeUrl}?_s=web-other&_t=${System.currentTimeMillis()}") } startActivity(intent) true } catch (e: ActivityNotFoundException) { false } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/ImgViewerDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.graphics.BitmapFactory import androidx.fragment.app.DialogFragment import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.ToastUtils import com.davemorrissey.labs.subscaleview.ImageSource import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogImgViewerBinding import com.lyy.keepassa.util.isInvalid /** * 图片浏览对话框 */ @Route(path = "/dialog/imgViewer") class ImgViewerDialog() : BaseDialog() { init { setStyle(DialogFragment.STYLE_NORMAL, R.style.FullScreenDialog) } @Autowired(name = "imgByteArray") lateinit var imgByteArray: ByteArray override fun setLayoutId(): Int { return R.layout.dialog_img_viewer } override fun initData() { super.initData() ARouter.getInstance().inject(this) binding.dialog = this val bm = BitmapFactory.decodeByteArray(imgByteArray, 0, imgByteArray.size) if (bm.isInvalid()){ ToastUtils.showLong(ResUtil.getString(R.string.invalid_img)) dismiss() return } binding.imageView.setImage( ImageSource.bitmap(bm) ) } override fun dismiss() { super.dismiss() binding.imageView.recycle() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/LoadingDialog.java ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog; import android.content.res.AssetManager; import android.view.ViewGroup; import androidx.fragment.app.DialogFragment; import com.alibaba.android.arouter.facade.annotation.Route; import com.lyy.keepassa.R; import com.lyy.keepassa.base.BaseApp; import com.lyy.keepassa.base.BaseDialog; import com.lyy.keepassa.databinding.DialogLoadingBinding; import java.io.IOException; import timber.log.Timber; /** * Created by AriaL on 2017/12/15. */ @Route(path = "/dialog/loading") public class LoadingDialog extends BaseDialog { @Override protected void initData() { super.initData(); setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Black_NoTitleBar_Fullscreen); getDialog().getWindow() .setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); setCancelable(false); try { getBinding().anim.setAnimation( requireContext().getAssets().open("loadingAnimation.json", AssetManager.ACCESS_STREAMING), "LottieCache"); } catch (IOException e) { Timber.e(e); } } @Override protected int setLayoutId() { return R.layout.dialog_loading; } public void dismiss(long delay) { if (delay == 0) { super.dismiss(); return; } BaseApp.handler.postDelayed(this::dismiss, delay); } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/ModifyGroupDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.view.View import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.keepassdroid.database.PwGroupV4 import com.keepassdroid.database.PwIconCustom import com.keepassdroid.database.PwIconStandard import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogAddGroupBinding import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.IconUtil import com.lyy.keepassa.util.KpaUtil import com.lyy.keepassa.view.icon.IconBottomSheetDialog import com.lyy.keepassa.view.icon.IconItemCallback /** * 编辑群组 */ @Route(path = "/dialog/modifyGroup") class ModifyGroupDialog : BaseDialog(), View.OnClickListener { private var icon: PwIconStandard = PwIconStandard(1) private var csIcon: PwIconCustom? = null @Autowired(name = "pwGroup") @JvmField var pwGroup: PwGroupV4? = null override fun setLayoutId(): Int { return R.layout.dialog_add_group } override fun initData() { super.initData() ARouter.getInstance().inject(this) if (pwGroup == null) { dismiss() return } binding.groupNameLayout.setEndIconOnClickListener { showIconDialog() } binding.enter.setOnClickListener(this) binding.cancel.setOnClickListener(this) binding.title.text = getString(R.string.modify_group) pwGroup?.let { binding.groupName.setText(it.name) binding.groupNameLayout.endIconDrawable = IconUtil.getGroupIconDrawable(requireContext(), it, true) icon = it.icon csIcon = it.customIcon } } private fun showIconDialog() { val iconDialog = IconBottomSheetDialog() iconDialog.setCallback(object : IconItemCallback { override fun onDefaultIcon(defIcon: PwIconStandard) { icon = defIcon binding.groupNameLayout.endIconDrawable = resources.getDrawable(IconUtil.getIconById(icon.iconId), requireContext().theme) csIcon = PwIconCustom.ZERO } override fun onCustomIcon(customIcon: PwIconCustom) { csIcon = customIcon binding.groupNameLayout.endIconDrawable = IconUtil.convertCustomIcon2Drawable(requireContext(), csIcon!!) } }) iconDialog.show(childFragmentManager, IconBottomSheetDialog::class.java.simpleName) } override fun onClick(v: View?) { when (v!!.id) { R.id.enter -> { val newTitle = binding.groupName.text.toString() .trim() if (newTitle.isEmpty()) { HitUtil.toaskShort(getString(R.string.error_group_name_null)) return } KpaUtil.kdbHandlerService.modifyGroup(newTitle, icon, csIcon, pwGroup!!) { dismiss() } } R.id.cancel -> { dismiss() } } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/ModifyPassDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.text.InputType import android.view.View import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogModifyPassBinding import com.lyy.keepassa.event.ModifyPassEvent import com.lyy.keepassa.util.HitUtil import org.greenrobot.eventbus.EventBus /** * 修改密码对话框 */ class ModifyPassDialog : BaseDialog(), View.OnClickListener { private var isShowPass = false override fun setLayoutId(): Int { return R.layout.dialog_modify_pass } override fun initData() { super.initData() handlePassLayout() binding.enter.setOnClickListener(this) binding.cancel.setOnClickListener(this) } /** * 处理密码 */ private fun handlePassLayout() { binding.passwordLayout.endIconDrawable = resources.getDrawable(R.drawable.ic_view_off) binding.passwordLayout.setEndIconOnClickListener { isShowPass = !isShowPass if (isShowPass) { binding.passwordLayout.endIconDrawable = resources.getDrawable(R.drawable.ic_view) binding.enterPasswordLayout.visibility = View.GONE binding.password.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD } else { binding.passwordLayout.endIconDrawable = resources.getDrawable(R.drawable.ic_view_off) binding.enterPasswordLayout.visibility = View.VISIBLE binding.password.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT } // 将光标移动到最后 binding.password.setSelection(binding.password.text!!.length) binding.password.requestFocus() } } override fun onClick(v: View?) { val pass = binding.password.text.toString() .trim() val enterPass = binding.enterPassword.text.toString() .trim() if (v!!.id == R.id.enter) { if (pass.length < 6) { HitUtil.toaskShort(getString(R.string.error_db_pass_too_short)) return } if (pass.isEmpty()) { HitUtil.toaskShort(getString(R.string.error_pass_null)) return } if (pass.isNotEmpty() && pass != enterPass) { HitUtil.toaskShort(getString(R.string.error_pass_unfit)) return } EventBus.getDefault().post(ModifyPassEvent(pass)) } dismiss() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/MsgDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.annotation.SuppressLint import android.graphics.drawable.Drawable import android.view.KeyEvent import android.view.View import android.widget.Button import androidx.lifecycle.lifecycleScope import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.util.ResUtil import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogMsgBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** * @author laoyuyu * @date 2021/9/5 */ @Route(path = "/dialog/msgDialog") class MsgDialog : BaseDialog(), View.OnClickListener { @Autowired(name = "enterBtTextColor") @JvmField var enterBtTextColor: Int = R.color.text_blue_color @Autowired(name = "cancelBtTextColor") @JvmField var cancelBtTextColor: Int = R.color.text_gray_color @Autowired(name = "coverBtTextColor") @JvmField var coverBtTextColor: Int = R.color.text_blue_color @Autowired(name = "msgTitle") @JvmField var msgTitle: CharSequence = "" @Autowired(name = "msgContent") @JvmField var msgContent: CharSequence = "" @Autowired(name = "showCancelBt") @JvmField var showCancelBt: Boolean = true // 显示取消按钮 @Autowired(name = "showEnterBt") @JvmField var showEnterBt: Boolean = true // 显示确认按钮 @Autowired(name = "showCoverBt") @JvmField var showCoverBt: Boolean = false // 显示覆盖按钮 @Autowired(name = "showCountDownTimer") @JvmField var showCountDownTimer: Pair = Pair(false, 5) // 显示确认按钮倒计时定时器,倒计时5s @Autowired(name = "interceptBackKey") @JvmField var interceptBackKey: Boolean = false // 是否拦截返回键 @Autowired(name = "msgTitleEndIcon") @JvmField var msgTitleEndIcon: Drawable? = null @Autowired(name = "msgTitleStartIcon") @JvmField var msgTitleStartIcon: Drawable? = null @Autowired(name = "enterText") @JvmField var enterText: CharSequence = "" @Autowired(name = "coverText") @JvmField var coverText: CharSequence = "" @Autowired(name = "cancelText") @JvmField var cancelText: CharSequence = "" @Autowired(name = "btnClickListener") @JvmField var btnClickListener: OnMsgBtClickListener? = null override fun setLayoutId(): Int { return R.layout.dialog_msg } override fun initData() { super.initData() ARouter.getInstance().inject(this) msgTitleEndIcon?.let { binding.tvTitle.setEndIcon(it) } msgTitleStartIcon?.let { binding.tvTitle.setLeftIcon(it) } handleCountDown() if (interceptBackKey) { dialog!!.setOnKeyListener { _, keyCode, _ -> return@setOnKeyListener keyCode == KeyEvent.KEYCODE_BACK } } binding.dialog = this } /** * 设置标题左边icon */ fun setTitleStartIcon(icon: Drawable): MsgDialog { msgTitleStartIcon = icon return this } /** * 设置标题右边icon */ fun setTitleEndIcon(icon: Drawable): MsgDialog { msgTitleEndIcon = icon return this } /** * 处理倒计时,倒计时结束前,确认按钮,取消按钮,覆盖按钮都不可选择 */ @SuppressLint("SetTextI18n") private fun handleCountDown() { if (showCountDownTimer.first) { setBtnsEnable(false) lifecycleScope.launch(Dispatchers.Main) { val oldText = binding.enter.text for (i in showCountDownTimer.second downTo 1) { binding.enter.text = "$oldText (${i} s) " withContext(Dispatchers.IO) { delay(1000) } } binding.enter.text = oldText setBtnsEnable(true) } } } private fun setBtnsEnable(enable: Boolean) { val btns = arrayListOf(binding.cover, binding.enter, binding.cancel) for (btn in btns) { if (btn.visibility == View.GONE) { continue } if (enable) { btn.isEnabled = true btn.setTextColor(ResUtil.getColor(R.color.text_blue_color)) btn.background = ResUtil.getDrawable(R.drawable.ripple_white_selector) } else { btn.isEnabled = false btn.setTextColor(ResUtil.getColor(R.color.text_gray_color)) btn.setBackgroundColor(ResUtil.getColor(R.color.transparent)) } } } override fun onClick(v: View?) { when (v!!.id) { R.id.enter -> { btnClickListener?.onEnter(v as Button) } R.id.cancel -> { btnClickListener?.onCancel(v as Button) } R.id.cover -> { btnClickListener?.onCover(v as Button) } } dismiss() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/OnMsgBtClickListener.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog; import android.widget.Button import java.io.Serializable /** * @author laoyuyu * @date 2021/9/5 */ interface OnMsgBtClickListener { fun onCover(v: Button){} fun onEnter(v: Button) fun onCancel(v: Button) } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/PlayDonateDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.view.View import android.view.ViewGroup import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.alibaba.android.arouter.facade.annotation.Route import com.blankj.utilcode.util.ToastUtils import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogPlayDonateBinding import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import timber.log.Timber /** * @Author laoyuyu * @Description * @Date 2022/2/7 **/ @Route(path = "/dialog/playDonate") class PlayDonateDialog : BaseDialog() { private lateinit var module: PlayDonateModule override fun setLayoutId(): Int { return R.layout.dialog_play_donate } override fun onStart() { super.onStart() dialog?.window?.setLayout( resources.getDimension(R.dimen.dialog_min_width).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT ) } override fun initData() { super.initData() module = ViewModelProvider(this)[PlayDonateModule::class.java] binding.dialog = this binding.slider.addOnChangeListener { _, value, _ -> Timber.d("value = $value") module.curIndex = value binding.tvMoney.text = module.convertValue(value) } binding.slider.setLabelFormatter { it -> module.convertValue(it) } lifecycleScope.launch { module.playFlow.collectLatest { if (it == PlayDonateModule.STATE_DEFAULT){ return@collectLatest } val resId = when (it) { PlayDonateModule.STATE_CONNECT_PLAY_SERVICE_ERROR -> { R.string.error_connect_play } PlayDonateModule.STATE_DONATE_SUCCESS -> { dismiss() R.string.thank_donate } PlayDonateModule.STATE_DONATE_FAIL -> { R.string.error_donate } else -> { R.string.error_donate } } ToastUtils.showLong(resId) } } } fun onClick(v: View) { when (v.id) { R.id.enter -> { module.startFlow(requireActivity(), module.curIndex) } R.id.cancel -> { dismiss() } } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/PlayDonateModule.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.app.Activity import androidx.lifecycle.viewModelScope import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.BillingClient.SkuType import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingResult import com.android.billingclient.api.ConsumeParams import com.android.billingclient.api.Purchase import com.android.billingclient.api.Purchase.PurchaseState import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.SkuDetails import com.android.billingclient.api.SkuDetailsParams import com.android.billingclient.api.SkuDetailsResult import com.android.billingclient.api.consumePurchase import com.android.billingclient.api.querySkuDetails import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.ToastUtils import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.BaseModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber /** * @Author laoyuyu * @Description https://developer.android.com/google/play/billing/integrate?hl=zh-cn#fetch * @Date 2:32 下午 2022/2/7 **/ class PlayDonateModule : BaseModule() { companion object{ const val STATE_CONNECT_PLAY_SERVICE_ERROR = -1 const val STATE_DONATE_SUCCESS = 0 const val STATE_DONATE_FAIL = 1 const val STATE_DEFAULT = 999 } private var isConnected = false private var skuResult: SkuDetailsResult? = null var curIndex = 1f val playFlow = MutableStateFlow(STATE_DEFAULT) private val skuList = arrayListOf().apply { add("d1_cddobn39u5ugvvvsn2fqvd5ktzfhnxqr") add("d5_wgi5ukrzfj4259m77s2ymn2fkzy5cvgc") add("d10_5h35r3juggi7pwyiy4mqi7zgbehd9kdb") add("d20_wswi2fefwoaswb9u9bmmk35vhd9rhpsy") add("d50_942stx4ouh4dk7suz7j9yrvjpwtwi9np") } private val skuDetailMap = hashMapOf() /** * 购买回调 */ private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> // To be implemented in a later section. Timber.d("result = ${billingResult}, purchases = $purchases") if (billingResult.responseCode == BillingResponseCode.OK && purchases != null) { Timber.d("purchases success") for (purchase in purchases) { handlePurchase(purchase) } } else if (billingResult.responseCode == BillingResponseCode.USER_CANCELED) { Timber.d("cancel") // Handle an error caused by a user cancelling the purchase flow. } else { // Handle any other error codes. } } private var billingClient = BillingClient.newBuilder(BaseApp.APP) .setListener(purchasesUpdatedListener) .enablePendingPurchases() .build() /** * 确认非消耗型商品的购买交易 */ private fun handlePurchase(purchase: Purchase) { if (purchase.purchaseState == PurchaseState.PURCHASED) { // if (!purchase.isAcknowledged) { // val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() // .setPurchaseToken(purchase.purchaseToken) // billingClient.acknowledgePurchase(acknowledgePurchaseParams.build()) { // Timber.d("code = ${it.responseCode}") // } // } val consumeParams = ConsumeParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() viewModelScope.launch { val consumeResult = withContext(Dispatchers.IO) { billingClient.consumePurchase(consumeParams) } playFlow.emit(STATE_DONATE_SUCCESS) } } } /** * 1、查询sku * 2、进行支付 */ fun startFlow(ac: Activity, value: Float) { if (isConnected) { startQuerySdk(ac, value) return } connectPlay { if (!it){ viewModelScope.launch { playFlow.emit(STATE_CONNECT_PLAY_SERVICE_ERROR) } return@connectPlay } if (it) { startQuerySdk(ac, value) } } } private fun startQuerySdk(ac: Activity, value: Float) { viewModelScope.launch { queryInappSkuDetails() val curSku = skuList[value.toInt() - 1] Timber.d("curSku = $curSku") val skuDetail = skuDetailMap[curSku] if (skuDetail == null) { playFlow.emit(STATE_DONATE_FAIL) return@launch } startPlayFlow(ac, skuDetail) } } /** * 开始支付流程 */ private fun startPlayFlow(ac: Activity, skuDetail: SkuDetails) { val flowParams = BillingFlowParams.newBuilder() .setSkuDetails(skuDetail) .build() val responseCode = billingClient.launchBillingFlow(ac, flowParams).responseCode if (responseCode != BillingResponseCode.OK){ viewModelScope.launch { playFlow.emit(STATE_DONATE_FAIL) } } Timber.d("responseCode = $responseCode") } /** * connect to play */ private fun connectPlay(callback: (Boolean) -> Unit) { billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingResponseCode.OK) { // The BillingClient is ready. You can query purchases here. isConnected = true callback.invoke(true) return } ToastUtils.showLong(ResUtil.getString(R.string.error_connect_play)) callback.invoke(false) } override fun onBillingServiceDisconnected() { // Try to restart the connection on the next request to // Google Play by calling the startConnection() method. isConnected = false } }) } /** * 普通商品 */ suspend fun queryInappSkuDetails(): SkuDetailsResult? { if (skuResult != null) { return skuResult } val params = SkuDetailsParams.newBuilder() params.setSkusList(skuList).setType(SkuType.INAPP) // leverage querySkuDetails Kotlin extension function return withContext(Dispatchers.IO) { skuResult = billingClient.querySkuDetails(params.build()) val skuList = skuResult?.skuDetailsList skuList?.forEach { skuDetailMap[it.sku] = it } // val skuResult = skuResult.billingResult Timber.d("get inapp sku success, size = ${skuList?.size}") return@withContext skuResult } // Process the result. } fun convertValue(value: Float): String { return when (value.toInt()) { 1 -> { "$1" } 2 -> { "$5" } 3 -> { "$10" } 4 -> { "$20" } 5 -> { "$50" } else -> { "$1" } } } override fun onCleared() { super.onCleared() billingClient.endConnection() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/TimeChangeDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.DatePicker import android.widget.FrameLayout import android.widget.TimePicker import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.viewpager2.adapter.FragmentStateAdapter import com.alibaba.android.arouter.facade.annotation.Route import com.arialyy.frame.util.DpUtils import com.google.android.material.tabs.TabLayoutMediator import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogTimerBinding import com.lyy.keepassa.event.TimeEvent import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch /** * 时间选择器 */ @Route(path = "/dialog/timeChange") class TimeChangeDialog : BaseDialog(), View.OnClickListener { private lateinit var vpAdapter: VpAdapter private val fragments = arrayListOf() companion object { val timeFlow = MutableStateFlow(null) } override fun setLayoutId(): Int { return R.layout.dialog_timer } override fun initData() { super.initData() val titles = listOf(getString(R.string.date), getString(R.string.time)) fragments.add(DatePickerFragment()) fragments.add(TimerPickerFragment()) vpAdapter = VpAdapter(fragments, this) binding.vp.adapter = vpAdapter binding.vp.offscreenPageLimit = 2 TabLayoutMediator(binding.tabLayout, binding.vp) { tab, position -> tab.text = titles[position] }.attach() binding.cancel.setOnClickListener(this) binding.save.setOnClickListener(this) } override fun onClick(v: View?) { when (v!!.id) { R.id.cancel -> dismiss() R.id.save -> { val date = (fragments[0] as DatePickerFragment).datePicker val time = (fragments[1] as TimerPickerFragment).timerPicker val event: TimeEvent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { TimeEvent( date.year, date.month + 1, date.dayOfMonth, time.currentHour, time.currentMinute ) } else { TimeEvent(date.year, date.month + 1, date.dayOfMonth, time.hour, time.minute) } lifecycleScope.launch { timeFlow.emit(event) } dismiss() } } } override fun onStart() { super.onStart() dialog?.window?.setLayout(DpUtils.dp2px(280), DpUtils.dp2px(530)) } private class VpAdapter( private val fragments: List, fm: Fragment ) : FragmentStateAdapter(fm) { override fun getItemCount(): Int { return fragments.size } override fun createFragment(position: Int): Fragment { return fragments[position] } } class DatePickerFragment : Fragment() { val datePicker by lazy { DatePicker(requireContext()) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { datePicker.layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT ) return datePicker } } class TimerPickerFragment : Fragment() { val timerPicker by lazy { TimePicker(requireContext()) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { timerPicker.layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT ) return timerPicker } } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/TipsDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.text.SpannableStringBuilder import com.alibaba.android.arouter.facade.annotation.Route import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.SpanUtils import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.base.KeyConstance import com.lyy.keepassa.databinding.DialogTipBinding import com.lyy.keepassa.util.CommonKVStorage import com.lyy.keepassa.util.doClick import com.zzhoujay.richtext.RichText /** * @Author laoyuyu * @Description * @Date 4:05 PM 2023/5/10 **/ @Route(path = "/dialog/tipsDialog") class TipsDialog : BaseDialog() { override fun setLayoutId(): Int { return R.layout.dialog_tip } override fun initData() { super.initData() val vector = ResUtil.getSvgIcon(R.drawable.ic_lightbulb_on, R.color.colorPrimary) binding.msgTitle = ResUtil.getString(R.string.title_tip_of_day) binding.includeLayout.tvTitle.setLeftIcon(vector!!) binding.cancel.doClick { dismiss() } binding.cbShow.isChecked = CommonKVStorage.getBoolean(KeyConstance.KEY_DONT_SHOW_TIP, false) binding.cbShow.setOnCheckedChangeListener { _, isChecked -> CommonKVStorage.put(KeyConstance.KEY_DONT_SHOW_TIP, isChecked) } bindingContent() } private fun bindingContent() { getDarkStr() } private fun getDarkStr(): SpannableStringBuilder { return SpanUtils.with(binding.tvContent) .append("夜间模式\n") .setBold() .setFontSize(16, true) .append("应用设置->UI设置->主题风格\n") .setFontSize(14, true) .appendImage(R.drawable.tip_1_0) .append("选择夜间模式\n") .setFontSize(14, true) .appendImage(R.drawable.tip_1_1) .create() } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/WebDavLoginModule.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog import android.content.Context import androidx.lifecycle.liveData import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseApp import com.lyy.keepassa.base.BaseModule import com.lyy.keepassa.entity.CloudServiceInfo import com.lyy.keepassa.util.HitUtil import com.lyy.keepassa.util.QuickUnLockUtil import com.lyy.keepassa.util.cloud.WebDavUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext import timber.log.Timber class WebDavLoginModule : BaseModule() { var curWebDavServer: String? = null fun isNextcloud() = curWebDavServer == WebDavUtil.SUPPORTED_WEBDAV_URLS[3] fun isOtherServer() = curWebDavServer == WebDavUtil.SUPPORTED_WEBDAV_URLS[WebDavUtil.SUPPORTED_WEBDAV_URLS.size - 1] fun isJGY() = curWebDavServer == WebDavUtil.SUPPORTED_WEBDAV_URLS[0] fun convertHost(hostName: String, port: String, userName: String): String { val hasHttp = hostName.startsWith("http", true) val isHttps = hostName.startsWith("https", true) || port == "443" var temp = hostName if (hasHttp) { val index = hostName.indexOf("://") if (index != -1) { temp = hostName.substring(index + 2, hostName.length) } } return "http${if (isHttps) "s" else ""}://${temp}${if (port.isEmpty()) "" else ":${port}"}/remote.php/dav/files/${userName}/" } fun checkLogin( uri: String, userName: String, pass: String, isPreemptive:Boolean ) = flow { val b = withContext(Dispatchers.IO) { return@withContext WebDavUtil.checkLogin(uri, userName, pass, isPreemptive) } emit(b) } /** * 检查登录状态 * 如果是创建数据库,不考虑文件是否存在 * 如果是打开云端数据,如果文件不存在,则表示登录失败 */ fun checkLogin( context: Context, uri: String, userName: String, pass: String ) = liveData { val success = withContext(Dispatchers.IO) { var isSuccess = false try { WebDavUtil.login(uri, userName, pass) val b = WebDavUtil.getFileInfo(uri) != null if (!b) { HitUtil.toaskShort(context.getString(R.string.db_file_no_exist)) return@withContext false } // 保存记录 val dao = BaseApp.appDatabase.cloudServiceInfoDao() var data = dao.queryServiceInfo(uri) if (data == null) { data = CloudServiceInfo( userName = QuickUnLockUtil.encryptStr(userName), password = QuickUnLockUtil.encryptStr(pass), cloudPath = uri ) dao.saveServiceInfo(data) } else { data.userName = QuickUnLockUtil.encryptStr(userName) data.password = QuickUnLockUtil.encryptStr(pass) dao.updateServiceInfo(data) } isSuccess = true } catch (e: Exception) { Timber.e(e) } return@withContext isSuccess } emit(success) } } ================================================ FILE: app/src/main/java/com/lyy/keepassa/view/dialog/otp/CreateOtpDialog.kt ================================================ /* * Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ package com.lyy.keepassa.view.dialog.otp import android.view.View import android.view.ViewGroup import android.view.ViewStub import android.widget.AdapterView import android.widget.Button import android.widget.RadioButton import android.widget.RadioGroup import android.widget.Spinner import androidx.constraintlayout.widget.Group import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.arialyy.frame.util.ResUtil import com.blankj.utilcode.util.ToastUtils import com.google.android.material.slider.Slider import com.google.android.material.textfield.TextInputEditText import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanIntentResult import com.journeyapps.barcodescanner.ScanOptions import com.lyy.keepassa.R import com.lyy.keepassa.base.BaseDialog import com.lyy.keepassa.databinding.DialogCreateTotpBinding import com.lyy.keepassa.entity.GoogleOtpBean import com.lyy.keepassa.entity.KeepassXcBean import com.lyy.keepassa.entity.TotpType import com.lyy.keepassa.entity.TotpType.CUSTOM import com.lyy.keepassa.entity.TotpType.DEFAULT import com.lyy.keepassa.entity.TotpType.STEAM import com.lyy.keepassa.entity.TrayTotpBean import com.lyy.keepassa.util.totp.OtpEnum import com.lyy.keepassa.util.totp.OtpEnum.GOOGLE_OTP import com.lyy.keepassa.util.totp.OtpEnum.KEEPASSXC import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm import com.lyy.keepassa.view.QrCodeScannerActivity import com.lyy.keepassa.widget.toPx import kotlinx.coroutines.launch import timber.log.Timber @Route(path = "/dialog/createOtp") internal class CreateOtpDialog : BaseDialog(), View.OnClickListener { private var arithmetic = HashAlgorithm.SHA1 private var time = 30 private var len = 6 private var totpType = DEFAULT private var secret = "" private lateinit var module: CreateOtpModule @Autowired(name = "entryTitle") @JvmField var entryTitle: String = "title" @Autowired(name = "entryUserName") @JvmField var entryUserName: String = "name" private val barcodeLauncher = registerForActivityResult(ScanContract()) { result: ScanIntentResult -> val str = result.contents if (str == null) { ToastUtils.showLong("Cancelled") return@registerForActivityResult } Timber.d("contents = ${str}") if (!str.startsWith("otpauth://")) { ToastUtils.showLong(ResUtil.getString(R.string.error_qr_code_str)) return@registerForActivityResult } lifecycleScope.launch { CreateOtpModule.otpFlow.emit( Pair(GOOGLE_OTP, GoogleOtpBean(str)) ) } dismiss() } override fun setLayoutId(): Int { return R.layout.dialog_create_totp } override fun initData() { super.initData() ARouter.getInstance().inject(this) module = ViewModelProvider(requireActivity())[CreateOtpModule::class.java] handleOptionSwitch() } private fun handleOptionSwitch() { binding.menuLayout.ivNormal.setOnClickListener(this) binding.menuLayout.ivQrCode.setOnClickListener(this) } /** * handle default create otp ui */ private fun handleDefaultFlow() { val vs = binding.root.findViewById(R.id.vsCustom) vs.setOnInflateListener { _, parent -> val clConstom = findViewById(R.id.group) findViewById