Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android #74

Merged
merged 1 commit into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ todo
WIP
android/.idea/*
keystore.properties
app-release.aab
app-release.aab
19 changes: 18 additions & 1 deletion android/motionUI/app/build.gradle
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -20,6 +36,7 @@ android {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
compileOptions {
Expand All @@ -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'
Expand Down
14 changes: 11 additions & 3 deletions android/motionUI/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Write to storage permissions -->
<!-- Permission to read files in external storage (Android 10 to 12) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Specific permissions for Android 13 (API 33+) depending on the type of media -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

<application
android:allowBackup="true"
Expand All @@ -15,9 +22,11 @@
android:theme="@style/Theme.MotionUI"
android:usesCleartextTraffic="true"
tools:targetApi="31">

<activity
android:name=".Startup"
android:exported="false" />
<activity
android:name=".SplashScreen"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand All @@ -27,8 +36,7 @@
</activity>
<activity
android:name=".MainActivity"
android:exported="true">
</activity>
android:exported="true"></activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
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;
import android.webkit.WebView;
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;
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
*/
Expand All @@ -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();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
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
* It contains a form to enter the URL of the motionUI server if it is not already saved in the app
* Then it redirects to the MainActivity (motionUI main page)
*/
public class Startup extends AppCompatActivity {

private Button button;
private EditText editText;
private String url;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SplashScreen"
android:background="@color/motionUIBlue">

<LinearLayout
android:layout_width="450px"
android:layout_height="450px"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5">

<ImageView
android:layout_width="450px"
android:layout_height="450px"
android:background="@drawable/motion">
</ImageView>
</LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
2 changes: 1 addition & 1 deletion android/motionUI/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<resources>
<string name="app_name">motionUI</string>
<string name="app_name">Motion-UI</string>
<color name="motionUIBlue">#112334</color>
<color name="motionUIGreen">#15bf7f</color>
</resources>
4 changes: 3 additions & 1 deletion www/public/resources/js/motion.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/**
Expand Down
Loading