Skip to content

Commit

Permalink
feat: Periodically check for updates and alert user (#236)
Browse files Browse the repository at this point in the history
Users can inadvertently get stuck on older versions of the app; e.g., by
installing from one F-Droid repository that stops hosting the app at
some later time.

Analytics from the Play Store also shows a long tail of users who are,
for some reason, on an older version.

On resuming `MainActivity`, and approximately once per day, check and
see if a newer version of Pachli is available, and prompt the user to
update by going to the relevant install location (Google Play, F-Droid,
or GitHub).

The dialog prompt allows them to ignore this specific version, or
disable all future update notifications. This is also exposed through
the preferences, so the user can adjust it there too.

A different update check method is used for each installation location.

- F-Droid: Use the F-Droid API to query for the newest released version
- GitHub: Use the GitHub API to query for the newest release, and check
the APK filename attached to that release
- Google Play: Use the Play in-app-updates library
(https://developer.android.com/guide/playcore/in-app-updates) to query
for the newest released version

These are kept in different build flavours (source sets), so that e.g.,
the build for the F-Droid store can only query the F-Droid API, the UI
strings are specific to F-Droid, etc. This also ensures that the update
service libraries are specific to that build and do not
"cross-contaminate".

Note that this *does not* update the app, it takes the user to either
the relevant store page (F-Droid, Play) or GitHub release page. The user
must still start the update from that page.

CI configuration is updated to build the different flavours.
  • Loading branch information
nikclayton authored Nov 8, 2023
1 parent 86dee94 commit dda9dde
Show file tree
Hide file tree
Showing 25 changed files with 675 additions and 31 deletions.
16 changes: 10 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:

jobs:
build:
strategy:
matrix:
color: ["orange"]
store: ["fdroid", "github", "google"]
name: Build
runs-on: ubuntu-latest
steps:
Expand All @@ -34,11 +38,11 @@ jobs:
- name: ktlint
run: ./gradlew clean ktlintCheck

- name: Regular lint
run: ./gradlew app:lintOrangeDebug
- name: Regular lint ${{ matrix.color }}${{ matrix.store }}Debug
run: ./gradlew app:lint${{ matrix.color }}${{ matrix.store }}Debug

- name: Test
run: ./gradlew app:testOrangeDebugUnitTest checks:test
- name: Test ${{ matrix.color }}${{ matrix.store }}DebugUnitTest checks:test
run: ./gradlew app:test${{ matrix.color }}${{ matrix.store }}DebugUnitTest checks:test

- name: Build
run: ./gradlew app:buildOrangeDebug
- name: Build ${{ matrix.color }}${{ matrix.store }}Debug
run: ./gradlew app:build${{ matrix.color }}${{ matrix.store }}Debug
6 changes: 5 additions & 1 deletion .github/workflows/populate-gradle-build-cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ on:

jobs:
build:
strategy:
matrix:
color: ["orange"]
store: ["fdroid", "github", "google"]
name: app:buildOrangeDebug
runs-on: ubuntu-latest
steps:
Expand All @@ -31,4 +35,4 @@ jobs:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}

- name: Run app:buildOrangeDebug
run: ./gradlew app:buildOrangeDebug
run: ./gradlew app:build${{ matrix.color }}${{ matrix.store }}Debug
18 changes: 9 additions & 9 deletions .github/workflows/upload-blue-release-google-play.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@ jobs:
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}

- name: Build APK
run: ./gradlew assembleBlueRelease --stacktrace
- name: Build GitHub APK
run: ./gradlew assembleBlueGithubRelease --stacktrace

- name: Build AAB
run: ./gradlew :app:bundleBlueRelease --stacktrace
- name: Build Google AAB
run: ./gradlew :app:bundleBlueGoogleRelease --stacktrace

- uses: r0adkll/[email protected]
name: Sign app APK
name: Sign GitHub APK
id: sign_app_apk
with:
releaseDirectory: app/build/outputs/apk/blue/release
releaseDirectory: app/build/outputs/apk/blueGithub/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
Expand All @@ -46,10 +46,10 @@ jobs:
BUILD_TOOLS_VERSION: "34.0.0"

- uses: r0adkll/[email protected]
name: Sign app AAB
name: Sign Google AAB
id: sign_app_aab
with:
releaseDirectory: app/build/outputs/bundle/blueRelease
releaseDirectory: app/build/outputs/bundle/blueGoogleRelease
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
Expand Down Expand Up @@ -80,4 +80,4 @@ jobs:
track: internal
whatsNewDirectory: googleplay/whatsnew
status: completed
mappingFile: app/build/outputs/mapping/blueRelease/mapping.txt
mappingFile: app/build/outputs/mapping/blueGoogleRelease/mapping.txt
12 changes: 6 additions & 6 deletions .github/workflows/upload-orange-release-google-play.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,19 @@ jobs:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}

- name: Test
run: ./gradlew app:testOrangeReleaseUnitTest --stacktrace
run: ./gradlew app:testOrangeGoogleReleaseUnitTest --stacktrace

- name: Build APK
run: ./gradlew assembleOrangeRelease --stacktrace
run: ./gradlew assembleOrangeGoogleRelease --stacktrace

