Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 1 addition & 13 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ on: [pull_request]
name: Check pull request
jobs:
test-pr:
runs-on: macos-latest
strategy:
matrix:
api-level: [23, 29]
runs-on: ubuntu-latest
steps:

#- name: Auto-cancel redundant workflow run
Expand Down Expand Up @@ -59,15 +56,6 @@ jobs:
with:
arguments: apiCheck testFreeDebug lintFreeDebug spotlessCheck

- name: Run instrumentation tests
if: ${{ steps.service-changed.outputs.result == 'true' }}
uses: reactivecircus/android-emulator-runner@599839e4285455fff52cd8e3614575e02f1b673f
with:
api-level: ${{ matrix.api-level }}
target: default
script: |
./gradlew :app:connectedFreeDebugAndroidTest

- name: (Fail-only) upload test report
if: failure()
uses: actions/upload-artifact@e448a9b857ee2131e752b06002bf0e093c65e571
Expand Down
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ android {
create("free") {}
create("nonFree") {}
}
testOptions { unitTests.isReturnDefaultValues = true }
}

dependencies {
Expand Down Expand Up @@ -106,5 +107,6 @@ dependencies {
androidTestImplementation(libs.bundles.testDependencies)
androidTestImplementation(libs.bundles.androidTestDependencies)
testImplementation(libs.testing.robolectric)
testImplementation(libs.testing.sharedPrefsMock)
testImplementation(libs.bundles.testDependencies)
}

This file was deleted.

14 changes: 10 additions & 4 deletions app/src/main/java/dev/msfjarvis/aps/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,24 @@ import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import com.github.ajalt.timberkt.Timber.DebugTree
import com.github.ajalt.timberkt.Timber.plant
import dagger.hilt.android.HiltAndroidApp
import dev.msfjarvis.aps.injection.context.FilesDirPath
import dev.msfjarvis.aps.injection.prefs.SettingsPreferences
import dev.msfjarvis.aps.util.extensions.getString
import dev.msfjarvis.aps.util.extensions.sharedPrefs
import dev.msfjarvis.aps.util.git.sshj.setUpBouncyCastleForSshj
import dev.msfjarvis.aps.util.proxy.ProxyUtils
import dev.msfjarvis.aps.util.settings.GitSettings
import dev.msfjarvis.aps.util.settings.PreferenceKeys
import dev.msfjarvis.aps.util.settings.runMigrations
import javax.inject.Inject

@Suppress("Unused")
@HiltAndroidApp
class Application : android.app.Application(), SharedPreferences.OnSharedPreferenceChangeListener {

private val prefs by lazy { sharedPrefs }
@Inject @SettingsPreferences lateinit var prefs: SharedPreferences
@Inject @FilesDirPath lateinit var filesDirPath: String
@Inject lateinit var proxyUtils: ProxyUtils
@Inject lateinit var gitSettings: GitSettings

override fun onCreate() {
super.onCreate()
Expand All @@ -42,8 +48,8 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
prefs.registerOnSharedPreferenceChangeListener(this)
setNightMode()
setUpBouncyCastleForSshj()
runMigrations(applicationContext)
ProxyUtils.setDefaultProxy()
runMigrations(filesDirPath, prefs, gitSettings)
proxyUtils.setDefaultProxy()
}

override fun onTerminate() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dev.msfjarvis.aps.injection.context

import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
class ContextModule {

/**
* We inject [Context.getFilesDir] to break the dependency on [Context], allowing tests to run on
* the JVM. The principle here is identical to why [dev.msfjarvis.aps.util.totp.TotpFinder]
* exists.
*
* @param context [ApplicationContext]
* @return the path of app-specific files directory.
*/
@Provides
@FilesDirPath
fun providesFilesDirPath(@ApplicationContext context: Context): String {
return context.filesDir.path
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.msfjarvis.aps.injection.context

import android.content.Context
import javax.inject.Qualifier

/** Qualifies a [String] representing the absolute path of [Context.getFilesDir]. */
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class FilesDirPath
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.msfjarvis.aps.injection.prefs

import android.content.SharedPreferences
import javax.inject.Qualifier

/**
* Qualifies a [SharedPreferences] instance specifically used for encrypted Git-related settings.
*/
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class GitPreferences
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package dev.msfjarvis.aps.injection.prefs

import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import dagger.Module
import dagger.Provides
import dagger.Reusable
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dev.msfjarvis.aps.BuildConfig

@Module
@InstallIn(SingletonComponent::class)
class PreferenceModule {

private fun provideBaseEncryptedPreferences(
context: Context,
fileName: String
): SharedPreferences {
val masterKeyAlias =
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
return EncryptedSharedPreferences.create(
context,
fileName,
masterKeyAlias,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}

@Provides
@SettingsPreferences
@Reusable
fun provideSettingsPreferences(@ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_preferences", MODE_PRIVATE)
}

@Provides
@GitPreferences
@Reusable
fun provideEncryptedPreferences(@ApplicationContext context: Context): SharedPreferences {
return provideBaseEncryptedPreferences(context, "git_operation")
}

@Provides
@ProxyPreferences
@Reusable
fun provideProxyPreferences(@ApplicationContext context: Context): SharedPreferences {
return provideBaseEncryptedPreferences(context, "http_proxy")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.msfjarvis.aps.injection.prefs

import android.content.SharedPreferences
import javax.inject.Qualifier

/**
* Qualifies a [SharedPreferences] instance specifically used for encrypted proxy-related settings.
*/
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class ProxyPreferences
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.msfjarvis.aps.injection.prefs

import javax.inject.Qualifier

@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class SettingsPreferences
19 changes: 12 additions & 7 deletions app/src/main/java/dev/msfjarvis/aps/ui/git/base/BaseGitActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.github.michaelbull.result.Result
import com.github.michaelbull.result.andThen
import com.github.michaelbull.result.mapError
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.util.extensions.getEncryptedGitPrefs
import dev.msfjarvis.aps.util.extensions.sharedPrefs
Expand All @@ -25,6 +26,7 @@ import dev.msfjarvis.aps.util.git.operation.SyncOperation
import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity
import dev.msfjarvis.aps.util.settings.GitSettings
import dev.msfjarvis.aps.util.settings.PreferenceKeys
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.schmizz.sshj.common.DisconnectReason
Expand All @@ -36,6 +38,7 @@ import net.schmizz.sshj.userauth.UserAuthException
* Abstract [AppCompatActivity] that holds some information that is commonly shared across
* git-related tasks and makes sense to be held here.
*/
@AndroidEntryPoint
abstract class BaseGitActivity : ContinuationContainerActivity() {

/** Enum of possible Git operations than can be run through [launchGitOperation]. */
Expand All @@ -48,29 +51,31 @@ abstract class BaseGitActivity : ContinuationContainerActivity() {
SYNC,
}

@Inject lateinit var gitSettings: GitSettings

/**
* Attempt to launch the requested Git operation.
* @param operation The type of git operation to launch
*/
suspend fun launchGitOperation(operation: GitOp): Result<Unit, Throwable> {
if (GitSettings.url == null) {
if (gitSettings.url == null) {
return Err(IllegalStateException("Git url is not set!"))
}
if (operation == GitOp.SYNC && !GitSettings.useMultiplexing) {
if (operation == GitOp.SYNC && !gitSettings.useMultiplexing) {
// If the server does not support multiple SSH channels per connection, we cannot run
// a sync operation without reconnecting and thus break sync into its two parts.
return launchGitOperation(GitOp.PULL).andThen { launchGitOperation(GitOp.PUSH) }
}
val op =
when (operation) {
GitOp.CLONE -> CloneOperation(this, GitSettings.url!!)
GitOp.PULL -> PullOperation(this, GitSettings.rebaseOnPull)
GitOp.CLONE -> CloneOperation(this, gitSettings.url!!)
GitOp.PULL -> PullOperation(this, gitSettings.rebaseOnPull)
GitOp.PUSH -> PushOperation(this)
GitOp.SYNC -> SyncOperation(this, GitSettings.rebaseOnPull)
GitOp.SYNC -> SyncOperation(this, gitSettings.rebaseOnPull)
GitOp.BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(this)
GitOp.RESET -> ResetToRemoteOperation(this)
}
return op.executeAfterAuthentication(GitSettings.authMode).mapError(::transformGitError)
return op.executeAfterAuthentication(gitSettings.authMode).mapError(::transformGitError)
}

fun finishOnSuccessHandler(@Suppress("UNUSED_PARAMETER") nothing: Unit) {
Expand Down Expand Up @@ -105,7 +110,7 @@ abstract class BaseGitActivity : ContinuationContainerActivity() {
val err = rootCauseException(throwable)
return when {
err.message?.contains("cannot open additional channels") == true -> {
GitSettings.useMultiplexing = false
gitSettings.useMultiplexing = false
SSHException(
DisconnectReason.TOO_MANY_CONNECTIONS,
"The server does not support multiple Git operations per SSH session. Please try again, a slower fallback mode will be used."
Expand Down
Loading