feat(android): add CAMERA permission and handle incoming intents with custom URI schemes

This commit is contained in:
Ivan committed 2026-04-17 23:27:09 -05:00
1 parent 5348370544
commit 7377a39644
2 files changed
+120 -31

No files matched your search

+19
View File
@@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
@@ -36,6 +37,24 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lxma" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lxmf" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lxm" />
</intent-filter>
</activity>
</application>
@@ -18,6 +18,7 @@ import android.webkit.WebChromeClient;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ImageView;
@@ -35,6 +36,7 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import org.json.JSONObject;
public class MainActivity extends AppCompatActivity {
private WebView webView;
@@ -45,7 +47,7 @@ public class MainActivity extends AppCompatActivity {
private static final String SERVER_URL = "https://127.0.0.1:8000";
private static final int SERVER_PORT = 8000;
private static final int RUNTIME_PERMISSIONS_REQUEST_CODE = 1001;
private static final int MICROPHONE_WEB_PERMISSION_REQUEST_CODE = 1002;
private static final int WEB_MEDIA_PERMISSION_REQUEST_CODE = 1003;
private static final String PREFS_NAME = "meshchatx";
private static final String PREF_BATTERY_OPT_REQUESTED = "battery_opt_requested";
private static final int MAX_CONNECTION_ATTEMPTS = 120;
@@ -58,6 +60,7 @@ public class MainActivity extends AppCompatActivity {
private boolean startupPageLoaded = false;
private boolean backendFailed = false;
private int connectionAttempts = 0;
private String pendingIntentUri = null;
private static final String[] STARTUP_PHASES = new String[] {
"Starting MeshChatX...",
"Initializing Reticulum network stack...",
@@ -134,6 +137,7 @@ public class MainActivity extends AppCompatActivity {
progressBar.setVisibility(android.view.View.GONE);
loadingText.setVisibility(android.view.View.GONE);
errorText.setVisibility(android.view.View.GONE);
dispatchPendingIntentUri();
}
@Override
@@ -182,6 +186,8 @@ public class MainActivity extends AppCompatActivity {
handler.proceed();
}
});
webView.addJavascriptInterface(new MeshChatXAndroidBridge(this), "MeshChatXAndroid");
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onPermissionRequest(final PermissionRequest request) {
@@ -191,26 +197,46 @@ public class MainActivity extends AppCompatActivity {
}
boolean needsAudioCapture = false;
boolean needsVideoCapture = false;
for (String resource : request.getResources()) {
if (PermissionRequest.RESOURCE_AUDIO_CAPTURE.equals(resource)) {
needsAudioCapture = true;
break;
} else if (PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(resource)) {
needsVideoCapture = true;
}
}
if (!needsAudioCapture) {
if (!needsAudioCapture && !needsVideoCapture) {
request.grant(request.getResources());
return;
}
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED) {
List<String> missingPermissions = new ArrayList<>();
if (
needsAudioCapture &&
ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED
) {
missingPermissions.add(Manifest.permission.RECORD_AUDIO);
}
if (
needsVideoCapture &&
ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED
) {
missingPermissions.add(Manifest.permission.CAMERA);
}
if (missingPermissions.isEmpty()) {
request.grant(request.getResources());
return;
}
pendingWebPermissionRequest = request;
requestMicrophonePermissionForWebView();
ActivityCompat.requestPermissions(
MainActivity.this,
missingPermissions.toArray(new String[0]),
WEB_MEDIA_PERMISSION_REQUEST_CODE
);
});
}
@@ -255,11 +281,22 @@ public class MainActivity extends AppCompatActivity {
return true;
}
});
handleIncomingIntent(getIntent());
startMeshChatServer();
scheduleConnectionRetry("Connecting to local server...");
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleIncomingIntent(intent);
if (startupPageLoaded) {
dispatchPendingIntentUri();
}
}
private void requestRuntimePermissionsIfNeeded() {
List<String> missingPermissions = new ArrayList<>();
addIfMissing(missingPermissions, Manifest.permission.RECORD_AUDIO);
@@ -279,22 +316,6 @@ public class MainActivity extends AppCompatActivity {
}
}
private void requestMicrophonePermissionForWebView() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
if (pendingWebPermissionRequest != null) {
pendingWebPermissionRequest.grant(pendingWebPermissionRequest.getResources());
pendingWebPermissionRequest = null;
}
return;
}
ActivityCompat.requestPermissions(
this,
new String[] { Manifest.permission.RECORD_AUDIO },
MICROPHONE_WEB_PERMISSION_REQUEST_CODE
);
}
private void requestBatteryOptimizationExemptionIfNeeded() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
@@ -333,18 +354,23 @@ public class MainActivity extends AppCompatActivity {
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == MICROPHONE_WEB_PERMISSION_REQUEST_CODE) {
if (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
if (pendingWebPermissionRequest != null) {
pendingWebPermissionRequest.deny();
pendingWebPermissionRequest = null;
}
if (requestCode == WEB_MEDIA_PERMISSION_REQUEST_CODE) {
if (pendingWebPermissionRequest == null) {
return;
}
if (pendingWebPermissionRequest != null) {
pendingWebPermissionRequest.grant(pendingWebPermissionRequest.getResources());
pendingWebPermissionRequest = null;
boolean granted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
granted = false;
break;
}
}
if (granted) {
pendingWebPermissionRequest.grant(pendingWebPermissionRequest.getResources());
} else {
pendingWebPermissionRequest.deny();
}
pendingWebPermissionRequest = null;
return;
}
if (requestCode != RUNTIME_PERMISSIONS_REQUEST_CODE) {
@@ -384,6 +410,34 @@ public class MainActivity extends AppCompatActivity {
return url != null && url.startsWith(SERVER_URL);
}
private void handleIncomingIntent(Intent intent) {
if (intent == null || !Intent.ACTION_VIEW.equals(intent.getAction())) {
return;
}
Uri data = intent.getData();
if (data == null || data.getScheme() == null) {
return;
}
String scheme = data.getScheme().toLowerCase();
if (!"lxma".equals(scheme) && !"lxmf".equals(scheme) && !"lxm".equals(scheme)) {
return;
}
pendingIntentUri = data.toString();
}
private void dispatchPendingIntentUri() {
if (webView == null || pendingIntentUri == null || pendingIntentUri.isEmpty()) {
return;
}
String uri = pendingIntentUri;
pendingIntentUri = null;
String js =
"window.dispatchEvent(new CustomEvent('meshchatx-intent-uri',{detail:" +
JSONObject.quote(uri) +
"}));";
webView.evaluateJavascript(js, null);
}
private void scheduleConnectionRetry(String message) {
if (startupPageLoaded || backendFailed) {
return;
@@ -484,5 +538,21 @@ public class MainActivity extends AppCompatActivity {
webView.destroy();
}
}
private static class MeshChatXAndroidBridge {
private final MainActivity activity;
MeshChatXAndroidBridge(MainActivity activity) {
this.activity = activity;
}
@JavascriptInterface
public void exitApp() {
activity.runOnUiThread(() -> {
activity.finishAffinity();
android.os.Process.killProcess(android.os.Process.myPid());
});
}
}
}