diff --git a/.github/workflows/firebase.yml b/.github/workflows/firebase.yml new file mode 100644 index 00000000..5bc3fdae --- /dev/null +++ b/.github/workflows/firebase.yml @@ -0,0 +1,71 @@ +name: FireBase + +on: + # allow to run manually + workflow_dispatch: + schedule: + # run every day at 04:00 UTC+0 + - cron: '0 4 * * *' + +jobs: + assemble_ui_test_artifacts: + name: Build artifacts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + + - name: Build APK for UI test after Unit tests + run: | + ./gradlew test + ./gradlew assembleDebug + ./gradlew assembleDebugAndroidTest + + - name: Upload app-debug APK + uses: actions/upload-artifact@v2 + with: + name: app-debug + path: app/build/outputs/apk/debug/app-debug.apk + + - name: Upload app-debug-androidTest APK + uses: actions/upload-artifact@v2 + with: + name: app-debug-androidTest + path: app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk + + run_ui_tests_on_firebase: + runs-on: ubuntu-latest + needs: assemble_ui_test_artifacts + steps: + - uses: actions/checkout@v2 + - name: Download app-debug APK + uses: actions/download-artifact@v1 + with: + name: app-debug + + - name: Download app-debug-androidTest APK + uses: actions/download-artifact@v1 + with: + name: app-debug-androidTest + + - name: Firebase auth with gcloud + uses: google-github-actions/setup-gcloud@master + with: + version: '290.0.1' + service_account_key: ${{ secrets.FIREBASE_KEY }} + project_id: ${{ secrets.PROJECT_ID }} + + - name: Run Instrumentation Tests in Firebase Test Lab + # first command print all available devices + # current device: + # | MODEL_ID | MAKE | MODEL_NAME | FORM | RESOLUTION | OS_VERSION_IDS | + # | blueline | Google | Pixel 3 | PHYSICAL | 2160 x 1080 | 28 | + run: | + gcloud firebase test android models list + gcloud firebase test android run --type instrumentation --use-orchestrator \ + --environment-variables clearPackageData=true \ + --app app-debug/app-debug.apk --test app-debug-androidTest/app-debug-androidTest.apk \ + --device model=blueline,version=28,locale=en,orientation=portrait --num-flaky-test-attempts 3 \ No newline at end of file diff --git a/.github/workflows/release_apk.yml b/.github/workflows/release_apk.yml new file mode 100644 index 00000000..e753ae22 --- /dev/null +++ b/.github/workflows/release_apk.yml @@ -0,0 +1,43 @@ +name: Test_and_build_artifacts_on_release + +on: + push: + branches: [ master ] + +env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }} + RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }} + +jobs: + build_apk_aab: + name: Build release artifacts + # ubuntu is faster and it's crucial for using in actions because we have a limited amount of time + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Checkout keystore repo + uses: actions/checkout@v2 + with: + repository: ${{ secrets.KEYSTORE_GIT_REPOSITORY }} + token: ${{ secrets.KEYSTORE_ACCESS_TOKEN }} + path: app/keystore + - name: Build release APK and AAB after test + run: | + ./gradlew test + ./gradlew assembleRelease + ./gradlew bundleRelease + - name: Upload APK + uses: actions/upload-artifact@v2 + with: + name: crosslingo-release.apk + path: app/build/outputs/apk/release/app-release.apk + - name: Upload AAB Bundle + uses: actions/upload-artifact@v2 + with: + name: crosslingo-release.aab + path: app/build/outputs/bundle/release/app-release.aab diff --git a/app/build.gradle b/app/build.gradle index b901bd02..4b24e69a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,26 +8,41 @@ android { defaultConfig { applicationId "com.example.crosswordToLearn" - minSdkVersion 16 - targetSdkVersion 30 + minSdkVersion 21 + targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments clearPackageData: 'true' - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } } testOptions { execution 'ANDROIDX_TEST_ORCHESTRATOR' } + + signingConfigs { + release { + def keystorePropsFile = file("keystore/my_app_keystore") + + if (keystorePropsFile.exists()) + { + storeFile file("keystore/my_app_keystore") + storePassword System.getenv('KEYSTORE_PASSWORD') + keyAlias System.getenv('RELEASE_SIGN_KEY_ALIAS') + keyPassword System.getenv('RELEASE_SIGN_KEY_PASSWORD') + } + } + } + buildTypes { release { + def keystorePropsFile = file("keystore/my_app_keystore") + + if (keystorePropsFile.exists()) { + signingConfig signingConfigs.release + } minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } @@ -44,9 +59,9 @@ android { dependencies { implementation 'androidx.legacy:legacy-support-v13:1.0.0' - implementation 'com.google.android.material:material:1.2.1' + implementation 'com.google.android.material:material:1.3.0' implementation project(':library') - implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.21" + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.30" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' diff --git a/app/src/androidTest/java/com/example/crosswordToLearn/BadImageReadingInstrumentedTest.kt b/app/src/androidTest/java/com/example/crosswordToLearn/BadImageReadingInstrumentedTest.kt index 20eafc49..0caa918a 100644 --- a/app/src/androidTest/java/com/example/crosswordToLearn/BadImageReadingInstrumentedTest.kt +++ b/app/src/androidTest/java/com/example/crosswordToLearn/BadImageReadingInstrumentedTest.kt @@ -9,6 +9,7 @@ import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import java.io.File @@ -17,6 +18,10 @@ import java.io.FileOutputStream @RunWith(AndroidJUnit4::class) class BadImageReadingInstrumentedTest { + @Rule + @JvmField + val retryTestRule = RetryTestRule() + @Before fun addBadData() { if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { diff --git a/app/src/androidTest/java/com/example/crosswordToLearn/ChooseTopicsInstrumentedTest.kt b/app/src/androidTest/java/com/example/crosswordToLearn/ChooseTopicsInstrumentedTest.kt index dbf2c823..cd2fb8d9 100644 --- a/app/src/androidTest/java/com/example/crosswordToLearn/ChooseTopicsInstrumentedTest.kt +++ b/app/src/androidTest/java/com/example/crosswordToLearn/ChooseTopicsInstrumentedTest.kt @@ -22,6 +22,10 @@ class ChooseTopicsInstrumentedTest { @get:Rule var activityTestRule: ActivityScenarioRule = ActivityScenarioRule(MainActivity::class.java) + + @Rule + @JvmField + val retryTestRule = RetryTestRule() @Test(timeout = 30000) fun chooseTopicsInstrumentedTest() { diff --git a/app/src/androidTest/java/com/example/crosswordToLearn/SolveCrosswordWithTipsInstrumentedTest.kt b/app/src/androidTest/java/com/example/crosswordToLearn/SolveCrosswordWithTipsInstrumentedTest.kt index 72751c91..5d2193d4 100644 --- a/app/src/androidTest/java/com/example/crosswordToLearn/SolveCrosswordWithTipsInstrumentedTest.kt +++ b/app/src/androidTest/java/com/example/crosswordToLearn/SolveCrosswordWithTipsInstrumentedTest.kt @@ -23,6 +23,10 @@ class SolveCrosswordWithTipsInstrumentedTest { var activityTestRule: ActivityScenarioRule = ActivityScenarioRule(MainActivity::class.java) + @Rule + @JvmField + val retryTestRule = RetryTestRule() + private fun menuClick(name: Int, id: Int, price: Int): Int { var stars = readConfig() openActionBarOverflowOrOptionsMenu( @@ -34,7 +38,7 @@ class SolveCrosswordWithTipsInstrumentedTest { pressBack() pressBack() loadFirstCrossword() - assertEquals(stars, readConfig()) + waitForCondition("Stars number checking", {stars==readConfig()}) return stars } diff --git a/app/src/androidTest/java/com/example/crosswordToLearn/Utils.kt b/app/src/androidTest/java/com/example/crosswordToLearn/Utils.kt index 705ce0bc..ff91a53a 100644 --- a/app/src/androidTest/java/com/example/crosswordToLearn/Utils.kt +++ b/app/src/androidTest/java/com/example/crosswordToLearn/Utils.kt @@ -2,6 +2,7 @@ package com.example.crosswordToLearn import android.content.Context import android.content.Intent +import android.util.Log import android.view.View import android.view.ViewGroup import android.view.WindowManager @@ -23,6 +24,8 @@ import org.akop.ararat.core.buildCrossword import org.akop.ararat.io.UClickJsonFormatter import org.hamcrest.* import org.junit.Rule +import org.junit.rules.TestRule +import org.junit.runners.model.Statement import java.io.File import java.util.concurrent.Callable import java.util.concurrent.TimeoutException @@ -228,12 +231,50 @@ class ToastMatcher : } } +class RetryTestRule(val retryCount: Int = 3) : TestRule { + + override fun apply(base: Statement?, description: org.junit.runner.Description?): Statement { + return statement(base, description) + } + + private fun statement(base: Statement?, description: org.junit.runner.Description?): Statement { + return object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + var caughtThrowable: Throwable? = null + + // implement retry logic here + for (i in 0 until retryCount) { + try { + base?.evaluate() + return + } catch (t: Throwable) { + caughtThrowable = t + if (description != null) { + Log.e("ERROR", description.displayName + ": run " + (i + 1) + " failed") + } + } + } + + if (description != null) { + Log.e("ERROR", description.displayName + ": giving up after " + retryCount + " failures") + } + throw caughtThrowable!! + } + } + } +} + open class ChoseTopicsToastTest { @get:Rule var activityTestRule: ActivityScenarioRule = ActivityScenarioRule(MainActivity::class.java) + @Rule + @JvmField + val retryTestRule = RetryTestRule() + private lateinit var scenario: ActivityScenario fun choseTopicsImpl(fileName: String, message: String) { @@ -262,6 +303,10 @@ open class SolveCrossword { var activityTestRule: ActivityScenarioRule = ActivityScenarioRule(MainActivity::class.java) + @Rule + @JvmField + val retryTestRule = RetryTestRule() + protected lateinit var crossword: Crossword fun solve() { @@ -293,6 +338,10 @@ abstract class BadCrosswordDataTest { var activityTestRule: ActivityScenarioRule = ActivityScenarioRule(MainActivity::class.java) + @Rule + @JvmField + val retryTestRule = RetryTestRule() + abstract fun spoil() lateinit var crossword: Crossword diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 28705de9..1b7cde21 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,7 +7,8 @@ android:label="" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + android:requestLegacyExternalStorage="true"> + app:layout_constraintWidth_percent="0.5"> diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ef5f0186..4612af6c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -14,6 +14,7 @@ parameters: - 27 - 28 - 29 + - 30 trigger: - master diff --git a/build.gradle b/build.gradle index 8282fa27..7bd0fc09 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,13 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.4.21" + ext.kotlin_version = "1.4.30" repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21" + classpath 'com.android.tools.build:gradle:4.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.30" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/library/build.gradle b/library/build.gradle index 7388d9ba..fe6c1788 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -11,8 +11,8 @@ android { } defaultConfig { - minSdkVersion 16 - targetSdkVersion 30 + minSdkVersion 21 + targetSdkVersion 29 versionCode 1 versionName "1.0" @@ -53,7 +53,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) //noinspection GradleDependency - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.20-RC" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.30" testImplementation 'junit:junit:4.13.1' //noinspection GradleDependency diff --git a/library/src/main/java/org/akop/ararat/view/CrosswordView.kt b/library/src/main/java/org/akop/ararat/view/CrosswordView.kt index dde8c8ae..bc650b30 100644 --- a/library/src/main/java/org/akop/ararat/view/CrosswordView.kt +++ b/library/src/main/java/org/akop/ararat/view/CrosswordView.kt @@ -342,7 +342,7 @@ class CrosswordView(context: Context, attrs: AttributeSet?) : viewR.viewTreeObserver.addOnGlobalLayoutListener { val r = Rect() viewR.getWindowVisibleDisplayFrame(r) - val heightDiff = viewR.rootView.height - r.height() + val heightDiff = viewR.rootView.height - toolbarHeight - r.height() val keyboardMinHeight = 300 if (heightDiff > keyboardMinHeight){ heightWithoutKeyboard = r.height() - toolbarHeight - hintView.height