diff --git a/android/motionUI/.idea/kotlinc.xml b/android/motionUI/.idea/kotlinc.xml new file mode 100644 index 00000000..0fc31131 --- /dev/null +++ b/android/motionUI/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/android/motionUI/app/build.gradle b/android/motionUI/app/build.gradle index 2ef9bc5b..2dd566f9 100644 --- a/android/motionUI/app/build.gradle +++ b/android/motionUI/app/build.gradle @@ -3,6 +3,8 @@ import java.io.FileInputStream plugins { id 'com.android.application' + id 'kotlin-android' + id 'kotlin-parcelize' } // Ici on importe les secrets à partir d'un fichier dédié (keystore.properties) @@ -11,6 +13,9 @@ def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) android { + namespace 'app.motionui.android' + compileSdk 34 + signingConfigs { release { storeFile file(keystoreProperties['storeFile']) @@ -18,9 +23,7 @@ android { keyPassword keystoreProperties['keyPassword'] keyAlias keystoreProperties['keyAlias'] } - } - namespace 'app.motionui.android' - compileSdk 34 + } defaultConfig { applicationId "app.motionui.android" @@ -43,15 +46,25 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + viewBinding true + } } dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.security:security-crypto:1.1.0-alpha03' implementation "androidx.core:core-splashscreen:1.0.0" + implementation 'com.squareup.okhttp3:okhttp:4.9.3' + implementation 'androidx.core:core-ktx:1.10.1' + implementation 'androidx.webkit:webkit:1.6.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' -} +} \ No newline at end of file diff --git a/android/motionUI/app/src/main/java/app/motionui/android/MainActivity.java b/android/motionUI/app/src/main/java/app/motionui/android/MainActivity.java index c254ee79..b9d9f794 100644 --- a/android/motionUI/app/src/main/java/app/motionui/android/MainActivity.java +++ b/android/motionUI/app/src/main/java/app/motionui/android/MainActivity.java @@ -22,6 +22,19 @@ import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.annotation.NonNull; +// WebSocket & Notifications +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import androidx.webkit.WebViewAssetLoader; +import okio.ByteString; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; /** * MainActivity @@ -33,6 +46,11 @@ public class MainActivity extends AppCompatActivity { private String url; // private Integer authTry = 0; + // Notification channel + private static final String CHANNEL_ID = "motionUI_notifications"; + private static final int NOTIFICATION_ID = 1; + private WebSocket webSocket; + /** * JavaScript interface to retrieve username and password from the login form when the user submits it and save them in SharedPreferences */ @@ -244,6 +262,9 @@ public void onDownloadStart(String url, String userAgent, String contentDisposit downloadManager.enqueue(request); } }); + + createNotificationChannel(); + connectWebSocket(); /** * Load motionUI URL in the WebView @@ -251,6 +272,88 @@ public void onDownloadStart(String url, String userAgent, String contentDisposit webView.loadUrl(url); } + + + + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = "MotionUI Notifications"; + String description = "Channel for MotionUI notifications"; + int importance = NotificationManager.IMPORTANCE_DEFAULT; + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); + channel.setDescription(description); + + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } + + private void showNotification(String title, String message) { + Intent intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(pendingIntent) + .setAutoCancel(true); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + notificationManager.notify(NOTIFICATION_ID, builder.build()); + } + + private void connectWebSocket() { + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder().url("ws://yourserver.com/websocket").build(); + webSocket = client.newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, okhttp3.Response response) { + // WebSocket connection opened + // TODO debug + showNotification("WS Opened", text); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + // Handle text message + showNotification("New Message", text); + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + // Handle binary message + showNotification("New Message", bytes.hex()); + } + + @Override + public void onClosing(WebSocket webSocket, int code, String reason) { + webSocket.close(1000, null); + // WebSocket connection closing + // TODO debug + showNotification("WS Closed", text); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) { + // WebSocket connection failed + t.printStackTrace(); + // TODO debug + showNotification("WS Failed", text); + } + }); + + client.dispatcher().executorService().shutdown(); + } + + + + + + /** * Prevent the back-button from closing the app */ diff --git a/android/motionUI/app/src/main/res/drawable/ic_notification.png b/android/motionUI/app/src/main/res/drawable/ic_notification.png new file mode 100644 index 00000000..2a545d5d Binary files /dev/null and b/android/motionUI/app/src/main/res/drawable/ic_notification.png differ diff --git a/android/motionUI/build.gradle b/android/motionUI/build.gradle index cfed4963..0d0ede76 100644 --- a/android/motionUI/build.gradle +++ b/android/motionUI/build.gradle @@ -1,4 +1,17 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext.kotlin_version = '1.8.10' + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:7.0.4" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + plugins { -id 'com.android.application' version '8.2.1' apply false + id 'com.android.application' version '8.2.1' apply false + id 'org.jetbrains.kotlin.android' version '1.8.10' apply false } \ No newline at end of file diff --git a/www/controllers/ajax/general.php b/www/controllers/ajax/general.php index 72af2870..7ce67f85 100644 --- a/www/controllers/ajax/general.php +++ b/www/controllers/ajax/general.php @@ -30,21 +30,6 @@ response(HTTP_OK, $content); } -/** - * Get all layout containers state - */ -// if ($action == "getContainerState") { -// $mycontainerState = new \Controllers\Layout\ContainerState(); - -// try { -// $result = $mycontainerState->get(); -// } catch (\Exception $e) { -// response(HTTP_BAD_REQUEST, $e->getMessage()); -// } - -// response(HTTP_OK, json_encode($result)); -// } - /** * Return specified table content */