From 4ba3b75f858251099f18f07be700f9900128f8e1 Mon Sep 17 00:00:00 2001 From: Aditya Wasan Date: Tue, 15 Sep 2020 21:53:12 +0530 Subject: [PATCH] Update on-boarding UI (#1099) * Add onboarding flow from v2 Signed-off-by: Aditya Wasan * Minor fixes Signed-off-by: Aditya Wasan * Add changelog entry Signed-off-by: Aditya Wasan * Remove old activity from manifest Signed-off-by: Aditya Wasan * Remove view type prefix from view ids Signed-off-by: Aditya Wasan * Review fixes Signed-off-by: Aditya Wasan * Treewide: Reformat code Signed-off-by: Aditya Wasan * Moar review fixes Signed-off-by: Aditya Wasan * Revert "Treewide: Reformat code" This reverts commit 348ef0050942526a55890b245afec8d7fee4d81e. Signed-off-by: Harsh Shandilya * onboarding: cleanup OnboardingActivity init Signed-off-by: Harsh Shandilya * Remove unused layout Signed-off-by: Harsh Shandilya * Remove unnecessary ConstraintLayout Signed-off-by: Harsh Shandilya * Shorten animation duration Signed-off-by: Harsh Shandilya * onboarding: use viewBinding extension in fragments Signed-off-by: Harsh Shandilya Co-authored-by: Harsh Shandilya --- CHANGELOG.md | 1 + app/src/main/AndroidManifest.xml | 2 +- .../java/com/zeapo/pwdstore/PasswordStore.kt | 1 + .../onboarding/activity/OnboardingActivity.kt | 26 ++++ .../ui/onboarding/fragments/CloneFragment.kt | 59 +++++++++ .../fragments/RepoLocationFragment.kt} | 125 ++++++++---------- .../onboarding/fragments/WelcomeFragment.kt | 25 ++++ .../com/zeapo/pwdstore/utils/Extensions.kt | 3 +- .../pwdstore/utils/FragmentExtensions.kt | 52 ++++++++ app/src/main/res/animator/slide_in_left.xml | 11 ++ app/src/main/res/animator/slide_in_right.xml | 11 ++ app/src/main/res/animator/slide_out_left.xml | 11 ++ app/src/main/res/animator/slide_out_right.xml | 11 ++ .../main/res/layout/activity_onboarding.xml | 76 ++--------- app/src/main/res/layout/fragment_clone.xml | 87 ++++++++++++ .../res/layout/fragment_repo_location.xml | 87 ++++++++++++ app/src/main/res/layout/fragment_welcome.xml | 51 +++++++ app/src/main/res/values/strings.xml | 20 ++- 18 files changed, 517 insertions(+), 142 deletions(-) create mode 100644 app/src/main/java/com/zeapo/pwdstore/ui/onboarding/activity/OnboardingActivity.kt create mode 100644 app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/CloneFragment.kt rename app/src/main/java/com/zeapo/pwdstore/{OnboardingActivity.kt => ui/onboarding/fragments/RepoLocationFragment.kt} (62%) create mode 100644 app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/WelcomeFragment.kt create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/FragmentExtensions.kt create mode 100644 app/src/main/res/animator/slide_in_left.xml create mode 100644 app/src/main/res/animator/slide_in_right.xml create mode 100644 app/src/main/res/animator/slide_out_left.xml create mode 100644 app/src/main/res/animator/slide_out_right.xml create mode 100644 app/src/main/res/layout/fragment_clone.xml create mode 100644 app/src/main/res/layout/fragment_repo_location.xml create mode 100644 app/src/main/res/layout/fragment_welcome.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 7672575f..2bd290d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file. - 'Show hidden folders' is now 'Show hidden files and folders' - Generated SSH keys are now stored in the Android Keystore if available, and encrypted at rest otherwise - Allow using device's screen lock credentials to secure generated SSH key +- Update onboarding UI ### Fixed diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 034124f9..3b0781f4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,7 +33,7 @@ android:label="@string/app_name" /> + if (result.resultCode == AppCompatActivity.RESULT_OK) { + settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } + finish() + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.cloneRemote.setOnClickListener { + cloneToHiddenDir() + } + binding.createLocal.setOnClickListener { + parentFragmentManager.performTransactionWithBackStack(RepoLocationFragment.newInstance()) + } + } + + /** + * Clones a remote Git repository to the app's private directory + */ + private fun cloneToHiddenDir() { + settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, false) } + cloneAction.launch(GitServerConfigActivity.createCloneIntent(requireContext())) + } + + companion object { + + fun newInstance(): CloneFragment = CloneFragment() + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/OnboardingActivity.kt b/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/RepoLocationFragment.kt similarity index 62% rename from app/src/main/java/com/zeapo/pwdstore/OnboardingActivity.kt rename to app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/RepoLocationFragment.kt index 45bf1d1c..7ff85237 100644 --- a/app/src/main/java/com/zeapo/pwdstore/OnboardingActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/RepoLocationFragment.kt @@ -1,55 +1,51 @@ /* * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. * SPDX-License-Identifier: GPL-3.0-only + * */ -package com.zeapo.pwdstore +package com.zeapo.pwdstore.ui.onboarding.fragments import android.Manifest -import android.content.Intent import android.os.Bundle -import androidx.activity.result.contract.ActivityResultContracts.RequestPermission -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import android.view.View +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit +import androidx.fragment.app.Fragment import com.github.ajalt.timberkt.d import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.runCatching import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.zeapo.pwdstore.databinding.ActivityOnboardingBinding -import com.zeapo.pwdstore.git.GitServerConfigActivity +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.UserPreference +import com.zeapo.pwdstore.databinding.FragmentRepoLocationBinding import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PreferenceKeys -import com.zeapo.pwdstore.utils.isPermissionGranted +import com.zeapo.pwdstore.utils.finish import com.zeapo.pwdstore.utils.getString +import com.zeapo.pwdstore.utils.isPermissionGranted import com.zeapo.pwdstore.utils.listFilesRecursively import com.zeapo.pwdstore.utils.sharedPrefs import com.zeapo.pwdstore.utils.viewBinding import java.io.File -class OnboardingActivity : AppCompatActivity() { +class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) { - private val binding by viewBinding(ActivityOnboardingBinding::inflate) - private val settings by lazy { applicationContext.sharedPrefs } + private val firstRunActivity by lazy { requireActivity() } + private val settings by lazy { firstRunActivity.applicationContext.sharedPrefs } + private val binding by viewBinding(FragmentRepoLocationBinding::bind) private val sortOrder: PasswordRepository.PasswordSortOrder get() = PasswordRepository.PasswordSortOrder.getSortOrder(settings) - private val cloneAction = registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } - finish() - } - } - - private val repositoryInitAction = registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { + private val repositoryInitAction = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK) { initializeRepositoryInfo() - finish() } } - private val externalDirectorySelectAction = registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { + private val externalDirectorySelectAction = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK) { if (checkExternalDirectory()) { finish() } else { @@ -58,40 +54,27 @@ class OnboardingActivity : AppCompatActivity() { } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - supportActionBar?.hide() - setContentView(binding.root) - binding.settingsButton.setOnClickListener { - startActivity(Intent(this, UserPreference::class.java)) - } - binding.localDirectoryButton.setOnClickListener { - MaterialAlertDialogBuilder(this) - .setTitle(resources.getString(R.string.location_dialog_title)) - .setMessage(resources.getString(R.string.location_dialog_create_text)) - .setPositiveButton(resources.getString(R.string.location_hidden)) { _, _ -> - createRepoInHiddenDir() - } - .setNegativeButton(resources.getString(R.string.location_sdcard)) { _, _ -> - createRepoFromExternalDir() - } - .show() - } - binding.cloneFromServerButton.setOnClickListener { - cloneToHiddenDir() - } + private val externalDirPermGrantedAction = createPermGrantedAction { + externalDirectorySelectAction.launch(UserPreference.createDirectorySelectionIntent(requireContext())) } - override fun onBackPressed() { - finishAffinity() + private val repositoryUsePermGrantedAction = createPermGrantedAction { + initializeRepositoryInfo() } - /** - * Clones a remote Git repository to the app's private directory - */ - private fun cloneToHiddenDir() { - settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, false) } - cloneAction.launch(GitServerConfigActivity.createCloneIntent(this)) + private val repositoryChangePermGrantedAction = createPermGrantedAction { + repositoryInitAction.launch(UserPreference.createDirectorySelectionIntent(requireContext())) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.hidden.setOnClickListener { + createRepoInHiddenDir() + } + + binding.sdcard.setOnClickListener { + createRepoFromExternalDir() + } } /** @@ -103,7 +86,6 @@ class OnboardingActivity : AppCompatActivity() { remove(PreferenceKeys.GIT_EXTERNAL_REPO) } initializeRepositoryInfo() - finish() } /** @@ -114,40 +96,28 @@ class OnboardingActivity : AppCompatActivity() { val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) if (externalRepo == null) { if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - registerForActivityResult(RequestPermission()) { granted -> - if (granted) { - externalDirectorySelectAction.launch(UserPreference.createDirectorySelectionIntent(this)) - } - }.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + externalDirPermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) } else { // Unlikely we have storage permissions without user ever selecting a directory, // but let's not assume. - externalDirectorySelectAction.launch(UserPreference.createDirectorySelectionIntent(this)) + externalDirectorySelectAction.launch(UserPreference.createDirectorySelectionIntent(requireContext())) } } else { - MaterialAlertDialogBuilder(this) + MaterialAlertDialogBuilder(requireActivity()) .setTitle(resources.getString(R.string.directory_selected_title)) .setMessage(resources.getString(R.string.directory_selected_message, externalRepo)) .setPositiveButton(resources.getString(R.string.use)) { _, _ -> if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - registerForActivityResult(RequestPermission()) { granted -> - if (granted) { - initializeRepositoryInfo() - } - }.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + repositoryUsePermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) } else { initializeRepositoryInfo() } } .setNegativeButton(resources.getString(R.string.change)) { _, _ -> if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - registerForActivityResult(RequestPermission()) { granted -> - if (granted) { - repositoryInitAction.launch(UserPreference.createDirectorySelectionIntent(this)) - } - }.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + repositoryChangePermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) } else { - repositoryInitAction.launch(UserPreference.createDirectorySelectionIntent(this)) + repositoryInitAction.launch(UserPreference.createDirectorySelectionIntent(requireContext())) } } .show() @@ -192,6 +162,7 @@ class OnboardingActivity : AppCompatActivity() { d { "Failed to delete local repository: $localDir" } } } + finish() } private fun initializeRepositoryInfo() { @@ -205,4 +176,16 @@ class OnboardingActivity : AppCompatActivity() { } createRepository() } + + private fun createPermGrantedAction(block: () -> Unit) = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + if (granted) { + block.invoke() + } + } + + companion object { + + fun newInstance(): RepoLocationFragment = RepoLocationFragment() + } } diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/WelcomeFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/WelcomeFragment.kt new file mode 100644 index 00000000..2cf4dd49 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/WelcomeFragment.kt @@ -0,0 +1,25 @@ +/* + * Copyright © 2019-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.ui.onboarding.fragments + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.databinding.FragmentWelcomeBinding +import com.zeapo.pwdstore.utils.performTransactionWithBackStack +import com.zeapo.pwdstore.utils.viewBinding + +@Suppress("unused") +class WelcomeFragment : Fragment(R.layout.fragment_welcome) { + + private val binding by viewBinding(FragmentWelcomeBinding::bind) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.letsGo.setOnClickListener { parentFragmentManager.performTransactionWithBackStack(CloneFragment.newInstance()) } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt index d9277bef..2a11f56a 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt @@ -56,7 +56,8 @@ fun String.base64(): String { return Base64.encodeToString(encodeToByteArray(), Base64.NO_WRAP) } -val Context.clipboard get() = getSystemService() +val Context.clipboard + get() = getSystemService() fun FragmentActivity.snackbar( view: View = findViewById(android.R.id.content), diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/FragmentExtensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/FragmentExtensions.kt new file mode 100644 index 00000000..01103d19 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/FragmentExtensions.kt @@ -0,0 +1,52 @@ +package com.zeapo.pwdstore.utils + +import android.content.pm.PackageManager +import android.view.View +import androidx.annotation.IdRes +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.commit +import com.zeapo.pwdstore.R + +fun Fragment.isPermissionGranted(permission: String): Boolean { + return ContextCompat.checkSelfPermission(requireActivity(), permission) == PackageManager.PERMISSION_GRANTED +} + +fun Fragment.finish() = requireActivity().finish() + +fun FragmentManager.performTransaction(destinationFragment: Fragment, @IdRes containerViewId: Int = android.R.id.content) { + this.commit { + beginTransaction() + setCustomAnimations( + R.animator.slide_in_left, + R.animator.slide_out_left, + R.animator.slide_in_right, + R.animator.slide_out_right) + replace(containerViewId, destinationFragment) + } +} + +fun FragmentManager.performTransactionWithBackStack(destinationFragment: Fragment, @IdRes containerViewId: Int = android.R.id.content) { + this.commit { + beginTransaction() + addToBackStack(destinationFragment.tag) + setCustomAnimations( + R.animator.slide_in_left, + R.animator.slide_out_left, + R.animator.slide_in_right, + R.animator.slide_out_right) + replace(containerViewId, destinationFragment) + } +} + +fun FragmentManager.performSharedElementTransaction(destinationFragment: Fragment, views: List, @IdRes containerViewId: Int = android.R.id.content) { + this.commit { + beginTransaction() + for (view in views) { + addSharedElement(view, view.transitionName) + } + addToBackStack(destinationFragment.tag) + replace(containerViewId, destinationFragment) + } +} diff --git a/app/src/main/res/animator/slide_in_left.xml b/app/src/main/res/animator/slide_in_left.xml new file mode 100644 index 00000000..5f5498e5 --- /dev/null +++ b/app/src/main/res/animator/slide_in_left.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/animator/slide_in_right.xml b/app/src/main/res/animator/slide_in_right.xml new file mode 100644 index 00000000..34420581 --- /dev/null +++ b/app/src/main/res/animator/slide_in_right.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/animator/slide_out_left.xml b/app/src/main/res/animator/slide_out_left.xml new file mode 100644 index 00000000..5bc22aa9 --- /dev/null +++ b/app/src/main/res/animator/slide_out_left.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/animator/slide_out_right.xml b/app/src/main/res/animator/slide_out_right.xml new file mode 100644 index 00000000..8447ae76 --- /dev/null +++ b/app/src/main/res/animator/slide_out_right.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_onboarding.xml b/app/src/main/res/layout/activity_onboarding.xml index f2c6f797..6d880c31 100644 --- a/app/src/main/res/layout/activity_onboarding.xml +++ b/app/src/main/res/layout/activity_onboarding.xml @@ -1,73 +1,13 @@ - - - - - - - - - - - - - - + + diff --git a/app/src/main/res/layout/fragment_clone.xml b/app/src/main/res/layout/fragment_clone.xml new file mode 100644 index 00000000..0e173428 --- /dev/null +++ b/app/src/main/res/layout/fragment_clone.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_repo_location.xml b/app/src/main/res/layout/fragment_repo_location.xml new file mode 100644 index 00000000..cc987d8c --- /dev/null +++ b/app/src/main/res/layout/fragment_repo_location.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_welcome.xml b/app/src/main/res/layout/fragment_welcome.xml new file mode 100644 index 00000000..98a0475d --- /dev/null +++ b/app/src/main/res/layout/fragment_welcome.xml @@ -0,0 +1,51 @@ + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 53140e6c..90295413 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,7 +77,7 @@ Clone from server Use local directory Repository location - Select where to create your password repository + Select where do you want to create your password repository SD-Card Hidden (Preferred) Choose where to store the passwords @@ -418,4 +418,22 @@ A folder by that name already exists Digits/Symbols (d/s) ds + + + Repository \nLocation + Select \nOpenPGP Provider + Let\'s Go + Select \nRepository Type + Select if you want to create a local repo or clone a remote repo. + Clone Remote Repo + Create Local Repo + Error getting directory uri + Select a directory to store passwords + Select \nStore Name + Select a name for your password store. + Store Name + Select an empty directory for password store + Enter a store name to continue + Cannot create new directory. +