Skip to content

Commit b0e9b10

Browse files
committed
Support opening hex files shared to CreateAI on android
1 parent 0a2ceff commit b0e9b10

File tree

6 files changed

+230
-17
lines changed

6 files changed

+230
-17
lines changed

android/app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,20 @@
3333
<data android:mimeType="application/octet-stream" />
3434
<data android:scheme="file" />
3535
<data android:scheme="content" />
36-
3736
</intent-filter>
37+
</activity>
3838

39-
39+
<activity
40+
android:name=".shareplugin.ShareReceiverActivity"
41+
android:label="@string/app_name"
42+
android:exported="true"
43+
android:parentActivityName=".MainActivity"
44+
android:excludeFromRecents="true"
45+
android:theme="@style/AppTheme.NoActionBar">
4046
<intent-filter>
4147
<action android:name="android.intent.action.SEND" />
4248
<category android:name="android.intent.category.DEFAULT" />
43-
44-
<data android:mimeType="*/*" />
49+
<data android:mimeType="application/octet-stream" />
4550
</intent-filter>
4651
</activity>
4752

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,53 @@
11
package org.microbit.createai;
22

3+
import android.content.Intent;
4+
import android.net.Uri;
5+
import android.os.Bundle;
6+
37
import androidx.activity.EdgeToEdge;
48

59
import com.getcapacitor.BridgeActivity;
10+
import com.getcapacitor.JSObject;
11+
12+
import org.microbit.createai.shareplugin.ShareReceiver;
613

