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)