Skip to content

Commit 82af5b9

Browse files
committed
add notification permission handling and update service foreground behavior
1 parent 1b36329 commit 82af5b9

21 files changed

Lines changed: 259 additions & 119 deletions

.github/dependabot.yml

Lines changed: 0 additions & 10 deletions
This file was deleted.

.github/workflows/cd.yml

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,29 @@ on:
88
jobs:
99
build_test_publish_job:
1010
name: Build-Test-Publish job
11-
runs-on: macos-latest
11+
runs-on: ubuntu-latest
1212
steps:
13-
- name: Checkout
14-
uses: actions/checkout@v2
15-
- uses: actions/setup-java@v1
13+
- name: Check out
14+
uses: actions/checkout@v4
15+
- name: Set up JDK 17
16+
uses: actions/setup-java@v4
1617
with:
17-
java-version: '17'
18-
- name: Cache gradle
19-
uses: actions/cache@v1
18+
distribution: 'oracle'
19+
java-version: 17
20+
- name: Setup Gradle
21+
uses: gradle/actions/setup-gradle@v4
2022
with:
21-
path: ~/.gradle/caches
22-
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
23-
restore-keys: |
24-
${{ runner.os }}-gradle-
23+
cache-read-only: false
24+
cache-overwrite-existing: true
2525
- name: Decrypt large secret
2626
run: ./.github/scripts/decrypt_secret.sh
2727
env:
2828
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }}
2929
- name: Build, Test & Upload to Google Play
3030
run: bundle exec fastlane build_bundle_publish
31-
- name: Archive output artifacts
32-
uses: actions/upload-artifact@v2
31+
- name: Archive build artifacts
32+
if: always()
33+
uses: actions/upload-artifact@v4
3334
with:
3435
name: output-artifacts
3536
path: |