- name: Build AAB
run: ./gradlew :app:bundleOrangeRelease --stacktrace
run: ./gradlew :app:bundleOrangeGoogleRelease --stacktrace

- uses: r0adkll/[email protected]
name: Sign app APK
id: sign_app_apk
with:
releaseDirectory: app/build/outputs/apk/orange/release
releaseDirectory: app/build/outputs/apk/orangeGoogle/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
Expand All @@ -54,7 +54,7 @@ jobs:
name: Sign app AAB
id: sign_app_aab
with:
releaseDirectory: app/build/outputs/bundle/orangeRelease
releaseDirectory: app/build/outputs/bundle/orangeGoogleRelease
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
Expand Down Expand Up @@ -85,4 +85,4 @@ jobs:
track: production
whatsNewDirectory: googleplay/whatsnew
status: completed
mappingFile: app/build/outputs/mapping/orangeRelease/mapping.txt
mappingFile: app/build/outputs/mapping/orangeGoogleRelease/mapping.txt
27 changes: 26 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ android {
buildConfigField("String", "SUPPORT_ACCOUNT_URL", "\"$SUPPORT_ACCOUNT_URL\"")
}
buildTypes {
debug {
getIsDefault().set(true)
}

release {
minifyEnabled true
shrinkResources true
Expand All @@ -54,13 +58,31 @@ android {
}

flavorDimensions += "color"
flavorDimensions += "store"

productFlavors {
blue {}
blue {
dimension "color"
}

orange {
dimension "color"
resValue "string", "app_name", APP_NAME + " Current"
applicationIdSuffix ".current"
versionNameSuffix "+" + gitSha
}

fdroid {
dimension "store"
}

github {
dimension "store"
}

google {
dimension "store"
}
}

lint {
Expand Down Expand Up @@ -199,6 +221,9 @@ dependencies {
implementation libs.bundles.aboutlibraries
implementation libs.timber

googleImplementation libs.app.update
googleImplementation libs.app.update.ktx

testImplementation libs.androidx.test.junit
testImplementation libs.robolectric
testImplementation libs.bundles.mockito
Expand Down
48 changes: 48 additions & 0 deletions app/src/fdroid/kotlin/app/pachli/di/UpdateCheckModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2023 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

package app.pachli.di

import app.pachli.updatecheck.FdroidService
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
import com.google.gson.Gson
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
@Module
object UpdateCheckModule {
@Provides
@Singleton
fun providesFdroidService(
httpClient: OkHttpClient,
gson: Gson
): FdroidService = Retrofit.Builder()
.baseUrl("https://f-droid.org")
.client(httpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
.build()
.create()
}
40 changes: 40 additions & 0 deletions app/src/fdroid/kotlin/app/pachli/updatecheck/FdroidService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2023 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

package app.pachli.updatecheck

import at.connyduck.calladapter.networkresult.NetworkResult
import retrofit2.http.GET
import retrofit2.http.Path

data class FdroidPackageVersion(
val versionName: String,
val versionCode: Int
)

data class FdroidPackage(
val packageName: String,
val suggestedVersionCode: Int,
val packages: List<FdroidPackageVersion>
)

interface FdroidService {
@GET("/api/v1/packages/{package}")
suspend fun getPackage(
@Path("package") pkg: String
): NetworkResult<FdroidPackage>
}
39 changes: 39 additions & 0 deletions app/src/fdroid/kotlin/app/pachli/updatecheck/UpdateCheck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2023 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

package app.pachli.updatecheck

import android.content.Intent
import android.net.Uri
import app.pachli.BuildConfig
import app.pachli.util.SharedPreferencesRepository
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class UpdateCheck @Inject constructor(
sharedPreferencesRepository: SharedPreferencesRepository,
private val fdroidService: FdroidService
) : UpdateCheckBase(sharedPreferencesRepository) {
override val updateIntent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("market://details?id=${BuildConfig.APPLICATION_ID}")
}

override suspend fun remoteFetchLatestVersionCode(): Int? {
return fdroidService.getPackage(BuildConfig.APPLICATION_ID).getOrNull()?.suggestedVersionCode
}
}
21 changes: 21 additions & 0 deletions app/src/fdroid/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Pachli Association
~
~ This file is a part of Pachli.
~
~ This program is free software; you can redistribute it and/or modify it under the terms of the
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
~ License, or (at your option) any later version.
~
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
~ Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
~ see <http://www.gnu.org/licenses>.
-->

<resources>
<string name="update_dialog_message">Open F-Droid to see the details?</string>
<string name="update_dialog_positive">Open F-Droid</string>
</resources>
48 changes: 48 additions & 0 deletions app/src/github/kotlin/app/pachli/di/UpdateCheckModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2023 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

package app.pachli.di

import app.pachli.updatecheck.GitHubService
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
import com.google.gson.Gson
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
@Module
object UpdateCheckModule {
@Provides
@Singleton
fun providesGitHubService(
httpClient: OkHttpClient,
gson: Gson
): GitHubService = Retrofit.Builder()
.baseUrl("https://api.github.com")
.client(httpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
.build()
.create()
}
Loading

0 comments on commit dda9dde

Please sign in to comment.