android: migrate back to gameactivity, update to API 36, fix back navigation at network panel

This commit is contained in:
ardocrat
2025-11-21 01:34:51 +03:00
parent 646a7c5e04
commit ed2dc880aa
17 changed files with 370 additions and 193 deletions
+18 -11
View File
@@ -3,15 +3,19 @@ plugins {
}
android {
compileSdk 35
compileSdk 36
ndkVersion '26.0.10792818'
defaultConfig {
applicationId "mw.gri.android"
minSdk 24
targetSdk 35
versionCode 4
versionName "0.2.4"
targetSdk 36
versionCode 5
versionName "0.3.0"
}
lint {
checkReleaseBuilds false
}
def keystorePropertiesFile = rootProject.file("keystore.properties")
@@ -27,6 +31,7 @@ android {
storePassword keystoreProperties['storePassword']
}
}
}
buildTypes {
@@ -46,8 +51,8 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
namespace 'mw.gri.android'
}
@@ -55,9 +60,11 @@ android {
dependencies {
implementation 'androidx.appcompat:appcompat:1.7.0'
// Android Camera
implementation 'androidx.camera:camera-core:1.4.2'
implementation 'androidx.camera:camera-camera2:1.4.2'
implementation 'androidx.camera:camera-lifecycle:1.4.2'
}
// To use the Games Activity library
implementation "androidx.games:games-activity:2.0.2"
// Android Camera
implementation 'androidx.camera:camera-core:1.5.1'
implementation 'androidx.camera:camera-camera2:1.5.1'
implementation 'androidx.camera:camera-lifecycle:1.5.1'
}
+21 -19
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"
>
<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
@@ -15,31 +14,33 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application
android:hardwareAccelerated="true"
android:largeHeap="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="Grim"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Main">
android:hardwareAccelerated="true"
android:largeHeap="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="Grim"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Main"
android:enableOnBackInvokedCallback="false"
tools:ignore="UnusedAttribute">
<receiver android:name=".NotificationActionsReceiver"/>
<provider
android:name=".FileProvider"
android:authorities="mw.gri.android.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
android:name=".FileProvider"
android:authorities="mw.gri.android.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/paths" />
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/paths" />
</provider>
<activity
android:launchMode="singleTask"
android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:exported="true">
android:launchMode="singleTask"
android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -52,6 +53,7 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
<data android:pathPattern=".*\\.slatepack" />
</intent-filter>
<intent-filter android:scheme="http" tools:ignore="AppLinkUrlError">
@@ -16,7 +16,7 @@ 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;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@@ -31,7 +31,6 @@ public class BackgroundService extends Service {
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";
private final Runnable mUpdateSyncStatus = new Runnable() {
@SuppressLint("RestrictedApi")
@@ -82,18 +81,6 @@ public class BackgroundService extends Service {
.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.
@@ -247,9 +234,6 @@ public class BackgroundService extends Service {
// 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();
}
}
@@ -3,7 +3,6 @@ package mw.gri.android;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.NativeActivity;
import android.content.*;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
@@ -13,19 +12,26 @@ import android.os.Process;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
import android.util.Size;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.window.OnBackInvokedDispatcher;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.camera.core.*;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.core.graphics.Insets;
import androidx.core.os.BuildCompat;
import androidx.core.view.DisplayCutoutCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.androidgamesdk.GameActivity;
import com.google.androidgamesdk.gametextinput.State;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.*;
@@ -36,30 +42,27 @@ import java.util.concurrent.Executors;
import static android.content.ClipDescription.MIMETYPE_TEXT_HTML;
import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN;
public class MainActivity extends NativeActivity {
private static final int FILE_PICK_REQUEST = 1001;
private static final int FILE_PERMISSIONS_REQUEST = 1002;
public class MainActivity extends GameActivity {
public static String STOP_APP_ACTION = "STOP_APP";
private static final int NOTIFICATIONS_PERMISSION_CODE = 1;
private static final int CAMERA_PERMISSION_CODE = 2;
public static final String STOP_APP_ACTION = "STOP_APP_ACTION";
static {
System.loadLibrary("grim");
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@SuppressLint("RestrictedApi")
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Objects.equals(intent.getAction(), MainActivity.STOP_APP_ACTION)) {
public void onReceive(Context ctx, Intent i) {
if (Objects.equals(i.getAction(), STOP_APP_ACTION)) {
exit();
}
}
};
private final ImageAnalysis mImageAnalysis = new ImageAnalysis.Builder()
.setTargetResolution(new Size(640, 480))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
@@ -68,6 +71,9 @@ public class MainActivity extends NativeActivity {
private ExecutorService mCameraExecutor = null;
private boolean mUseBackCamera = true;
private ActivityResultLauncher<Intent> mFilePickResult = null;
private ActivityResultLauncher<Intent> mOpenFilePermissionsResult = null;
@SuppressLint("UnspecifiedRegisterReceiverFlag")
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -79,15 +85,14 @@ public class MainActivity extends NativeActivity {
}
// Clear cache on start.
String cacheDir = Objects.requireNonNull(getExternalCacheDir()).getPath();
if (savedInstanceState == null) {
Utils.deleteDirectoryContent(new File(cacheDir), false);
if (savedInstanceState == null && getExternalCacheDir() != null) {
Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false);
}
// Setup environment variables for native code.
try {
Os.setenv("HOME", Objects.requireNonNull(getExternalFilesDir("")).getPath(), true);
Os.setenv("XDG_CACHE_HOME", cacheDir, true);
Os.setenv("XDG_CACHE_HOME", Objects.requireNonNull(getExternalCacheDir()).getPath(), true);
Os.setenv("ARTI_FS_DISABLE_PERMISSION_CHECKS", "true", true);
} catch (ErrnoException e) {
throw new RuntimeException(e);
@@ -95,10 +100,56 @@ public class MainActivity extends NativeActivity {
super.onCreate(null);
ContextCompat.registerReceiver(this, mReceiver, new IntentFilter(STOP_APP_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED);
// Register receiver to finish activity from the BackgroundService.
ContextCompat.registerReceiver(this, mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED);
// Register associated file opening result.
mOpenFilePermissionsResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (Build.VERSION.SDK_INT >= 30) {
if (Environment.isExternalStorageManager()) {
onFile();
}
} else if (result.getResultCode() == RESULT_OK) {
onFile();
}
}
);
// Register file pick result.
mFilePickResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
int resultCode = result.getResultCode();
Intent data = result.getData();
if (resultCode == Activity.RESULT_OK) {
String path = "";
if (data != null && data.getData() != null) {
Uri uri = data.getData();
String name = "pick" + Utils.getFileExtension(uri, this);
File file = new File(getExternalCacheDir(), name);
try (InputStream is = getContentResolver().openInputStream(uri);
OutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[1024];
int length;
while (true) {
assert is != null;
if (!((length = is.read(buffer)) > 0)) break;
os.write(buffer, 0, length);
}
} catch (Exception e) {
Log.e("grim", e.toString());
}
path = file.getPath();
}
onFilePick(path);
} else {
onFilePick("");
}
});
// Listener for display insets (cutouts) to pass values into native code.
View content = findViewById(android.R.id.content).getRootView();
View content = getWindow().getDecorView().findViewById(android.R.id.content);
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
// Get display cutouts.
DisplayCutoutCompat dc = insets.getDisplayCutout();
@@ -127,7 +178,7 @@ public class MainActivity extends NativeActivity {
return insets;
});
content.post(() -> {
findViewById(android.R.id.content).post(() -> {
// Request notifications permissions if needed.
if (Build.VERSION.SDK_INT >= 33) {
String notificationsPermission = Manifest.permission.POST_NOTIFICATIONS;
@@ -149,43 +200,8 @@ public class MainActivity extends NativeActivity {
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case FILE_PICK_REQUEST:
if (Build.VERSION.SDK_INT >= 30) {
if (Environment.isExternalStorageManager()) {
onFile();
}
} else if (resultCode == RESULT_OK) {
onFile();
}
case FILE_PERMISSIONS_REQUEST:
if (resultCode == Activity.RESULT_OK) {
String path = "";
if (data != null) {
Uri uri = data.getData();
String name = "pick" + Utils.getFileExtension(uri, this);
File file = new File(getExternalCacheDir(), name);
try (InputStream is = getContentResolver().openInputStream(uri);
OutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
} catch (Exception e) {
e.printStackTrace();
}
path = file.getPath();
}
onFilePick(path);
} else {
onFilePick("");
}
}
}
// Pass display insets into native code.
public native void onDisplayInsets(int[] cutouts);
@Override
protected void onNewIntent(Intent intent) {
@@ -209,12 +225,13 @@ public class MainActivity extends NativeActivity {
if (Build.VERSION.SDK_INT >= 30) {
if (!Environment.isExternalStorageManager()) {
Intent i = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivityForResult(i, FILE_PERMISSIONS_REQUEST);
mOpenFilePermissionsResult.launch(i);
return;
}
}
try {
ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r");
assert parcelFile != null;
FileReader fileReader = new FileReader(parcelFile.getFileDescriptor());
BufferedReader reader = new BufferedReader(fileReader);
String line;
@@ -228,7 +245,7 @@ public class MainActivity extends NativeActivity {
// Provide file content into native code.
onData(buff.toString());
} catch (Exception e) {
e.printStackTrace();
Log.e("grim", e.toString());
}
}
@@ -251,20 +268,87 @@ public class MainActivity extends NativeActivity {
if (results.length != 0 && results[0] == PackageManager.PERMISSION_GRANTED) {
switch (requestCode) {
case NOTIFICATIONS_PERMISSION_CODE: {
// Start notification service.
BackgroundService.start(this);
return;
}
case CAMERA_PERMISSION_CODE: {
// Start camera.
startCamera();
}
}
}
}
// Implemented into native code to handle display insets change.
native void onDisplayInsets(int[] cutouts);
@Override
protected void onTextInputEventNative(long l, State state) {
super.onTextInputEventNative(l, state);
if (state.selectionEnd > state.composingRegionStart && state.composingRegionStart >= 0) {
String input = String.valueOf(state.text.charAt(state.composingRegionStart));
if (input.contains("\n")) {
onEnterInput();
} else {
onTextInput(input);
}
}
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
onBack();
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
onClearInput();
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
onEnterInput();
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_0) {
onTextInput("0");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_1) {
onTextInput("1");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_2) {
onTextInput("2");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_3) {
onTextInput("3");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_4) {
onTextInput("4");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_5) {
onTextInput("5");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_6) {
onTextInput("6");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_7) {
onTextInput("7");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_8) {
onTextInput("8");
return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_9) {
onTextInput("9");
return false;
}
}
return super.dispatchKeyEvent(event);
}
// Pass back navigation event into native code.
public native void onBack();
// Pass clear key event into native code.
public native void onClearInput();
// Pass enter key event into native code.
public native void onEnterInput();
// Pass last entered character from soft keyboard into native code.
public native void onTextInput(String character);
// Called from native code to exit app.
public void exit() {
@@ -273,6 +357,7 @@ public class MainActivity extends NativeActivity {
@Override
protected void onDestroy() {
unregisterReceiver(mBroadcastReceiver);
BackgroundService.stop(this);
// Kill process after 3 secs if app was terminated from recent apps to prevent app hang.
@@ -317,18 +402,6 @@ public class MainActivity extends NativeActivity {
return text;
}
// Called from native code to show keyboard.
public void showKeyboard() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(getWindow().getDecorView(), InputMethodManager.SHOW_IMPLICIT);
}
// Called from native code to hide keyboard.
public void hideKeyboard() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
}
// Called from native code to start camera.
public void startCamera() {
String notificationsPermission = Manifest.permission.CAMERA;
@@ -379,7 +452,7 @@ public class MainActivity extends NativeActivity {
}
// Apply declared configs to CameraX using the same lifecycle owner
mCameraProvider.unbindAll();
// mCameraProvider.bindToLifecycle(this, cameraSelector, mImageAnalysis);
mCameraProvider.bindToLifecycle(this, cameraSelector, mImageAnalysis);
}
// Called from native code to stop camera.
@@ -418,7 +491,7 @@ public class MainActivity extends NativeActivity {
Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.setType("*/*");
intent.setType("text/*");
startActivity(Intent.createChooser(intent, "Share data"));
}
@@ -431,10 +504,10 @@ public class MainActivity extends NativeActivity {
// Called from native code to pick the file.
public void pickFile() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.setType("text/*");
try {
startActivityForResult(Intent.createChooser(intent, "Pick file"), FILE_PICK_REQUEST);
} catch (ActivityNotFoundException ex) {
mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
} catch (android.content.ActivityNotFoundException ex) {
onFilePick("");
}
}
@@ -153,4 +153,4 @@ public class Utils {
String fileType = context.getContentResolver().getType(uri);
return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType);
}
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.10.0' apply false
id 'com.android.library' version '8.10.0' apply false
id 'com.android.application' version '8.13.1' apply false
id 'com.android.library' version '8.13.1' apply false
}
+1 -1
View File
@@ -1,6 +1,6 @@
#Mon May 02 15:39:12 BST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://code.gri.mw/ardocrat/gradle/releases/download/v8.11.1/gradle-8.11.1-bin.zip
distributionUrl=https\://code.gri.mw/ardocrat/gradle/releases/download/v8.13/gradle-8.13-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME