Skip to content

Commit

Permalink
[Android] Revert back to target API 21
Browse files Browse the repository at this point in the history
In #3418 I changed the behavior of BOINC to run always in the foreground mode.
This lead to the situation when notification was always shown, even when BOINC in not running, and thus making a lot of notification noise.
3 years ago I thought that this is good decision and a necessary evil.
Unfortunately, I was wrong.
My main motivation of this was to be able to be on track, and have our application in the Play Store.
But every new API increase the limitations from the Android side become stronger and stronger, and prevent us from doing what we want.
For example, in the Android versions starting from Oreo, it is not possible to start a service from the background.
And while we can leave with that fact, we received and issue with the notifications.
I thought, that if we will have BOINC always running on the foreground, it will be possible to keep it always running and prevent it from being killed by the system.
Unfortunately, this is not the case.
The system still kills the application, and the notification is still shown.
And this is not the only problem.
Other problem is that starting from API 29, we are not able to start executables downloaded from the server.
Since this is a core functionality, we are not able to target API 29 or higher.
And since we are not able to target API 29 or higher, we are not able to put BOINC in the Play Store.
And thus it makes no sense to target any API higher than 21 because it will just kill the functionality of the application.
So, I decided to revert back to the API 21, and make the application work as it was before.
But even in this case there is a chance that on the newer version of Android, some of the gard limitations will be applied, and it will prevent (again) BOINC from working.
But at least currently application is working, and we can add necessary features to it.
The only issue I see now is that the user will see a notification, that this application was developed for the older version of Android and thus might not work correctly,
but I believe we can live with that.

This fixes: #4189, #4218 and #4190

Signed-off-by: Vitalii Koshura <lestat.de.lionkur@gmail.com>
  • Loading branch information
AenBleidd committed Aug 19, 2023
1 parent 640be40 commit ee5f079
Show file tree
Hide file tree
Showing 11 changed files with 84 additions and 19 deletions.
9 changes: 6 additions & 3 deletions android/BOINC/.idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions android/BOINC/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,8 @@ android {

defaultConfig {
applicationId "edu.berkeley.boinc"
minSdkVersion 16
targetSdkVersion 28
minSdkVersion 19
targetSdkVersion 21
versionCode buildVersionCode().toInteger()
versionName buildVersionName()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,12 @@ List<ImageWrapper> getSlideshowForProject(in String url); // clientStatus.getS
////// app preference ////////////////////////////////////////////
void setAutostart(in boolean isAutoStart); // Monitor.getAppPrefs().setAutostart(boolean);
void setShowNotificationForNotices(in boolean isShow); // Monitor.getAppPrefs().setShowNotificationForNotices(boolean);
void setShowNotificationDuringSuspend(in boolean isShow); // Monitor.getAppPrefs().setShowNotificationDuringSuspend(boolean);
boolean getShowAdvanced(); // Monitor.getAppPrefs().getShowAdvanced();
boolean getIsRemote(); // Monitor.getAppPrefs().getIsRemote();
boolean getAutostart(); // Monitor.getAppPrefs().getAutostart();
boolean getShowNotificationForNotices(); // Monitor.getAppPrefs().getShowNotificationForNotices();
boolean getShowNotificationDuringSuspend(); // Monitor.getAppPrefs().getShowNotificationDuringSuspend();
int getLogLevel(); // Monitor.getAppPrefs().getLogLevel();
void setLogLevel(in int level); // Monitor.getAppPrefs().setLogLevel(int);
List<String> getLogCategories();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class AppPreferences @Inject constructor(val context: Context) {
var autostart = prefs.getBoolean("autostart", context.resources.getBoolean(R.bool.prefs_default_autostart))
var showNotificationForNotices = prefs.getBoolean("showNotifications",
context.resources.getBoolean(R.bool.prefs_default_notification_notices))
var showNotificationDuringSuspend = prefs.getBoolean("showNotificationsDuringSuspend",
context.resources.getBoolean(R.bool.prefs_default_notification_suspended))
var showAdvanced = prefs.getBoolean("showAdvanced", context.resources.getBoolean(R.bool.prefs_default_advanced))
var isRemote = prefs.getBoolean("remoteEnable", context.resources.getBoolean(R.bool.prefs_default_remote))
var logLevel = prefs.getInt("logLevel", context.resources.getInteger(R.integer.prefs_default_loglevel))
Expand All @@ -58,6 +60,7 @@ class AppPreferences @Inject constructor(val context: Context) {

Logging.logDebug(Logging.Category.SETTINGS,
"appPrefs read successful: autostart: [$autostart] showNotificationForNotices: [$showNotificationForNotices] " +
"showNotificationDuringSuspend: [$showNotificationDuringSuspend] " +
"showAdvanced: [$showAdvanced] logLevel: [$logLevel] powerSourceAc: [$powerSourceAc] powerSourceUsb: [$powerSourceUsb] " +
"powerSourceWireless: [$powerSourceWireless] stationaryDeviceMode: [$stationaryDeviceMode] suspendWhenScreenOn: [$suspendWhenScreenOn]")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service.STOP_FOREGROUND_REMOVE
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
Expand Down Expand Up @@ -77,9 +80,25 @@ class ClientNotification @Inject constructor(private val context: Context) {
return
}

// stop service foreground, if not active anymore
if(!active && foreground) {
setForegroundState(service, false);
}

// if not active, check preference whether to show notification during suspension
if(!active && !service.appPreferences.showNotificationDuringSuspend) {
// cancel notification if necessary
if(notificationShown) {
Logging.logInfo(Logging.Category.CLIENT, "ClientNotification: cancel suspension notification due to preference.");
nm.cancel(notificationId);
notificationShown = false;
}
return;
}

//check if active tasks have changed to force update
var activeTasksChanged = false
if (active && updatedStatus.computingStatus == ClientStatus.COMPUTING_STATUS_COMPUTING) {
if (updatedStatus.computingStatus == ClientStatus.COMPUTING_STATUS_COMPUTING) {
val activeTasks = updatedStatus.executingTasks
if (activeTasks.size != mOldActiveTasks.size) {
activeTasksChanged = true
Expand All @@ -99,9 +118,6 @@ class ClientNotification @Inject constructor(private val context: Context) {
if (activeTasksChanged) {
mOldActiveTasks = activeTasks
}
} else if (mOldActiveTasks.isNotEmpty()) {
mOldActiveTasks.clear()
activeTasksChanged = true
}

// update notification, only if it hasn't been shown before, or after change in status
Expand Down Expand Up @@ -140,10 +156,24 @@ class ClientNotification @Inject constructor(private val context: Context) {

// Notification must be built, before setting service to foreground!
@VisibleForTesting
internal fun setForegroundState(service: Monitor) {
service.startForeground(notificationId, n)
Logging.logInfo(Logging.Category.CLIENT, "ClientNotification.setForeground() start service as foreground.")
foreground = true
internal fun setForegroundState(service: Monitor, foregroundState: Boolean = true) {
if(foregroundState) {
service.startForeground(notificationId, n);
Logging.logInfo(Logging.Category.CLIENT, "ClientNotification.setForeground() start service as foreground.");
foreground = true;
}
else {
foreground = false;
if (VERSION.SDK_INT >= VERSION_CODES.N) {
service.stopForeground(STOP_FOREGROUND_REMOVE);
}
else {
@Suppress("DEPRECATION")
service.stopForeground(true);

Check failure on line 172 in android/BOINC/app/src/main/java/edu/berkeley/boinc/client/ClientNotification.kt

View workflow job for this annotation

GitHub Actions / Android Tests Report

edu.berkeley.boinc.client.ClientNotificationTest ► When active is false and foreground is true then expect foreground to be false

Failed test found in: TEST-edu.berkeley.boinc.client.ClientNotificationTest.xml Error: io.mockk.MockKException: no answer found for Monitor(#45).stopForeground(true) among the configured answers: (Monitor(#45).startForeground(any(), any())))
Raw output
io.mockk.MockKException: no answer found for Monitor(#45).stopForeground(true) among the configured answers: (Monitor(#45).startForeground(any(), any())))
	at io.mockk.impl.stub.MockKStub.defaultAnswer(MockKStub.kt:91)
	at io.mockk.impl.stub.MockKStub.answer(MockKStub.kt:42)
	at io.mockk.impl.recording.states.AnsweringState.call(AnsweringState.kt:16)
	at io.mockk.impl.recording.CommonCallRecorder.call(CommonCallRecorder.kt:53)
	at io.mockk.impl.stub.MockKStub.handleInvocation(MockKStub.kt:270)
	at io.mockk.impl.instantiation.JvmMockFactoryHelper$mockHandler$1.invocation(JvmMockFactoryHelper.kt:24)
	at app//io.mockk.proxy.jvm.advice.Interceptor.call(Interceptor.kt:21)
	at android.app.Service.stopForeground(Service.java)
	at edu.berkeley.boinc.client.ClientNotification.setForegroundState$app_debug(ClientNotification.kt:172)
	at edu.berkeley.boinc.client.ClientNotification.update(ClientNotification.kt:85)
	at edu.berkeley.boinc.client.ClientNotificationTest.When active is false and foreground is true then expect foreground to be false(ClientNotificationTest.kt:265)
	at java.base@17.0.8/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base@17.0.8/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base@17.0.8/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base@17.0.8/java.lang.reflect.Method.invoke(Method.java:568)
	at app//org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at app//org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at app//org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at app//org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at app//org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at app//org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at app//org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:589)
	at app//org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$2(SandboxTestRunner.java:290)
	at app//org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:99)
	at java.base@17.0.8/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base@17.0.8/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base@17.0.8/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base@17.0.8/java.lang.Thread.run(Thread.java:833)
}
notificationShown = false;
Logging.logInfo(Logging.Category.CLIENT, "ClientNotification.setForeground() stop service as foreground.");
}
}

@SuppressLint("InlinedApi")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,16 @@ class Monitor : LifecycleService() {
noticeNotification.cancelNotification()
}

@Throws(RemoteException::class)
override fun setShowNotificationDuringSuspend(isShow: Boolean) {
appPreferences.showNotificationDuringSuspend = isShow
}

@Throws(RemoteException::class)
override fun getShowNotificationDuringSuspend(): Boolean {
return appPreferences.showNotificationDuringSuspend
}

@Throws(RemoteException::class)
override fun runBenchmarks(): Boolean {
return clientInterface.runBenchmarks()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package edu.berkeley.boinc.client

import android.graphics.Bitmap
import android.os.IBinder
import android.os.RemoteException
import edu.berkeley.boinc.rpc.AccountIn
import edu.berkeley.boinc.rpc.AccountManager
import edu.berkeley.boinc.rpc.AccountOut
Expand Down Expand Up @@ -195,6 +196,14 @@ class MonitorAsync(monitor: IMonitor?) : IMonitor {
return monitor.cancelNoticeNotification()
}

override fun setShowNotificationDuringSuspend(isShow: Boolean) {
monitor.showNotificationDuringSuspend = isShow
}

override fun getShowNotificationDuringSuspend(): Boolean {
return monitor.showNotificationDuringSuspend
}

override fun getWelcomeStateFile(): Boolean {
return monitor.welcomeStateFile
}
Expand Down
7 changes: 4 additions & 3 deletions android/BOINC/app/src/main/res/values-en/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
This file is part of BOINC.
http://boinc.berkeley.edu
Copyright (C) 2022 University of California
BOINC is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
BOINC 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 Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with BOINC. If not, see <http://www.gnu.org/licenses/>.
-->
Expand Down Expand Up @@ -151,6 +151,7 @@
<string name="prefs_remote_header">Enable Remote Control</string>
<string name="prefs_remote_summary">When changed, the BOINC client will be relaunched.</string>
<string name="prefs_show_notification_notices_header">Show notification for new notices</string>
<string name="prefs_show_notification_suspended_header">Show notification when suspended</string>
<string name="prefs_cpu_number_cpus_header">Used CPU cores</string>
<string name="prefs_cpu_number_cpus_description">Limits the number of CPU cores BOINC uses for computation.</string>
<string name="prefs_cpu_other_load_suspension_header">Pause at CPU usage above (%)</string>
Expand Down
1 change: 1 addition & 0 deletions android/BOINC/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
<string name="prefs_remote_header">Enable Remote Control</string>
<string name="prefs_remote_summary">When changed, the BOINC client will be relaunched.</string>
<string name="prefs_show_notification_notices_header">Show notification for new notices</string>
<string name="prefs_show_notification_suspended_header">Show notification when suspended</string>
<string name="prefs_cpu_number_cpus_header">Used CPU cores</string>
<string name="prefs_cpu_number_cpus_description">Limits the number of CPU cores BOINC uses for computation.</string>
<string name="prefs_cpu_other_load_suspension_header">Pause at CPU usage above (%)</string>
Expand Down
6 changes: 6 additions & 0 deletions android/BOINC/app/src/main/res/xml/root_preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
app:key="showNotifications"
app:title="@string/prefs_show_notification_notices_header" />

<CheckBoxPreference
app:defaultValue="@bool/prefs_default_notification_suspended"
app:iconSpaceReserved="false"
app:key="showErrorNotifications"
app:title="@string/prefs_show_notification_suspended_header" />

<CheckBoxPreference
app:defaultValue="@bool/prefs_default_advanced"
app:iconSpaceReserved="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ class ClientNotificationTest {
}

@Test
fun `When active is false then expect foreground to be true`() {
fun `When active is false then expect foreground to be false`() {
val monitor = mockkClass(Monitor::class)
justRun { monitor.startForeground(any(), any()) }
val clientStatus = ClientStatus(ApplicationProvider.getApplicationContext(), null, null)
Expand All @@ -255,7 +255,7 @@ class ClientNotificationTest {
}

@Test
fun `When active is false and foreground is true then expect foreground to be true`() {
fun `When active is false and foreground is true then expect foreground to be false`() {
val monitor = mockkClass(Monitor::class)
justRun { monitor.startForeground(any(), any()) }
val clientStatus = ClientStatus(ApplicationProvider.getApplicationContext(), null, null)
Expand All @@ -264,7 +264,7 @@ class ClientNotificationTest {
clientNotification.foreground = true
clientNotification.update(clientStatus, monitor, false)

Assert.assertTrue(clientNotification.foreground)
Assert.assertFalse(clientNotification.foreground)
}

@Test
Expand Down

0 comments on commit ee5f079

Please sign in to comment.