diff --git a/android/app/src/main/java/net/minetest/minetest/GameActivity.java b/android/app/src/main/java/net/minetest/minetest/GameActivity.java
index 22e861fb0..770c1c4f2 100644
--- a/android/app/src/main/java/net/minetest/minetest/GameActivity.java
+++ b/android/app/src/main/java/net/minetest/minetest/GameActivity.java
@@ -22,14 +22,19 @@ package net.minetest.minetest;
import org.libsdl.app.SDLActivity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
import android.content.Intent;
import android.content.ActivityNotFoundException;
import android.net.Uri;
-import android.os.Bundle;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
import android.text.InputType;
import android.util.Log;
import android.view.KeyEvent;
-import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
@@ -91,6 +96,9 @@ public class GameActivity extends SDLActivity {
saveSettings();
}
+ private NotificationManager mNotifyManager;
+ private boolean gameNotificationShown = false;
+
public void showTextInputDialog(String hint, String current, int editType) {
runOnUiThread(() -> showTextInputDialogUI(hint, current, editType));
}
@@ -263,4 +271,67 @@ public class GameActivity extends SDLActivity {
public boolean hasPhysicalKeyboard() {
return getContext().getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS;
}
+
+ // TODO: share code with UnzipService.createNotification
+ private void updateGameNotification() {
+ if (mNotifyManager == null) {
+ mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ if (!gameNotificationShown) {
+ mNotifyManager.cancel(MainActivity.NOTIFICATION_ID_GAME);
+ return;
+ }
+
+ Notification.Builder builder;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ builder = new Notification.Builder(this, MainActivity.NOTIFICATION_CHANNEL_ID);
+ } else {
+ builder = new Notification.Builder(this);
+ }
+
+ Intent notificationIntent = new Intent(this, GameActivity.class);
+ notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ int pendingIntentFlag = 0;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ pendingIntentFlag = PendingIntent.FLAG_MUTABLE;
+ }
+ PendingIntent intent = PendingIntent.getActivity(this, 0,
+ notificationIntent, pendingIntentFlag);
+
+ builder.setContentTitle(getString(R.string.game_notification_title))
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setContentIntent(intent)
+ .setOngoing(true);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // This avoids a stuck notification if the app is killed while
+ // in-game: (1) if the user closes the app from the "Recents" screen
+ // or (2) if the system kills the app while it is in background.
+ // onStop is called too early to remove the notification and
+ // onDestroy is often not called at all, so there's this hack instead.
+ builder.setTimeoutAfter(16000);
+
+ // Replace the notification just before it expires as long as the app is
+ // running (and we're still in-game).
+ final Handler handler = new Handler(Looper.getMainLooper());
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (gameNotificationShown) {
+ updateGameNotification();
+ }
+ }
+ }, 15000);
+ }
+
+ mNotifyManager.notify(MainActivity.NOTIFICATION_ID_GAME, builder.build());
+ }
+
+
+ public void setPlayingNowNotification(boolean show) {
+ gameNotificationShown = show;
+ updateGameNotification();
+ }
}
diff --git a/android/app/src/main/java/net/minetest/minetest/MainActivity.java b/android/app/src/main/java/net/minetest/minetest/MainActivity.java
index 7735e8b3d..a2edb2d7a 100644
--- a/android/app/src/main/java/net/minetest/minetest/MainActivity.java
+++ b/android/app/src/main/java/net/minetest/minetest/MainActivity.java
@@ -43,6 +43,8 @@ import static net.minetest.minetest.UnzipService.*;
public class MainActivity extends AppCompatActivity {
public static final String NOTIFICATION_CHANNEL_ID = "Minetest channel";
+ public static final int NOTIFICATION_ID_UNZIP = 1;
+ public static final int NOTIFICATION_ID_GAME = 2;
private final static int versionCode = BuildConfig.VERSION_CODE;
private static final String SETTINGS = "MinetestSettings";
diff --git a/android/app/src/main/java/net/minetest/minetest/UnzipService.java b/android/app/src/main/java/net/minetest/minetest/UnzipService.java
index 542b2a159..895ad66fc 100644
--- a/android/app/src/main/java/net/minetest/minetest/UnzipService.java
+++ b/android/app/src/main/java/net/minetest/minetest/UnzipService.java
@@ -51,7 +51,6 @@ public class UnzipService extends IntentService {
public static final int SUCCESS = -1;
public static final int FAILURE = -2;
public static final int INDETERMINATE = -3;
- private final int id = 1;
private NotificationManager mNotifyManager;
private boolean isSuccess = true;
private String failureMessage;
@@ -100,11 +99,14 @@ public class UnzipService extends IntentService {
}
}
+ // TODO: share code with GameActivity.updateGameNotification
@NonNull
private Notification.Builder createNotification() {
- Notification.Builder builder;
- if (mNotifyManager == null)
+ if (mNotifyManager == null) {
mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new Notification.Builder(this, MainActivity.NOTIFICATION_CHANNEL_ID);
} else {
@@ -128,7 +130,7 @@ public class UnzipService extends IntentService {
.setOngoing(true)
.setProgress(0, 0, true);
- mNotifyManager.notify(id, builder.build());
+ mNotifyManager.notify(MainActivity.NOTIFICATION_ID_UNZIP, builder.build());
return builder;
}
@@ -200,14 +202,14 @@ public class UnzipService extends IntentService {
} else {
notificationBuilder.setProgress(100, progress, false);
}
- mNotifyManager.notify(id, notificationBuilder.build());
+ mNotifyManager.notify(MainActivity.NOTIFICATION_ID_UNZIP, notificationBuilder.build());
}
}
@Override
public void onDestroy() {
super.onDestroy();
- mNotifyManager.cancel(id);
+ mNotifyManager.cancel(MainActivity.NOTIFICATION_ID_UNZIP);
publishProgress(null, R.string.loading, isSuccess ? SUCCESS : FAILURE);
}
}
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 98511e72c..8b8bc625b 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -6,6 +6,7 @@
Notifications from Luanti
Loading Luanti
Less than 1 minute…
+ Luanti is running
Done
No web browser found
diff --git a/src/client/game.cpp b/src/client/game.cpp
index 08be9c809..42b605685 100644
--- a/src/client/game.cpp
+++ b/src/client/game.cpp
@@ -1021,6 +1021,10 @@ void Game::run()
const bool initial_window_maximized = !g_settings->getBool("fullscreen") &&
g_settings->getBool("window_maximized");
+#ifdef __ANDROID__
+ porting::setPlayingNowNotification(true);
+#endif
+
auto framemarker = FrameMarker("Game::run()-frame").started();
while (m_rendering_engine->run()
@@ -1103,6 +1107,10 @@ void Game::run()
framemarker.end();
+#ifdef __ANDROID__
+ porting::setPlayingNowNotification(false);
+#endif
+
RenderingEngine::autosaveScreensizeAndCo(initial_screen_size, initial_window_maximized);
}
diff --git a/src/porting_android.cpp b/src/porting_android.cpp
index 620d9d224..bc44a4e32 100644
--- a/src/porting_android.cpp
+++ b/src/porting_android.cpp
@@ -187,6 +187,18 @@ void shareFileAndroid(const std::string &path)
jnienv->CallVoidMethod(activity, url_open, jurl);
}
+void setPlayingNowNotification(bool show)
+{
+ jmethodID play_notification = jnienv->GetMethodID(activityClass,
+ "setPlayingNowNotification", "(Z)V");
+
+ FATAL_ERROR_IF(play_notification == nullptr,
+ "porting::setPlayingNowNotification unable to find Java setPlayingNowNotification method");
+
+ jboolean jshow = show;
+ jnienv->CallVoidMethod(activity, play_notification, jshow);
+}
+
AndroidDialogType getLastInputDialogType()
{
jmethodID lastdialogtype = jnienv->GetMethodID(activityClass,
diff --git a/src/porting_android.h b/src/porting_android.h
index 86601f450..6f69c918e 100644
--- a/src/porting_android.h
+++ b/src/porting_android.h
@@ -36,6 +36,13 @@ void showComboBoxDialog(const std::string *optionList, s32 listSize, s32 selecte
*/
void shareFileAndroid(const std::string &path);
+/**
+ * Shows/hides notification that the game is running
+ *
+ * @param show whether to show/hide the notification
+ */
+void setPlayingNowNotification(bool show);
+
/*
* Types of Android input dialog:
* 1. Text input (single/multi-line text and password field)