Add FCM device registration and Gitea bug reporter

This commit is contained in:
2026-09-15 01:01:08 +02:00
parent b2af866cf0
commit b96b1890d2
33 changed files with 1232 additions and 18 deletions
+21 -9
View File
@@ -1,14 +1,15 @@
apply plugin: 'com.android.application'
android {
buildFeatures { buildConfig true }
namespace "de.pinguholic.concerts"
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "de.pinguholic.concerts"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
versionCode 2
versionName "1.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -17,6 +18,10 @@ android {
}
}
buildTypes {
debug {
versionNameSuffix '-debug'
resValue 'string', 'app_name', 'MetalCircle (Test)'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
@@ -36,6 +41,8 @@ dependencies {
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
// Also used by MetalCircleDevice to await token invalidation at account boundaries.
implementation "com.google.firebase:firebase-messaging:$firebaseMessagingVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -44,11 +51,16 @@ dependencies {
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
if (!file('google-services.json').exists()) {
throw new GradleException('Add the local Firebase google-services.json to the Android app module before building.')
}
apply plugin: 'com.google.gms.google-services'
gradle.taskGraph.whenReady { graph ->
if (graph.allTasks.any { it.name.toLowerCase().contains('release') }) {
def capacitorConfig = new groovy.json.JsonSlurper().parse(file('src/main/assets/capacitor.config.json'))
if (capacitorConfig.server?.cleartext || !capacitorConfig.server?.url?.startsWith('https://')) {
throw new GradleException('Release builds require an HTTPS server configuration. Run cap sync without local test mode.')
}
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Only debug APKs can load the localhost server reached through adb reverse. -->
<application android:usesCleartextTraffic="true" />
</manifest>
@@ -8,6 +8,9 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
<meta-data android:name="firebase_analytics_collection_enabled" android:value="false" />
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" />
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
@@ -38,4 +41,5 @@
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>
@@ -1,5 +1,12 @@
package de.pinguholic.concerts;
import com.getcapacitor.BridgeActivity;
import android.os.Bundle;
public class MainActivity extends BridgeActivity {}
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle state) {
registerPlugin(MetalCircleDevicePlugin.class);
super.onCreate(state);
}
}
@@ -0,0 +1,91 @@
package de.pinguholic.concerts;
import android.app.AlertDialog;
import android.app.NotificationManager;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.SharedPreferences;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.google.firebase.messaging.FirebaseMessaging;
import java.util.UUID;
@CapacitorPlugin(name = "MetalCircleDevice")
public class MetalCircleDevicePlugin extends Plugin {
private SharedPreferences preferences() {
return getContext().getSharedPreferences("metalcircle_push", Context.MODE_PRIVATE);
}
@PluginMethod
public void getInfo(PluginCall call) {
SharedPreferences prefs = preferences();
String id = prefs.getString("device_id", null);
if (id == null) {
id = UUID.randomUUID().toString();
prefs.edit().putString("device_id", id).commit();
}
JSObject result = new JSObject();
result.put("deviceId", id);
result.put("appVersion", BuildConfig.VERSION_NAME);
result.put("debug", BuildConfig.DEBUG);
result.put("binding", prefs.getString("binding", ""));
call.resolve(result);
}
@PluginMethod
public void prepareSession(PluginCall call) {
String binding = call.getString("binding", "");
if (!binding.isEmpty() && !binding.matches("[a-f0-9]{64}")) {
call.reject("Invalid session binding");
return;
}
if (binding.equals(preferences().getString("binding", ""))) {
call.resolve();
return;
}
FirebaseMessaging messaging = FirebaseMessaging.getInstance();
messaging.setAutoInitEnabled(false);
NotificationManager manager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancelAll();
messaging.deleteToken().addOnCompleteListener(task -> {
if (!task.isSuccessful()) {
call.reject("Push reset unavailable; please retry");
return;
}
preferences().edit().putString("binding", binding).commit();
call.resolve();
});
}
@PluginMethod
public void showDebugToken(PluginCall call) {
// A release APK can never display or copy a registration token through this method.
if (!BuildConfig.DEBUG || preferences().getString("binding", "").isEmpty()) {
call.reject("Debug token unavailable");
return;
}
boolean english = "en".equals(call.getString("language"));
FirebaseMessaging.getInstance().getToken().addOnCompleteListener(task -> {
if (!task.isSuccessful()) {
call.reject("Push token unavailable");
return;
}
getActivity().runOnUiThread(() -> {
new AlertDialog.Builder(getActivity())
.setTitle("FCM token — DEBUG ONLY")
.setMessage(task.getResult())
.setPositiveButton(english ? "Copy" : "Kopieren", (dialog, which) -> {
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(ClipData.newPlainText("FCM debug token", task.getResult()));
})
.setNegativeButton(english ? "Close" : "Schließen", null)
.show();
call.resolve();
});
});
}
}
@@ -0,0 +1,4 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M12,2a10,10 0,1 0,0 20a10,10 0,1 0,0 -20M12,5a7,7 0,1 1,0 14a7,7 0,1 1,0 -14M7,8h2l3,4 3,-4h2v8h-2v-5l-3,4 -3,-4v5H7z" />
</vector>
+3 -1
View File
@@ -2,6 +2,8 @@ ext {
minSdkVersion = 22
compileSdkVersion = 34
targetSdkVersion = 34
// Capacitor Push Notifications 6.x's supported default (minSdk 22).
firebaseMessagingVersion = '23.3.1'
androidxActivityVersion = '1.8.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'
@@ -13,4 +15,4 @@ ext {
androidxJunitVersion = '1.1.5'
androidxEspressoCoreVersion = '3.5.1'
cordovaAndroidVersion = '10.1.1'
}
}
+12 -2
View File
@@ -1,12 +1,22 @@
import type { CapacitorConfig } from '@capacitor/cli';
const serverUrl = process.env.METALCIRCLE_SERVER_URL || 'https://konzerte.pinguholic.de/';
const localTest = process.env.METALCIRCLE_LOCAL_TEST === '1';
if (!serverUrl.startsWith('https://') && !(localTest && /^http:\/\/(127\.0\.0\.1|localhost):\d+\/?$/.test(serverUrl))) {
throw new Error('MetalCircle requires HTTPS, or explicit localhost test mode.');
}
const config: CapacitorConfig = {
appId: 'de.pinguholic.concerts',
appName: 'MetalCircle',
webDir: 'www',
loggingBehavior: 'none',
plugins: {
PushNotifications: { presentationOptions: ['sound', 'alert'] }
},
server: {
url: 'https://konzerte.pinguholic.de/',
cleartext: false
url: serverUrl,
cleartext: localTest
}
};
+1 -1
View File
@@ -10,7 +10,7 @@
"dependencies": {
"@capacitor/android": "6.2.1",
"@capacitor/core": "6.2.1",
"@capacitor/push-notifications": "^6.0.5"
"@capacitor/push-notifications": "6.0.5"
},
"devDependencies": {
"@capacitor/cli": "6.2.1",
+1 -1
View File
@@ -11,7 +11,7 @@
"dependencies": {
"@capacitor/android": "6.2.1",
"@capacitor/core": "6.2.1",
"@capacitor/push-notifications": "^6.0.5"
"@capacitor/push-notifications": "6.0.5"
},
"devDependencies": {
"@capacitor/cli": "6.2.1",