diff options
39 files changed, 512 insertions, 389 deletions
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 688ac96e..8306d009 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -92,3 +92,10 @@ jobs: adb shell settings put global transition_animation_scale 0 adb shell settings put global window_animation_scale 0 ./gradlew :app:connectedNonFreeDebugAndroidTest + + - name: (Fail-only) upload test report + if: failure() + uses: actions/upload-artifact@v2 + with: + name: Test report + path: app/build/reports diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 43cd9fac..10484521 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -117,7 +117,7 @@ jobs: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} body: ${{ steps.changelog_reader.outputs.log_entry }} - draft: false + draft: true prerelease: false - name: Get the version diff --git a/.github/workflows/update_publicsuffix_data.yml b/.github/workflows/update_publicsuffix_data.yml index 27a2ab14..527f406f 100644 --- a/.github/workflows/update_publicsuffix_data.yml +++ b/.github/workflows/update_publicsuffix_data.yml @@ -1,3 +1,4 @@ +name: Update Publix Suffix List data on: schedule: - cron: '0 0 * * 6' @@ -20,7 +21,7 @@ jobs: run: | git checkout -b bot/update-psl git add app/src/main/assets/publicsuffixes - git commit --message "Update Public Suffix List data' + git commit --message "Update Public Suffix List data" - name: Create update PR uses: thomaseizinger/create-pull-request@1.0.0 diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 2df14bed..f7e941c7 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -12,6 +12,7 @@ <set> <option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$/app" /> + <option value="$PROJECT_DIR$/buildSrc" /> </set> </option> <option name="resolveModulePerSourceSet" value="false" /> diff --git a/CHANGELOG.md b/CHANGELOG.md index 204704be..c6e8b397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- Allow changing the branch used for Git operations + +### Changed + +- Slightly reduce APK size +- Always show the parent path in entries + +### Fixed + +- Allow creating nested directories directly +- I keep saying this but for real: error message for wrong SSH/HTTPS password is properly fixed now + ## [1.10.2] - 2020-07-30 ### Fixed @@ -25,6 +25,9 @@ Pull requests are more than welcome (see [TODO](https://github.com/android-passw height="80" /> </a> +## Build types + +We generate release binaries under two separate configurations titled `free` and `nonFree`. The distinction was created following the merge of [#900](https://msfjarvis.dev/aps/pr/900), that introduced a dependency on closed source GMS libraries. Since F-Droid is a FOSS-only app store, we created the `free` flavor where we do not ship the GMS dependency and thus the feature to fill SMS OTPs is unavailable. ## Features diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index ca356b83..00000000 --- a/app/build.gradle +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -plugins { - id 'kotlin-android' -} - -final def keystorePropertiesFile = rootProject.file 'keystore.properties' - -static final def isSnapshot() { - return System.env['GITHUB_WORKFLOW'] != null && System.env['SNAPSHOT'] != null -} - -android { - if (isSnapshot()) { - android.applicationVariants.all { final variant -> - variant.outputs.all { - outputFileName = "aps-${variant.getFlavorName()}_${defaultConfig.versionName}.apk" - } - } - } - - buildFeatures.viewBinding = true - - defaultConfig { - applicationId 'dev.msfjarvis.aps' - versionCode 11020 - versionName '1.10.2' - } - - lintOptions { - abortOnError = true // make sure build fails with lint errors! - disable 'MissingTranslation', 'PluralsCandidate' - } - - packagingOptions { - exclude '.readme' - exclude 'META-INF/LICENSE.txt' - exclude 'META-INF/NOTICE.txt' - } - - buildTypes { - release { - minifyEnabled = true - proguardFiles 'proguard-android-optimize.txt', 'proguard-rules.pro' - buildConfigField 'boolean', 'ENABLE_DEBUG_FEATURES', isSnapshot() ? 'true' : 'false' - } - debug { - applicationIdSuffix = '.debug' - versionNameSuffix = '-debug' - minifyEnabled = false - buildConfigField 'boolean', 'ENABLE_DEBUG_FEATURES', 'true' - } - } - - if (keystorePropertiesFile.exists()) { - final def keystoreProperties = new Properties() - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) - signingConfigs { - release { - keyAlias = keystoreProperties['keyAlias'] - keyPassword = keystoreProperties['keyPassword'] - storeFile = rootProject.file keystoreProperties['storeFile'] - storePassword = keystoreProperties['storePassword'] - } - } - buildTypes.release.signingConfig = signingConfigs.release - buildTypes.debug.signingConfig = signingConfigs.release - } - - flavorDimensions "free" - productFlavors { - free { - versionNameSuffix "-free" - } - nonFree { - } - } -} - -dependencies { - implementation deps.androidx.activity_ktx - implementation deps.androidx.annotation - implementation deps.androidx.autofill - implementation deps.androidx.appcompat - implementation deps.androidx.biometric - implementation deps.androidx.constraint_layout - implementation deps.androidx.core_ktx - implementation deps.androidx.documentfile - implementation deps.androidx.fragment_ktx - implementation deps.androidx.lifecycle_common - implementation deps.androidx.lifecycle_livedata_ktx - implementation deps.androidx.lifecycle_viewmodel_ktx - implementation deps.androidx.material - implementation deps.androidx.preference - implementation deps.androidx.recycler_view - implementation deps.androidx.recycler_view_selection - implementation deps.androidx.security - implementation deps.androidx.swiperefreshlayout - - implementation deps.kotlin.coroutines.android - implementation deps.kotlin.coroutines.core - - implementation deps.first_party.openpgp_ktx - implementation deps.first_party.zxing_android_embedded - - implementation deps.third_party.commons_codec - implementation deps.third_party.fastscroll - implementation(deps.third_party.jgit) { - exclude group: 'org.apache.httpcomponents', module: 'httpclient' - } - implementation deps.third_party.jsch - implementation deps.third_party.sshj - implementation deps.third_party.bouncycastle - implementation deps.third_party.plumber - implementation deps.third_party.ssh_auth - implementation deps.third_party.timber - implementation deps.third_party.timberkt - - if (isSnapshot()) { - implementation deps.third_party.leakcanary - implementation deps.third_party.whatthestack - } else { - debugImplementation deps.third_party.leakcanary - debugImplementation deps.third_party.whatthestack - } - - nonFreeImplementation deps.non_free.google_play_auth_api_phone - - // Testing-only dependencies - androidTestImplementation deps.testing.junit - androidTestImplementation deps.testing.kotlin_test_junit - androidTestImplementation deps.testing.androidx.runner - androidTestImplementation deps.testing.androidx.rules - androidTestImplementation deps.testing.androidx.junit - androidTestImplementation deps.testing.androidx.espresso_core - androidTestImplementation deps.testing.androidx.espresso_intents - - testImplementation deps.testing.junit - testImplementation deps.testing.kotlin_test_junit -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..a8e3a058 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,147 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +import java.util.Properties +import com.android.build.gradle.internal.api.BaseVariantOutputImpl + +plugins { + kotlin("android") +} + +val keystorePropertiesFile = rootProject.file("keystore.properties") + +fun isSnapshot(): Boolean { + return System.getenv("GITHUB_WORKFLOW") != null && System.getenv("SNAPSHOT") != null +} + +android { + if (isSnapshot()) { + applicationVariants.all { + outputs.all { + (this as BaseVariantOutputImpl).outputFileName = "aps-${flavorName}_$versionName.apk" + } + } + } + + buildFeatures.viewBinding = true + + defaultConfig { + applicationId = "dev.msfjarvis.aps" + versionCode = 11021 + versionName = "1.11.0-SNAPSHOT" + } + + lintOptions { + isAbortOnError = true + isCheckReleaseBuilds = false + disable("MissingTranslation", "PluralsCandidate") + } + + packagingOptions { + exclude("**/*.version") + exclude("**/*.txt") + exclude("**/*.kotlin_module") + } + + buildTypes { + named("release") { + isMinifyEnabled = true + setProguardFiles(listOf("proguard-android-optimize.txt", "proguard-rules.pro")) + buildConfigField("boolean", "ENABLE_DEBUG_FEATURES", if (isSnapshot()) "true" else "false") + } + named("debug") { + applicationIdSuffix = ".debug" + versionNameSuffix = "-debug" + isMinifyEnabled = false + buildConfigField("boolean", "ENABLE_DEBUG_FEATURES", "true") + } + } + + if (keystorePropertiesFile.exists()) { + val keystoreProperties = Properties() + keystoreProperties.load(keystorePropertiesFile.inputStream()) + signingConfigs { + register("release") { + keyAlias = keystoreProperties["keyAlias"] as String + keyPassword = keystoreProperties["keyPassword"] as String + storeFile = rootProject.file(keystoreProperties["storeFile"] as String) + storePassword = keystoreProperties["storePassword"] as String + } + } + listOf("release", "debug").map { + buildTypes.getByName(it).signingConfig = signingConfigs.getByName(it) + } + } + + flavorDimensions("free") + productFlavors { + create("free") { + versionNameSuffix = "-free" + } + create("nonFree") { + } + } +} + +dependencies { + compileOnly(Dependencies.AndroidX.annotation) + implementation(Dependencies.AndroidX.activity_ktx) + implementation(Dependencies.AndroidX.autofill) + implementation(Dependencies.AndroidX.appcompat) + implementation(Dependencies.AndroidX.biometric) + implementation(Dependencies.AndroidX.constraint_layout) + implementation(Dependencies.AndroidX.core_ktx) + implementation(Dependencies.AndroidX.documentfile) + implementation(Dependencies.AndroidX.fragment_ktx) + implementation(Dependencies.AndroidX.lifecycle_common) + implementation(Dependencies.AndroidX.lifecycle_livedata_ktx) + implementation(Dependencies.AndroidX.lifecycle_viewmodel_ktx) + implementation(Dependencies.AndroidX.material) + implementation(Dependencies.AndroidX.preference) + implementation(Dependencies.AndroidX.recycler_view) + implementation(Dependencies.AndroidX.recycler_view_selection) + implementation(Dependencies.AndroidX.security) + implementation(Dependencies.AndroidX.swiperefreshlayout) + + implementation(Dependencies.Kotlin.Coroutines.android) + implementation(Dependencies.Kotlin.Coroutines.core) + + implementation(Dependencies.FirstParty.openpgp_ktx) + implementation(Dependencies.FirstParty.zxing_android_embedded) + + implementation(Dependencies.ThirdParty.commons_codec) + implementation(Dependencies.ThirdParty.fastscroll) + implementation(Dependencies.ThirdParty.jgit) { + exclude(group = "org.apache.httpcomponents", module = "httpclient") + } + implementation(Dependencies.ThirdParty.jsch) + implementation(Dependencies.ThirdParty.sshj) + implementation(Dependencies.ThirdParty.bouncycastle) + implementation(Dependencies.ThirdParty.plumber) + implementation(Dependencies.ThirdParty.ssh_auth) + implementation(Dependencies.ThirdParty.timber) + implementation(Dependencies.ThirdParty.timberkt) + + if (isSnapshot()) { + implementation(Dependencies.ThirdParty.leakcanary) + implementation(Dependencies.ThirdParty.whatthestack) + } else { + debugImplementation(Dependencies.ThirdParty.leakcanary) + debugImplementation(Dependencies.ThirdParty.whatthestack) + } + + "nonFreeImplementation"(Dependencies.NonFree.google_play_auth_api_phone) + + // Testing-only dependencies + androidTestImplementation(Dependencies.Testing.junit) + androidTestImplementation(Dependencies.Testing.kotlin_test_junit) + androidTestImplementation(Dependencies.Testing.AndroidX.runner) + androidTestImplementation(Dependencies.Testing.AndroidX.rules) + androidTestImplementation(Dependencies.Testing.AndroidX.junit) + androidTestImplementation(Dependencies.Testing.AndroidX.espresso_core) + androidTestImplementation(Dependencies.Testing.AndroidX.espresso_intents) + + testImplementation(Dependencies.Testing.junit) + testImplementation(Dependencies.Testing.kotlin_test_junit) +} diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt index fc32b8da..1cd7c1ea 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt @@ -21,6 +21,7 @@ import androidx.fragment.app.activityViewModels import androidx.fragment.app.setFragmentResultListener import androidx.lifecycle.observe import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding @@ -125,6 +126,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) { } val recyclerView = binding.passRecycler recyclerView.apply { + addItemDecoration(DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)) layoutManager = LinearLayoutManager(requireContext()) itemAnimator = OnOffItemAnimator() adapter = recyclerAdapter diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index e2cde88e..8938df00 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -95,8 +95,11 @@ class UserPreference : AppCompatActivity() { if (!PasswordRepository.isGitRepo()) { listOfNotNull( - gitServerPreference, gitConfigPreference, sshKeyPreference, - viewSshKeyPreference, clearSavedPassPreference + gitServerPreference, + gitConfigPreference, + sshKeyPreference, + viewSshKeyPreference, + clearSavedPassPreference, ).forEach { it.parent?.removePreference(it) } @@ -119,12 +122,12 @@ class UserPreference : AppCompatActivity() { autoFillAppsPreference, autoFillDefaultPreference, autoFillAlwaysShowDialogPreference, - autoFillShowFullNamePreference + autoFillShowFullNamePreference, ) oreoAutofillDependencies = listOfNotNull( oreoAutofillDirectoryStructurePreference, oreoAutofillDefaultUsername, - oreoAutofillCustomPublixSuffixes + oreoAutofillCustomPublixSuffixes, ) oreoAutofillCustomPublixSuffixes?.apply { setOnBindEditTextListener { diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategyDsl.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategyDsl.kt index ec333bec..cae84d54 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategyDsl.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategyDsl.kt @@ -232,7 +232,8 @@ class AutofillRule private constructor( fun genericPassword(optional: Boolean = false, block: FieldMatcher.Builder.() -> Unit) { require(matchers.none { it.type in listOf( - FillableFieldType.CurrentPassword, FillableFieldType.NewPassword + FillableFieldType.CurrentPassword, + FillableFieldType.NewPassword, ) }) { "Every rule block can only have either genericPassword or {current,new}Password blocks" } matchers.add( diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FeatureAndTrustDetection.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FeatureAndTrustDetection.kt index 8864f877..5c8b6b3b 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FeatureAndTrustDetection.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FeatureAndTrustDetection.kt @@ -74,7 +74,7 @@ private val TRUSTED_BROWSER_CERTIFICATE_HASH = mapOf( "org.mozilla.firefox_beta" to "p4tipRZbRJSy/q2edqKA0i2Tf+5iUa7OWZRGsuoxmwQ=", "org.mozilla.focus" to "YgOkc7421k7jf4f6UA7bx56rkwYQq5ufpMp9XB8bT/w=", "org.mozilla.klar" to "YgOkc7421k7jf4f6UA7bx56rkwYQq5ufpMp9XB8bT/w=", - "org.torproject.torbrowser" to "IAYfBF5zfGc3XBd5TP7bQ2oDzsa6y3y5+WZCIFyizsg=" + "org.torproject.torbrowser" to "IAYfBF5zfGc3XBd5TP7bQ2oDzsa6y3y5+WZCIFyizsg=", ) private fun isTrustedBrowser(context: Context, appPackage: String): Boolean { @@ -112,7 +112,7 @@ private val BROWSER_MULTI_ORIGIN_METHOD = mapOf( "org.mozilla.firefox_beta" to BrowserMultiOriginMethod.WebView, "org.mozilla.focus" to BrowserMultiOriginMethod.Field, "org.mozilla.klar" to BrowserMultiOriginMethod.Field, - "org.torproject.torbrowser" to BrowserMultiOriginMethod.WebView + "org.torproject.torbrowser" to BrowserMultiOriginMethod.WebView, ) private fun getBrowserMultiOriginMethod(appPackage: String): BrowserMultiOriginMethod = @@ -135,7 +135,7 @@ private val BROWSER_SAVE_FLAG = mapOf( "org.mozilla.fennec_aurora" to 0, "com.opera.mini.native" to 0, "com.opera.mini.native.beta" to 0, - "com.opera.touch" to 0 + "com.opera.touch" to 0, ) @RequiresApi(Build.VERSION_CODES.O) @@ -162,7 +162,7 @@ private val FLAKY_BROWSERS = listOf( "com.android.chrome", "com.chrome.beta", "com.chrome.canary", - "com.chrome.dev" + "com.chrome.dev", ) enum class BrowserAutofillSupportLevel { @@ -170,7 +170,7 @@ enum class BrowserAutofillSupportLevel { FlakyFill, PasswordFill, GeneralFill, - GeneralFillAndSave + GeneralFillAndSave, } @RequiresApi(Build.VERSION_CODES.O) diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt index 2b18bbb6..2bfa5075 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt @@ -33,19 +33,19 @@ class FormField( private val HINTS_USERNAME = listOf( HintConstants.AUTOFILL_HINT_USERNAME, - HintConstants.AUTOFILL_HINT_NEW_USERNAME + HintConstants.AUTOFILL_HINT_NEW_USERNAME, ) private val HINTS_NEW_PASSWORD = listOf( - HintConstants.AUTOFILL_HINT_NEW_PASSWORD + HintConstants.AUTOFILL_HINT_NEW_PASSWORD, ) private val HINTS_PASSWORD = HINTS_NEW_PASSWORD + listOf( - HintConstants.AUTOFILL_HINT_PASSWORD + HintConstants.AUTOFILL_HINT_PASSWORD, ) private val HINTS_OTP = listOf( - HintConstants.AUTOFILL_HINT_SMS_OTP + HintConstants.AUTOFILL_HINT_SMS_OTP, ) @Suppress("DEPRECATION") @@ -54,7 +54,7 @@ class FormField( HintConstants.AUTOFILL_HINT_NAME, HintConstants.AUTOFILL_HINT_PERSON_NAME, HintConstants.AUTOFILL_HINT_PHONE, - HintConstants.AUTOFILL_HINT_PHONE_NUMBER + HintConstants.AUTOFILL_HINT_PHONE_NUMBER, ) private val ANDROID_TEXT_FIELD_CLASS_NAMES = listOf( @@ -62,7 +62,7 @@ class FormField( "android.widget.AutoCompleteTextView", "androidx.appcompat.widget.AppCompatEditText", "android.support.v7.widget.AppCompatEditText", - "com.google.android.material.textfield.TextInputEditText" + "com.google.android.material.textfield.TextInputEditText", ) private const val ANDROID_WEB_VIEW_CLASS_NAME = "android.webkit.WebView" @@ -75,15 +75,24 @@ class FormField( InputType.TYPE_CLASS_TEXT -> typeVariation in listOf( InputType.TYPE_TEXT_VARIATION_PASSWORD, InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD, - InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD + InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD, ) else -> false } } - private val HTML_INPUT_FIELD_TYPES_USERNAME = listOf("email", "tel", "text") - private val HTML_INPUT_FIELD_TYPES_PASSWORD = listOf("password") - private val HTML_INPUT_FIELD_TYPES_OTP = listOf("tel", "text") + private val HTML_INPUT_FIELD_TYPES_USERNAME = listOf( + "email", + "tel", + "text", + ) + private val HTML_INPUT_FIELD_TYPES_PASSWORD = listOf( + "password", + ) + private val HTML_INPUT_FIELD_TYPES_OTP = listOf( + "tel", + "text", + ) private val HTML_INPUT_FIELD_TYPES_FILLABLE = (HTML_INPUT_FIELD_TYPES_USERNAME + HTML_INPUT_FIELD_TYPES_PASSWORD + HTML_INPUT_FIELD_TYPES_OTP).toSet().toList() @@ -95,20 +104,29 @@ class FormField( "url_field", // Opera address bar "location_bar_edit_text", // Samsung address bar "search", "find", "captcha", - "postal" // Prevent postal code fields from being mistaken for OTP fields + "postal", // Prevent postal code fields from being mistaken for OTP fields ) private val PASSWORD_HEURISTIC_TERMS = listOf( - "pass", "pswd", "pwd" + "pass", + "pswd", + "pwd", ) private val USERNAME_HEURISTIC_TERMS = listOf( - "alias", "e-mail", "email", "login", "user" + "alias", + "e-mail", + "email", + "login", + "user", ) private val OTP_HEURISTIC_TERMS = listOf( - "einmal", "otp" + "einmal", + "otp", + "challenge", + "verification", ) private val OTP_WEAK_HEURISTIC_TERMS = listOf( - "code" + "code", ) } @@ -214,7 +232,8 @@ class FormField( // TODO: Revisit this decision in the future private val excludedByHints = excludedByAutofillHints - val relevantField = isTextField && hasAutofillTypeText && !excludedByHints + // Only offer to fill into custom views if they explicitly opted into Autofill. + val relevantField = hasAutofillTypeText && (isTextField || autofillHints.isNotEmpty()) && !excludedByHints // Exclude fields based on hint, resource ID or HTML name. // Note: We still report excluded fields as relevant since they count for adjacency heuristics, @@ -232,7 +251,7 @@ class FormField( if (isCertainPasswordField) CertaintyLevel.Certain else if (isLikelyPasswordField) CertaintyLevel.Likely else if (isPossiblePasswordField) CertaintyLevel.Possible else CertaintyLevel.Impossible // OTP field heuristics (based only on the current field) - private val isPossibleOtpField = notExcluded && !isPossiblePasswordField && isTextField + private val isPossibleOtpField = notExcluded && !isPossiblePasswordField private val isCertainOtpField = isPossibleOtpField && hasHintOtp private val isLikelyOtpField = isPossibleOtpField && ( isCertainOtpField || OTP_HEURISTIC_TERMS.anyMatchesFieldInfo || @@ -241,7 +260,7 @@ class FormField( if (isCertainOtpField) CertaintyLevel.Certain else if (isLikelyOtpField) CertaintyLevel.Likely else if (isPossibleOtpField) CertaintyLevel.Possible else CertaintyLevel.Impossible // Username field heuristics (based only on the current field) - private val isPossibleUsernameField = notExcluded && !isPossiblePasswordField && !isCertainOtpField && isTextField + private val isPossibleUsernameField = notExcluded && !isPossiblePasswordField && !isCertainOtpField private val isCertainUsernameField = isPossibleUsernameField && hasHintUsername private val isLikelyUsernameField = isPossibleUsernameField && (isCertainUsernameField || (USERNAME_HEURISTIC_TERMS.anyMatchesFieldInfo)) val usernameCertainty = diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt index b6404c6e..fd2997d8 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt @@ -33,7 +33,7 @@ class OreoAutofillService : AutofillService() { "com.android.settings.intelligence", "com.android.systemui", "com.oneplus.applocker", - "org.sufficientlysecure.keychain" + "org.sufficientlysecure.keychain", ) private const val DISABLE_AUTOFILL_DURATION_MS = 1000 * 60 * 60 * 24L diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt index 615b695e..a05da554 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt @@ -11,7 +11,6 @@ import android.text.InputType import android.view.Menu import android.view.MenuItem import android.view.View -import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult @@ -59,7 +58,7 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB private var oldCategory: String? = null private var copy: Boolean = false - private val userInteractionRequiredResult: ActivityResultLauncher<IntentSenderRequest> = registerForActivityResult(StartIntentSenderForResult()) { result -> + private val userInteractionRequiredResult = registerForActivityResult(StartIntentSenderForResult()) { result -> if (result.data == null) { setResult(RESULT_CANCELED, null) finish() @@ -315,7 +314,11 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB .filter { it.isNotBlank() } .map { line -> parseGpgIdentifier(line) ?: run { - snackbar(message = resources.getString(R.string.invalid_gpg_id)) + if (line.removePrefix("0x").matches("[a-fA-F0-9]{8}".toRegex())) { + snackbar(message = resources.getString(R.string.short_key_ids_unsupported)) + } else { + snackbar(message = resources.getString(R.string.invalid_gpg_id)) + } return@with } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt index a8f4c5b3..7a818bcf 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt @@ -41,6 +41,7 @@ abstract class BaseGitActivity : AppCompatActivity() { lateinit var serverPath: String lateinit var username: String lateinit var email: String + lateinit var branch: String private var identityBuilder: SshApiSessionFactory.IdentityBuilder? = null private var identity: SshApiSessionFactory.ApiIdentity? = null lateinit var settings: SharedPreferences @@ -61,6 +62,7 @@ abstract class BaseGitActivity : AppCompatActivity() { serverPath = settings.getString(PreferenceKeys.GIT_REMOTE_LOCATION, null) ?: "" username = settings.getString(PreferenceKeys.GIT_CONFIG_USER_NAME, null) ?: "" email = settings.getString(PreferenceKeys.GIT_CONFIG_USER_EMAIL, null) ?: "" + branch = settings.getString(PreferenceKeys.GIT_BRANCH_NAME, null) ?: "master" updateUrl() } @@ -248,10 +250,9 @@ abstract class BaseGitActivity : AppCompatActivity() { const val REQUEST_PULL = 101 const val REQUEST_PUSH = 102 const val REQUEST_CLONE = 103 - const val REQUEST_INIT = 104 - const val REQUEST_SYNC = 105 - const val BREAK_OUT_OF_DETACHED = 106 - const val REQUEST_RESET = 107 + const val REQUEST_SYNC = 104 + const val BREAK_OUT_OF_DETACHED = 105 + const val REQUEST_RESET = 106 const val TAG = "AbstractGitActivity" } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/BreakOutOfDetached.kt b/app/src/main/java/com/zeapo/pwdstore/git/BreakOutOfDetached.kt index 664497bd..149cabd8 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/BreakOutOfDetached.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/BreakOutOfDetached.kt @@ -5,8 +5,10 @@ package com.zeapo.pwdstore.git import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.utils.PreferenceKeys import java.io.File import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.GitCommand @@ -16,6 +18,9 @@ import org.eclipse.jgit.api.RebaseCommand class BreakOutOfDetached(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { private lateinit var commands: List<GitCommand<out Any>> + private val gitBranch = PreferenceManager + .getDefaultSharedPreferences(callingActivity.applicationContext) + .getString(PreferenceKeys.GIT_BRANCH_NAME, "master") /** * Sets the command @@ -24,7 +29,7 @@ class BreakOutOfDetached(fileDir: File, callingActivity: AppCompatActivity) : Gi */ fun setCommands(): BreakOutOfDetached { val git = Git(repository) - val branchName = "conflicting-master-${System.currentTimeMillis()}" + val branchName = "conflicting-$gitBranch-${System.currentTimeMillis()}" this.commands = listOf( // abort the rebase @@ -33,8 +38,8 @@ class BreakOutOfDetached(fileDir: File, callingActivity: AppCompatActivity) : Gi git.checkout().setCreateBranch(true).setName(branchName), // push the changes git.push().setRemote("origin"), - // switch back to master - git.checkout().setName("master") + // switch back to ${gitBranch} + git.checkout().setName(gitBranch) ) return this } @@ -76,7 +81,7 @@ class BreakOutOfDetached(fileDir: File, callingActivity: AppCompatActivity) : Gi MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title)) .setMessage("There was a conflict when trying to rebase. " + - "Your local master branch was pushed to another branch named conflicting-master-....\n" + + "Your local $gitBranch branch was pushed to another branch named conflicting-$gitBranch-....\n" + "Use this branch to resolve conflict on your computer") .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt index 35cbc68a..0498f6ed 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt @@ -36,7 +36,7 @@ class GitConfigActivity : BaseGitActivity() { if (repo != null) { try { val objectId = repo.resolve(Constants.HEAD) - val ref = repo.getRef("refs/heads/master") + val ref = repo.getRef("refs/heads/$branch") val head = if (ref.objectId.equals(objectId)) ref.name else "DETACHED" binding.gitCommitHash.text = String.format("%s (%s)", objectId.abbreviate(8).name(), head) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt index 9d2c629f..90f72b7c 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt @@ -9,12 +9,12 @@ import android.content.Intent import android.view.LayoutInflater import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.preference.PreferenceManager import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout import com.zeapo.pwdstore.R import com.zeapo.pwdstore.UserPreference import com.zeapo.pwdstore.git.config.ConnectionMode @@ -37,7 +37,6 @@ import org.eclipse.jgit.transport.CredentialItem import org.eclipse.jgit.transport.CredentialsProvider import org.eclipse.jgit.transport.SshSessionFactory import org.eclipse.jgit.transport.URIish -import com.google.android.material.R as materialR private class GitOperationCredentialFinder( @@ -78,13 +77,13 @@ private class GitOperationCredentialFinder( @SuppressLint("InflateParams") val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null) + val credentialLayout = dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout) val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential) editCredential.setHint(hintRes) val rememberCredential = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential) rememberCredential.setText(rememberRes) if (isRetry) - editCredential.setError(callingActivity.resources.getString(errorRes), - ContextCompat.getDrawable(callingActivity, materialR.drawable.mtrl_ic_error)) + credentialLayout.error = callingActivity.resources.getString(errorRes) MaterialAlertDialogBuilder(callingActivity).run { setTitle(R.string.passphrase_dialog_title) setMessage(messageRes) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt index 10f44960..6c87e0f6 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt @@ -102,6 +102,13 @@ class GitServerConfigActivity : BaseGitActivity() { } } + binding.serverBranch.apply { + setText(branch) + doOnTextChanged { text, _, _, _ -> + branch = text.toString().trim() + } + } + binding.saveButton.setOnClickListener { if (isClone && PasswordRepository.getRepository(null) == null) PasswordRepository.initialize(this) @@ -114,12 +121,14 @@ class GitServerConfigActivity : BaseGitActivity() { putString(PreferenceKeys.GIT_REMOTE_PORT, serverPort) putString(PreferenceKeys.GIT_REMOTE_USERNAME, serverUser) putString(PreferenceKeys.GIT_REMOTE_LOCATION, serverPath) + putString(PreferenceKeys.GIT_BRANCH_NAME, branch) } if (!isClone) { Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show() Handler().postDelayed(500) { finish() } - } else + } else { cloneRepository() + } } else -> Snackbar.make(binding.root, getString(R.string.git_server_config_save_error_prefix, getString(result.textRes)), Snackbar.LENGTH_LONG).show() } @@ -157,7 +166,7 @@ class GitServerConfigActivity : BaseGitActivity() { !(localDirFiles.size == 1 && localDirFiles[0].name == ".git")) { MaterialAlertDialogBuilder(this) .setTitle(R.string.dialog_delete_title) - .setMessage(resources.getString(R.string.dialog_delete_msg) + " " + localDir.toString()) + .setMessage(resources.getString(R.string.dialog_delete_msg, localDir.toString())) .setCancelable(false) .setPositiveButton(R.string.dialog_delete) { dialog, _ -> try { diff --git a/app/src/main/java/com/zeapo/pwdstore/git/ResetToRemoteOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/ResetToRemoteOperation.kt index c570bf27..60a9fbf3 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/ResetToRemoteOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/ResetToRemoteOperation.kt @@ -6,13 +6,15 @@ package com.zeapo.pwdstore.git import android.content.Intent import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.utils.PreferenceKeys import java.io.File -import org.eclipse.jgit.api.AddCommand -import org.eclipse.jgit.api.FetchCommand import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.GitCommand import org.eclipse.jgit.api.ResetCommand +import org.eclipse.jgit.api.TransportCommand /** * Creates a new git operation @@ -22,9 +24,7 @@ import org.eclipse.jgit.api.ResetCommand */ class ResetToRemoteOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { - private var addCommand: AddCommand? = null - private var fetchCommand: FetchCommand? = null - private var resetCommand: ResetCommand? = null + private lateinit var commands: List<GitCommand<out Any>> /** * Sets the command @@ -32,17 +32,27 @@ class ResetToRemoteOperation(fileDir: File, callingActivity: AppCompatActivity) * @return the current object */ fun setCommands(): ResetToRemoteOperation { + val remoteBranch = PreferenceManager + .getDefaultSharedPreferences(callingActivity.applicationContext) + .getString(PreferenceKeys.GIT_BRANCH_NAME, "master") val git = Git(repository) - this.addCommand = git.add().addFilepattern(".") - this.fetchCommand = git.fetch().setRemote("origin") - this.resetCommand = git.reset().setRef("origin/master").setMode(ResetCommand.ResetType.HARD) + val cmds = arrayListOf( + git.add().addFilepattern("."), + git.fetch().setRemote("origin"), + git.reset().setRef("origin/$remoteBranch").setMode(ResetCommand.ResetType.HARD) + ) + if (git.branchList().call().none { it.name == remoteBranch }) { + cmds.add( + git.branchCreate().setName(remoteBranch).setForce(true) + ) + } + commands = cmds return this } override fun execute() { - this.fetchCommand?.setCredentialsProvider(this.provider) - GitAsyncTask(callingActivity, this, Intent()) - .execute(this.addCommand, this.fetchCommand, this.resetCommand) + commands.filterIsInstance<TransportCommand<*, *>>().map { it.setCredentialsProvider(provider) } + GitAsyncTask(callingActivity, this, Intent()).execute(*commands.toTypedArray()) } override fun onError(err: Exception) { diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshjConfig.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/SshjConfig.kt index 7ba246be..1ea0359c 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/config/SshjConfig.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/config/SshjConfig.kt @@ -217,7 +217,7 @@ class SshjConfig : ConfigImpl() { ECDHNistP.Factory521(), ECDHNistP.Factory384(), ECDHNistP.Factory256(), - DHGexSHA256.Factory() + DHGexSHA256.Factory(), ) } @@ -228,7 +228,7 @@ class SshjConfig : ConfigImpl() { SignatureECDSA.Factory384(), SignatureECDSA.Factory521(), SignatureRSA.Factory(), - FactoryCERT() + FactoryCERT(), ) } @@ -242,7 +242,7 @@ class SshjConfig : ConfigImpl() { PKCS8KeyFile.Factory(), PKCS5KeyFile.Factory(), OpenSSHKeyFile.Factory(), - PuTTYKeyFile.Factory() + PuTTYKeyFile.Factory(), ) } @@ -251,7 +251,7 @@ class SshjConfig : ConfigImpl() { cipherFactories = listOf( BlockCiphers.AES128CTR(), BlockCiphers.AES192CTR(), - BlockCiphers.AES256CTR() + BlockCiphers.AES256CTR(), ) } @@ -260,13 +260,13 @@ class SshjConfig : ConfigImpl() { Macs.HMACSHA2256(), Macs.HMACSHA2256Etm(), Macs.HMACSHA2512(), - Macs.HMACSHA2512Etm() + Macs.HMACSHA2512Etm(), ) } private fun initCompressionFactories() { compressionFactories = listOf( - NoneCompression.Factory() + NoneCompression.Factory(), ) } } diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPasswordGenerator.kt b/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPasswordGenerator.kt index e841ed76..67d8fc40 100644 --- a/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPasswordGenerator.kt +++ b/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPasswordGenerator.kt @@ -22,14 +22,13 @@ object RandomPasswordGenerator { * set, the password will not contain any symbols. * - [PasswordGenerator.NO_AMBIGUOUS]: If set, the password will not contain any ambiguous * characters. - * - [PasswordGenerator.NO_VOWELS]: If set, the password will not contain any vowels. */ fun generate(targetLength: Int, pwFlags: Int): String? { val bank = listOfNotNull( PasswordGenerator.DIGITS_STR.takeIf { pwFlags hasFlag PasswordGenerator.DIGITS }, PasswordGenerator.UPPERS_STR.takeIf { pwFlags hasFlag PasswordGenerator.UPPERS }, PasswordGenerator.LOWERS_STR.takeIf { pwFlags hasFlag PasswordGenerator.LOWERS }, - PasswordGenerator.SYMBOLS_STR.takeIf { pwFlags hasFlag PasswordGenerator.SYMBOLS } + PasswordGenerator.SYMBOLS_STR.takeIf { pwFlags hasFlag PasswordGenerator.SYMBOLS }, ).joinToString("") var password = "" diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt index 15e13745..4e153b88 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt @@ -52,7 +52,15 @@ open class PasswordItemRecyclerAdapter : val settings = PreferenceManager.getDefaultSharedPreferences(itemView.context.applicationContext) val showHidden = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_FOLDERS, false) - name.text = item.toString() + val parentPath = item.fullPathToParent.replace("(^/)|(/$)".toRegex(), "") + val source = if (parentPath.isNotEmpty()) { + "$parentPath\n$item" + } else { + "$item" + } + val spannable = SpannableString(source) + spannable.setSpan(RelativeSizeSpan(0.7f), 0, parentPath.length, 0) + name.text = spannable if (item.type == PasswordItem.TYPE_CATEGORY) { folderIndicator.visibility = View.VISIBLE val children = item.file.listFiles { pathname -> @@ -62,15 +70,6 @@ open class PasswordItemRecyclerAdapter : childCount.visibility = if (count > 0) View.VISIBLE else View.GONE childCount.text = "$count" } else { - val parentPath = item.fullPathToParent.replace("(^/)|(/$)".toRegex(), "") - val source = if (parentPath.isNotEmpty()) { - "$parentPath\n$item" - } else { - "$item" - } - val spannable = SpannableString(source) - spannable.setSpan(RelativeSizeSpan(0.7f), 0, parentPath.length, 0) - name.text = spannable childCount.visibility = View.GONE folderIndicator.visibility = View.GONE } diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt index 5776187b..25a935bb 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt @@ -37,7 +37,7 @@ class FolderCreationDialogFragment : DialogFragment() { val materialTextView = dialog.findViewById<TextInputEditText>(R.id.folder_name_text) val folderName = materialTextView.text.toString() val newFolder = File("$currentDir/$folderName") - newFolder.mkdir() + newFolder.mkdirs() (requireActivity() as PasswordStore).refreshPasswordList(newFolder) dismiss() } diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt index 9eb549da..24bf2096 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt @@ -35,6 +35,7 @@ object PreferenceKeys { const val GIT_REMOTE_SERVER = "git_remote_server" const val GIT_REMOTE_USERNAME = "git_remote_username" const val GIT_SERVER_INFO = "git_server_info" + const val GIT_BRANCH_NAME = "git_branch" const val HTTPS_PASSWORD = "https_password" const val LENGTH = "length" const val OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES = "oreo_autofill_custom_public_suffixes" diff --git a/app/src/main/res/layout/activity_git_clone.xml b/app/src/main/res/layout/activity_git_clone.xml index 8026fa4a..f9bec00a 100644 --- a/app/src/main/res/layout/activity_git_clone.xml +++ b/app/src/main/res/layout/activity_git_clone.xml @@ -20,7 +20,7 @@ <androidx.appcompat.widget.AppCompatTextView android:id="@+id/server_label" style="@style/TextAppearance.MaterialComponents.Headline5" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:text="@string/server_name" @@ -32,7 +32,7 @@ <androidx.appcompat.widget.AppCompatTextView android:id="@+id/label_server_protocol" style="@style/TextAppearance.MaterialComponents.Headline6" - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:text="@string/server_protocol" @@ -42,7 +42,7 @@ <com.google.android.material.button.MaterialButtonToggleGroup android:id="@+id/clone_protocol_group" style="@style/TextAppearance.MaterialComponents.Headline1" - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" app:layout_constraintStart_toStartOf="parent" @@ -67,10 +67,12 @@ <com.google.android.material.textfield.TextInputLayout android:id="@+id/server_user_layout" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:hint="@string/server_user" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/clone_protocol_group"> <com.google.android.material.textfield.TextInputEditText @@ -125,10 +127,11 @@ <com.google.android.material.textfield.TextInputLayout android:id="@+id/label_server_path" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:hint="@string/server_path" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/label_server_url"> @@ -136,11 +139,31 @@ android:id="@+id/server_path" android:layout_width="match_parent" android:layout_height="wrap_content" - android:imeOptions="actionDone" + android:imeOptions="actionNext" + android:nextFocusForward="@id/server_branch" android:inputType="textWebEmailAddress" /> </com.google.android.material.textfield.TextInputLayout> + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/label_server_branch" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:hint="Branch" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/label_server_path"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/server_branch" + android:imeOptions="actionDone" + android:layout_width="match_parent" + android:inputType="textNoSuggestions" + android:layout_height="wrap_content" /> + + </com.google.android.material.textfield.TextInputLayout> + <androidx.appcompat.widget.AppCompatTextView android:id="@+id/label_connection_mode" style="@style/TextAppearance.MaterialComponents.Headline6" @@ -151,7 +174,7 @@ android:layout_marginBottom="16dp" android:text="@string/connection_mode" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/label_server_path" /> + app:layout_constraintTop_toBottomOf="@id/label_server_branch" /> <com.google.android.material.button.MaterialButtonToggleGroup android:id="@+id/connection_mode_group" diff --git a/app/src/main/res/layout/git_credential_layout.xml b/app/src/main/res/layout/git_credential_layout.xml index 757658e6..180b904a 100644 --- a/app/src/main/res/layout/git_credential_layout.xml +++ b/app/src/main/res/layout/git_credential_layout.xml @@ -15,6 +15,7 @@ android:layout_height="wrap_content" app:endIconMode="password_toggle" app:hintEnabled="true" + app:errorEnabled="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a59d5965..9aaebf98 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -368,6 +368,7 @@ <string name="exporting_passwords">Exporting passwords…</string> <string name="failed_to_find_key_id">Failed to locate .gpg-id, is your store set up correctly?</string> <string name="invalid_gpg_id">Found .gpg-id, but it contains an invalid key ID, fingerprint or user ID</string> + <string name="short_key_ids_unsupported">A key ID in .gpg-id is too short, please use either long key IDs (16 characters) or fingerprints (40 characters)</string> <string name="invalid_filename_text">File name must not contain \'/\', set directory above</string> <string name="directory_hint">Directory</string> </resources> diff --git a/app/src/main/res/xml/oreo_autofill_service.xml b/app/src/main/res/xml/oreo_autofill_service.xml index a790da21..8b76c803 100644 --- a/app/src/main/res/xml/oreo_autofill_service.xml +++ b/app/src/main/res/xml/oreo_autofill_service.xml @@ -3,7 +3,9 @@ ~ SPDX-License-Identifier: GPL-3.0-only --> -<autofill-service xmlns:android="http://schemas.android.com/apk/res/android"> +<autofill-service xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + tools:ignore="UnusedAttribute"> <compatibility-package android:name="com.android.chrome" /> <compatibility-package android:name="com.brave.browser" /> <compatibility-package android:name="com.chrome.beta" /> @@ -12,7 +14,13 @@ <compatibility-package android:name="com.microsoft.emmx" /> <compatibility-package android:name="com.opera.mini.native" /> <compatibility-package android:name="com.opera.mini.native.beta" /> - <compatibility-package android:name="org.mozilla.fennec_fdroid" /> - <compatibility-package android:name="org.mozilla.firefox" /> - <compatibility-package android:name="org.mozilla.firefox_beta" /> + <compatibility-package + android:name="org.mozilla.fennec_fdroid" + android:maxLongVersionCode="679999" /> + <compatibility-package + android:name="org.mozilla.firefox" + android:maxLongVersionCode="679999" /> + <compatibility-package + android:name="org.mozilla.firefox_beta" + android:maxLongVersionCode="679999" /> </autofill-service> diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 7490782c..00000000 --- a/build.gradle +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -buildscript { - apply from: rootProject.file('dependencies.gradle') - repositories { - google() - jcenter() - } - dependencies { - classpath deps.gradle_plugin.android - classpath deps.gradle_plugin.kotlin - } -} - -plugins { - id 'com.github.ben-manes.versions' version '0.28.0' -} - -subprojects { - apply from: rootProject.file('dependencies.gradle') - repositories { - google() - jcenter() - maven { - url 'https://jitpack.io' - } - } - pluginManager.withPlugin('kotlin-android') { - dependencies { - implementation deps.kotlin.stdlib8 - } - } - if (name == 'app') { - apply plugin: 'com.android.application' - } else { - apply plugin: 'com.android.library' - } - android { - compileSdkVersion versions.compileSdk - buildToolsVersion = versions.buildTools - defaultConfig { - minSdkVersion versions.minSdk - targetSdkVersion versions.targetSdk - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - } - tasks.withType(JavaCompile) { - options.compilerArgs << '-Xlint:unchecked' - options.deprecation = true - } - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { - kotlinOptions { - jvmTarget = '1.8' - freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - } - } -} - -tasks.wrapper { - gradleVersion = "6.5.1" - distributionType = Wrapper.DistributionType.ALL - distributionSha256Sum = "143a28f54f1ae93ef4f72d862dbc3c438050d81bb45b4601eb7076e998362920" -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..b4821208 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,66 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import com.android.build.gradle.BaseExtension + +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath(Plugins.agp) + classpath(Plugins.kotlin) + } +} + +plugins { + id("com.github.ben-manes.versions") version "0.29.0" + id("com.autonomousapps.dependency-analysis") version "0.53.0" +} + +subprojects { + repositories { + google() + jcenter() + maven { + setUrl("https://jitpack.io") + } + } + if (name == "app") { + apply(plugin = "com.android.application") + } else { + apply(plugin = "com.android.library") + } + configure<BaseExtension> { + compileSdkVersion(29) + defaultConfig { + minSdkVersion(23) + targetSdkVersion(29) + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + tasks.withType<JavaCompile> { + options.compilerArgs.add("-Xlint:unchecked") + options.isDeprecation = true + } + } + tasks.withType<KotlinCompile> { + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn") + languageVersion = "1.4" + } + } +} + +tasks.wrapper { + gradleVersion = "6.5.1" + distributionType = Wrapper.DistributionType.ALL + distributionSha256Sum = "143a28f54f1ae93ef4f72d862dbc3c438050d81bb45b4601eb7076e998362920" +} diff --git a/buildSrc/.gitignore b/buildSrc/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/buildSrc/.gitignore @@ -0,0 +1 @@ +/build diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..3457e2c2 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + `kotlin-dsl` +} + +repositories { + jcenter() +} + +kotlinDslPluginOptions { + experimentalWarning.set(false) +} diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt new file mode 100644 index 00000000..63d267db --- /dev/null +++ b/buildSrc/src/main/java/Dependencies.kt @@ -0,0 +1,84 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +object Plugins { + + const val agp = "com.android.tools.build:gradle:4.0.1" + const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0-rc" +} + +object Dependencies { + object Kotlin { + object Coroutines { + + const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8-1.4.0-rc" + const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8-1.4.0-rc" + } + } + + object AndroidX { + + const val activity_ktx = "androidx.activity:activity-ktx:1.2.0-alpha07" + const val annotation = "androidx.annotation:annotation:1.2.0-alpha01" + const val autofill = "androidx.autofill:autofill:1.1.0-alpha01" + const val appcompat = "androidx.appcompat:appcompat:1.3.0-alpha01" + const val biometric = "androidx.biometric:biometric:1.1.0-alpha01" + const val constraint_layout = "androidx.constraintlayout:constraintlayout:2.0.0-beta8" + const val core_ktx = "androidx.core:core-ktx:1.5.0-alpha01" + const val documentfile = "androidx.documentfile:documentfile:1.0.1" + const val fragment_ktx = "androidx.fragment:fragment-ktx:1.3.0-alpha07" + const val lifecycle_common = "androidx.lifecycle:lifecycle-common-java8:2.3.0-alpha06" + const val lifecycle_livedata_ktx = "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-alpha06" + const val lifecycle_viewmodel_ktx = "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha06" + const val material = "com.google.android.material:material:1.3.0-alpha01" + const val preference = "androidx.preference:preference:1.1.1" + const val recycler_view = "androidx.recyclerview:recyclerview:1.2.0-alpha05" + const val recycler_view_selection = "androidx.recyclerview:recyclerview-selection:1.1.0-rc01" + const val security = "androidx.security:security-crypto:1.1.0-alpha01" + const val swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01" + } + + object FirstParty { + + const val openpgp_ktx = "com.github.android-password-store:openpgp-ktx:2.0.0" + const val zxing_android_embedded = "com.github.android-password-store:zxing-android-embedded:4.1.0-aps" + } + + object ThirdParty { + + const val bouncycastle = "org.bouncycastle:bcprov-jdk15on:1.66" + const val commons_codec = "commons-codec:commons-codec:1.14" + const val fastscroll = "me.zhanghai.android.fastscroll:library:1.1.4" + const val jsch = "com.jcraft:jsch:0.1.55" + const val jgit = "org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r" + const val leakcanary = "com.squareup.leakcanary:leakcanary-android:2.4" + const val plumber = "com.squareup.leakcanary:plumber-android:2.4" + const val sshj = "com.hierynomus:sshj:0.29.0" + const val ssh_auth = "org.sufficientlysecure:sshauthentication-api:1.0" + const val timber = "com.jakewharton.timber:timber:4.7.1" + const val timberkt = "com.github.ajalt:timberkt:1.5.1" + const val whatthestack = "com.github.haroldadmin:WhatTheStack:0.0.4" + } + + object NonFree { + + const val google_play_auth_api_phone = "com.google.android.gms:play-services-auth-api-phone:17.4.0" + } + + object Testing { + + const val junit = "junit:junit:4.13" + const val kotlin_test_junit = "org.jetbrains.kotlin:kotlin-test-junit:1.3.72" + + object AndroidX { + + const val runner = "androidx.test:runner:1.3.0-rc01" + const val rules = "androidx.test:rules:1.3.0-rc01" + const val junit = "androidx.test.ext:junit:1.1.2-rc01" + const val espresso_core = "androidx.test.espresso:espresso-core:3.3.0-rc01" + const val espresso_intents = "androidx.test.espresso:espresso-intents:3.3.0-rc01" + } + } +} diff --git a/dependencies.gradle b/dependencies.gradle deleted file mode 100644 index d05e0e6d..00000000 --- a/dependencies.gradle +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -ext.versions = [ - minSdk: 23, - targetSdk: 29, - compileSdk: 29, - buildTools: '29.0.3' -] - -ext.deps = [ - gradle_plugin: [ - android: 'com.android.tools.build:gradle:4.0.0', - kotlin: 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72', - ], - - kotlin: [ - coroutines: [ - android: 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7', - core: 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7', - ], - stdlib8: 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72' - ], - - androidx: [ - activity_ktx: 'androidx.activity:activity-ktx:1.2.0-alpha06', - annotation: 'androidx.annotation:annotation:1.2.0-alpha01', - autofill: 'androidx.autofill:autofill:1.0.0', - appcompat: 'androidx.appcompat:appcompat:1.3.0-alpha01', - biometric: 'androidx.biometric:biometric:1.1.0-alpha01', - constraint_layout: 'androidx.constraintlayout:constraintlayout:2.0.0-beta7', - core_ktx: 'androidx.core:core-ktx:1.5.0-alpha01', - documentfile: 'androidx.documentfile:documentfile:1.0.1', - fragment_ktx: 'androidx.fragment:fragment-ktx:1.3.0-alpha06', - lifecycle_common: 'androidx.lifecycle:lifecycle-common-java8:2.3.0-alpha05', - lifecycle_livedata_ktx: 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-alpha05', - lifecycle_viewmodel_ktx: 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha05', - material: 'com.google.android.material:material:1.3.0-alpha01', - preference: 'androidx.preference:preference:1.1.1', - recycler_view: 'androidx.recyclerview:recyclerview:1.2.0-alpha04', - recycler_view_selection: 'androidx.recyclerview:recyclerview-selection:1.1.0-rc01', - security: 'androidx.security:security-crypto:1.1.0-alpha01', - swiperefreshlayout: 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - ], - - first_party: [ - openpgp_ktx: 'com.github.android-password-store:openpgp-ktx:2.0.0', - zxing_android_embedded: 'com.github.android-password-store:zxing-android-embedded:v4.1.0-aps' - ], - - third_party: [ - bouncycastle: 'org.bouncycastle:bcprov-jdk15on:1.65.01', - commons_codec: 'commons-codec:commons-codec:1.13', - fastscroll: 'me.zhanghai.android.fastscroll:library:1.1.4', - jsch: 'com.jcraft:jsch:0.1.55', - jgit: 'org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r', - leakcanary: 'com.squareup.leakcanary:leakcanary-android:2.4', - plumber: 'com.squareup.leakcanary:plumber-android:2.4', - sshj: 'com.hierynomus:sshj:0.29.0', - ssh_auth: 'org.sufficientlysecure:sshauthentication-api:1.0', - timber: 'com.jakewharton.timber:timber:4.7.1', - timberkt: 'com.github.ajalt:timberkt:1.5.1', - whatthestack: 'com.github.haroldadmin:WhatTheStack:0.0.3', - ], - - non_free: [ - google_play_auth_api_phone: 'com.google.android.gms:play-services-auth-api-phone:17.4.0', - ], - - testing: [ - junit: 'junit:junit:4.13', - kotlin_test_junit: 'org.jetbrains.kotlin:kotlin-test-junit:1.3.72', - androidx: [ - runner: 'androidx.test:runner:1.3.0-rc01', - rules: 'androidx.test:rules:1.3.0-rc01', - junit: 'androidx.test.ext:junit:1.1.2-rc01', - espresso_core: 'androidx.test.espresso:espresso-core:3.3.0-rc01', - espresso_intents: 'androidx.test.espresso:espresso-intents:3.3.0-rc01' - ] - ] -] diff --git a/gradle.properties b/gradle.properties index 218affe7..ca214f0f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,3 +20,6 @@ android.useMinimalKeepRules=true # references to the resources it declares instead of declarations plus all # transitive dependency references. android.namespacedRClass=true + +# Gradle FS watching +org.gradle.unsafe.watch-fs=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6fa3f76a..28b21a0c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,3 @@ -# -# Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. -# SPDX-License-Identifier: GPL-3.0-only -# - distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionSha256Sum=143a28f54f1ae93ef4f72d862dbc3c438050d81bb45b4601eb7076e998362920 diff --git a/settings.gradle b/settings.gradle.kts index 47fd3b82..a418994b 100644 --- a/settings.gradle +++ b/settings.gradle.kts @@ -2,4 +2,4 @@ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. * SPDX-License-Identifier: GPL-3.0-only */ -include ':app' +include(":app") |