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); /**