diff --git a/android/ScratchJr/app/src/main/java/org/scratchjr/android/DatabaseManager.java b/android/ScratchJr/app/src/main/java/org/scratchjr/android/DatabaseManager.java index 63ab117..ffccffa 100644 --- a/android/ScratchJr/app/src/main/java/org/scratchjr/android/DatabaseManager.java +++ b/android/ScratchJr/app/src/main/java/org/scratchjr/android/DatabaseManager.java @@ -1,6 +1,8 @@ package org.scratchjr.android; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Locale; import org.json.JSONArray; @@ -14,6 +16,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; +import android.text.TextUtils; import android.util.Log; /** @@ -200,6 +203,27 @@ public class DatabaseManager { return Long.toString(id); } + public String insert(String table, JSONObject data) throws DatabaseException { + List values = new ArrayList<>(); + List keys = new ArrayList<>(); + List placeholders = new ArrayList<>(); + JSONArray names = data.names(); + for (int i = 0; i < names.length(); i++) { + String key = names.optString(i); + keys.add(key); + placeholders.add("?"); + values.add(data.optString(key)); + } + String statement = String.format( + "INSERT INTO %s (%s) VALUES (%s)", + table, + TextUtils.join(",", keys), + TextUtils.join(",", placeholders) + ); + + return stmt(statement, values.toArray(new String[0])); + } + private JSONObject getRowDataAsJsonObject(Cursor cursor) throws SQLiteException, JSONException { diff --git a/android/ScratchJr/app/src/main/java/org/scratchjr/android/IOManager.java b/android/ScratchJr/app/src/main/java/org/scratchjr/android/IOManager.java index d89013a..a8f45e9 100644 --- a/android/ScratchJr/app/src/main/java/org/scratchjr/android/IOManager.java +++ b/android/ScratchJr/app/src/main/java/org/scratchjr/android/IOManager.java @@ -8,12 +8,16 @@ import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.UUID; import org.json.JSONArray; import org.json.JSONException; +import org.json.JSONObject; import android.content.Context; +import android.net.Uri; import android.util.Base64; import android.util.Log; @@ -272,4 +276,98 @@ public class IOManager { } return new String(hexChars); } + + public void receiveProject(ScratchJrActivity activity, Uri uri) throws JSONException, IOException, DatabaseException { + File tempDir = new File(activity.getCacheDir() + File.separator + UUID.randomUUID().toString()); + tempDir.mkdir(); + List entries = ScratchJrUtil.unzip(uri.getPath(), tempDir.getPath()); + if (entries.isEmpty()) { + Log.e(LOG_TAG, "no entries found"); + // no files + return; + } + // read project json + JSONObject json = ScratchJrUtil.readJson(tempDir + "/project/data.json"); + JSONObject projectData = json.optJSONObject("json"); + JSONObject projectJson = new JSONObject(); + projectJson.put("isgift", "1"); + projectJson.put("deleted", "NO"); + projectJson.put("json", projectData.toString()); + JSONObject thumbnail = json.optJSONObject("thumbnail"); + projectJson.put("thumbnail", thumbnail.toString()); + projectJson.put("version", "iOSv01"); + projectJson.put("name", projectData.optString("name")); + _databaseManager.insert("projects", projectJson); + + HashMap spriteMap = new HashMap<>(); + JSONArray pages = projectData.optJSONArray("pages"); + for (int i = 0; i < pages.length(); i++) { + String pageName = pages.optString(i); + if (pageName == null) { + continue; + } + JSONObject page = projectData.optJSONObject(pageName); + JSONArray spriteNames = page.optJSONArray("sprites"); + for (int j = 0; j < spriteNames.length(); j++) { + String spriteName = spriteNames.optString(j); + JSONObject sprite = page.optJSONObject(spriteName); + spriteMap.put(sprite.optString("md5"), sprite); + } + } + for (int i = 0; i < entries.size(); i++) { + String entry = entries.get(i); + if (entry == null) { + continue; + } + if (!(entry.endsWith(".png") || entry.endsWith(".wav") || entry.endsWith(".svg"))) { + continue; + } + // copy file to target file + File sourceFile = new File(tempDir + File.separator + entry); + + String fileName = sourceFile.getName(); + File targetFile = new File(activity.getFilesDir().getPath() + File.separator + fileName); + if (!targetFile.exists()) { + ScratchJrUtil.copyFile(sourceFile, targetFile); + } + String folderName = sourceFile.getParentFile().getName(); + if ("thumbnails".equals(folderName) || "sounds".equals(folderName)) { + continue; + } + String table = "characters".equals(folderName) ? "usershapes" : "userbkgs"; + String statement = String.format("SELECT id FROM %s WHERE md5 = ?", table); + JSONArray rows = _databaseManager.query(statement, new String[]{fileName}); + if (rows.length() > 0) { + Log.e(LOG_TAG, "asset for " + fileName + "exists"); + continue; + } + String pngName = fileName.replace(".svg", ".png"); + JSONObject asset = new JSONObject(); + asset.put("version", "iOSv01"); + asset.put("md5", fileName); + asset.put("altmd5", pngName); + asset.put("width", "480"); + asset.put("height", "360"); + asset.put("ext", fileName.split("\\.")[1]); + if ("characters".equals(folderName)) { + JSONObject sprite = spriteMap.get(fileName); + if (sprite == null) { + continue; + } + asset.put("width", sprite.optString("w")); + asset.put("height", sprite.optString("h")); + asset.put("scale", sprite.optString("scale")); + asset.put("name", sprite.optString("name")); + } + File png = new File(activity.getFilesDir().getPath() + File.separator + pngName); + if (!png.exists()) { + String js = String.format("ScratchJr.makeThumb('%s', %s, %s)", fileName, asset.optString("width"), asset.optString("height")); + Log.d(LOG_TAG, js); + activity.runJavaScript(js); + } + _databaseManager.insert(table, asset); + } + // refresh lobby + activity.runJavaScript("Lobby.refresh();"); + } } diff --git a/android/ScratchJr/app/src/main/java/org/scratchjr/android/ScratchJrActivity.java b/android/ScratchJr/app/src/main/java/org/scratchjr/android/ScratchJrActivity.java index b5d1c1d..5e1e623 100644 --- a/android/ScratchJr/app/src/main/java/org/scratchjr/android/ScratchJrActivity.java +++ b/android/ScratchJr/app/src/main/java/org/scratchjr/android/ScratchJrActivity.java @@ -17,7 +17,6 @@ import android.os.Handler; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; -import android.util.Base64; import android.util.Log; import android.view.KeyEvent; import android.view.View; @@ -36,10 +35,7 @@ import android.widget.RelativeLayout; import com.google.firebase.analytics.FirebaseAnalytics; -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.Vector; @@ -110,6 +106,11 @@ public class ScratchJrActivity /* Firebase analytics tracking */ private FirebaseAnalytics _FirebaseAnalytics; + /** + * Project uri that need to be imported. + */ + private ArrayList projectUris = new ArrayList<>(); + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -310,7 +311,15 @@ public class ScratchJrActivity } } - private void receiveProject(Uri projectUri) { + private void receiveProject(final Uri projectUri) { + if (!isSplashDone()) { + projectUris.add(projectUri); + return; + } + importProject(projectUri); + } + + private void importProject(final Uri projectUri) { String PROJECT_EXTENSION = getApplicationContext().getString(R.string.share_extension_filter); String scheme = projectUri.getScheme(); Log.i(LOG_TAG, "receiveProject(scheme): " + scheme); @@ -324,25 +333,16 @@ public class ScratchJrActivity if (scheme.equals(ContentResolver.SCHEME_FILE) && !projectUri.getPath().matches(PROJECT_EXTENSION)) { return; } - // Read the project one byte at a time into a buffer - ByteArrayOutputStream projectData = new ByteArrayOutputStream(); - try { - InputStream is = getContentResolver().openInputStream(projectUri); - - byte[] readByte = new byte[1]; - while ((is.read(readByte)) == 1) { - projectData.write(readByte[0]); + runOnUiThread(new Runnable() { + @Override + public void run() { + try { + _ioManager.receiveProject(ScratchJrActivity.this, projectUri); + } catch (Exception e) { + e.printStackTrace(); + } } - } catch (FileNotFoundException e) { - Log.i(LOG_TAG, "File not found in project load"); - return; - } catch (IOException e) { - Log.i(LOG_TAG, "IOException in project load"); - return; - } - // We send the project Base64-encoded to JavaScript where it's processed and unpacked - String base64Project = Base64.encodeToString(projectData.toByteArray(), Base64.DEFAULT); - runJavaScript("OS.loadProjectFromSjr('" + base64Project + "');"); + }); } public RelativeLayout getContainer() { @@ -483,6 +483,10 @@ public class ScratchJrActivity public void setSplashDone(boolean done) { _splashDone = done; + while (projectUris.size() > 0) { + Uri uri = projectUris.remove(0); + importProject(uri); + } } /** diff --git a/android/ScratchJr/app/src/main/java/org/scratchjr/android/ScratchJrUtil.java b/android/ScratchJr/app/src/main/java/org/scratchjr/android/ScratchJrUtil.java index 837a198..ad5e96f 100644 --- a/android/ScratchJr/app/src/main/java/org/scratchjr/android/ScratchJrUtil.java +++ b/android/ScratchJr/app/src/main/java/org/scratchjr/android/ScratchJrUtil.java @@ -2,16 +2,22 @@ package org.scratchjr.android; import org.json.JSONArray; import org.json.JSONException; +import org.json.JSONObject; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; /** @@ -186,4 +192,77 @@ public class ScratchJrUtil { } return segments[segments.length - 1]; } + + public static List unzip(String zipPath, String toPath) { + List entries = new ArrayList<>(); + File zipFile = new File(zipPath); + ZipInputStream zin; + try { + zin = new ZipInputStream(new FileInputStream(zipFile)); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return entries; + } + try { + ZipEntry ze; + while ((ze = zin.getNextEntry()) != null) { + String path = toPath + File.separator + ze.getName(); + File unzipFile = new File(path); + if (ze.isDirectory()) { + if(!unzipFile.isDirectory()) { + unzipFile.mkdirs(); + } + continue; + } + File folder = unzipFile.getParentFile(); + if (!folder.isDirectory()) { + folder.mkdirs(); + } + FileOutputStream fout = new FileOutputStream(path, false); + BufferedOutputStream bout = new BufferedOutputStream(fout); + try { + byte[] buffer = new byte[1024]; + int read; + while ((read = zin.read(buffer)) != -1) { + bout.write(buffer, 0, read); + } + bout.flush(); + zin.closeEntry(); + entries.add(ze.getName()); + } catch (IOException e) { + e.printStackTrace(); + } finally { + fout.close(); + bout.close(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + try { + zin.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return entries; + } + + public static JSONObject readJson(String path) throws IOException, JSONException { + byte[] data; + InputStream in = new FileInputStream(new File(path)); + + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + int len; + byte[] buffer = new byte[1024]; + while ((len = in.read(buffer)) != -1) { + bos.write(buffer, 0, len); + } + bos.close(); + data = bos.toByteArray(); + } finally { + in.close(); + } + return new JSONObject(new String(data)); + } }