.github/workflows/ci.yml

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,24 @@ jobs:
1515
runs-on: ubuntu-latest
1616
steps:
1717
- name: Checkout
18-
uses: actions/checkout@v2
19-
- uses: actions/setup-java@v1
18+
uses: actions/checkout@v4
19+
- name: Gradle Wrapper Validation
20+
uses: gradle/actions/wrapper-validation@v4
21+
- name: Set up JDK 17
22+
uses: actions/setup-java@v4
2023
with:
21-
java-version: '17'
22-
- name: Cache gradle
23-
uses: actions/cache@v1
24+
distribution: 'oracle'
25+
java-version: 17
26+
- name: Setup Gradle
27+
uses: gradle/actions/setup-gradle@v4
2428
with:
25-
path: ~/.gradle/caches
26-
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
27-
restore-keys: |
28-
${{ runner.os }}-gradle-
29+
cache-read-only: false
30+
cache-overwrite-existing: true
2931
- name: Build
3032
run: ./gradlew build
31-
- name: Archive output artifacts
32-
if: ${{ always() }}
33-
uses: actions/upload-artifact@v2
33+
- name: Archive build-output artifacts
34+
if: always()
35+
uses: actions/upload-artifact@v4
3436
with:
3537
name: output-artifacts
3638
path: |
@@ -39,33 +41,67 @@ jobs:
3941
android_ui_test_job:
4042
name: Android UI-tests on emulator
4143
needs: build_job
42-
runs-on: macos-latest
44+
runs-on: ubuntu-latest
4345
continue-on-error: true
4446
strategy:
4547
fail-fast: false
4648
matrix:
47-
api-level: [16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
49+
api-level: [ 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36 ]
50+
target: [ default ]
4851
steps:
49-
- uses: actions/checkout@v1
50-
- uses: actions/setup-java@v1
52+
- name: Check out
53+
uses: actions/checkout@v4
54+
- name: Set up JDK 17
55+
uses: actions/setup-java@v4
56+
with:
57+
distribution: 'oracle'
58+
java-version: 17
59+
- name: Setup Gradle
60+
uses: gradle/actions/setup-gradle@v4
5161
with:
52-
java-version: '17'
53-
- name: Cache gradle
54-
uses: actions/cache@v1
62+
cache-read-only: false
63+
cache-overwrite-existing: true
64+
- name: Enable KVM
65+
run: |
66+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
67+
sudo udevadm control --reload-rules
68+
sudo udevadm trigger --name-match=kvm
69+
- name: AVD cache
70+
uses: actions/cache@v4
71+
id: avd-cache
5572
with:
56-
path: ~/.gradle/caches
57-
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
58-
restore-keys: |
59-
${{ runner.os }}-gradle-
60-
- name: Run debug UI-tests on emulator
73+
path: |
74+
~/.android/avd/*
75+
~/.android/adb*
76+
key: avd-${{ matrix.api-level }}-${{ matrix.target }}
77+
- name: Create AVD and generate snapshot for caching
78+
if: steps.avd-cache.outputs.cache-hit != 'true'
6179
uses: reactivecircus/android-emulator-runner@v2
6280
with:
6381
api-level: ${{ matrix.api-level }}
64-
target: google_apis
65-
script: ./gradlew :app:connectedCheck -PtestBuildType=debug
66-
- name: Run release UI-tests on emulator
82+
target: ${{ matrix.target }}
83+
arch: x86_64
84+
profile: Nexus 6
85+
force-avd-creation: false
86+
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
87+
disable-animations: false
88+
script: echo "Generated AVD snapshot for caching."
89+
- name: Run UI-tests on emulator
6790
uses: reactivecircus/android-emulator-runner@v2
6891
with:
6992
api-level: ${{ matrix.api-level }}
70-
target: google_apis
71-
script: ./gradlew :app:connectedCheck -PtestBuildType=release
93+
target: ${{ matrix.target }}
94+
arch: x86_64
95+
profile: Nexus 6
96+
force-avd-creation: false
97+
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
98+
disable-animations: true
99+
script: ./gradlew :app:connectedCheck
100+
- name: Archive ui-tests-output artifacts
101+
if: always()
102+
uses: actions/upload-artifact@v4
103+
with:
104+
name: output-ui-tests-artifacts-${{ matrix.api-level }}-${{ matrix.target }}
105+
path: |
106+
app/build/outputs
107+
app/build/reports

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
<p>
77
<a href="https://play.google.com/store/apps/details?id=com.softartdev.conwaysgameoflife"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge-border.png" height="75px"/></a>
88
</p>
9-
<img src="https://raw.githubusercontent.com/softartdev/ConwaysGameOfLife/master/demo.gif" height="500" />
10-
<img src="https://raw.githubusercontent.com/softartdev/ConwaysGameOfLife/master/demo2rotated.gif" height="350" />
9+
<img src="https://raw.githubusercontent.com/softartdev/ConwaysGameOfLife/master/misc/demo.gif" height="500" />
10+
<img src="https://raw.githubusercontent.com/softartdev/ConwaysGameOfLife/master/misc/demo2rotated.gif" height="350" />

app/build.gradle

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2+
13
apply plugin: 'com.android.application'
24
apply plugin: 'kotlin-android'
35
apply plugin: 'com.google.gms.google-services'
@@ -8,14 +10,14 @@ def keystoreProperties = new Properties()
810
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
911

1012
android {
11-
compileSdk 33
13+
compileSdkVersion 36
1214
defaultConfig {
1315
namespace 'com.softartdev.conwaysgameoflife'
1416
applicationId "com.softartdev.conwaysgameoflife"
15-
minSdkVersion 16
16-
targetSdk 33
17-
versionCode 211
18-
versionName '2.1.1'
17+
minSdkVersion 23
18+
targetSdkVersion 36
19+
versionCode 212
20+
versionName '2.1.2'
1921
resourceConfigurations += ['en']
2022
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
2123
}
@@ -50,29 +52,28 @@ android {
5052
sourceCompatibility JavaVersion.VERSION_1_8
5153
targetCompatibility JavaVersion.VERSION_1_8
5254
}
53-
kotlinOptions {
54-
jvmTarget = '1.8'
55-
}
55+
kotlin.compilerOptions.jvmTarget = JvmTarget.JVM_1_8
5656
testBuildType = project.hasProperty("testBuildType") ? project.property("testBuildType") : "debug"
5757
}
5858

5959
dependencies {
6060
implementation fileTree(dir: 'libs', include: ['*.jar'])
6161
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
62-
implementation 'androidx.core:core-ktx:1.10.0'
63-
implementation 'androidx.appcompat:appcompat:1.6.1'
62+
implementation 'androidx.core:core-ktx:1.17.0'
63+
implementation 'androidx.appcompat:appcompat:1.7.1'
64+
implementation 'com.google.android.material:material:1.13.0'
6465
implementation 'com.jakewharton.timber:timber:5.0.1'
65-
implementation platform('com.google.firebase:firebase-bom:31.5.0')
66+
implementation platform('com.google.firebase:firebase-bom:34.3.0')
6667
implementation 'com.google.firebase:firebase-analytics'
6768
implementation 'com.google.firebase:firebase-crashlytics'
68-
def leak_canary_version = '2.10'
69+
def leak_canary_version = '2.14'
6970
debugImplementation "com.squareup.leakcanary:leakcanary-android-process:$leak_canary_version"
7071
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leak_canary_version"
7172
implementation "com.squareup.leakcanary:plumber-android:$leak_canary_version"
7273
testImplementation 'junit:junit:4.13.2'
73-
androidTestImplementation 'androidx.test:core:1.5.0'
74-
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
75-
androidTestImplementation 'androidx.test:rules:1.5.0'
76-
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
74+
androidTestImplementation 'androidx.test:core:1.7.0'
75+
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
76+
androidTestImplementation 'androidx.test:rules:1.7.0'
77+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
7778
androidTestImplementation "com.squareup.leakcanary:leakcanary-android-instrumentation:$leak_canary_version"
7879
}

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
xmlns:tools="http://schemas.android.com/tools">
44

55
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
6+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
67
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
78

89
<uses-sdk tools:overrideLibrary="com.google.firebase.firebase_analytics, com.google.firebase.measurement, com.google.android.gms.measurement.api, com.google.android.gms.measurement.sdk, com.google.firebase.measurement_impl, com.google.android.gms.measurement.sdk.api, com.google.android.gms.measurement_base" />
@@ -26,6 +27,7 @@
2627
</activity>
2728
<service
2829
android:name=".MainService"
30+
android:foregroundServiceType="dataSync"
2931
android:exported="false" />
3032
</application>
3133

app/src/main/java/com/softartdev/conwaysgameoflife/MainService.kt

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
package com.softartdev.conwaysgameoflife
22

3-
import android.app.*
3+
import android.Manifest
4+
import android.app.Notification
5+
import android.app.NotificationChannel
6+
import android.app.NotificationManager
7+
import android.app.PendingIntent
8+
import android.app.Service
49
import android.content.Context
510
import android.content.Intent
6-
import android.os.*
11+
import android.content.pm.PackageManager
12+
import android.content.pm.ServiceInfo
13+
import android.os.Binder
14+
import android.os.Build
15+
import android.os.Handler
16+
import android.os.IBinder
17+
import android.os.Looper
718
import androidx.core.app.NotificationCompat
819
import androidx.core.app.NotificationManagerCompat
20+
import androidx.core.app.ServiceCompat
21+
import androidx.core.content.ContextCompat
922
import com.softartdev.conwaysgameoflife.model.CellState
1023
import com.softartdev.conwaysgameoflife.model.ICellState
1124
import com.softartdev.conwaysgameoflife.ui.MainActivity
@@ -38,8 +51,7 @@ class MainService : Service() {
3851
if (iCellState.isGoNextGeneration) {
3952
val processed = iCellState.processNextGeneration()
4053
if (serviceRunningInForeground) {
41-
val notification = createNotification(applicationContext, iCellState.countGeneration, notificationBuilder)
42-
notificationManager.notify(NOTIFICATION_ID, notification)
54+
updateNotificationSafely(applicationContext, iCellState, notificationBuilder, notificationManager)
4355
} else {
4456
uiHandler.post { uiRepaint?.invoke(processed) }
4557
}
@@ -62,22 +74,27 @@ class MainService : Service() {
6274

6375
override fun onBind(intent: Intent): IBinder {
6476
Timber.d("onBind")
65-
stopForeground(true)
77+
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
6678
serviceRunningInForeground = false
6779
return mainBinder
6880
}
6981

7082
override fun onRebind(intent: Intent?) {
7183
Timber.d("onRebind")
72-
stopForeground(true)
84+
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
7385
serviceRunningInForeground = false
7486
}
7587

7688
override fun onUnbind(intent: Intent?): Boolean {
7789
Timber.d("onUnbind")
7890
if (iCellState.isGoNextGeneration) {
7991
val notification = createNotification(applicationContext, iCellState.countGeneration, notificationBuilder)
80-
startForeground(NOTIFICATION_ID, notification)
92+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
93+
val serviceType: Int = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
94+
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, serviceType)
95+
} else {
96+
startForeground(NOTIFICATION_ID, notification)
97+
}
8198
}
8299
serviceRunningInForeground = true
83100
return true
@@ -115,10 +132,7 @@ class MainService : Service() {
115132
): NotificationCompat.Builder {
116133
val channelId = applicationContext.getString(R.string.notification_channel_id)
117134
val contentIntent = Intent(applicationContext, MainActivity::class.java)
118-
var flags = PendingIntent.FLAG_UPDATE_CURRENT
119-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
120-
flags = PendingIntent.FLAG_IMMUTABLE or flags
121-
}
135+
val flags: Int = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
122136
val contentPendingIntent = PendingIntent.getActivity(applicationContext, 0, contentIntent, flags)
123137
val cancelIntent = Intent(applicationContext, MainService::class.java)
124138
.putExtra(EXTRA_CANCEL_FROM_NOTIFICATION, true)
@@ -138,11 +152,25 @@ class MainService : Service() {
138152
applicationContext: Context,
139153
stepCount: Int,
140154
notificationBuilder: NotificationCompat.Builder
141-
): Notification {
142-
val contentText = applicationContext.getString(R.string.steps, stepCount)
143-
return notificationBuilder
144-
.setContentText(contentText)
145-
.build()
155+
): Notification = notificationBuilder
156+
.setContentText(applicationContext.getString(R.string.steps, stepCount))
157+
.build()
158+
159+
private fun updateNotificationSafely(
160+
applicationContext: Context,
161+
iCellState: ICellState,
162+
notificationBuilder: NotificationCompat.Builder,
163+
notificationManager: NotificationManagerCompat,
164+
) {
165+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
166+
&& ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.POST_NOTIFICATIONS)
167+
!= PackageManager.PERMISSION_GRANTED
168+
) {
169+
Timber.w("No notification permission, cannot update notification")
170+
return
171+
}
172+
val notification = createNotification(applicationContext, iCellState.countGeneration, notificationBuilder)
173+
notificationManager.notify(NOTIFICATION_ID, notification)
146174
}
147175
}
148176
}

0 commit comments

Comments
 (0)