From 34613b29fa73797db68c86d1dc5e3dc95272c29b Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Thu, 19 Jan 2023 00:04:59 +0530 Subject: [PATCH] copied xml code to compose module (#305) --- .../drawable/ic_twotone_content_copy_24.xml | 0 .../ic_twotone_qr_code_scanner_24.xml | 0 .../res/drawable/ic_twotone_totp.xml | 0 .../common}/utils/AnimationUtils.kt | 0 keypasscompose/build.gradle | 66 ++++ keypasscompose/src/main/AndroidManifest.xml | 50 ++- .../keypasscompose/MainActivity.kt | 89 ----- .../keypasscompose/MyApplication.kt | 20 ++ .../custom_views/MaskedCardView.kt | 47 +++ .../keypasscompose/data/MyAccountModel.kt | 36 ++ .../listener/AccountsClickListener.kt | 14 + .../listener/UniversalClickListener.kt | 13 + .../keypasscompose/ui/CrashActivity.kt | 65 ++++ .../ui/addTOTP/AddTOTPActivity.kt | 139 ++++++++ .../ui/addTOTP/AddTOTPViewModel.kt | 85 +++++ .../ui/addTOTP/ScannerActivity.kt | 55 +++ .../ui/auth/AuthenticationActivity.kt | 133 +++++++ .../ui/backup/BackupActivity.kt | 332 ++++++++++++++++++ .../ui/detail/DetailActivity.kt | 132 +++++++ .../ui/detail/DetailViewModel.kt | 68 ++++ .../ui/generate/GeneratePasswordActivity.kt | 47 +++ .../ui/home/DashboardViewModel.kt | 57 +++ .../keypasscompose/ui/home/HomeFragment.kt | 121 +++++++ .../ui/nav/BottomNavDrawerFragment.kt | 173 +++++++++ .../ui/nav/BottomNavViewModel.kt | 67 ++++ .../ui/nav/BottomNavigationDrawerCallback.kt | 92 +++++ .../ui/nav/DashboardActivity.kt | 233 ++++++++++++ .../ui/nav/NavigationAdapter.kt | 89 +++++ .../keypasscompose/ui/nav/NavigationModel.kt | 34 ++ .../ui/nav/NavigationModelItem.kt | 63 ++++ .../ui/nav/NavigationViewHolder.kt | 51 +++ .../keypasscompose/ui/nav/OnSlideAction.kt | 45 +++ .../ui/nav/OnStateChangedAction.kt | 90 +++++ .../ui/settings/MySettingsFragment.kt | 203 +++++++++++ .../keypasscompose/ui/theme/Color.kt | 33 -- .../ui/theme/Material3Components.kt | 70 ---- .../keypasscompose/ui/theme/Shape.kt | 1 - .../keypasscompose/ui/theme/Theme.kt | 51 --- .../keypasscompose/ui/theme/Type.kt | 28 -- .../keypasscompose/utils/BindingAdapter.kt | 219 ++++++++++++ .../utils/ContentViewBindingDelegate.kt | 48 +++ .../keypasscompose/utils/LogHelper.kt | 49 +++ .../keypasscompose/utils/StringDiffUtil.kt | 12 + .../keypasscompose/utils/ViewExtensions.kt | 30 ++ .../color_navigation_drawer_menu_item.xml | 5 + .../color_on_primary_surface_divider.xml | 4 + ...lor_on_primary_surface_emphasis_medium.xml | 4 + .../color/color_on_surface_emphasis_high.xml | 4 + .../color_on_surface_emphasis_medium.xml | 4 + .../src/main/res/drawable/asl_add_save.xml | 37 ++ .../src/main/res/drawable/avatar_none.xml | 5 + .../src/main/res/drawable/avd_add_to_save.xml | 126 +++++++ .../src/main/res/drawable/avd_save_to_add.xml | 127 +++++++ .../ic_baseline_arrow_back_ios_24.xml | 11 + .../res/drawable/ic_baseline_casino_24.xml | 10 + .../res/drawable/ic_baseline_feedback_24.xml | 10 + .../res/drawable/ic_baseline_refresh_24.xml | 10 + .../res/drawable/ic_baseline_settings_24.xml | 10 + .../res/drawable/ic_baseline_share_24.xml | 10 + .../src/main/res/drawable/ic_round_add_24.xml | 10 + .../main/res/drawable/ic_round_done_24.xml | 10 + .../main/res/drawable/ic_round_menu_24.xml | 10 + .../main/res/drawable/ic_round_refresh_24.xml | 10 + .../drawable/ic_twotone_content_copy_24.xml | 15 + .../res/drawable/ic_twotone_delete_24.xml | 15 + .../main/res/drawable/ic_twotone_folder.xml | 14 + .../main/res/drawable/ic_twotone_home_24.xml | 15 + .../ic_twotone_qr_code_scanner_24.xml | 10 + .../src/main/res/drawable/ic_twotone_totp.xml | 10 + .../res/drawable/ic_twotone_vpn_key_24.xml | 15 + .../drawable/ic_undraw_empty_street_sfxm.xml | 204 +++++++++++ .../res/drawable/ic_undraw_unlock_24mb.xml | 194 ++++++++++ .../src/main/res/drawable/nav_divider_top.xml | 6 + .../src/main/res/drawable/white_circle.xml | 13 + .../src/main/res/font/work_sans.xml | 7 + .../src/main/res/font/work_sans_medium.xml | 7 + .../res/layout/activity_add_totpactivity.xml | 78 ++++ .../res/layout/activity_authentication.xml | 31 ++ .../src/main/res/layout/activity_crash.xml | 39 ++ .../main/res/layout/activity_dashboard.xml | 104 ++++++ .../res/layout/activity_generate_password.xml | 104 ++++++ .../src/main/res/layout/activity_scanner.xml | 28 ++ .../src/main/res/layout/backup_activity.xml | 25 ++ .../res/layout/fragment_bottom_nav_drawer.xml | 46 +++ .../src/main/res/layout/fragment_detail.xml | 217 ++++++++++++ .../src/main/res/layout/fragment_home.xml | 20 ++ .../src/main/res/layout/item_accounts.xml | 96 +++++ .../src/main/res/layout/item_totp.xml | 118 +++++++ .../res/layout/layout_backup_keypharse.xml | 51 +++ .../res/layout/layout_custom_keypharse.xml | 60 ++++ .../main/res/layout/layout_no_accounts.xml | 38 ++ .../res/layout/layout_restore_keypharse.xml | 58 +++ .../layout/multidataset_service_list_item.xml | 38 ++ .../res/layout/nav_divider_item_layout.xml | 36 ++ .../layout/nav_email_folder_item_layout.xml | 48 +++ .../main/res/layout/nav_menu_item_layout.xml | 48 +++ .../src/main/res/menu/bottom_app_bar.xml | 5 + .../main/res/menu/bottom_app_bar_detail.xml | 8 + .../res/menu/bottom_app_bar_settings_menu.xml | 8 + .../src/main/res/menu/menu_delete.xml | 6 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 - .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 1404 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2898 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 982 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 1772 -> 0 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 1900 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3918 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 2884 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 5914 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 3844 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 7778 -> 0 bytes .../main/res/navigation/navigation_graph.xml | 51 +++ .../src/main/res/values-hi/strings.xml | 86 +++++ .../src/main/res/values-night/themes.xml | 41 ++- .../src/main/res/values-pt-rBR/strings.xml | 84 +++++ .../src/main/res/values-zh-rCN/strings.xml | 85 +++++ keypasscompose/src/main/res/values/arrays.xml | 4 + keypasscompose/src/main/res/values/attrs.xml | 21 ++ keypasscompose/src/main/res/values/colors.xml | 37 ++ keypasscompose/src/main/res/values/dimens.xml | 28 ++ .../src/main/res/values/elevation.xml | 19 + .../src/main/res/values/font_certs.xml | 17 + keypasscompose/src/main/res/values/ids.xml | 18 + keypasscompose/src/main/res/values/layout.xml | 29 ++ keypasscompose/src/main/res/values/motion.xml | 8 + .../src/main/res/values/preloaded_fonts.xml | 7 + keypasscompose/src/main/res/values/shapes.xml | 31 ++ .../src/main/res/values/strings.xml | 84 ++++- .../main/res/values/strings_no_translate.xml | 16 + .../src/main/res/values/style_text.xml | 21 ++ keypasscompose/src/main/res/values/styles.xml | 24 ++ keypasscompose/src/main/res/values/themes.xml | 80 ++++- keypasscompose/src/main/res/values/type.xml | 84 +++++ .../src/main/res/xml/backup_preferences.xml | 54 +++ .../src/main/res/xml/preferences.xml | 37 ++ keypasscompose/src/main/res/xml/shortcuts.xml | 19 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1582 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 1565 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3443 bytes .../res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1095 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1076 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2149 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2012 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 2120 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 4728 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3113 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 3495 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 7561 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 4484 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 4874 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 10953 bytes .../res/values/ic_launcher_background.xml | 4 + .../src/production/res/values/strings.xml | 5 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../staging/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1495 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 1565 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 2762 bytes .../staging/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1027 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1076 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 1771 bytes .../staging/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 1906 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 2120 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 3661 bytes .../staging/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 2868 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 3495 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 5931 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 4149 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 4874 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 8522 bytes .../res/values/ic_launcher_background.xml | 4 + .../src/staging/res/values/strings.xml | 5 + 175 files changed, 6216 insertions(+), 319 deletions(-) rename app/src/{staging => main}/res/drawable/ic_twotone_content_copy_24.xml (100%) rename app/src/{staging => main}/res/drawable/ic_twotone_qr_code_scanner_24.xml (100%) rename app/src/{staging => main}/res/drawable/ic_twotone_totp.xml (100%) rename {app/src/main/java/com/yogeshpaliyal/keypass => common/src/main/java/com/yogeshpaliyal/common}/utils/AnimationUtils.kt (100%) delete mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MainActivity.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MyApplication.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/custom_views/MaskedCardView.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/data/MyAccountModel.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/AccountsClickListener.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/UniversalClickListener.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/CrashActivity.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPActivity.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPViewModel.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/ScannerActivity.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/auth/AuthenticationActivity.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/backup/BackupActivity.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailActivity.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailViewModel.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/generate/GeneratePasswordActivity.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/DashboardViewModel.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/HomeFragment.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavDrawerFragment.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavViewModel.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavigationDrawerCallback.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/DashboardActivity.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationAdapter.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModel.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModelItem.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationViewHolder.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnSlideAction.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnStateChangedAction.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/settings/MySettingsFragment.kt delete mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Color.kt delete mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Material3Components.kt delete mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Shape.kt delete mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Theme.kt delete mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Type.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/BindingAdapter.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ContentViewBindingDelegate.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/LogHelper.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/StringDiffUtil.kt create mode 100644 keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ViewExtensions.kt create mode 100644 keypasscompose/src/main/res/color/color_navigation_drawer_menu_item.xml create mode 100644 keypasscompose/src/main/res/color/color_on_primary_surface_divider.xml create mode 100644 keypasscompose/src/main/res/color/color_on_primary_surface_emphasis_medium.xml create mode 100644 keypasscompose/src/main/res/color/color_on_surface_emphasis_high.xml create mode 100644 keypasscompose/src/main/res/color/color_on_surface_emphasis_medium.xml create mode 100644 keypasscompose/src/main/res/drawable/asl_add_save.xml create mode 100644 keypasscompose/src/main/res/drawable/avatar_none.xml create mode 100644 keypasscompose/src/main/res/drawable/avd_add_to_save.xml create mode 100644 keypasscompose/src/main/res/drawable/avd_save_to_add.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_baseline_casino_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_baseline_feedback_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_baseline_refresh_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_baseline_settings_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_baseline_share_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_round_add_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_round_done_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_round_menu_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_round_refresh_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_twotone_content_copy_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_twotone_delete_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_twotone_folder.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_twotone_home_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_twotone_qr_code_scanner_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_twotone_totp.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_twotone_vpn_key_24.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_undraw_empty_street_sfxm.xml create mode 100644 keypasscompose/src/main/res/drawable/ic_undraw_unlock_24mb.xml create mode 100644 keypasscompose/src/main/res/drawable/nav_divider_top.xml create mode 100644 keypasscompose/src/main/res/drawable/white_circle.xml create mode 100644 keypasscompose/src/main/res/font/work_sans.xml create mode 100644 keypasscompose/src/main/res/font/work_sans_medium.xml create mode 100644 keypasscompose/src/main/res/layout/activity_add_totpactivity.xml create mode 100644 keypasscompose/src/main/res/layout/activity_authentication.xml create mode 100644 keypasscompose/src/main/res/layout/activity_crash.xml create mode 100644 keypasscompose/src/main/res/layout/activity_dashboard.xml create mode 100644 keypasscompose/src/main/res/layout/activity_generate_password.xml create mode 100644 keypasscompose/src/main/res/layout/activity_scanner.xml create mode 100644 keypasscompose/src/main/res/layout/backup_activity.xml create mode 100644 keypasscompose/src/main/res/layout/fragment_bottom_nav_drawer.xml create mode 100644 keypasscompose/src/main/res/layout/fragment_detail.xml create mode 100644 keypasscompose/src/main/res/layout/fragment_home.xml create mode 100644 keypasscompose/src/main/res/layout/item_accounts.xml create mode 100644 keypasscompose/src/main/res/layout/item_totp.xml create mode 100644 keypasscompose/src/main/res/layout/layout_backup_keypharse.xml create mode 100644 keypasscompose/src/main/res/layout/layout_custom_keypharse.xml create mode 100644 keypasscompose/src/main/res/layout/layout_no_accounts.xml create mode 100644 keypasscompose/src/main/res/layout/layout_restore_keypharse.xml create mode 100644 keypasscompose/src/main/res/layout/multidataset_service_list_item.xml create mode 100644 keypasscompose/src/main/res/layout/nav_divider_item_layout.xml create mode 100644 keypasscompose/src/main/res/layout/nav_email_folder_item_layout.xml create mode 100644 keypasscompose/src/main/res/layout/nav_menu_item_layout.xml create mode 100644 keypasscompose/src/main/res/menu/bottom_app_bar.xml create mode 100644 keypasscompose/src/main/res/menu/bottom_app_bar_detail.xml create mode 100644 keypasscompose/src/main/res/menu/bottom_app_bar_settings_menu.xml create mode 100644 keypasscompose/src/main/res/menu/menu_delete.xml delete mode 100644 keypasscompose/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 keypasscompose/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 keypasscompose/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 keypasscompose/src/main/res/mipmap-hdpi/ic_launcher_round.webp delete mode 100644 keypasscompose/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 keypasscompose/src/main/res/mipmap-mdpi/ic_launcher_round.webp delete mode 100644 keypasscompose/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 keypasscompose/src/main/res/mipmap-xhdpi/ic_launcher_round.webp delete mode 100644 keypasscompose/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 keypasscompose/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp delete mode 100644 keypasscompose/src/main/res/mipmap-xxxhdpi/ic_launcher.webp delete mode 100644 keypasscompose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 keypasscompose/src/main/res/navigation/navigation_graph.xml create mode 100644 keypasscompose/src/main/res/values-hi/strings.xml create mode 100644 keypasscompose/src/main/res/values-pt-rBR/strings.xml create mode 100644 keypasscompose/src/main/res/values-zh-rCN/strings.xml create mode 100644 keypasscompose/src/main/res/values/arrays.xml create mode 100644 keypasscompose/src/main/res/values/attrs.xml create mode 100644 keypasscompose/src/main/res/values/dimens.xml create mode 100644 keypasscompose/src/main/res/values/elevation.xml create mode 100644 keypasscompose/src/main/res/values/font_certs.xml create mode 100644 keypasscompose/src/main/res/values/ids.xml create mode 100644 keypasscompose/src/main/res/values/layout.xml create mode 100644 keypasscompose/src/main/res/values/motion.xml create mode 100644 keypasscompose/src/main/res/values/preloaded_fonts.xml create mode 100644 keypasscompose/src/main/res/values/shapes.xml create mode 100644 keypasscompose/src/main/res/values/strings_no_translate.xml create mode 100644 keypasscompose/src/main/res/values/style_text.xml create mode 100644 keypasscompose/src/main/res/values/styles.xml create mode 100644 keypasscompose/src/main/res/values/type.xml create mode 100644 keypasscompose/src/main/res/xml/backup_preferences.xml create mode 100644 keypasscompose/src/main/res/xml/preferences.xml create mode 100644 keypasscompose/src/main/res/xml/shortcuts.xml create mode 100644 keypasscompose/src/production/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 keypasscompose/src/production/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 keypasscompose/src/production/res/mipmap-hdpi/ic_launcher.png create mode 100644 keypasscompose/src/production/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 keypasscompose/src/production/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 keypasscompose/src/production/res/mipmap-mdpi/ic_launcher.png create mode 100644 keypasscompose/src/production/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 keypasscompose/src/production/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 keypasscompose/src/production/res/mipmap-xhdpi/ic_launcher.png create mode 100644 keypasscompose/src/production/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 keypasscompose/src/production/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 keypasscompose/src/production/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 keypasscompose/src/production/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 keypasscompose/src/production/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 keypasscompose/src/production/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 keypasscompose/src/production/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 keypasscompose/src/production/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 keypasscompose/src/production/res/values/ic_launcher_background.xml create mode 100644 keypasscompose/src/production/res/values/strings.xml create mode 100644 keypasscompose/src/staging/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 keypasscompose/src/staging/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 keypasscompose/src/staging/res/mipmap-hdpi/ic_launcher.png create mode 100644 keypasscompose/src/staging/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 keypasscompose/src/staging/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 keypasscompose/src/staging/res/mipmap-mdpi/ic_launcher.png create mode 100644 keypasscompose/src/staging/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 keypasscompose/src/staging/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 keypasscompose/src/staging/res/mipmap-xhdpi/ic_launcher.png create mode 100644 keypasscompose/src/staging/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 keypasscompose/src/staging/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 keypasscompose/src/staging/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 keypasscompose/src/staging/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 keypasscompose/src/staging/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 keypasscompose/src/staging/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 keypasscompose/src/staging/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 keypasscompose/src/staging/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 keypasscompose/src/staging/res/values/ic_launcher_background.xml create mode 100644 keypasscompose/src/staging/res/values/strings.xml diff --git a/app/src/staging/res/drawable/ic_twotone_content_copy_24.xml b/app/src/main/res/drawable/ic_twotone_content_copy_24.xml similarity index 100% rename from app/src/staging/res/drawable/ic_twotone_content_copy_24.xml rename to app/src/main/res/drawable/ic_twotone_content_copy_24.xml diff --git a/app/src/staging/res/drawable/ic_twotone_qr_code_scanner_24.xml b/app/src/main/res/drawable/ic_twotone_qr_code_scanner_24.xml similarity index 100% rename from app/src/staging/res/drawable/ic_twotone_qr_code_scanner_24.xml rename to app/src/main/res/drawable/ic_twotone_qr_code_scanner_24.xml diff --git a/app/src/staging/res/drawable/ic_twotone_totp.xml b/app/src/main/res/drawable/ic_twotone_totp.xml similarity index 100% rename from app/src/staging/res/drawable/ic_twotone_totp.xml rename to app/src/main/res/drawable/ic_twotone_totp.xml diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/utils/AnimationUtils.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/AnimationUtils.kt similarity index 100% rename from app/src/main/java/com/yogeshpaliyal/keypass/utils/AnimationUtils.kt rename to common/src/main/java/com/yogeshpaliyal/common/utils/AnimationUtils.kt diff --git a/keypasscompose/build.gradle b/keypasscompose/build.gradle index cdae997e..fe821900 100644 --- a/keypasscompose/build.gradle +++ b/keypasscompose/build.gradle @@ -1,6 +1,10 @@ plugins { id 'com.android.application' id 'kotlin-android' + id 'kotlin-kapt' + id 'androidx.navigation.safeargs.kotlin' + id("dagger.hilt.android.plugin") + } android { @@ -35,7 +39,27 @@ android { } buildFeatures { compose true + viewBinding = true + dataBinding = true } + + flavorDimensions "default" + productFlavors { + production { + } + + staging { + applicationIdSuffix ".staging" + } + } + sourceSets { + main { + res { + srcDirs 'src\\main\\res', 'src\\staging\\res' + } + } + } + composeOptions { kotlinCompilerExtensionVersion = "1.2.0-beta01" } @@ -67,4 +91,46 @@ dependencies { debugImplementation "androidx.compose.ui:ui-tooling:1.3.3" implementation 'androidx.compose.material3:material3:1.1.0-alpha04' + + + // XML Libraries + implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.preference:preference-ktx:1.2.0' + + + implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version" + implementation "androidx.navigation:navigation-ui-ktx:$navigation_version" + + + kapt "androidx.room:room-compiler:$room_version" + + + implementation 'com.yogeshpaliyal:universal-adapter:3.0.1' + + + // dependency injection + implementation("com.google.dagger:hilt-android:$hilt_version") + kapt("com.google.dagger:hilt-android-compiler:$hilt_version") + implementation("androidx.hilt:hilt-work:1.0.0") + // When using Kotlin. + kapt("androidx.hilt:hilt-compiler:1.0.0") + + + // zxing library + // implementation "com.googl.ezxing:android-core:3.4.1" + implementation 'com.journeyapps:zxing-android-embedded:4.3.0' + + + // For instrumented tests. + androidTestImplementation("com.google.dagger:hilt-android-testing:2.44.2") + // ...with Kotlin. + kaptAndroidTest("com.google.dagger:hilt-android-compiler:$hilt_version") + + // For Robolectric tests. + testImplementation("com.google.dagger:hilt-android-testing:2.44") + // ...with Kotlin. + kaptTest("com.google.dagger:hilt-android-compiler:$hilt_version") + + } \ No newline at end of file diff --git a/keypasscompose/src/main/AndroidManifest.xml b/keypasscompose/src/main/AndroidManifest.xml index be22dea9..37bb7821 100644 --- a/keypasscompose/src/main/AndroidManifest.xml +++ b/keypasscompose/src/main/AndroidManifest.xml @@ -1,25 +1,67 @@ + + + + + android:name=".ui.CrashActivity" + android:exported="false" /> + + + + + + + + + + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MainActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MainActivity.kt deleted file mode 100644 index 78e5efb9..00000000 --- a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MainActivity.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.yogeshpaliyal.keypasscompose - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.FabPosition -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material.icons.filled.Star -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview -import com.yogeshpaliyal.keypasscompose.ui.theme.KeyPassTheme -import com.yogeshpaliyal.keypasscompose.ui.theme.Material3BottomAppBar -import com.yogeshpaliyal.keypasscompose.ui.theme.Material3Scaffold - -class MainActivity : ComponentActivity() { - @OptIn(ExperimentalMaterial3Api::class) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - KeyPassTheme { - // A surface container using the 'background' color from the theme - Surface(color = MaterialTheme.colorScheme.background) { - Material3Scaffold( - bottomBar = { - Material3BottomAppBar(cutoutShape = RoundedCornerShape(50)) { - IconButton( - onClick = { - /* doSomething() */ - } - ) { - Icon(Icons.Filled.Menu, "") - } - - Spacer(Modifier.weight(1f, true)) - IconButton( - onClick = { - /* doSomething() */ - } - ) { - Icon(Icons.Filled.Star, "") - } - } - }, - floatingActionButton = { - FloatingActionButton( - onClick = { - }, - contentColor = Color.White, - shape = RoundedCornerShape(50) - ) { - Icon(Icons.Filled.Add, "") - } - }, - floatingActionButtonPosition = FabPosition.Center, - isFloatingActionButtonDocked = true - ) { - } - Greeting("Android") - } - } - } - } -} - -@Composable -fun Greeting(name: String) { - Text(text = "Hello $name!") -} - -@Preview(showBackground = true) -@Composable -fun DefaultPreview() { - KeyPassTheme { - Greeting("Android") - } -} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MyApplication.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MyApplication.kt new file mode 100644 index 00000000..a1c5e98b --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MyApplication.kt @@ -0,0 +1,20 @@ +package com.yogeshpaliyal.keypasscompose + +import android.content.Intent +import com.yogeshpaliyal.common.CommonMyApplication +import com.yogeshpaliyal.keypasscompose.ui.CrashActivity +import dagger.hilt.android.HiltAndroidApp + +/* +* @author Yogesh Paliyal +* yogeshpaliyal.foss@gmail.com +* https://techpaliyal.com +* created on 22-01-2021 22:41 +*/ +@HiltAndroidApp +class MyApplication : CommonMyApplication() { + + override fun getCrashActivityIntent(throwable: Throwable): Intent { + return CrashActivity.getIntent(this, throwable.localizedMessage) + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/custom_views/MaskedCardView.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/custom_views/MaskedCardView.kt new file mode 100644 index 00000000..495c437e --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/custom_views/MaskedCardView.kt @@ -0,0 +1,47 @@ +package com.yogeshpaliyal.keypasscompose.custom_views + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Path +import android.graphics.RectF +import android.util.AttributeSet +import com.google.android.material.card.MaterialCardView +import com.google.android.material.shape.ShapeAppearanceModel +import com.google.android.material.shape.ShapeAppearancePathProvider +import com.yogeshpaliyal.keypasscompose.R + +/** + * A Card view that clips the content of any shape, this should be done upstream in card, + * working around it for now. + */ +class MaskedCardView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = R.attr.materialCardViewStyle, +) : MaterialCardView(context, attrs, defStyle) { + @SuppressLint("RestrictedApi") + private val pathProvider = ShapeAppearancePathProvider() + private val path: Path = Path() + private val shapeAppearance: ShapeAppearanceModel = ShapeAppearanceModel.builder( + context, + attrs, + defStyle, + R.style.Widget_MaterialComponents_CardView + ).build() + + private val rectF = RectF(0f, 0f, 0f, 0f) + + override fun onDraw(canvas: Canvas) { + canvas.clipPath(path) + super.onDraw(canvas) + } + + @SuppressLint("RestrictedApi") + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + rectF.right = w.toFloat() + rectF.bottom = h.toFloat() + pathProvider.calculatePath(shapeAppearance, 1f, rectF, path) + super.onSizeChanged(w, h, oldw, oldh) + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/data/MyAccountModel.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/data/MyAccountModel.kt new file mode 100644 index 00000000..36b55ac8 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/data/MyAccountModel.kt @@ -0,0 +1,36 @@ +package com.yogeshpaliyal.keypasscompose.data + +import com.google.gson.Gson +import com.yogeshpaliyal.common.constants.AccountType +import com.yogeshpaliyal.common.data.AccountModel +import com.yogeshpaliyal.keypasscompose.R +import com.yogeshpaliyal.universalAdapter.listener.UniversalViewType +import com.yogeshpaliyal.universalAdapter.model.BaseDiffUtil + +class MyAccountModel : AccountModel(), BaseDiffUtil, UniversalViewType { + override fun getDiffId(): Any? { + return id + } + + override fun getDiffBody(): Any? { + return if (type == AccountType.TOTP) { + super.getDiffBody() + } else { + Gson().toJson(this) + } + } + + override fun getLayoutId(): Int = if (type == AccountType.TOTP) R.layout.item_totp else R.layout.item_accounts + + fun map(accountModel: AccountModel) { + this.id = accountModel.id + this.title = accountModel.title + this.uniqueId = accountModel.uniqueId + this.username = accountModel.username + this.password = accountModel.password + this.site = accountModel.site + this.notes = accountModel.notes + this.tags = accountModel.tags + this.type = accountModel.type + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/AccountsClickListener.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/AccountsClickListener.kt new file mode 100644 index 00000000..4bca61ac --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/AccountsClickListener.kt @@ -0,0 +1,14 @@ +package com.yogeshpaliyal.keypasscompose.listener + +import android.view.View + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 31-01-2021 09:00 +*/ +interface AccountsClickListener { + fun onItemClick(view: View, model: T) + fun onCopyClicked(model: T) +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/UniversalClickListener.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/UniversalClickListener.kt new file mode 100644 index 00000000..e6df2176 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/UniversalClickListener.kt @@ -0,0 +1,13 @@ +package com.yogeshpaliyal.keypasscompose.listener + +import android.view.View + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 31-01-2021 09:00 +*/ +interface UniversalClickListener { + fun onItemClick(view: View, model: T) +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/CrashActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/CrashActivity.kt new file mode 100644 index 00000000..8fe52504 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/CrashActivity.kt @@ -0,0 +1,65 @@ +package com.yogeshpaliyal.keypasscompose.ui + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.yogeshpaliyal.keypasscompose.BuildConfig +import com.yogeshpaliyal.keypasscompose.databinding.ActivityCrashBinding +import dagger.hilt.android.AndroidEntryPoint +import java.lang.StringBuilder + +@AndroidEntryPoint +class CrashActivity : AppCompatActivity() { + + private lateinit var binding: ActivityCrashBinding + + companion object { + private const val ARG_DATA = "arg_data" + + fun getIntent(context: Context, data: String?): Intent { + return Intent(context, CrashActivity::class.java).also { + it.putExtra(ARG_DATA, data) + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityCrashBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.txtCrash.text = intent.extras?.getString(ARG_DATA) + + binding.btnSendFeedback.setOnClickListener { + + val deviceInfo = StringBuilder() + try { + deviceInfo.append("\n") + deviceInfo.append("App Version: " + BuildConfig.VERSION_NAME) + deviceInfo.append("\n") + deviceInfo.append("Brand Name: " + Build.BRAND) + deviceInfo.append("\n") + deviceInfo.append("Manufacturer Name: " + Build.MANUFACTURER) + deviceInfo.append("\n") + deviceInfo.append("Device Name: " + Build.MODEL) + deviceInfo.append("\n") + deviceInfo.append("Device API Version: " + Build.VERSION.SDK_INT) + deviceInfo.append("\n") + } catch (e: Exception) { + e.printStackTrace() + } + + val intent = Intent(Intent.ACTION_SENDTO) + intent.data = Uri.parse("mailto:") + + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("yogeshpaliyal.foss@gmail.com")) + intent.putExtra(Intent.EXTRA_SUBJECT, "Crash Report in KeyPass") + intent.putExtra(Intent.EXTRA_TEXT, binding.txtCrash.text.toString() + "$deviceInfo") + + startActivity(Intent.createChooser(intent, "")) + } + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPActivity.kt new file mode 100644 index 00000000..ed1a4eda --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPActivity.kt @@ -0,0 +1,139 @@ +package com.yogeshpaliyal.keypasscompose.ui.addTOTP + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.google.zxing.integration.android.IntentIntegrator +import com.yogeshpaliyal.common.utils.TOTPHelper +import com.yogeshpaliyal.keypasscompose.R +import com.yogeshpaliyal.keypasscompose.databinding.ActivityAddTotpactivityBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class AddTOTPActivity : AppCompatActivity() { + + companion object { + + private const val ARG_ACCOUNT_ID = "account_id" + + @JvmStatic + fun start(context: Context?, accountId: String? = null) { + + val starter = Intent(context, AddTOTPActivity::class.java) + starter.putExtra(ARG_ACCOUNT_ID, accountId) + context?.startActivity(starter) + } + } + + private lateinit var binding: ActivityAddTotpactivityBinding + + private val mViewModel by viewModels() + + private val accountId by lazy { + intent.extras?.getString(ARG_ACCOUNT_ID) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityAddTotpactivityBinding.inflate(layoutInflater) + binding.mViewModel = mViewModel + binding.lifecycleOwner = this + setContentView(binding.root) + + setSupportActionBar(binding.toolbar) + + binding.tilSecretKey.isVisible = accountId == null + mViewModel.loadOldAccount(accountId) + + binding.toolbar.setNavigationOnClickListener { + onBackPressed() + } + + binding.tilSecretKey.setEndIconOnClickListener { + // ScannerActivity.start(this) + IntentIntegrator(this).setPrompt("").initiateScan() + } + + mViewModel.error.observe( + this, + Observer { + it?.getContentIfNotHandled()?.let { + Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show() + } + } + ) + + mViewModel.goBack.observe( + this, + Observer { + it.getContentIfNotHandled()?.let { + onBackPressed() + } + } + ) + + binding.btnSave.setOnClickListener { + mViewModel.saveAccount(accountId) + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + if (accountId != null) + menuInflater.inflate(R.menu.menu_delete, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.action_delete) { + deleteAccount() + } + return super.onOptionsItemSelected(item) + } + + private fun deleteAccount() { + MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.delete_account_title)) + .setMessage(getString(R.string.delete_account_msg)) + .setPositiveButton( + getString(R.string.delete) + ) { dialog, which -> + dialog?.dismiss() + + if (accountId != null) { + mViewModel.deleteAccount(accountId!!) { + onBackPressed() + } + } + } + .setNegativeButton(getString(R.string.cancel)) { dialog, which -> + dialog.dismiss() + }.show() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) + + if (result != null) { + if (result.contents != null) { + try { + val totp = TOTPHelper(result.contents) + mViewModel.setSecretKey(totp.secret) + mViewModel.setAccountName(totp.label) + } catch (e: Exception) { + e.printStackTrace() + } + } + } else { + super.onActivityResult(requestCode, resultCode, data) + } + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPViewModel.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPViewModel.kt new file mode 100644 index 00000000..b9247d0b --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPViewModel.kt @@ -0,0 +1,85 @@ +package com.yogeshpaliyal.keypasscompose.ui.addTOTP + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.yogeshpaliyal.common.AppDatabase +import com.yogeshpaliyal.common.constants.AccountType +import com.yogeshpaliyal.common.data.AccountModel +import com.yogeshpaliyal.common.utils.Event +import com.yogeshpaliyal.keypasscompose.R +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AddTOTPViewModel @Inject constructor(private val appDatabase: AppDatabase) : + ViewModel() { + + private val _goBack = MutableLiveData>() + val goBack: LiveData> = _goBack + + private val _error = MutableLiveData>() + val error: LiveData> = _error + + val secretKey = MutableLiveData("") + + val accountName = MutableLiveData("") + + fun loadOldAccount(accountId: String?) { + accountId ?: return + + viewModelScope.launch(Dispatchers.IO) { + appDatabase.getDao().getAccount(accountId)?.let { accountModel -> + accountName.postValue(accountModel.title ?: "") + } + } + } + + fun saveAccount(accountId: String?) { + viewModelScope.launch { + val secretKey = secretKey.value + val accountName = accountName.value + + if (accountId == null) { + if (secretKey.isNullOrEmpty()) { + _error.postValue(Event(R.string.alert_black_secret_key)) + return@launch + } + } + + if (accountName.isNullOrEmpty()) { + _error.postValue(Event(R.string.alert_black_account_name)) + return@launch + } + + val accountModel = if (accountId == null) { + AccountModel(password = secretKey, title = accountName, type = AccountType.TOTP) + } else { + appDatabase.getDao().getAccount(accountId)?.also { + it.title = accountName + } + } + + accountModel?.let { appDatabase.getDao().insertOrUpdateAccount(it) } + _goBack.postValue(Event(Unit)) + } + } + + fun setSecretKey(secretKey: String) { + this.secretKey.value = secretKey + } + + fun setAccountName(accountName: String) { + this.accountName.value = accountName + } + + fun deleteAccount(accountId: String, onDeleted: () -> Unit) { + viewModelScope.launch { + appDatabase.getDao().deleteAccount(accountId) + onDeleted() + } + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/ScannerActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/ScannerActivity.kt new file mode 100644 index 00000000..70a1488a --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/ScannerActivity.kt @@ -0,0 +1,55 @@ +package com.yogeshpaliyal.keypasscompose.ui.addTOTP + +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.yogeshpaliyal.common.constants.RequestCodes +import com.yogeshpaliyal.keypasscompose.databinding.ActivityScannerBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ScannerActivity : AppCompatActivity() { + + private lateinit var binding: ActivityScannerBinding + + private val REQUEST_CAM_PERMISSION = 432 + + companion object { + @JvmStatic + fun start(activity: Activity) { + val starter = Intent(activity, ScannerActivity::class.java) + activity.startActivityForResult(starter, RequestCodes.SCANNER) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityScannerBinding.inflate(layoutInflater) + + setContentView(binding.root) + + setSupportActionBar(binding.toolbar) + + binding.toolbar.setNavigationOnClickListener { + onBackPressed() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == REQUEST_CAM_PERMISSION) { + if (isAllRequestGranted(grantResults)) { + // codeScanner?.startPreview() + } + } + } + + private fun isAllRequestGranted(grantResults: IntArray) = + grantResults.all { it == PackageManager.PERMISSION_GRANTED } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/auth/AuthenticationActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/auth/AuthenticationActivity.kt new file mode 100644 index 00000000..2a153812 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/auth/AuthenticationActivity.kt @@ -0,0 +1,133 @@ +package com.yogeshpaliyal.keypasscompose.ui.auth + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.util.Log +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import com.yogeshpaliyal.keypasscompose.R +import com.yogeshpaliyal.keypasscompose.databinding.ActivityAuthenticationBinding +import com.yogeshpaliyal.keypasscompose.ui.nav.DashboardActivity +import dagger.hilt.android.AndroidEntryPoint +import java.util.concurrent.Executor + +private const val AUTHENTICATION_RESULT = 707 + +@AndroidEntryPoint +class AuthenticationActivity : AppCompatActivity() { + + private lateinit var binding: ActivityAuthenticationBinding + + private lateinit var executor: Executor + private lateinit var biometricPrompt: BiometricPrompt + private lateinit var promptInfo: BiometricPrompt.PromptInfo + + private val biometricManager by lazy { + BiometricManager.from(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityAuthenticationBinding.inflate(layoutInflater) + setContentView(binding.root) + + executor = ContextCompat.getMainExecutor(this) + biometricPrompt = BiometricPrompt( + this, executor, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence + ) { + super.onAuthenticationError(errorCode, errString) + Toast.makeText( + applicationContext, + "Authentication error: $errString", Toast.LENGTH_SHORT + ) + .show() + // finish() + } + + override fun onAuthenticationSucceeded( + result: BiometricPrompt.AuthenticationResult + ) { + super.onAuthenticationSucceeded(result) + + onAuthenticated() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Toast.makeText( + applicationContext, getString(R.string.authentication_failed), + Toast.LENGTH_SHORT + ) + .show() + } + } + ) + + promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.app_name)) + .setSubtitle(getString(R.string.login_to_enter_keypass)) + .setAllowedAuthenticators(DEVICE_CREDENTIAL or BIOMETRIC_WEAK or BIOMETRIC_STRONG) + .build() + + // Prompt appears when user clicks "Log in". + // Consider integrating with the keystore to unlock cryptographic operations, + // if needed by your app. + + biometricPrompt.authenticate(promptInfo) + + binding.btnRetry.setOnClickListener { + val allowedAuths = DEVICE_CREDENTIAL or BIOMETRIC_WEAK or BIOMETRIC_STRONG + val canAuthentication = + biometricManager.canAuthenticate(allowedAuths) + when (canAuthentication) { + BiometricManager.BIOMETRIC_SUCCESS -> { + Log.d("MY_APP_TAG", "App can authenticate using biometrics.") + biometricPrompt.authenticate(promptInfo) + } + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE, + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { + Log.e( + "MY_APP_TAG", + "$canAuthentication Biometric features are currently unavailable." + ) + // Prompts the user to create credentials that your app accepts. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply { + putExtra( + Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, + BIOMETRIC_STRONG or DEVICE_CREDENTIAL + ) + } + startActivityForResult(enrollIntent, AUTHENTICATION_RESULT) + } else { + Toast.makeText( + this, + "Please set password for your device first from phone settings", + Toast.LENGTH_SHORT + ).show() + } + } + } + } + } + + private fun onAuthenticated() { + // binding.passCodeView.isVisible = false + val dashboardIntent = Intent(this, DashboardActivity::class.java) + startActivity(dashboardIntent) + finish() + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/backup/BackupActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/backup/BackupActivity.kt new file mode 100644 index 00000000..8d5002e0 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/backup/BackupActivity.kt @@ -0,0 +1,332 @@ +package com.yogeshpaliyal.keypasscompose.ui.backup + +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.yogeshpaliyal.common.utils.BACKUP_KEY_LENGTH +import com.yogeshpaliyal.common.utils.backupAccounts +import com.yogeshpaliyal.common.utils.canUserAccessBackupDirectory +import com.yogeshpaliyal.common.utils.clearBackupKey +import com.yogeshpaliyal.common.utils.formatCalendar +import com.yogeshpaliyal.common.utils.getBackupDirectory +import com.yogeshpaliyal.common.utils.getBackupTime +import com.yogeshpaliyal.common.utils.getOrCreateBackupKey +import com.yogeshpaliyal.common.utils.isAutoBackupEnabled +import com.yogeshpaliyal.common.utils.isKeyPresent +import com.yogeshpaliyal.common.utils.overrideAutoBackup +import com.yogeshpaliyal.common.utils.saveKeyphrase +import com.yogeshpaliyal.common.utils.setAutoBackupEnabled +import com.yogeshpaliyal.common.utils.setBackupDirectory +import com.yogeshpaliyal.common.utils.setBackupTime +import com.yogeshpaliyal.common.utils.setOverrideAutoBackup +import com.yogeshpaliyal.keypasscompose.R +import com.yogeshpaliyal.keypasscompose.databinding.BackupActivityBinding +import com.yogeshpaliyal.keypasscompose.databinding.LayoutBackupKeypharseBinding +import com.yogeshpaliyal.keypasscompose.databinding.LayoutCustomKeypharseBinding +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.net.URLDecoder +import javax.inject.Inject + +@AndroidEntryPoint +class BackupActivity : AppCompatActivity() { + + companion object { + @JvmStatic + fun start(context: Context?) { + val starter = Intent(context, BackupActivity::class.java) + context?.startActivity(starter) + } + } + + private lateinit var binding: BackupActivityBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = BackupActivityBinding.inflate(layoutInflater) + setContentView(binding.root) + setSupportActionBar(binding.toolbar) + binding.toolbar.setNavigationOnClickListener { + onBackPressed() + } + + if (savedInstanceState == null) { + supportFragmentManager + .beginTransaction() + .replace(R.id.settings, SettingsFragment()) + .commit() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + @AndroidEntryPoint + class SettingsFragment : PreferenceFragmentCompat() { + + @Inject + lateinit var appDb: com.yogeshpaliyal.common.AppDatabase + + private val CHOOSE_BACKUPS_LOCATION_REQUEST_CODE = 26212 + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.backup_preferences, rootKey) + updateItems() + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + when (preference.key) { + getString(R.string.settings_start_backup) -> { + startBackup() + } + getString(R.string.settings_create_backup) -> { + lifecycleScope.launch { + if (context.canUserAccessBackupDirectory()) { + val selectedDirectory = Uri.parse(context.getBackupDirectory()) + passwordSelection(selectedDirectory) + } + } + } + getString(R.string.settings_backup_folder) -> { + changeBackupFolder() + } + getString(R.string.settings_verify_key_phrase) -> { + verifyKeyPhrase() + } + getString(R.string.settings_stop_backup) -> { + lifecycleScope.launch { + stopBackup() + } + } + getString(R.string.settings_auto_backup) -> { + lifecycleScope.launch { + context.setAutoBackupEnabled(context.isAutoBackupEnabled().not()) + updateItems() + } + } + getString(R.string.settings_override_auto_backup) -> { + lifecycleScope.launch { + context.setOverrideAutoBackup(context.overrideAutoBackup().not()) + updateItems() + } + } + } + return super.onPreferenceTreeClick(preference) + } + + private suspend fun passwordSelection(selectedDirectory: Uri) { + + val isKeyPresent = context?.isKeyPresent() ?: return + if (isKeyPresent) { + backup(selectedDirectory) + return + } + + val builder = MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.alert) + .setMessage(getString(R.string.custom_generated_keyphrase_info)) + .setPositiveButton( + getString(R.string.custom_keyphrase) + ) { dialog, which -> + dialog?.dismiss() + setCustomKeyphrase(selectedDirectory) + } + .setNegativeButton(R.string.generate_keyphrase) { dialog, which -> + dialog?.dismiss() + backup(selectedDirectory) + } + builder.show() + } + + private fun setCustomKeyphrase(selectedDirectory: Uri) { + val binding = LayoutCustomKeypharseBinding.inflate(layoutInflater) + val dialog = MaterialAlertDialogBuilder(requireContext()).setView(binding.root) + .setPositiveButton( + getString(R.string.yes) + ) { dialog, which -> + + dialog?.dismiss() + }.create() + dialog.setOnShowListener { + val positiveBtn = dialog.getButton(DialogInterface.BUTTON_POSITIVE) + positiveBtn.setOnClickListener { + val keyphrase = binding.etKeyPhrase.text.toString().trim() + if (keyphrase.isEmpty()) { + Toast.makeText(context, R.string.alert_blank_keyphrase, Toast.LENGTH_SHORT) + .show() + return@setOnClickListener + } + + if (keyphrase.length != BACKUP_KEY_LENGTH) { + Toast.makeText( + context, + R.string.alert_invalid_keyphrase, + Toast.LENGTH_SHORT + ).show() + return@setOnClickListener + } + lifecycleScope.launch { + context?.saveKeyphrase(keyphrase) + backup(selectedDirectory) + } + dialog.dismiss() + } + } + dialog.show() + } + + fun backup(selectedDirectory: Uri) { + lifecycleScope.launch { + context.backupAccounts(appDb, selectedDirectory)?.let { keyPair -> + if (keyPair.first) { + val binding = LayoutBackupKeypharseBinding.inflate(layoutInflater) + binding.txtCode.text = context?.getOrCreateBackupKey()?.second ?: "" + binding.txtCode.setOnClickListener { + val clipboard = + ContextCompat.getSystemService( + requireContext(), + ClipboardManager::class.java + ) + val clip = ClipData.newPlainText("KeyPass", binding.txtCode.text) + clipboard?.setPrimaryClip(clip) + Toast.makeText( + context, + getString(R.string.copied_to_clipboard), + Toast.LENGTH_SHORT + ).show() + } + MaterialAlertDialogBuilder(requireContext()).setView(binding.root) + .setPositiveButton( + getString(R.string.yes) + ) { dialog, which -> + updateItems() + dialog?.dismiss() + }.show() + } else { + updateItems() + Toast.makeText( + context, + getString(R.string.backup_completed), + Toast.LENGTH_SHORT + ).show() + } + } + } + } + + private fun updateItems() { + lifecycleScope.launch(Dispatchers.IO) { + val isBackupEnabled = + context.canUserAccessBackupDirectory() && (context?.isKeyPresent() ?: false) + + val isAutoBackupEnabled = context.isAutoBackupEnabled() + val overrideAutoBackup = context.overrideAutoBackup() + + val lastBackupTime = context.getBackupTime() + val backupDirectory = context.getBackupDirectory() + + withContext(Dispatchers.Main) { + + findPreference(getString(R.string.settings_start_backup))?.isVisible = + isBackupEnabled.not() + findPreference(getString(R.string.settings_stop_backup))?.isVisible = + isBackupEnabled + + findPreference(getString(R.string.settings_auto_backup))?.isVisible = + isBackupEnabled + findPreference(getString(R.string.settings_auto_backup))?.summary = + if (isAutoBackupEnabled) getString(R.string.enabled) else getString(R.string.disabled) + + findPreference(getString(R.string.settings_cat_auto_backup))?.isVisible = + isBackupEnabled && isAutoBackupEnabled + + findPreference(getString(R.string.settings_override_auto_backup))?.summary = + if (overrideAutoBackup) getString(R.string.enabled) else getString(R.string.disabled) + + findPreference(getString(R.string.settings_create_backup))?.isVisible = + isBackupEnabled + findPreference(getString(R.string.settings_create_backup))?.summary = + getString( + R.string.last_backup_date, + lastBackupTime.formatCalendar("dd MMM yyyy hh:mm aa") + ) + findPreference(getString(R.string.settings_backup_folder))?.isVisible = + isBackupEnabled + val directory = URLDecoder.decode(backupDirectory, "utf-8").split("/") + val folderName = directory.get(directory.lastIndex) + findPreference(getString(R.string.settings_backup_folder))?.summary = + folderName + findPreference(getString(R.string.settings_verify_key_phrase))?.isVisible = + false + findPreference(getString(R.string.settings_backup))?.isVisible = + isBackupEnabled + } + } + } + + private fun startBackup() { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + + intent.addFlags( + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + + try { + startActivityForResult(intent, CHOOSE_BACKUPS_LOCATION_REQUEST_CODE) + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == CHOOSE_BACKUPS_LOCATION_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + val contentResolver = context?.contentResolver + val selectedDirectory = data?.data + if (contentResolver != null && selectedDirectory != null) { + contentResolver.takePersistableUriPermission( + selectedDirectory, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + + lifecycleScope.launch { + context.setBackupDirectory(selectedDirectory.toString()) + passwordSelection(selectedDirectory) + } + } + } + } + + private fun changeBackupFolder() { + startBackup() + } + + private fun verifyKeyPhrase() { + Toast.makeText(context, getString(R.string.coming_soon), Toast.LENGTH_SHORT).show() + } + + private suspend fun stopBackup() { + context.clearBackupKey() + context.setBackupDirectory("") + context.setBackupTime(-1) + context.setOverrideAutoBackup(false) + context.setAutoBackupEnabled(false) + updateItems() + } + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailActivity.kt new file mode 100644 index 00000000..48fc78bd --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailActivity.kt @@ -0,0 +1,132 @@ +package com.yogeshpaliyal.keypasscompose.ui.detail + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.zxing.integration.android.IntentIntegrator +import com.yogeshpaliyal.common.utils.PasswordGenerator +import com.yogeshpaliyal.keypasscompose.R +import com.yogeshpaliyal.keypasscompose.databinding.FragmentDetailBinding +import dagger.hilt.android.AndroidEntryPoint + +/* +* @author Yogesh Paliyal +* yogeshpaliyal.foss@gmail.com +* https://techpaliyal.com +* created on 31-01-2021 10:38 +*/ +@AndroidEntryPoint +class DetailActivity : AppCompatActivity() { + + lateinit var binding: FragmentDetailBinding + + companion object { + + private const val ARG_ACCOUNT_ID = "ARG_ACCOUNT_ID" + + @JvmStatic + fun start(context: Context?, accountId: Long? = null) { + val starter = Intent(context, DetailActivity::class.java) + .putExtra(ARG_ACCOUNT_ID, accountId) + context?.startActivity(starter) + } + } + + private val mViewModel by viewModels() + + private val accountId by lazy { + intent?.extras?.getLong(ARG_ACCOUNT_ID) ?: -1 + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = FragmentDetailBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.lifecycleOwner = this + + mViewModel.loadAccount(accountId) + mViewModel.accountModel.observe( + this, + Observer { + binding.accountData = it + } + ) + + if (accountId > 0) { + binding.bottomAppBar.replaceMenu(R.menu.bottom_app_bar_detail) + + binding.tilPassword.startIconDrawable = null + } else { + binding.tilPassword.setStartIconDrawable(R.drawable.ic_round_refresh_24) + + binding.tilPassword.setStartIconOnClickListener { + binding.etPassword.setText(PasswordGenerator().generatePassword()) + } + } + + binding.bottomAppBar.setNavigationOnClickListener { + onBackPressed() + } + binding.bottomAppBar.setOnMenuItemClickListener { item -> + if (item.itemId == R.id.action_delete) { + deleteAccount() + return@setOnMenuItemClickListener true + } + + return@setOnMenuItemClickListener false + } + + binding.btnSave.setOnClickListener { + mViewModel.insertOrUpdate { + onBackPressed() + } + } + + binding.btnScan.setOnClickListener { + IntentIntegrator(this).setPrompt("").initiateScan() + } + } + + private fun deleteAccount() { + MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.delete_account_title)) + .setMessage(getString(R.string.delete_account_msg)) + .setPositiveButton( + getString(R.string.delete) + ) { dialog, which -> + dialog?.dismiss() + + if (accountId > 0L) { + mViewModel.deleteAccount { + onBackPressed() + } + } + } + .setNegativeButton(getString(R.string.cancel)) { dialog, which -> + dialog.dismiss() + }.show() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.bottom_app_bar_detail, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) + + if (result != null) { + if (result.contents != null) { + binding.etPassword.setText(result.contents) + } + } else { + super.onActivityResult(requestCode, resultCode, data) + } + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailViewModel.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailViewModel.kt new file mode 100644 index 00000000..c4bb494b --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailViewModel.kt @@ -0,0 +1,68 @@ +package com.yogeshpaliyal.keypasscompose.ui.detail + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.yogeshpaliyal.common.data.AccountModel +import com.yogeshpaliyal.common.worker.executeAutoBackup +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 31-01-2021 11:52 +*/ +@HiltViewModel +class DetailViewModel @Inject constructor( + val app: Application, + val appDb: com.yogeshpaliyal.common.AppDatabase +) : AndroidViewModel(app) { + + private val _accountModel by lazy { MutableLiveData() } + val accountModel: LiveData = _accountModel + + fun loadAccount(accountId: Long?) { + viewModelScope.launch(Dispatchers.IO) { + _accountModel.postValue( + appDb.getDao().getAccount(accountId) ?: AccountModel() + ) + } + } + + fun deleteAccount(onExecCompleted: () -> Unit) { + viewModelScope.launch { + accountModel.value?.let { + withContext(Dispatchers.IO) { + appDb.getDao().deleteAccount(it) + } + autoBackup() + onExecCompleted() + } + } + } + + fun insertOrUpdate(onExecCompleted: () -> Unit) { + viewModelScope.launch { + accountModel.value?.let { + withContext(Dispatchers.IO) { + appDb.getDao().insertOrUpdateAccount(it) + autoBackup() + } + } + onExecCompleted() + } + } + + private fun autoBackup() { + viewModelScope.launch { + app.executeAutoBackup() + } + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/generate/GeneratePasswordActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/generate/GeneratePasswordActivity.kt new file mode 100644 index 00000000..3b0be14c --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/generate/GeneratePasswordActivity.kt @@ -0,0 +1,47 @@ +package com.yogeshpaliyal.keypasscompose.ui.generate + +import android.content.ClipData +import android.content.ClipboardManager +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.yogeshpaliyal.common.utils.PasswordGenerator +import com.yogeshpaliyal.keypasscompose.R +import com.yogeshpaliyal.keypasscompose.databinding.ActivityGeneratePasswordBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class GeneratePasswordActivity : AppCompatActivity() { + private lateinit var binding: ActivityGeneratePasswordBinding + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityGeneratePasswordBinding.inflate(layoutInflater) + setContentView(binding.root) + + generatePassword() + + binding.btnRefresh.setOnClickListener { + generatePassword() + } + + binding.tilPassword.setEndIconOnClickListener { + val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("random_password", binding.etPassword.text) + clipboard.setPrimaryClip(clip) + Toast.makeText(this, getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show() + } + } + + private fun generatePassword() { + val password = PasswordGenerator( + length = binding.sliderPasswordLength.value.toInt(), + includeUpperCaseLetters = binding.cbCapAlphabets.isChecked, + includeLowerCaseLetters = binding.cbLowerAlphabets.isChecked, + includeSymbols = binding.cbSymbols.isChecked, + includeNumbers = binding.cbNumbers.isChecked + ).generatePassword() + + binding.etPassword.setText(password) + binding.etPassword.setSelection(password.length) + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/DashboardViewModel.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/DashboardViewModel.kt new file mode 100644 index 00000000..5ec08bb2 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/DashboardViewModel.kt @@ -0,0 +1,57 @@ +package com.yogeshpaliyal.keypasscompose.ui.home + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.yogeshpaliyal.common.data.AccountModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 30-01-2021 23:02 +*/ +@HiltViewModel +class DashboardViewModel @Inject constructor(application: Application, val appDb: com.yogeshpaliyal.common.AppDatabase) : + AndroidViewModel(application) { + + val keyword by lazy { + MutableLiveData("") + } + val tag by lazy { + MutableLiveData() + } + + private val appDao = appDb.getDao() + + val mediator = MediatorLiveData>>() + + init { + mediator.addSource(keyword) { + mediator.postValue(appDao.getAllAccounts(keyword.value, tag.value)) + } + mediator.addSource(tag) { + mediator.postValue(appDao.getAllAccounts(keyword.value, tag.value)) + } + mediator.postValue(appDao.getAllAccounts(keyword.value, tag.value)) + + reloadData() + } + + private fun reloadData() { + viewModelScope.launch(Dispatchers.IO) { + while (true) { + delay(1000) + mediator.postValue(appDao.getAllAccounts(keyword.value, tag.value)) + } + } + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/HomeFragment.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/HomeFragment.kt new file mode 100644 index 00000000..d2793ac7 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/HomeFragment.kt @@ -0,0 +1,121 @@ +package com.yogeshpaliyal.keypasscompose.ui.home + +import android.content.ClipData +import android.content.ClipboardManager +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import com.yogeshpaliyal.common.constants.AccountType +import com.yogeshpaliyal.common.data.AccountModel +import com.yogeshpaliyal.keypasscompose.R +import com.yogeshpaliyal.keypasscompose.data.MyAccountModel +import com.yogeshpaliyal.keypasscompose.databinding.FragmentHomeBinding +import com.yogeshpaliyal.keypasscompose.listener.AccountsClickListener +import com.yogeshpaliyal.keypasscompose.ui.addTOTP.AddTOTPActivity +import com.yogeshpaliyal.keypasscompose.ui.detail.DetailActivity +import com.yogeshpaliyal.universalAdapter.adapter.UniversalAdapterViewType +import com.yogeshpaliyal.universalAdapter.adapter.UniversalRecyclerAdapter +import com.yogeshpaliyal.universalAdapter.utils.Resource +import dagger.hilt.android.AndroidEntryPoint + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 31-01-2021 09:25 +*/ +@AndroidEntryPoint +class HomeFragment : Fragment() { + private lateinit var binding: FragmentHomeBinding + + private val mViewModel by lazy { + activityViewModels().value + } + + private val mAdapter by lazy { + UniversalRecyclerAdapter.Builder( + this, + content = UniversalAdapterViewType.Content( + R.layout.item_accounts, + listener = mListener + ), + noData = UniversalAdapterViewType.NoData(R.layout.layout_no_accounts) + ).build() + } + + val mListener = object : AccountsClickListener { + override fun onItemClick(view: View, model: AccountModel) { + if (model.type == AccountType.TOTP) { + AddTOTPActivity.start(context, model.uniqueId) + } else { + DetailActivity.start(context, model.id) + } + } + + private fun getPassword(model: AccountModel): String { + if (model.type == AccountType.TOTP) { + return model.getOtp() + } + return model.password.orEmpty() + } + + override fun onCopyClicked(model: AccountModel) { + val clipboard = + ContextCompat.getSystemService( + requireContext(), + ClipboardManager::class.java + ) + val clip = ClipData.newPlainText("KeyPass", getPassword(model)) + clipboard?.setPrimaryClip(clip) + Toast.makeText(context, getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT) + .show() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentHomeBinding.inflate(layoutInflater, container, false) + return binding.root + } + + private val observer = Observer> { + val newList = it.map { accountModel -> + MyAccountModel().also { + it.map(accountModel) + } + } + mAdapter.updateData(Resource.success(ArrayList(newList))) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.recyclerView.adapter = mAdapter.getAdapter() + + mViewModel.mediator.observe( + viewLifecycleOwner, + Observer { + it.removeObserver(observer) + it.observe(viewLifecycleOwner, observer) + } + ) + + /* lifecycleScope.launch() { + mViewModel.result + mViewModel.loadData(args.tag).collect { + withContext(Dispatchers.Main) { + mAdapter.updateData(Resource.success(ArrayList(it))) + } + } + }*/ + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavDrawerFragment.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavDrawerFragment.kt new file mode 100644 index 00000000..195db503 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavDrawerFragment.kt @@ -0,0 +1,173 @@ +package com.yogeshpaliyal.keypasscompose.ui.nav + +import android.content.res.ColorStateList +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.activity.OnBackPressedCallback +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.observe +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN +import com.google.android.material.bottomsheet.BottomSheetBehavior.from +import com.google.android.material.shape.MaterialShapeDrawable +import com.yogeshpaliyal.common.utils.themeColor +import com.yogeshpaliyal.keypasscompose.R +import com.yogeshpaliyal.keypasscompose.databinding.FragmentBottomNavDrawerBinding +import dagger.hilt.android.AndroidEntryPoint +import kotlin.LazyThreadSafetyMode.NONE + +/** + * A [Fragment] which acts as a bottom navigation drawer. + */ +@AndroidEntryPoint +class BottomNavDrawerFragment : + Fragment(), + NavigationAdapter.NavigationAdapterListener { + + private lateinit var binding: FragmentBottomNavDrawerBinding + + private val behavior: BottomSheetBehavior by lazy(NONE) { + from(binding.foregroundContainer) + } + + private val mViewModel by viewModels() + + private val bottomSheetCallback = BottomNavigationDrawerCallback() + + private val navigationListeners: MutableList = + mutableListOf() + + private val foregroundShapeDrawable: MaterialShapeDrawable by lazy(NONE) { + val foregroundContext = binding.foregroundContainer.context + MaterialShapeDrawable( + foregroundContext, + null, + R.attr.bottomSheetStyle, + 0 + ).apply { + fillColor = ColorStateList.valueOf( + foregroundContext.themeColor(R.attr.colorSurface) + ) + elevation = resources.getDimension(R.dimen.plane_16) + shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_NEVER + initializeElevationOverlay(requireContext()) + } + } + + private val closeDrawerOnBackPressed = object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + close() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requireActivity().onBackPressedDispatcher.addCallback(this, closeDrawerOnBackPressed) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentBottomNavDrawerBinding.inflate(inflater, container, false) + binding.foregroundContainer.setOnApplyWindowInsetsListener { view, windowInsets -> + // Record the window's top inset so it can be applied when the bottom sheet is slide up + // to meet the top edge of the screen. + view.setTag( + R.id.tag_system_window_inset_top, + windowInsets.systemWindowInsetTop + ) + windowInsets + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.run { + // backgroundContainer.background = backgroundShapeDrawable + foregroundContainer.background = foregroundShapeDrawable + + scrimView.setOnClickListener { close() } + + bottomSheetCallback.apply { + // Scrim view transforms + addOnSlideAction(AlphaSlideAction(scrimView)) + addOnStateChangedAction(VisibilityStateAction(scrimView)) + + // Recycler transforms + addOnStateChangedAction(ScrollToTopStateAction(navRecyclerView)) + // Close the sandwiching account picker if open + addOnStateChangedAction(object : OnStateChangedAction { + override fun onStateChanged(sheet: View, newState: Int) { + } + }) + // If the drawer is open, pressing the system back button should close the drawer. + addOnStateChangedAction(object : OnStateChangedAction { + override fun onStateChanged(sheet: View, newState: Int) { + closeDrawerOnBackPressed.isEnabled = newState != STATE_HIDDEN + } + }) + } + + behavior.addBottomSheetCallback(bottomSheetCallback) + behavior.state = STATE_HIDDEN + + val adapter = NavigationAdapter(this@BottomNavDrawerFragment) + + navRecyclerView.adapter = adapter + mViewModel.navigationList.observe(viewLifecycleOwner) { + adapter.submitList(it) + } + mViewModel.setNavigationMenuItemChecked(0) + } + } + + fun open() { + behavior.state = STATE_HALF_EXPANDED + } + + fun close() { + behavior.state = STATE_HIDDEN + } + + fun addOnSlideAction(action: OnSlideAction) { + bottomSheetCallback.addOnSlideAction(action) + } + + fun addOnStateChangedAction(action: OnStateChangedAction) { + bottomSheetCallback.addOnStateChangedAction(action) + } + + fun addNavigationListener(listener: NavigationAdapter.NavigationAdapterListener) { + navigationListeners.add(listener) + } + + override fun onNavMenuItemClicked(item: NavigationModelItem.NavMenuItem) { + // mViewModel.setNavigationMenuItemChecked(item.id) + close() + navigationListeners.forEach { it.onNavMenuItemClicked(item) } + } + + override fun onNavEmailFolderClicked(folder: NavigationModelItem.NavEmailFolder) { + navigationListeners.forEach { it.onNavEmailFolderClicked(folder) } + } + + fun toggle() { + when { + behavior.state == STATE_HIDDEN -> open() + behavior.state == STATE_HIDDEN || + behavior.state == STATE_HALF_EXPANDED || + behavior.state == STATE_EXPANDED + || behavior.state == STATE_COLLAPSED -> close() + } + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavViewModel.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavViewModel.kt new file mode 100644 index 00000000..4ea30767 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavViewModel.kt @@ -0,0 +1,67 @@ +package com.yogeshpaliyal.keypasscompose.ui.nav + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import javax.inject.Inject + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 31-01-2021 14:11 +*/ +@HiltViewModel +class BottomNavViewModel @Inject constructor (application: Application, val appDb: com.yogeshpaliyal.common.AppDatabase) : AndroidViewModel(application) { + private val _navigationList: MutableLiveData> = MutableLiveData() + private val tagsDb = appDb.getDao().getTags() + + private var tagsList: List ? = null + + val navigationList: LiveData> + get() = _navigationList + + init { + postListUpdate() + + viewModelScope.launch { + tagsDb.collect { + tagsList = it + postListUpdate() + } + } + } + + /** + * Set the currently selected menu item. + * + * @return true if the currently selected item has changed. + */ + fun setNavigationMenuItemChecked(id: Int): Boolean { + var updated = false + NavigationModel.navigationMenuItems.forEachIndexed { index, item -> + val shouldCheck = item.id == id + if (item.checked != shouldCheck) { + NavigationModel.navigationMenuItems[index] = item.copy(checked = shouldCheck) + updated = true + } + } + if (updated) postListUpdate() + return updated + } + + private fun postListUpdate() { + val newList = if (tagsList.isNullOrEmpty().not()) { + NavigationModel.navigationMenuItems + NavigationModelItem.NavDivider("Tags") + (tagsList?.filter { it != null }?.map { NavigationModelItem.NavEmailFolder(it) } ?: listOf()) + } else { + NavigationModel.navigationMenuItems + } + + _navigationList.value = newList + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavigationDrawerCallback.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavigationDrawerCallback.kt new file mode 100644 index 00000000..402521bf --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavigationDrawerCallback.kt @@ -0,0 +1,92 @@ +package com.yogeshpaliyal.keypasscompose.ui.nav + +import android.annotation.SuppressLint +import android.view.View +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.R +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.yogeshpaliyal.common.utils.normalize +import kotlin.math.max + +/** + * A [BottomSheetBehavior.BottomSheetCallback] which helps break apart clients who would like to + * react to changed in either the bottom sheet's slide offset or state. Clients can dynamically + * add or remove [OnSlideAction]s or [OnStateChangedAction]s which will be run when the + * sheet's slideOffset or state are changed. + * + * This callback's behavior differs slightly in that the slideOffset passed to [OnSlideAction]s + * in [onSlide] is corrected to guarantee that the offset 0.0 always be exactly at the + * [BottomSheetBehavior.STATE_HALF_EXPANDED] state. + */ +class BottomNavigationDrawerCallback : BottomSheetBehavior.BottomSheetCallback() { + + private val onSlideActions: MutableList = mutableListOf() + private val onStateChangedActions: MutableList = mutableListOf() + + private var lastSlideOffset = -1.0F + private var halfExpandedSlideOffset = Float.MAX_VALUE + + override fun onSlide(sheet: View, slideOffset: Float) { + if (halfExpandedSlideOffset == Float.MAX_VALUE) + calculateInitialHalfExpandedSlideOffset(sheet) + + lastSlideOffset = slideOffset + // Correct for the fact that the slideOffset is not zero when half expanded + val trueOffset = if (slideOffset <= halfExpandedSlideOffset) { + slideOffset.normalize(-1F, halfExpandedSlideOffset, -1F, 0F) + } else { + slideOffset.normalize(halfExpandedSlideOffset, 1F, 0F, 1F) + } + + onSlideActions.forEach { it.onSlide(sheet, trueOffset) } + } + + override fun onStateChanged(sheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_HALF_EXPANDED) { + halfExpandedSlideOffset = lastSlideOffset + onSlide(sheet, lastSlideOffset) + } + + onStateChangedActions.forEach { it.onStateChanged(sheet, newState) } + } + + /** + * Calculate the onSlideOffset which will be given when the bottom sheet is in the + * [BottomSheetBehavior.STATE_HALF_EXPANDED] state. + * + * Recording the correct slide offset for the half expanded state happens in [onStateChanged]. + * Since the first time the sheet is opened, we haven't yet received a call to [onStateChanged], + * this method is used to calculate the initial value manually so we can smoothly normalize + * slideOffset values received between -1 and 1. + * + * See: + * [BottomSheetBehavior.calculateCollapsedOffset] + * [BottomSheetBehavior.calculateHalfExpandedOffset] + * [BottomSheetBehavior.dispatchOnSlide] + */ + @SuppressLint("PrivateResource") + private fun calculateInitialHalfExpandedSlideOffset(sheet: View) { + val parent = sheet.parent as CoordinatorLayout + val behavior = BottomSheetBehavior.from(sheet) + + val halfExpandedOffset = parent.height * (1 - behavior.halfExpandedRatio) + val peekHeightMin = parent.resources.getDimensionPixelSize( + R.dimen.design_bottom_sheet_peek_height_min + ) + val peek = max(peekHeightMin, parent.height - parent.width * 9 / 16) + val collapsedOffset = max( + parent.height - peek, + max(0, parent.height - sheet.height) + ) + halfExpandedSlideOffset = + (collapsedOffset - halfExpandedOffset) / (parent.height - collapsedOffset) + } + + fun addOnSlideAction(action: OnSlideAction): Boolean { + return onSlideActions.add(action) + } + + fun addOnStateChangedAction(action: OnStateChangedAction): Boolean { + return onStateChangedActions.add(action) + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/DashboardActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/DashboardActivity.kt new file mode 100644 index 00000000..ed900419 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/DashboardActivity.kt @@ -0,0 +1,233 @@ +package com.yogeshpaliyal.keypasscompose.ui.nav + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.WindowManager +import androidx.activity.viewModels +import androidx.annotation.MenuRes +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.navigation.NavController +import androidx.navigation.NavDestination +import androidx.navigation.fragment.NavHostFragment +import com.google.android.material.transition.MaterialElevationScale +import com.yogeshpaliyal.keypasscompose.R +import com.yogeshpaliyal.keypasscompose.databinding.ActivityDashboardBinding +import com.yogeshpaliyal.keypasscompose.ui.addTOTP.AddTOTPActivity +import com.yogeshpaliyal.keypasscompose.ui.detail.DetailActivity +import com.yogeshpaliyal.keypasscompose.ui.generate.GeneratePasswordActivity +import com.yogeshpaliyal.keypasscompose.ui.home.DashboardViewModel +import com.yogeshpaliyal.keypasscompose.ui.home.HomeFragmentDirections +import com.yogeshpaliyal.keypasscompose.ui.settings.MySettingsFragmentDirections +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class DashboardActivity : + AppCompatActivity(), + Toolbar.OnMenuItemClickListener, + NavController.OnDestinationChangedListener, + NavigationAdapter.NavigationAdapterListener { + lateinit var binding: ActivityDashboardBinding + + private val bottomNavDrawer: BottomNavDrawerFragment by lazy(LazyThreadSafetyMode.NONE) { + supportFragmentManager.findFragmentById(R.id.bottom_nav_drawer) as BottomNavDrawerFragment + } + + private val mViewModel by viewModels() + + private val currentNavigationFragment: Fragment? + get() = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) + ?.childFragmentManager + ?.fragments + ?.first() + + private val navController by lazy { + (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) + binding = ActivityDashboardBinding.inflate(layoutInflater) + setContentView(binding.root) + + // setSupportActionBar(binding.bottomAppBar) + + binding.lifecycleOwner = this + binding.viewModel = mViewModel + + navController.addOnDestinationChangedListener( + this@DashboardActivity + ) + + /* val intent = Intent(this, AuthenticationActivity::class.java) + startActivity(intent)*/ + + /* val autoFillService = getAutoFillService() + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + if (autoFillService?.isAutofillSupported == true && autoFillService.hasEnabledAutofillServices().not()) { + val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE) + intent.data = Uri.parse("package:$packageName") + startActivityForResult(intent,777) + } + }*/ + + binding.btnAdd.setOnClickListener { + currentNavigationFragment?.apply { + exitTransition = MaterialElevationScale(false).apply { + duration = + resources.getInteger(R.integer.keypass_motion_duration_large).toLong() + } + reenterTransition = MaterialElevationScale(true).apply { + duration = + resources.getInteger(R.integer.keypass_motion_duration_large).toLong() + } + } + + DetailActivity.start(this) + } + + bottomNavDrawer.apply { + // addOnSlideAction(HalfClockwiseRotateSlideAction(binding.bottomAppBar)) + // addOnSlideAction(AlphaSlideAction(binding.bottomAppBarTitle, true)) + addOnStateChangedAction(ShowHideFabStateAction(binding.btnAdd)) + addOnStateChangedAction( + ChangeSettingsMenuStateAction { showSettings -> + // Toggle between the current destination's BAB menu and the menu which should + // be displayed when the BottomNavigationDrawer is open. + binding.bottomAppBar.replaceMenu( + if (showSettings) { + R.menu.bottom_app_bar_settings_menu + } else { + getBottomAppBarMenuForDestination() + } + ) + } + ) + + // addOnSandwichSlideAction(HalfCounterClockwiseRotateSlideAction(binding.bottomAppBarChevron)) + addNavigationListener(this@DashboardActivity) + } + + // Set up the BottomAppBar menu + binding.bottomAppBar.apply { + setNavigationOnClickListener { + bottomNavDrawer.toggle() + } + setOnMenuItemClickListener(this@DashboardActivity) + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.action_settings -> { + val settingDestination = MySettingsFragmentDirections.actionGlobalSettings() + navController.navigate(settingDestination) + bottomNavDrawer.close() + } + } + return true + } + + override fun onDestinationChanged( + controller: NavController, + destination: NavDestination, + arguments: Bundle? + ) { + + binding.searchAppBar.isVisible = destination.id == R.id.homeFragment + when (destination.id) { + R.id.homeFragment -> { + binding.btnAdd.isActivated = false + setBottomAppBarForHome(getBottomAppBarMenuForDestination(destination)) + } + } + } + + override fun onNavMenuItemClicked(item: NavigationModelItem.NavMenuItem) { + // Swap the list of emails for the given mailbox + // navigateToHome(item.titleRes, item.mailbox) + when (item.id) { + NavigationModel.GENERATE_PASSWORD -> { + val intent = Intent(this, GeneratePasswordActivity::class.java) + startActivity(intent) + } + NavigationModel.HOME -> { + val args = HomeFragmentDirections.actionGlobalHomeFragment() + navController.navigate(args) + } + NavigationModel.ADD_TOPT -> { + AddTOTPActivity.start(this) + } + } + } + + override fun onNavEmailFolderClicked(folder: NavigationModelItem.NavEmailFolder) { + mViewModel.tag.postValue(folder.category) + val destination = HomeFragmentDirections.actionGlobalHomeFragmentTag() + navController.navigate(destination) + bottomNavDrawer.close() + } + + private fun hideBottomAppBar() { + binding.run { + bottomAppBar.performHide() + // Get a handle on the animator that hides the bottom app bar so we can wait to hide + // the fab and bottom app bar until after it's exit animation finishes. + bottomAppBar.animate().setListener(object : AnimatorListenerAdapter() { + var isCanceled = false + override fun onAnimationEnd(animation: Animator) { + if (isCanceled) return + + // Hide the BottomAppBar to avoid it showing above the keyboard + // when composing a new email. + bottomAppBar.visibility = View.GONE + btnAdd.visibility = View.INVISIBLE + } + + override fun onAnimationCancel(animation: Animator) { + isCanceled = true + } + }) + } + } + + /** + * Helper function which returns the menu which should be displayed for the current + * destination. + * + * Used both when the destination has changed, centralizing destination-to-menu mapping, as + * well as switching between the alternate menu used when the BottomNavigationDrawer is + * open and closed. + */ + @MenuRes + private fun getBottomAppBarMenuForDestination(destination: NavDestination? = null): Int { + val dest = destination ?: navController.currentDestination + return when (dest?.id) { + R.id.homeFragment -> R.menu.bottom_app_bar_settings_menu + // R.id.emailFragment -> R.menu.bottom_app_bar_email_menu + else -> R.menu.bottom_app_bar_settings_menu + } + } + + private fun setBottomAppBarForHome(@MenuRes menuRes: Int) { + binding.run { + btnAdd.setImageState(intArrayOf(-android.R.attr.state_activated), true) + bottomAppBar.visibility = View.VISIBLE + bottomAppBar.replaceMenu(menuRes) + // btnAdd.contentDescription = getString(R.string.fab_compose_email_content_description) + // bottomAppBarTitle.visibility = View.VISIBLE + bottomAppBar.performShow() + btnAdd.show() + } + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationAdapter.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationAdapter.kt new file mode 100644 index 00000000..9d85f0c7 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationAdapter.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yogeshpaliyal.keypasscompose.ui.nav + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import com.yogeshpaliyal.keypasscompose.databinding.NavDividerItemLayoutBinding +import com.yogeshpaliyal.keypasscompose.databinding.NavEmailFolderItemLayoutBinding +import com.yogeshpaliyal.keypasscompose.databinding.NavMenuItemLayoutBinding + +private const val VIEW_TYPE_NAV_MENU_ITEM = 4 +private const val VIEW_TYPE_NAV_DIVIDER = 6 +private const val VIEW_TYPE_NAV_EMAIL_FOLDER_ITEM = 5 + +class NavigationAdapter( + private val listener: NavigationAdapterListener +) : ListAdapter>( + NavigationModelItem.NavModelItemDiff +) { + + interface NavigationAdapterListener { + fun onNavMenuItemClicked(item: NavigationModelItem.NavMenuItem) + fun onNavEmailFolderClicked(folder: NavigationModelItem.NavEmailFolder) + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is NavigationModelItem.NavMenuItem -> VIEW_TYPE_NAV_MENU_ITEM + is NavigationModelItem.NavDivider -> VIEW_TYPE_NAV_DIVIDER + is NavigationModelItem.NavEmailFolder -> VIEW_TYPE_NAV_EMAIL_FOLDER_ITEM + else -> throw RuntimeException("Unsupported ItemViewType for obj ${getItem(position)}") + } + } + + @Suppress("unchecked_cast") + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): NavigationViewHolder { + return when (viewType) { + VIEW_TYPE_NAV_MENU_ITEM -> NavigationViewHolder.NavMenuItemViewHolder( + NavMenuItemLayoutBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + listener + ) + VIEW_TYPE_NAV_DIVIDER -> NavigationViewHolder.NavDividerViewHolder( + NavDividerItemLayoutBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + VIEW_TYPE_NAV_EMAIL_FOLDER_ITEM -> NavigationViewHolder.EmailFolderViewHolder( + NavEmailFolderItemLayoutBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + listener + ) + else -> throw RuntimeException("Unsupported view holder type") + } as NavigationViewHolder + } + + override fun onBindViewHolder( + holder: NavigationViewHolder, + position: Int + ) { + holder.bind(getItem(position)) + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModel.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModel.kt new file mode 100644 index 00000000..e76ccf5e --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModel.kt @@ -0,0 +1,34 @@ +package com.yogeshpaliyal.keypasscompose.ui.nav + +import com.yogeshpaliyal.keypasscompose.R + +/** + * A class which maintains and generates a navigation list to be displayed by [NavigationAdapter]. + */ +object NavigationModel { + + const val HOME = 0 + const val GENERATE_PASSWORD = 1 + const val ADD_TOPT = 2 + + var navigationMenuItems = mutableListOf( + NavigationModelItem.NavMenuItem( + id = HOME, + icon = R.drawable.ic_twotone_home_24, + titleRes = R.string.home, + checked = false, + ), + NavigationModelItem.NavMenuItem( + id = GENERATE_PASSWORD, + icon = R.drawable.ic_twotone_vpn_key_24, + titleRes = R.string.generate_password, + checked = false, + ), + NavigationModelItem.NavMenuItem( + id = ADD_TOPT, + icon = R.drawable.ic_twotone_totp, + titleRes = R.string.add_totp, + checked = false, + ) + ) +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModelItem.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModelItem.kt new file mode 100644 index 00000000..a7bd8609 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModelItem.kt @@ -0,0 +1,63 @@ +package com.yogeshpaliyal.keypasscompose.ui.nav + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.recyclerview.widget.DiffUtil +import com.yogeshpaliyal.keypasscompose.utils.StringDiffUtil + +/** + * A sealed class which encapsulates all objects [NavigationAdapter] is able to display. + */ +sealed class NavigationModelItem { + + /** + * A class which represents a checkable, navigation destination such as 'Inbox' or 'Sent'. + */ + data class NavMenuItem( + val id: Int, + @DrawableRes val icon: Int, + @StringRes val titleRes: Int, + var checked: Boolean + ) : NavigationModelItem() + + /** + * A class which is used to show a section divider (a subtitle and underline) between + * sections of different NavigationModelItem types. + */ + data class NavDivider(val title: String) : NavigationModelItem() + + /** + * A class which is used to show an [EmailFolder] in the [NavigationAdapter]. + */ + data class NavEmailFolder(val category: String) : NavigationModelItem() + + object NavModelItemDiff : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: NavigationModelItem, + newItem: NavigationModelItem + ): Boolean { + return when { + oldItem is NavMenuItem && newItem is NavMenuItem -> + oldItem.id == newItem.id + oldItem is NavEmailFolder && newItem is NavEmailFolder -> + StringDiffUtil.areItemsTheSame(oldItem.category, newItem.category) + else -> oldItem == newItem + } + } + + override fun areContentsTheSame( + oldItem: NavigationModelItem, + newItem: NavigationModelItem + ): Boolean { + return when { + oldItem is NavMenuItem && newItem is NavMenuItem -> + oldItem.icon == newItem.icon && + oldItem.titleRes == newItem.titleRes && + oldItem.checked == newItem.checked + oldItem is NavEmailFolder && newItem is NavEmailFolder -> + StringDiffUtil.areContentsTheSame(oldItem.category, newItem.category) + else -> false + } + } + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationViewHolder.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationViewHolder.kt new file mode 100644 index 00000000..4183f421 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationViewHolder.kt @@ -0,0 +1,51 @@ +package com.yogeshpaliyal.keypasscompose.ui.nav + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.yogeshpaliyal.keypasscompose.databinding.NavDividerItemLayoutBinding +import com.yogeshpaliyal.keypasscompose.databinding.NavEmailFolderItemLayoutBinding +import com.yogeshpaliyal.keypasscompose.databinding.NavMenuItemLayoutBinding + +sealed class NavigationViewHolder( + view: View +) : RecyclerView.ViewHolder(view) { + + abstract fun bind(navItem: T) + + class NavMenuItemViewHolder( + private val binding: NavMenuItemLayoutBinding, + private val listener: NavigationAdapter.NavigationAdapterListener + ) : NavigationViewHolder(binding.root) { + + override fun bind(navItem: NavigationModelItem.NavMenuItem) { + binding.run { + navMenuItem = navItem + navListener = listener + executePendingBindings() + } + } + } + + class NavDividerViewHolder( + private val binding: NavDividerItemLayoutBinding + ) : NavigationViewHolder(binding.root) { + override fun bind(navItem: NavigationModelItem.NavDivider) { + binding.navDivider = navItem + binding.executePendingBindings() + } + } + + class EmailFolderViewHolder( + private val binding: NavEmailFolderItemLayoutBinding, + private val listener: NavigationAdapter.NavigationAdapterListener + ) : NavigationViewHolder(binding.root) { + + override fun bind(navItem: NavigationModelItem.NavEmailFolder) { + binding.run { + navEmailFolder = navItem + navListener = listener + executePendingBindings() + } + } + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnSlideAction.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnSlideAction.kt new file mode 100644 index 00000000..c944bee3 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnSlideAction.kt @@ -0,0 +1,45 @@ +package com.yogeshpaliyal.keypasscompose.ui.nav + +import android.view.View +import androidx.annotation.FloatRange +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.yogeshpaliyal.common.utils.normalize + +/** + * An action to be performed when a bottom sheet's slide offset is changed. + */ +interface OnSlideAction { + /** + * Called when the bottom sheet's [slideOffset] is changed. [slideOffset] will always be a + * value between -1.0 and 1.0. -1.0 is equal to [BottomSheetBehavior.STATE_HIDDEN], 0.0 + * is equal to [BottomSheetBehavior.STATE_HALF_EXPANDED] and 1.0 is equal to + * [BottomSheetBehavior.STATE_EXPANDED]. + */ + fun onSlide( + sheet: View, + @FloatRange( + from = -1.0, + fromInclusive = true, + to = 1.0, + toInclusive = true + ) slideOffset: Float + ) +} + +/** + * Change the alpha of [view] when a bottom sheet is slid. + * + * @param reverse Setting reverse to true will cause the view's alpha to approach 0.0 as the sheet + * slides up. The default behavior, false, causes the view's alpha to approach 1.0 as the sheet + * slides up. + */ +class AlphaSlideAction( + private val view: View, + private val reverse: Boolean = false +) : OnSlideAction { + + override fun onSlide(sheet: View, slideOffset: Float) { + val alpha = slideOffset.normalize(-1F, 0F, 0F, 1F) + view.alpha = if (!reverse) alpha else 1F - alpha + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnStateChangedAction.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnStateChangedAction.kt new file mode 100644 index 00000000..1b7c24a7 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnStateChangedAction.kt @@ -0,0 +1,90 @@ + +package com.yogeshpaliyal.keypasscompose.ui.nav + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.floatingactionbutton.FloatingActionButton + +/** + * An action to be performed when a bottom sheet's state is changed. + */ +interface OnStateChangedAction { + fun onStateChanged(sheet: View, newState: Int) +} + +/** + * A state change action that tells the calling client when a open-sheet specific menu should be + * used. + */ +class ChangeSettingsMenuStateAction( + private val onShouldShowSettingsMenu: (showSettings: Boolean) -> Unit +) : OnStateChangedAction { + + private var hasCalledShowSettingsMenu: Boolean = false + + override fun onStateChanged(sheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_HIDDEN) { + hasCalledShowSettingsMenu = false + onShouldShowSettingsMenu(false) + } else { + if (!hasCalledShowSettingsMenu) { + hasCalledShowSettingsMenu = true + onShouldShowSettingsMenu(true) + } + } + } +} + +/** + * A state change action that handles showing the fab when the sheet is hidden and hiding the fab + * when the sheet is not hidden. + */ +class ShowHideFabStateAction( + private val fab: FloatingActionButton +) : OnStateChangedAction { + + override fun onStateChanged(sheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_HIDDEN) { + fab.show() + } else { + fab.hide() + } + } +} + +/** + * A state change action that sets a view's visibility depending on whether the sheet is hidden + * or not. + * + * By default, the view will be hidden when the sheet is hidden and shown when the sheet is shown + * (not hidden). If [reverse] is set to true, the view will be shown when the sheet is hidden and + * hidden when the sheet is shown (not hidden). + */ +class VisibilityStateAction( + private val view: View, + private val reverse: Boolean = false +) : OnStateChangedAction { + override fun onStateChanged(sheet: View, newState: Int) { + val stateHiddenVisibility = if (!reverse) View.GONE else View.VISIBLE + val stateDefaultVisibility = if (!reverse) View.VISIBLE else View.GONE + when (newState) { + BottomSheetBehavior.STATE_HIDDEN -> view.visibility = stateHiddenVisibility + else -> view.visibility = stateDefaultVisibility + } + } +} + +/** + * A state change action which scrolls a [RecyclerView] to the top when the sheet is hidden. + * + * This is used to make sure the navigation drawer's [RecyclerView] is never half-scrolled when + * opened to the half-expanded state, which can happen if the sheet is hidden while scrolled. + */ +class ScrollToTopStateAction( + private val recyclerView: RecyclerView +) : OnStateChangedAction { + override fun onStateChanged(sheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_HIDDEN) recyclerView.scrollToPosition(0) + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/settings/MySettingsFragment.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/settings/MySettingsFragment.kt new file mode 100644 index 00000000..cc59ec11 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/settings/MySettingsFragment.kt @@ -0,0 +1,203 @@ +package com.yogeshpaliyal.keypasscompose.ui.settings + +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.widget.Toast +import androidx.core.content.ContextCompat.getSystemService +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.yogeshpaliyal.common.dbhelper.createBackup +import com.yogeshpaliyal.common.dbhelper.restoreBackup +import com.yogeshpaliyal.common.utils.email +import com.yogeshpaliyal.common.utils.getOrCreateBackupKey +import com.yogeshpaliyal.common.utils.setBackupDirectory +import com.yogeshpaliyal.keypasscompose.BuildConfig +import com.yogeshpaliyal.keypasscompose.R +import com.yogeshpaliyal.keypasscompose.databinding.LayoutBackupKeypharseBinding +import com.yogeshpaliyal.keypasscompose.databinding.LayoutRestoreKeypharseBinding +import com.yogeshpaliyal.keypasscompose.ui.backup.BackupActivity +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import javax.inject.Inject + +private const val CHOOSE_BACKUPS_LOCATION_REQUEST_CODE = 26212 +private const val CHOOSE_RESTORE_FILE_REQUEST_CODE = 26213 + +@AndroidEntryPoint +class MySettingsFragment : PreferenceFragmentCompat() { + + @Inject + lateinit var appDb: com.yogeshpaliyal.common.AppDatabase + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.preferences, rootKey) + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + return when (preference.key) { + "feedback" -> { + context?.email( + getString(R.string.feedback_to_keypass), + "yogeshpaliyal.foss@gmail.com" + ) + true + } + + "backup" -> { + BackupActivity.start(context) + true + } + + getString(R.string.settings_restore_backup) -> { + selectRestoreFile() + true + } + + "share" -> { + val sendIntent = Intent() + sendIntent.action = Intent.ACTION_SEND + sendIntent.putExtra( + Intent.EXTRA_TEXT, + "KeyPass Password Manager\n Offline, Secure, Open Source https://play.google.com/store/apps/details?id=" + BuildConfig.APPLICATION_ID + ) + sendIntent.type = "text/plain" + startActivity(Intent.createChooser(sendIntent, getString(R.string.share_keypass))) + true + } + else -> super.onPreferenceTreeClick(preference) + } + } + + private fun selectRestoreFile() { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" + + intent.addFlags( + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + + try { + startActivityForResult(intent, CHOOSE_RESTORE_FILE_REQUEST_CODE) + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == CHOOSE_BACKUPS_LOCATION_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + val contentResolver = context?.contentResolver + val selectedDirectory = data?.data + if (contentResolver != null && selectedDirectory != null) { + contentResolver.takePersistableUriPermission( + selectedDirectory, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + + lifecycleScope.launch { + context?.setBackupDirectory(selectedDirectory.toString()) + + backup(selectedDirectory) + } + } + } else if (requestCode == CHOOSE_RESTORE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + val contentResolver = context?.contentResolver + val selectedFile = data?.data + if (contentResolver != null && selectedFile != null) { + + val binding = LayoutRestoreKeypharseBinding.inflate(layoutInflater) + + MaterialAlertDialogBuilder(requireContext()).setView(binding.root) + .setNegativeButton( + "Cancel" + ) { dialog, which -> + dialog.dismiss() + } + .setPositiveButton( + "Restore" + ) { dialog, which -> + lifecycleScope.launch { + val result = appDb.restoreBackup( + binding.etKeyPhrase.text.toString(), + contentResolver, + selectedFile + ) + if (result) { + dialog?.dismiss() + Toast.makeText( + context, + getString(R.string.backup_restored), + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + context, + getString(R.string.invalid_keyphrase), + Toast.LENGTH_SHORT + ).show() + } + } + }.show() + } + } + } + + suspend fun backup(selectedDirectory: Uri) { + + val keyPair = requireContext().getOrCreateBackupKey() + + val tempFile = DocumentFile.fromTreeUri(requireContext(), selectedDirectory)?.createFile( + "*/*", + "key_pass_backup_${System.currentTimeMillis()}.keypass" + ) + + lifecycleScope.launch { + context?.contentResolver?.let { + appDb.createBackup( + keyPair.second, + it, + tempFile?.uri + ) + if (keyPair.first) { + val binding = LayoutBackupKeypharseBinding.inflate(layoutInflater) + binding.txtCode.text = requireContext().getOrCreateBackupKey().second + binding.txtCode.setOnClickListener { + val clipboard = + getSystemService(requireContext(), ClipboardManager::class.java) + val clip = ClipData.newPlainText( + getString(R.string.app_name), + binding.txtCode.text + ) + clipboard?.setPrimaryClip(clip) + Toast.makeText( + context, + getString(R.string.copied_to_clipboard), + Toast.LENGTH_SHORT + ).show() + } + MaterialAlertDialogBuilder(requireContext()).setView(binding.root) + .setPositiveButton( + "Yes" + ) { dialog, which -> + dialog?.dismiss() + }.show() + } else { + Toast.makeText( + context, + getString(R.string.backup_completed), + Toast.LENGTH_SHORT + ).show() + } + } + } + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Color.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Color.kt deleted file mode 100644 index ed07d567..00000000 --- a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Color.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.yogeshpaliyal.keypasscompose.ui.theme - -import androidx.compose.ui.graphics.Color - -@Suppress -val KeyPassWhite50 = Color(0xFFffffff) - -val KeyPassBlack800 = Color(0xFF121212) -val KeyPassBlack900 = Color(0xFF000000) - -val KeyPassBlue50 = Color(0xFFeef0f2) -val KeyPassBlue100 = Color(0xFFd2dbe0) -val KeyPassBlue200 = Color(0xFFadbbc4) -val KeyPassBlue300 = Color(0xFF8ca2ae) -val KeyPassBlue600 = Color(0xFF4a6572) -val KeyPassBlue700 = Color(0xFF344955) -val KeyPassBlue800 = Color(0xFF232f34) -val Blue800 = Color(0xFF6c63ff) - -val KeyPassOrange300 = Color(0xFFfbd790) -val KeyPassOrange400 = Color(0xFFf9be64) -val KeyPassOrange500 = Color(0xFFf9aa33) - -val KeyPassRed200 = Color(0xFFcf7779) -val KeyPassRed400 = Color(0xFFff4c5d) - -val KeyPassWhite50alpha060 = Color(0x99ffffff) - -val KeyPassBlue50alpha060 = Color(0x99eef0f2) - -val KeyPassBlack900alpha020 = Color(0x33000000) -val KeyPassBlack900alpha087 = Color(0xde000000) -val KeyPassBlack900alpha060 = Color(0x99000000) diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Material3Components.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Material3Components.kt deleted file mode 100644 index f43317ec..00000000 --- a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Material3Components.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.yogeshpaliyal.keypasscompose.ui.theme - -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.RowScope -import androidx.compose.material.AppBarDefaults -import androidx.compose.material.BottomAppBar -import androidx.compose.material.DrawerDefaults -import androidx.compose.material.FabPosition -import androidx.compose.material.Scaffold -import androidx.compose.material.ScaffoldState -import androidx.compose.material.SnackbarHost -import androidx.compose.material.SnackbarHostState -import androidx.compose.material.rememberScaffoldState -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.contentColorFor -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.unit.Dp - -@Composable -fun Material3BottomAppBar( - modifier: Modifier = Modifier, - backgroundColor: Color = MaterialTheme.colorScheme.surface, - contentColor: Color = contentColorFor(backgroundColor), - cutoutShape: Shape? = null, - elevation: Dp = AppBarDefaults.BottomAppBarElevation, - contentPadding: PaddingValues = AppBarDefaults.ContentPadding, - content: @Composable RowScope.() -> Unit -) { - BottomAppBar( - modifier, - backgroundColor, - contentColor, - cutoutShape, - elevation, - contentPadding, - content - ) -} - -@Composable -fun Material3Scaffold( - modifier: Modifier = Modifier, - scaffoldState: ScaffoldState = rememberScaffoldState(), - topBar: @Composable () -> Unit = {}, - bottomBar: @Composable () -> Unit = {}, - snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, - floatingActionButton: @Composable () -> Unit = {}, - floatingActionButtonPosition: FabPosition = FabPosition.End, - isFloatingActionButtonDocked: Boolean = false, - drawerContent: @Composable (ColumnScope.() -> Unit)? = null, - drawerGesturesEnabled: Boolean = true, - drawerShape: Shape = androidx.compose.material.MaterialTheme.shapes.large, - drawerElevation: Dp = DrawerDefaults.Elevation, - drawerBackgroundColor: Color = MaterialTheme.colorScheme.surface, - drawerContentColor: Color = contentColorFor( - drawerBackgroundColor - ), - drawerScrimColor: Color = DrawerDefaults.scrimColor, - backgroundColor: Color = MaterialTheme.colorScheme.background, - contentColor: Color = contentColorFor( - backgroundColor - ), - content: @Composable (PaddingValues) -> Unit -) { - Scaffold(modifier, scaffoldState, topBar, bottomBar, snackbarHost, floatingActionButton, floatingActionButtonPosition, isFloatingActionButtonDocked, drawerContent, drawerGesturesEnabled, drawerShape, drawerElevation, drawerBackgroundColor, drawerContentColor, drawerScrimColor, backgroundColor, contentColor, content) -} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Shape.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Shape.kt deleted file mode 100644 index 8a765e43..00000000 --- a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Shape.kt +++ /dev/null @@ -1 +0,0 @@ -package com.yogeshpaliyal.keypasscompose.ui.theme diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Theme.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Theme.kt deleted file mode 100644 index 54571a3d..00000000 --- a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Theme.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.yogeshpaliyal.keypasscompose.ui.theme - -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable - -private val DarkColorPalette = darkColorScheme( - primary = KeyPassBlue700, - primaryContainer = KeyPassOrange500, - secondary = KeyPassOrange500 -) - -private val LightColorPalette = lightColorScheme( - primary = KeyPassBlue700, - primaryContainer = KeyPassOrange500, - secondary = KeyPassOrange500, - tertiaryContainer = Blue800, - background = KeyPassBlue50, - surface = KeyPassWhite50, - error = KeyPassRed400, - onPrimary = KeyPassWhite50, - onSecondary = KeyPassBlack900, - onSurface = KeyPassBlack900, - onError = KeyPassBlack900 - - /* Other default colors to override - background = Color.White, - surface = Color.White, - onPrimary = Color.White, - onSecondary = Color.Black, - onBackground = Color.Black, - onSurface = Color.Black, - */ -) - -@Composable -fun KeyPassTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { - val colors = if (darkTheme) { - DarkColorPalette - } else { - LightColorPalette - } - - MaterialTheme( - colorScheme = colors, - typography = Typography, - content = content - ) -} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Type.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Type.kt deleted file mode 100644 index b07440d3..00000000 --- a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Type.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.yogeshpaliyal.keypasscompose.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - bodyMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp - ) - /* Other default text styles to override - button = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.W500, - fontSize = 14.sp - ), - caption = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 12.sp - ) - */ -) diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/BindingAdapter.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/BindingAdapter.kt new file mode 100644 index 00000000..98d0c38c --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/BindingAdapter.kt @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License + * + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yogeshpaliyal.keypasscompose.utils + +import android.graphics.drawable.ColorDrawable +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.view.WindowInsets +import android.widget.Spinner +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.core.view.updateLayoutParams +import androidx.databinding.BindingAdapter +import com.google.android.material.elevation.ElevationOverlayProvider +import com.yogeshpaliyal.common.utils.getDrawableOrNull + +@BindingAdapter( + "popupElevationOverlay" +) +fun Spinner.bindPopupElevationOverlay(popupElevationOverlay: Float) { + setPopupBackgroundDrawable( + ColorDrawable( + ElevationOverlayProvider(context) + .compositeOverlayWithThemeSurfaceColorIfNeeded(popupElevationOverlay) + ) + ) +} + +@BindingAdapter( + "drawableStart", + "drawableLeft", + "drawableTop", + "drawableEnd", + "drawableRight", + "drawableBottom", + requireAll = false +) +fun TextView.bindDrawables( + @DrawableRes drawableStart: Int? = null, + @DrawableRes drawableLeft: Int? = null, + @DrawableRes drawableTop: Int? = null, + @DrawableRes drawableEnd: Int? = null, + @DrawableRes drawableRight: Int? = null, + @DrawableRes drawableBottom: Int? = null +) { + setCompoundDrawablesWithIntrinsicBounds( + context.getDrawableOrNull(drawableStart ?: drawableLeft), + context.getDrawableOrNull(drawableTop), + context.getDrawableOrNull(drawableEnd ?: drawableRight), + context.getDrawableOrNull(drawableBottom) + ) +} + +@BindingAdapter("goneIf") +fun View.bindGoneIf(gone: Boolean) { + visibility = if (gone) { + GONE + } else { + VISIBLE + } +} + +@BindingAdapter("layoutFullscreen") +fun View.bindLayoutFullscreen(previousFullscreen: Boolean, fullscreen: Boolean) { + if (previousFullscreen != fullscreen && fullscreen) { + systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + } +} + +@BindingAdapter( + "paddingLeftSystemWindowInsets", + "paddingTopSystemWindowInsets", + "paddingRightSystemWindowInsets", + "paddingBottomSystemWindowInsets", + requireAll = false +) +fun View.applySystemWindowInsetsPadding( + previousApplyLeft: Boolean, + previousApplyTop: Boolean, + previousApplyRight: Boolean, + previousApplyBottom: Boolean, + applyLeft: Boolean, + applyTop: Boolean, + applyRight: Boolean, + applyBottom: Boolean +) { + if (previousApplyLeft == applyLeft && + previousApplyTop == applyTop && + previousApplyRight == applyRight && + previousApplyBottom == applyBottom + ) { + return + } + + doOnApplyWindowInsets { view, insets, padding, _, _ -> + val left = if (applyLeft) insets.systemWindowInsetLeft else 0 + val top = if (applyTop) insets.systemWindowInsetTop else 0 + val right = if (applyRight) insets.systemWindowInsetRight else 0 + val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0 + + view.setPadding( + padding.left + left, + padding.top + top, + padding.right + right, + padding.bottom + bottom + ) + } +} + +@BindingAdapter( + "marginLeftSystemWindowInsets", + "marginTopSystemWindowInsets", + "marginRightSystemWindowInsets", + "marginBottomSystemWindowInsets", + requireAll = false +) +fun View.applySystemWindowInsetsMargin( + previousApplyLeft: Boolean, + previousApplyTop: Boolean, + previousApplyRight: Boolean, + previousApplyBottom: Boolean, + applyLeft: Boolean, + applyTop: Boolean, + applyRight: Boolean, + applyBottom: Boolean +) { + if (previousApplyLeft == applyLeft && + previousApplyTop == applyTop && + previousApplyRight == applyRight && + previousApplyBottom == applyBottom + ) { + return + } + + doOnApplyWindowInsets { view, insets, _, margin, _ -> + val left = if (applyLeft) insets.systemWindowInsetLeft else 0 + val top = if (applyTop) insets.systemWindowInsetTop else 0 + val right = if (applyRight) insets.systemWindowInsetRight else 0 + val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0 + + view.updateLayoutParams { + leftMargin = margin.left + left + topMargin = margin.top + top + rightMargin = margin.right + right + bottomMargin = margin.bottom + bottom + } + } +} + +fun View.doOnApplyWindowInsets( + block: (View, WindowInsets, InitialPadding, InitialMargin, Int) -> Unit +) { + // Create a snapshot of the view's padding & margin states + val initialPadding = recordInitialPaddingForView(this) + val initialMargin = recordInitialMarginForView(this) + val initialHeight = recordInitialHeightForView(this) + // Set an actual OnApplyWindowInsetsListener which proxies to the given + // lambda, also passing in the original padding & margin states + setOnApplyWindowInsetsListener { v, insets -> + block(v, insets, initialPadding, initialMargin, initialHeight) + // Always return the insets, so that children can also use them + insets + } + // request some insets + requestApplyInsetsWhenAttached() +} + +class InitialPadding(val left: Int, val top: Int, val right: Int, val bottom: Int) + +class InitialMargin(val left: Int, val top: Int, val right: Int, val bottom: Int) + +private fun recordInitialPaddingForView(view: View) = InitialPadding( + view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom +) + +private fun recordInitialMarginForView(view: View): InitialMargin { + val lp = view.layoutParams as? ViewGroup.MarginLayoutParams + ?: throw IllegalArgumentException("Invalid view layout params") + return InitialMargin(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin) +} + +private fun recordInitialHeightForView(view: View): Int { + return view.layoutParams.height +} + +fun View.requestApplyInsetsWhenAttached() { + if (isAttachedToWindow) { + // We're already attached, just request as normal + requestApplyInsets() + } else { + // We're not attached to the hierarchy, add a listener to + // request when we are + addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + v.removeOnAttachStateChangeListener(this) + v.requestApplyInsets() + } + + override fun onViewDetachedFromWindow(v: View) = Unit + }) + } +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ContentViewBindingDelegate.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ContentViewBindingDelegate.kt new file mode 100644 index 00000000..05ae5245 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ContentViewBindingDelegate.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yogeshpaliyal.keypasscompose.utils + +import android.app.Activity +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import kotlin.reflect.KProperty + +/** + * A delegate who lazily inflates a data binding layout, calls [Activity.setContentView], sets + * the lifecycle owner and returns the binding. + */ +class ContentViewBindingDelegate( + @LayoutRes private val layoutRes: Int +) { + + private var binding: T? = null + + operator fun getValue(activity: R, property: KProperty<*>): T { + if (binding == null) { + binding = DataBindingUtil.setContentView(activity, layoutRes).apply { + lifecycleOwner = activity + } + } + return binding!! + } +} + +fun contentView( + @LayoutRes layoutRes: Int +): ContentViewBindingDelegate = ContentViewBindingDelegate(layoutRes) diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/LogHelper.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/LogHelper.kt new file mode 100644 index 00000000..aaf57d60 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/LogHelper.kt @@ -0,0 +1,49 @@ +package com.yogeshpaliyal.keypasscompose.utils + +import android.util.Log +import com.yogeshpaliyal.keypasscompose.BuildConfig + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 26-12-2020 20:21 +*/ + +@JvmName("LogHelper") + +fun Any?.systemOutPrint() { + if (BuildConfig.DEBUG) println(this) +} + +fun Any?.systemErrPrint() { + if (BuildConfig.DEBUG) System.err.println(this) +} + +fun Exception?.debugPrintStackTrace() { + if (BuildConfig.DEBUG) this?.printStackTrace() +} + +fun Throwable?.debugPrintStackTrace() { + if (BuildConfig.DEBUG) this?.printStackTrace() +} + +fun Any?.logD(tag: String?) { + if (BuildConfig.DEBUG) Log.d(tag, this.toString()) +} + +fun Any?.logE(tag: String?) { + if (BuildConfig.DEBUG) Log.e(tag, this.toString()) +} + +fun Any?.logI(tag: String?) { + if (BuildConfig.DEBUG) Log.i(tag, this.toString()) +} + +fun Any?.logV(tag: String?) { + if (BuildConfig.DEBUG) Log.v(tag, this.toString()) +} + +fun Any?.logW(tag: String?) { + if (BuildConfig.DEBUG) Log.w(tag, this.toString()) +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/StringDiffUtil.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/StringDiffUtil.kt new file mode 100644 index 00000000..a418c972 --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/StringDiffUtil.kt @@ -0,0 +1,12 @@ +package com.yogeshpaliyal.keypasscompose.utils + +import androidx.recyclerview.widget.DiffUtil + +/** + * Alias to represent a folder (a String title) into which emails can be placed. + */ + +object StringDiffUtil : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem + override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem +} diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ViewExtensions.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ViewExtensions.kt new file mode 100644 index 00000000..203b8e1d --- /dev/null +++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ViewExtensions.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yogeshpaliyal.keypasscompose.utils + +import android.content.Context +import android.os.Build +import android.widget.TextView + +@Suppress("DEPRECATION") +fun TextView.setTextAppearanceCompat(context: Context, resId: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + setTextAppearance(resId) + } else { + setTextAppearance(context, resId) + } +} diff --git a/keypasscompose/src/main/res/color/color_navigation_drawer_menu_item.xml b/keypasscompose/src/main/res/color/color_navigation_drawer_menu_item.xml new file mode 100644 index 00000000..c714257d --- /dev/null +++ b/keypasscompose/src/main/res/color/color_navigation_drawer_menu_item.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/color/color_on_primary_surface_divider.xml b/keypasscompose/src/main/res/color/color_on_primary_surface_divider.xml new file mode 100644 index 00000000..3e4b2b99 --- /dev/null +++ b/keypasscompose/src/main/res/color/color_on_primary_surface_divider.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/color/color_on_primary_surface_emphasis_medium.xml b/keypasscompose/src/main/res/color/color_on_primary_surface_emphasis_medium.xml new file mode 100644 index 00000000..cefe6ba4 --- /dev/null +++ b/keypasscompose/src/main/res/color/color_on_primary_surface_emphasis_medium.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/color/color_on_surface_emphasis_high.xml b/keypasscompose/src/main/res/color/color_on_surface_emphasis_high.xml new file mode 100644 index 00000000..8e3ec6b9 --- /dev/null +++ b/keypasscompose/src/main/res/color/color_on_surface_emphasis_high.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/color/color_on_surface_emphasis_medium.xml b/keypasscompose/src/main/res/color/color_on_surface_emphasis_medium.xml new file mode 100644 index 00000000..bfbf26df --- /dev/null +++ b/keypasscompose/src/main/res/color/color_on_surface_emphasis_medium.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/drawable/asl_add_save.xml b/keypasscompose/src/main/res/drawable/asl_add_save.xml new file mode 100644 index 00000000..7fa37cb0 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/asl_add_save.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/drawable/avatar_none.xml b/keypasscompose/src/main/res/drawable/avatar_none.xml new file mode 100644 index 00000000..b09a6b43 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/avatar_none.xml @@ -0,0 +1,5 @@ + + + + diff --git a/keypasscompose/src/main/res/drawable/avd_add_to_save.xml b/keypasscompose/src/main/res/drawable/avd_add_to_save.xml new file mode 100644 index 00000000..7a51b491 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/avd_add_to_save.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/keypasscompose/src/main/res/drawable/avd_save_to_add.xml b/keypasscompose/src/main/res/drawable/avd_save_to_add.xml new file mode 100644 index 00000000..56320f6c --- /dev/null +++ b/keypasscompose/src/main/res/drawable/avd_save_to_add.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/keypasscompose/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml b/keypasscompose/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml new file mode 100644 index 00000000..6d147990 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/keypasscompose/src/main/res/drawable/ic_baseline_casino_24.xml b/keypasscompose/src/main/res/drawable/ic_baseline_casino_24.xml new file mode 100644 index 00000000..fb4b03aa --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_baseline_casino_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/keypasscompose/src/main/res/drawable/ic_baseline_feedback_24.xml b/keypasscompose/src/main/res/drawable/ic_baseline_feedback_24.xml new file mode 100644 index 00000000..170a74b1 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_baseline_feedback_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/keypasscompose/src/main/res/drawable/ic_baseline_refresh_24.xml b/keypasscompose/src/main/res/drawable/ic_baseline_refresh_24.xml new file mode 100644 index 00000000..f2be45ba --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_baseline_refresh_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/keypasscompose/src/main/res/drawable/ic_baseline_settings_24.xml b/keypasscompose/src/main/res/drawable/ic_baseline_settings_24.xml new file mode 100644 index 00000000..41a82ede --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_baseline_settings_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/keypasscompose/src/main/res/drawable/ic_baseline_share_24.xml b/keypasscompose/src/main/res/drawable/ic_baseline_share_24.xml new file mode 100644 index 00000000..2f13bb3e --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_baseline_share_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/keypasscompose/src/main/res/drawable/ic_round_add_24.xml b/keypasscompose/src/main/res/drawable/ic_round_add_24.xml new file mode 100644 index 00000000..24877ee9 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_round_add_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/keypasscompose/src/main/res/drawable/ic_round_done_24.xml b/keypasscompose/src/main/res/drawable/ic_round_done_24.xml new file mode 100644 index 00000000..6a86ea58 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_round_done_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/keypasscompose/src/main/res/drawable/ic_round_menu_24.xml b/keypasscompose/src/main/res/drawable/ic_round_menu_24.xml new file mode 100644 index 00000000..ff67e556 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_round_menu_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/keypasscompose/src/main/res/drawable/ic_round_refresh_24.xml b/keypasscompose/src/main/res/drawable/ic_round_refresh_24.xml new file mode 100644 index 00000000..5cdbeb07 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_round_refresh_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_content_copy_24.xml b/keypasscompose/src/main/res/drawable/ic_twotone_content_copy_24.xml new file mode 100644 index 00000000..86652df4 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_twotone_content_copy_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_delete_24.xml b/keypasscompose/src/main/res/drawable/ic_twotone_delete_24.xml new file mode 100644 index 00000000..b77afdc9 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_twotone_delete_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_folder.xml b/keypasscompose/src/main/res/drawable/ic_twotone_folder.xml new file mode 100644 index 00000000..b56127e2 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_twotone_folder.xml @@ -0,0 +1,14 @@ + + + + diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_home_24.xml b/keypasscompose/src/main/res/drawable/ic_twotone_home_24.xml new file mode 100644 index 00000000..347e5a63 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_twotone_home_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_qr_code_scanner_24.xml b/keypasscompose/src/main/res/drawable/ic_twotone_qr_code_scanner_24.xml new file mode 100644 index 00000000..597e8d7b --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_twotone_qr_code_scanner_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_totp.xml b/keypasscompose/src/main/res/drawable/ic_twotone_totp.xml new file mode 100644 index 00000000..3c6d07f4 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_twotone_totp.xml @@ -0,0 +1,10 @@ + + + diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_vpn_key_24.xml b/keypasscompose/src/main/res/drawable/ic_twotone_vpn_key_24.xml new file mode 100644 index 00000000..9913dd79 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_twotone_vpn_key_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/keypasscompose/src/main/res/drawable/ic_undraw_empty_street_sfxm.xml b/keypasscompose/src/main/res/drawable/ic_undraw_empty_street_sfxm.xml new file mode 100644 index 00000000..9dc244ee --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_undraw_empty_street_sfxm.xml @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/keypasscompose/src/main/res/drawable/ic_undraw_unlock_24mb.xml b/keypasscompose/src/main/res/drawable/ic_undraw_unlock_24mb.xml new file mode 100644 index 00000000..437a7ea0 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/ic_undraw_unlock_24mb.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/keypasscompose/src/main/res/drawable/nav_divider_top.xml b/keypasscompose/src/main/res/drawable/nav_divider_top.xml new file mode 100644 index 00000000..2ece804c --- /dev/null +++ b/keypasscompose/src/main/res/drawable/nav_divider_top.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/keypasscompose/src/main/res/drawable/white_circle.xml b/keypasscompose/src/main/res/drawable/white_circle.xml new file mode 100644 index 00000000..0a981d74 --- /dev/null +++ b/keypasscompose/src/main/res/drawable/white_circle.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/font/work_sans.xml b/keypasscompose/src/main/res/font/work_sans.xml new file mode 100644 index 00000000..9b5385f8 --- /dev/null +++ b/keypasscompose/src/main/res/font/work_sans.xml @@ -0,0 +1,7 @@ + + + diff --git a/keypasscompose/src/main/res/font/work_sans_medium.xml b/keypasscompose/src/main/res/font/work_sans_medium.xml new file mode 100644 index 00000000..9225d6e9 --- /dev/null +++ b/keypasscompose/src/main/res/font/work_sans_medium.xml @@ -0,0 +1,7 @@ + + + diff --git a/keypasscompose/src/main/res/layout/activity_add_totpactivity.xml b/keypasscompose/src/main/res/layout/activity_add_totpactivity.xml new file mode 100644 index 00000000..9564f274 --- /dev/null +++ b/keypasscompose/src/main/res/layout/activity_add_totpactivity.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/layout/activity_authentication.xml b/keypasscompose/src/main/res/layout/activity_authentication.xml new file mode 100644 index 00000000..15f78de2 --- /dev/null +++ b/keypasscompose/src/main/res/layout/activity_authentication.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/layout/activity_crash.xml b/keypasscompose/src/main/res/layout/activity_crash.xml new file mode 100644 index 00000000..4d8b0c7e --- /dev/null +++ b/keypasscompose/src/main/res/layout/activity_crash.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/layout/activity_dashboard.xml b/keypasscompose/src/main/res/layout/activity_dashboard.xml new file mode 100644 index 00000000..f06dccc2 --- /dev/null +++ b/keypasscompose/src/main/res/layout/activity_dashboard.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/layout/activity_generate_password.xml b/keypasscompose/src/main/res/layout/activity_generate_password.xml new file mode 100644 index 00000000..401dd93c --- /dev/null +++ b/keypasscompose/src/main/res/layout/activity_generate_password.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/layout/activity_scanner.xml b/keypasscompose/src/main/res/layout/activity_scanner.xml new file mode 100644 index 00000000..69c4b788 --- /dev/null +++ b/keypasscompose/src/main/res/layout/activity_scanner.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/layout/backup_activity.xml b/keypasscompose/src/main/res/layout/backup_activity.xml new file mode 100644 index 00000000..10af8e65 --- /dev/null +++ b/keypasscompose/src/main/res/layout/backup_activity.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/keypasscompose/src/main/res/layout/fragment_bottom_nav_drawer.xml b/keypasscompose/src/main/res/layout/fragment_bottom_nav_drawer.xml new file mode 100644 index 00000000..589a9b93 --- /dev/null +++ b/keypasscompose/src/main/res/layout/fragment_bottom_nav_drawer.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + diff --git a/keypasscompose/src/main/res/layout/fragment_detail.xml b/keypasscompose/src/main/res/layout/fragment_detail.xml new file mode 100644 index 00000000..ac248174 --- /dev/null +++ b/keypasscompose/src/main/res/layout/fragment_detail.xml @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +