Makes .monster files open with MonsterCards.

This commit is contained in:
2026-09-07 17:52:05 -07:00
parent 60a26ba81d
commit b3ac787360
3 changed files with 88 additions and 29 deletions

View File

@@ -22,32 +22,40 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Intent filter for viewing/opening content:// URIs with binary/octet-stream MIME type (.monster) -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" />
<data android:mimeType="application/octet-stream" />
</intent-filter>
<!-- Intent filter for file:// URIs ending in .monster or .monster.txt -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" android:host="*" />
<data android:pathPattern=".*\\.monster" />
<data android:pathPattern=".*\\.monster\\.txt" />
</intent-filter>
<!-- Intent filter for explicitly sharing files/text to Monster Cards -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<action android:name="android.intent.action.PICK" />
<action android:name="android.intent.action.INSERT" />
<action android:name="android.intent.action.INSERT_OR_EDIT" />
<category android:name="android.intent.category.ALTERNATIVE" />
<category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="text/plain"
android:scheme="content" />
<data
android:mimeType="application/octet-stream"
android:scheme="content" />
<data
android:mimeType="text/plain"
android:scheme="file" />
<data android:mimeType="application/octet-stream" />
</intent-filter>
<nav-graph android:value="@navigation/mobile_navigation" />

View File

@@ -1,8 +1,10 @@
package com.majinnaibu.monstercards;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.view.MenuItem;
import androidx.annotation.NonNull;
@@ -23,6 +25,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Locale;
import java.util.Objects;
public class MainActivity extends AppCompatActivity {
@@ -76,26 +79,61 @@ public class MainActivity extends AppCompatActivity {
private String readMonsterJSONFromIntent(@NonNull Intent intent) {
String action = intent.getAction();
Bundle extras = intent.getExtras();
String type = intent.getType();
String json;
Uri uri = null;
if ("android.intent.action.SEND".equals(action) && "text/plain".equals(type)) {
uri = extras.getParcelable("android.intent.extra.STREAM");
} else if ("android.intent.action.VIEW".equals(action) && ("text/plain".equals(type) || "application/octet-stream".equals(type))) {
if ("android.intent.action.SEND".equals(action)) {
if (extras != null) {
uri = extras.getParcelable(Intent.EXTRA_STREAM);
}
} else if ("android.intent.action.VIEW".equals(action) || "android.intent.action.EDIT".equals(action)) {
uri = intent.getData();
} else {
Logger.logError(String.format("unexpected launch configuration action: %s, type: %s", action, type));
Logger.logError(String.format("unexpected launch configuration action: %s", action));
}
if (uri == null || !isMonsterFile(uri)) {
if (uri != null) {
Logger.logError("Ignored file because extension is not .monster or .monster.txt: " + uri);
}
if (uri == null) {
return null;
}
json = readContentsOfUri(uri);
String json = readContentsOfUri(uri);
if (StringHelper.isNullOrEmpty(json)) {
return null;
}
return json;
}
private boolean isMonsterFile(@NonNull Uri uri) {
String fileName = getFileNameFromUri(uri);
if (fileName == null) {
return false;
}
String lowerName = fileName.toLowerCase(Locale.ROOT);
return lowerName.endsWith(".monster") || lowerName.endsWith(".monster.txt");
}
@Nullable
private String getFileNameFromUri(@NonNull Uri uri) {
String displayName = null;
if ("content".equals(uri.getScheme())) {
try (Cursor cursor = getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (nameIndex != -1) {
displayName = cursor.getString(nameIndex);
}
}
} catch (Exception e) {
Logger.logError("Error querying display name from content URI", e);
}
}
if (displayName == null) {
displayName = uri.getPath();
}
return displayName;
}
@Nullable
private String readContentsOfUri(Uri uri) {
StringBuilder builder = new StringBuilder();

View File

@@ -162,3 +162,16 @@ Extend the generic sharing feature with specialized, direct sharing channels (to
- **Sub-Step 7.1 (NFC Sharing)**: Share monster data directly between devices via NFC (NDEF records / Android Beam).
- **Sub-Step 7.2 (Bluetooth / Wi-Fi Direct)**: Share monster files directly between nearby Android devices via Bluetooth / Wi-Fi.
- **Sub-Step 7.3 (Web URL with Embedded Payload)**: Generate a shareable Web URL containing compressed/base64-encoded monster JSON data.
---
## 3. Implementation Checklist & Status
- [x] **Step 0**: Restrict app intent filters in `AndroidManifest.xml` (`.monster` & `.monster.txt`) and add runtime filename validation in `MainActivity.java`.
- [ ] **Step 1**: Refactor import & conversion code into a shared `EntityImporter<T>` interface and `TetraCubeMonsterImporter` class.
- [ ] **Step 2**: Update Tetra-cube importer class to support the newest Tetra-cube format (`bonusActions`, `mythics`, `blind`, intro descriptions).
- [ ] **Step 3**: Import from D&D Beyond URL (`https://www.dndbeyond.com/characters/49074997` fetching from character service endpoint `character/v2/character/49074997`).
- [ ] **Step 4**: Export to internal format described by Open5e document (`Open5eExporter`).
- [ ] **Step 5**: Import from internal format described by Open5e document (`Open5eImporter`).
- [ ] **Step 6**: Generic Android Share button feature (`ACTION_SEND`).
- [ ] **Step 7**: Specific share targets (7.1 NFC, 7.2 Bluetooth, 7.3 Embedded Web URL).