714
public class MainActivity extends BridgeActivity {
15+
@Override
16+
protected void onCreate(Bundle savedInstanceState) {
17+
registerPlugin(ShareReceiver.class);
18+
super.onCreate(savedInstanceState);
19+
handleIncomingIntent(getIntent());
20+
}
821

922
@Override
1023
public void onStart() {
1124
super.onStart();
1225
EdgeToEdge.enable(this);
1326
}
1427

28+
@Override
29+
protected void onNewIntent(Intent intent) {
30+
super.onNewIntent(intent);
31+
handleIncomingIntent(intent);
32+
}
33+
34+
private void handleIncomingIntent(Intent intent) {
35+
if (intent == null) return;
36+
37+
String action = intent.getAction();
38+
39+
if ("org.microbit.createai.SHARE_PRIVATE".equals(action)) {
40+
41+
Uri shared = intent.getParcelableExtra("sharedUri");
42+
43+
if (shared != null) {
44+
45+
JSObject payload = new JSObject();
46+
payload.put("uri", shared.toString());
47+
48+
getBridge().triggerWindowJSEvent("indirectShareReceived", payload.toString());
49+
50+
}
51+
}
52+
}
1553
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package org.microbit.createai.shareplugin;
2+
3+
import android.app.Activity;
4+
import android.content.Context;
5+
import android.content.Intent;
6+
import android.database.Cursor;
7+
import android.net.Uri;
8+
import android.provider.OpenableColumns;
9+
10+
import com.getcapacitor.annotation.CapacitorPlugin;
11+
import com.getcapacitor.JSObject;
12+
import com.getcapacitor.Plugin;
13+
import com.getcapacitor.PluginCall;
14+
import com.getcapacitor.PluginMethod;
15+
16+
import org.microbit.createai.MainActivity;
17+
18+
import java.io.File;
19+
import java.io.FileOutputStream;
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
23+
24+
@CapacitorPlugin(name = "ShareReceiver")
25+
public class ShareReceiver extends Plugin {
26+
27+
@PluginMethod
28+
public void isShare(PluginCall call) {
29+
Intent intent = bridge.getActivity().getIntent();
30+
String action = intent.getAction();
31+
String type = intent.getType();
32+
var result = new JSObject();
33+
result.put("isShare", Intent.ACTION_SEND.equals(action) && type != null);
34+
call.resolve(result);
35+
}
36+
37+
@PluginMethod
38+
public void finish(PluginCall call) {
39+
40+
bridge.getActivity().finish();
41+
}
42+
43+
@PluginMethod
44+
public void openMainActivity (PluginCall call) {
45+
Activity receiver = getActivity();
46+
Intent incoming = receiver.getIntent();
47+
Uri shared = incoming.getParcelableExtra(Intent.EXTRA_STREAM);
48+
49+
Uri localCopy = makeFileLocal(shared);
50+
51+
int flags = incoming.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION;
52+
53+
Intent forward = new Intent(this.getActivity(), MainActivity.class);
54+
forward.setAction("org.microbit.createai.SHARE_PRIVATE");
55+
forward.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
56+
forward.addFlags(flags);
57+
forward.putExtra("sharedUri", localCopy);
58+
receiver.startActivity(forward);
59+
call.resolve(new JSObject());
60+
}
61+
62+
Uri makeFileLocal(Uri uri) {
63+
// get the name
64+
Cursor contentCursor =
65+
getContext().getContentResolver().query(uri, null, null, null, null);
66+
contentCursor.moveToFirst();
67+
int nameCol = contentCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
68+
String fileName = contentCursor.getString(nameCol);
69+
contentCursor.close();
70+
71+
File file = new File(getContext().getFilesDir(), fileName);
72+
73+
try (FileOutputStream outputStream = getContext().openFileOutput(fileName, Context.MODE_PRIVATE);
74+
InputStream inputStream = getContext().getContentResolver().openInputStream(uri)) {
75+
76+
byte[] buffer = new byte[8192];
77+
int n;
78+
while ((n = inputStream.read(buffer)) != -1) {
79+
outputStream.write(buffer, 0, n);
80+
}
81+
82+
return Uri.fromFile(file);
83+
} catch (IOException ioException) {
84+
ioException.printStackTrace();
85+
}
86+
return null;
87+
}
88+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.microbit.createai.shareplugin;
2+
3+
import android.os.Bundle;
4+
5+
import com.getcapacitor.BridgeActivity;
6+
7+
public class ShareReceiverActivity extends BridgeActivity {
8+
9+
@Override
10+
protected void onCreate(Bundle savedInstanceState) {
11+
registerPlugin(ShareReceiver.class);
12+
super.onCreate(savedInstanceState);
13+
}
14+
15+
@Override
16+
public void onPause() {
17+
super.onPause();
18+
finish();
19+
}
20+
21+
@Override
22+
public void onStop() {
23+
super.onStop();
24+
finish();
25+
}
26+
}

src/components/LoadProjectInput.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ const LoadProjectInput = forwardRef<LoadProjectInputRef, LoadProjectInputProps>(
4242
inputRef.current!.value = "";
4343
if (filesArray.length === 1) {
4444
const { path, name } = filesArray[0];
45-
console.log("Attempting to load", path, name);
4645
if (path && name) {
4746
loadNativeUrl(path, name);
4847
}

src/hooks/project-hooks.tsx

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,17 @@ import {
4343
readFileAsText,
4444
} from "../utils/fs-util";
4545
import { useDownloadActions } from "./download-hooks";
46-
import { Capacitor } from "@capacitor/core";
46+
import { Capacitor, registerPlugin } from "@capacitor/core";
4747
import { App as CapacitorApp } from "@capacitor/app";
4848
import { Encoding, Filesystem } from "@capacitor/filesystem";
4949

50+
interface ShareReceiverI {
51+
isShare(): Promise<{ isShare: boolean }>;
52+
openMainActivity(): Promise<void>;
53+
finish(): Promise<void>;
54+
}
55+
const ShareReceiver = registerPlugin<ShareReceiverI>("ShareReceiver");
56+
5057
class CodeEditorError extends Error {}
5158

5259
/**
@@ -173,21 +180,57 @@ export const ProjectProvider = ({
173180
path: evt.url,
174181
encoding: Encoding.UTF8,
175182
});
176-
let filename = evt.url.substring(evt.url.lastIndexOf("/") + 1);
177-
// Forgivingly shim broken filenames to hex files,
178-
// we can't rely on android to maintain file data.
179-
// Even Android's Files app often passes us a broken
180-
// filename. MakeCode is resilient to bad files.
181-
if (filename.length === 0) {
182-
filename = "Unnamed.hex";
183-
}
184-
if (!filename.includes(".")) {
185-
filename += ".hex";
186-
}
183+
const filename = filenameProbablyHex(
184+
evt.url.substring(evt.url.lastIndexOf("/") + 1)
185+
);
186+
187187
await importHex(contents.data as string, filename);
188188
}
189189
);
190190

191+
// This part receives projects sent using the Share interface.
192+
// The send intent is received using a separate activity, and
193+
// sent back to CreateAI's main activity so that it persists
194+
// in the app's main process.
195+
// See https://github.com/carsten-klaffke/send-intent/issues/69#issuecomment-1544619608
196+
const initSendIntent = async () => {
197+
const isShare = await ShareReceiver.isShare();
198+
if (!isShare.isShare) {
199+
return;
200+
}
201+
202+
logging.log("Passing share action to main activity.");
203+
await ShareReceiver.openMainActivity(); // reuses the shared object passed in
204+
void ShareReceiver.finish();
205+
};
206+
207+
void initSendIntent();
208+
209+
// This is the portion that gets called in the receiving MainActivity
210+
window.addEventListener("indirectShareReceived", async (evt) => {
211+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
212+
const path = (evt as any).uri as string;
213+
214+
const fileExtension = getLowercaseFileExtension(path);
215+
logging.event({
216+
type: "app-file-share-open",
217+
detail: {
218+
extension: fileExtension || "none",
219+
},
220+
});
221+
222+
const contents = await Filesystem.readFile({
223+
path,
224+
encoding: Encoding.UTF8,
225+
});
226+
227+
const filename = filenameProbablyHex(
228+
path.substring(path.lastIndexOf("/") + 1)
229+
);
230+
231+
await importHex(contents.data as string, filename);
232+
});
233+
191234
return () => {
192235
const removeListenerHandler = async () => {
193236
void (await appUrlListener).remove();
@@ -580,3 +623,17 @@ export const ProjectProvider = ({
580623
<ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>
581624
);
582625
};
626+
627+
// Forgivingly shim broken filenames into hex files,
628+
// we can't rely on android to maintain file data.
629+
// Even Android's Files app often passes us a broken
630+
// filename. MakeCode is resilient to bad files.
631+
const filenameProbablyHex = (filename: string) => {
632+
if (filename.length === 0) {
633+
return "Unnamed.hex";
634+
}
635+
if (!filename.includes(".")) {
636+
return filename + ".hex";
637+
}
638+
return filename;
639+
};

0 commit comments

Comments
 (0)