android: node control and exit from the notification

This commit is contained in:
ardocrat 2024-07-09 00:36:44 +03:00
parent afe204c046
commit ab9117cceb
19 changed files with 325 additions and 19 deletions

View file

@ -3,12 +3,14 @@
>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.CAMERA"/>
<application
android:hardwareAccelerated="true"
android:largeHeap="true"
@ -19,6 +21,8 @@
android:supportsRtl="true"
android:theme="@style/Theme.Main">
<receiver android:name=".NotificationActionsReceiver"/>
<provider
android:name=".FileProvider"
android:authorities="mw.gri.android.fileprovider"

View file

@ -1,16 +1,20 @@
package mw.gri.android;
import android.annotation.SuppressLint;
import android.app.*;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.*;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import java.util.List;
public class BackgroundService extends Service {
import static android.app.Notification.EXTRA_NOTIFICATION_ID;
public class BackgroundService extends Service {
private static final String TAG = BackgroundService.class.getSimpleName();
private PowerManager.WakeLock mWakeLock;
@ -18,12 +22,38 @@ public class BackgroundService extends Service {
private final Handler mHandler = new Handler(Looper.getMainLooper());
private boolean mStopped = false;
private static final int SYNC_STATUS_NOTIFICATION_ID = 1;
private static final int NOTIFICATION_ID = 1;
private NotificationCompat.Builder mNotificationBuilder;
private String mNotificationContentText = "";
private Boolean mCanStart = null;
private Boolean mCanStop = null;
public static final String ACTION_START_NODE = "start_node";
public static final String ACTION_STOP_NODE = "stop_node";
public static final String ACTION_EXIT = "exit";
public static final String ACTION_REFRESH = "refresh";
public static final String ACTION_STOP = "stop";
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@SuppressLint("RestrictedApi")
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ACTION_STOP)) {
mStopped = true;
// Remove actions buttons.
mNotificationBuilder.mActions.clear();
NotificationManager manager = getSystemService(NotificationManager.class);
manager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
} else {
mHandler.removeCallbacks(mUpdateSyncStatus);
mHandler.post(mUpdateSyncStatus);
}
}
};
private final Runnable mUpdateSyncStatus = new Runnable() {
@SuppressLint("RestrictedApi")
@Override
public void run() {
if (mStopped) {
@ -31,11 +61,11 @@ public class BackgroundService extends Service {
}
// Update sync status at notification.
String syncStatusText = getSyncStatusText();
if (!mNotificationContentText.equals(syncStatusText)) {
boolean textChanged = !mNotificationContentText.equals(syncStatusText);
if (textChanged) {
mNotificationContentText = syncStatusText;
mNotificationBuilder.setContentText(mNotificationContentText);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.notify(SYNC_STATUS_NOTIFICATION_ID, mNotificationBuilder.build());
mNotificationBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(mNotificationContentText));
}
// Send broadcast to MainActivity if exit from the app is needed after node stop.
@ -44,13 +74,60 @@ public class BackgroundService extends Service {
mStopped = true;
}
// Repeat notification update if service is not stopped.
if (!mStopped) {
mHandler.postDelayed(this, 500);
boolean canStart = canStartNode();
boolean canStop = canStopNode();
boolean buttonsChanged = mCanStart == null || mCanStop == null ||
mCanStart != canStart || mCanStop != canStop;
mCanStart = canStart;
mCanStop = canStop;
if (buttonsChanged) {
mNotificationBuilder.mActions.clear();
// Set up buttons to start/stop node.
Intent startStopIntent = new Intent(BackgroundService.this, NotificationActionsReceiver.class);
if (Build.VERSION.SDK_INT > 25) {
startStopIntent.putExtra(EXTRA_NOTIFICATION_ID, NOTIFICATION_ID);
}
if (canStart) {
startStopIntent.setAction(ACTION_START_NODE);
PendingIntent i = PendingIntent
.getBroadcast(BackgroundService.this, 1, startStopIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
mNotificationBuilder.addAction(R.drawable.ic_start, getStartText(), i);
} else if (canStop) {
startStopIntent.setAction(ACTION_STOP_NODE);
PendingIntent i = PendingIntent
.getBroadcast(BackgroundService.this, 1, startStopIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
mNotificationBuilder.addAction(R.drawable.ic_stop, getStopText(), i);
}
// Set up a button to exit from the app.
if (canStart || canStop) {
Intent exitIntent = new Intent(BackgroundService.this, NotificationActionsReceiver.class);
if (Build.VERSION.SDK_INT > 25) {
exitIntent.putExtra(EXTRA_NOTIFICATION_ID, NOTIFICATION_ID);
}
exitIntent.setAction(ACTION_EXIT);
PendingIntent i = PendingIntent
.getBroadcast(BackgroundService.this, 1, exitIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
mNotificationBuilder.addAction(R.drawable.ic_close, getExitText(), i);
}
}
// Update notification.
if (textChanged || buttonsChanged) {
NotificationManager manager = getSystemService(NotificationManager.class);
manager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
}
// Repeat notification update.
mHandler.postDelayed(this, 1000);
}
}
};
@SuppressLint({"WakelockTimeout", "UnspecifiedRegisterReceiverFlag"})
@Override
public void onCreate() {
if (mStopped) {
@ -78,15 +155,20 @@ public class BackgroundService extends Service {
mNotificationBuilder = new NotificationCompat.Builder(this, TAG)
.setContentTitle(this.getSyncTitle())
.setContentText(this.getSyncStatusText())
.setStyle(new NotificationCompat.BigTextStyle().bigText(this.getSyncStatusText()))
.setSmallIcon(R.drawable.ic_stat_name)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentIntent(pendingIntent);
Notification notification = mNotificationBuilder.build();
// Start service at foreground state to prevent killing by system.
startForeground(SYNC_STATUS_NOTIFICATION_ID, notification);
startForeground(NOTIFICATION_ID, notification);
// Update sync status at notification.
mHandler.post(mUpdateSyncStatus);
// Register receiver to refresh notifications by intent.
registerReceiver(mReceiver, new IntentFilter(ACTION_REFRESH));
}
@Override
@ -117,22 +199,26 @@ public class BackgroundService extends Service {
// Stop updating the notification.
mHandler.removeCallbacks(mUpdateSyncStatus);
unregisterReceiver(mReceiver);
clearNotification();
// Remove service from foreground state.
stopForeground(Service.STOP_FOREGROUND_REMOVE);
// Remove notification.
// Release wake lock to allow CPU to sleep at background.
if (mWakeLock != null && mWakeLock.isHeld()) {
mWakeLock.release();
mWakeLock = null;
}
}
// Remove notification.
private void clearNotification() {
NotificationManager notificationManager = getSystemService(NotificationManager.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.deleteNotificationChannel(TAG);
}
notificationManager.cancel(SYNC_STATUS_NOTIFICATION_ID);
// Release wake lock to allow CPU to sleep at background.
if (mWakeLock.isHeld()) {
mWakeLock.release();
mWakeLock = null;
}
notificationManager.cancel(NOTIFICATION_ID);
}
// Start the service.
@ -165,10 +251,24 @@ public class BackgroundService extends Service {
return false;
}
// Get sync status text for notification from native code.
// Get sync status text for notification.
private native String getSyncStatusText();
// Get sync title text for notification from native code.
// Get sync title text for notification.
private native String getSyncTitle();
// Check if app from the app is needed after node stop from native code.
// Get start text for notification.
private native String getStartText();
// Get stop text for notification.
private native String getStopText();
// Check if start node is possible.
private native boolean canStartNode();
// Check if stop node is possible.
private native boolean canStopNode();
// Get exit text for notification.
private native String getExitText();
// Check if app from the app is needed after node stop.
private native boolean exitAppAfterNodeStop();
}

View file

@ -0,0 +1,35 @@
package mw.gri.android;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class NotificationActionsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent i) {
String a = i.getAction();
if (a.equals(BackgroundService.ACTION_START_NODE)) {
startNode();
context.sendBroadcast(new Intent(BackgroundService.ACTION_REFRESH));
} else if (a.equals(BackgroundService.ACTION_STOP_NODE)) {
stopNode();
context.sendBroadcast(new Intent(BackgroundService.ACTION_REFRESH));
} else {
if (isNodeRunning()) {
stopNodeToExit();
context.sendBroadcast(new Intent(BackgroundService.ACTION_REFRESH));
} else {
context.sendBroadcast(new Intent(MainActivity.STOP_APP_ACTION));
}
}
}
// Start integrated node.
native void startNode();
// Stop integrated node.
native void stopNode();
// Stop node and exit from the app.
native void stopNodeToExit();
// Check if node is running.
native boolean isNodeRunning();
}

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</group>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="@android:color/white"
android:pathData="M8,5v14l11,-7z"/>
</group>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="@android:color/white"
android:pathData="M6,6h12v12H6z"/>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

View file

@ -658,6 +658,128 @@ pub extern "C" fn Java_mw_gri_android_BackgroundService_getSyncTitle(
return j_text.unwrap().into_raw();
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Get start text for Android notification in Java string format.
pub extern "C" fn Java_mw_gri_android_BackgroundService_getStartText(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) -> jni::sys::jstring {
let j_text = _env.new_string(t!("network_settings.enable"));
return j_text.unwrap().into_raw();
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Get stop text for Android notification in Java string format.
pub extern "C" fn Java_mw_gri_android_BackgroundService_getStopText(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) -> jni::sys::jstring {
let j_text = _env.new_string(t!("network_settings.disable"));
return j_text.unwrap().into_raw();
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Get exit text for Android notification in Java string format.
pub extern "C" fn Java_mw_gri_android_BackgroundService_getExitText(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) -> jni::sys::jstring {
let j_text = _env.new_string(t!("modal_exit.exit"));
return j_text.unwrap().into_raw();
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Check if node launch is possible.
pub extern "C" fn Java_mw_gri_android_BackgroundService_canStartNode(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) -> jni::sys::jboolean {
let loading = Node::is_stopping() || Node::is_restarting() || Node::is_starting();
return (!loading && !Node::is_running()) as jni::sys::jboolean;
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Check if node stop is possible.
pub extern "C" fn Java_mw_gri_android_BackgroundService_canStopNode(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) -> jni::sys::jboolean {
let loading = Node::is_stopping() || Node::is_restarting() || Node::is_starting();
return (!loading && Node::is_running()) as jni::sys::jboolean;
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Check if node stop is possible.
pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_isNodeRunning(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) -> jni::sys::jboolean {
return Node::is_running() as jni::sys::jboolean;
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Start node from Android Java code.
pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_startNode(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) {
Node::start();
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Stop node from Android Java code.
pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_stopNode(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) {
Node::stop(false);
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[no_mangle]
/// Stop node from Android Java code.
pub extern "C" fn Java_mw_gri_android_NotificationActionsReceiver_stopNodeToExit(
_env: jni::JNIEnv,
_class: jni::objects::JObject,
_activity: jni::objects::JObject,
) {
Node::stop(true);
}
#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]