diff --git a/.gitignore b/.gitignore
index 4ae15773..1d8cf02e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,4 @@ todo
WIP
android/.idea/*
keystore.properties
-app-release.aab
\ No newline at end of file
+app-release.aab
diff --git a/android/motionUI/app/build.gradle b/android/motionUI/app/build.gradle
index c261709b..e579f1f0 100644
--- a/android/motionUI/app/build.gradle
+++ b/android/motionUI/app/build.gradle
@@ -1,8 +1,24 @@
+import java.util.Properties
+import java.io.FileInputStream
+
plugins {
id 'com.android.application'
}
+// Ici on importe les secrets à partir d'un fichier dédié (keystore.properties)
+def keystorePropertiesFile = rootProject.file('keystore.properties')
+def keystoreProperties = new Properties()
+keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+
android {
+ signingConfigs {
+ release {
+ storeFile file(keystoreProperties['storeFile'])
+ storePassword keystoreProperties['storePassword']
+ keyPassword keystoreProperties['keyPassword']
+ keyAlias keystoreProperties['keyAlias']
+ }
+ }
namespace 'com.example.motionui'
compileSdk 34
@@ -20,6 +36,7 @@ android {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ signingConfig signingConfigs.release
}
}
compileOptions {
@@ -29,11 +46,11 @@ android {
}
dependencies {
-
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"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
diff --git a/android/motionUI/app/src/main/AndroidManifest.xml b/android/motionUI/app/src/main/AndroidManifest.xml
index fd10f1b7..73745859 100644
--- a/android/motionUI/app/src/main/AndroidManifest.xml
+++ b/android/motionUI/app/src/main/AndroidManifest.xml
@@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools">
+
+
+
+
+
+
+
-
+
@@ -27,8 +36,7 @@
-
+ android:exported="true">
\ No newline at end of file
diff --git a/android/motionUI/app/src/main/java/com/example/motionui/MainActivity.java b/android/motionUI/app/src/main/java/com/example/motionui/MainActivity.java
index 6bdbdb7f..221ba144 100644
--- a/android/motionUI/app/src/main/java/com/example/motionui/MainActivity.java
+++ b/android/motionUI/app/src/main/java/com/example/motionui/MainActivity.java
@@ -1,10 +1,15 @@
package com.example.motionui;
import androidx.appcompat.app.AppCompatActivity;
+import android.widget.Toast;
+import android.app.DownloadManager;
import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
+import android.webkit.DownloadListener;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
@@ -12,13 +17,19 @@
import android.webkit.WebViewClient;
import android.os.Build;
import android.webkit.CookieManager;
+import android.Manifest;
+import android.content.pm.PackageManager;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.annotation.NonNull;
+
/**
* MainActivity
* This is the main activity of the app (motionUI main page)
*/
public class MainActivity extends AppCompatActivity {
-
+ private static final int STORAGE_PERMISSION_REQUEST_CODE = 1001;
private WebView webView;
private String url;
// private Integer authTry = 0;
@@ -59,6 +70,12 @@ protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
+ /**
+ * Check that the app has the necessary permissions to access the storage
+ * This is required to download files (videos and images from motion)
+ */
+ checkAndRequestStoragePermissions();
+
/**
* Retrieve motionUI URL from Startup activity
*/
@@ -181,6 +198,54 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request
}
});
+ /**
+ * Permit download of video files
+ */
+ webView.setDownloadListener(new DownloadListener() {
+ @Override
+ public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) {
+ String fileName = "";
+
+ /**
+ * Try to extract the file name from the URL
+ * The URL should contain the file name as a query parameter, like this: http://example.com/media.php?id=xxxxx&filename=myfile.mp4
+ */
+ try {
+ Uri uri = Uri.parse(url);
+ fileName = uri.getQueryParameter("filename");
+ } catch (Exception e) {
+ // Notify the user that the download failed because the file name could not be extracted
+ Toast.makeText(getApplicationContext(), "Download failed: error while extracting file name from URL", Toast.LENGTH_LONG).show();
+ }
+
+ Log.d("Download", "Downloading file: " + fileName);
+
+ // Check that the file name is not empty
+ if (fileName == null || fileName.isEmpty()) {
+ // Notify the user that the download failed because the file name could not be extracted
+ Toast.makeText(getApplicationContext(), "Download failed: could not extract file name from URL", Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ /**
+ * Start the download
+ */
+ DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
+ request.setMimeType(mimeType);
+ String cookies = CookieManager.getInstance().getCookie(url);
+ request.addRequestHeader("cookie", cookies);
+ request.addRequestHeader("User-Agent", userAgent);
+ request.setDescription("Downloading file...");
+ request.setTitle(fileName);
+ request.allowScanningByMediaScanner();
+ request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+ request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
+
+ DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
+ downloadManager.enqueue(request);
+ }
+ });
+
/**
* Load motionUI URL in the WebView
*/
@@ -200,4 +265,60 @@ public boolean onKeyDown(int keyCode, KeyEvent event) {
return super.onKeyDown(keyCode, event);
}
+
+ /**
+ * Check that the app has the necessary permissions to access the storage
+ */
+ private void checkAndRequestStoragePermissions() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // Android 13 and higher: request specific media permissions
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED ||
+ ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_VIDEO) != PackageManager.PERMISSION_GRANTED ||
+ ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_AUDIO) != PackageManager.PERMISSION_GRANTED) {
+
+ ActivityCompat.requestPermissions(this,
+ new String[]{
+ Manifest.permission.READ_MEDIA_IMAGES,
+ Manifest.permission.READ_MEDIA_VIDEO,
+ Manifest.permission.READ_MEDIA_AUDIO
+ },
+ STORAGE_PERMISSION_REQUEST_CODE);
+ }
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // Android 10 to Android 12: use READ_EXTERNAL_STORAGE
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
+ STORAGE_PERMISSION_REQUEST_CODE);
+ }
+ } else {
+ // Android 9 and earlier: WRITE_EXTERNAL_STORAGE is required
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ STORAGE_PERMISSION_REQUEST_CODE);
+ }
+ }
+ }
+
+ /**
+ * This method is called after the user's response to the permissions
+ */
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == STORAGE_PERMISSION_REQUEST_CODE) {
+ boolean allPermissionsGranted = true;
+ for (int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ allPermissionsGranted = false;
+ break;
+ }
+ }
+ if (!allPermissionsGranted) {
+ // One or more permissions were denied
+ Toast.makeText(this, "Write permission is denied. You will not be able to download files.", Toast.LENGTH_LONG).show();
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/android/motionUI/app/src/main/java/com/example/motionui/SplashScreen.java b/android/motionUI/app/src/main/java/com/example/motionui/SplashScreen.java
new file mode 100644
index 00000000..16fb8a2e
--- /dev/null
+++ b/android/motionUI/app/src/main/java/com/example/motionui/SplashScreen.java
@@ -0,0 +1,26 @@
+package com.example.motionui;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+
+public class SplashScreen extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_splash_screen);
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ Intent intent = new Intent(SplashScreen.this, Startup.class);
+ startActivity(intent);
+ finish();
+
+ }
+ }, 500);
+ }
+}
\ No newline at end of file
diff --git a/android/motionUI/app/src/main/java/com/example/motionui/Startup.java b/android/motionUI/app/src/main/java/com/example/motionui/Startup.java
index 4bb0cf7c..1fd56a79 100644
--- a/android/motionUI/app/src/main/java/com/example/motionui/Startup.java
+++ b/android/motionUI/app/src/main/java/com/example/motionui/Startup.java
@@ -1,19 +1,13 @@
package com.example.motionui;
import androidx.appcompat.app.AppCompatActivity;
-
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
-
import android.util.Log;
-// Encrypted SharedPreferences
-// import androidx.security.crypto.EncryptedSharedPreferences;
-// import androidx.security.crypto.MasterKeys;
-
/**
* Startup activity
* This is the first activity that is opened when the app is launched
@@ -21,7 +15,6 @@
* Then it redirects to the MainActivity (motionUI main page)
*/
public class Startup extends AppCompatActivity {
-
private Button button;
private EditText editText;
private String url;
diff --git a/android/motionUI/app/src/main/res/drawable/motion.png b/android/motionUI/app/src/main/res/drawable/motion.png
new file mode 100644
index 00000000..2a545d5d
Binary files /dev/null and b/android/motionUI/app/src/main/res/drawable/motion.png differ
diff --git a/android/motionUI/app/src/main/res/layout/activity_splash_screen.xml b/android/motionUI/app/src/main/res/layout/activity_splash_screen.xml
new file mode 100644
index 00000000..14b80c34
--- /dev/null
+++ b/android/motionUI/app/src/main/res/layout/activity_splash_screen.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/motionUI/app/src/main/res/layout/activity_startup.xml b/android/motionUI/app/src/main/res/layout/activity_startup.xml
index de9f94ce..798127ac 100644
--- a/android/motionUI/app/src/main/res/layout/activity_startup.xml
+++ b/android/motionUI/app/src/main/res/layout/activity_startup.xml
@@ -22,7 +22,7 @@
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="Enter motionUI server URL:"
+ android:text="Enter motion-UI server URL:"
android:textAlignment="center"
android:textSize="16dp"
android:textColor="@color/white" />
diff --git a/android/motionUI/app/src/main/res/values/strings.xml b/android/motionUI/app/src/main/res/values/strings.xml
index 957972e3..6cfca47a 100644
--- a/android/motionUI/app/src/main/res/values/strings.xml
+++ b/android/motionUI/app/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
- motionUI
+ Motion-UI
#112334
#15bf7f
\ No newline at end of file
diff --git a/www/public/resources/js/motion.js b/www/public/resources/js/motion.js
index d61a1c00..470c49ba 100644
--- a/www/public/resources/js/motion.js
+++ b/www/public/resources/js/motion.js
@@ -56,7 +56,9 @@ function downloadMedia()
for (var n = 0; n < filesForDownload.length; n++) {
var download = filesForDownload[n];
- temporaryDownloadLink.setAttribute('href', '/media?id=' + download.fileId);
+ // Set the href attribute to the file path, also include the filename for the android app to make sure it downloads the file with the correct name
+ temporaryDownloadLink.setAttribute('href', '/media?id=' + download.fileId + '&filename=' + download.filename);
+ // Set the download attribute to force download
temporaryDownloadLink.setAttribute('download', download.filename);
/**