switch zip to native

This commit is contained in:
Yueyu 2020-11-13 08:11:38 +08:00
parent a5c182d05c
commit 07ce9bf7c8
12 changed files with 287 additions and 115 deletions

View file

@ -2,11 +2,14 @@ package org.scratchjr.android;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Locale;
import java.util.UUID;
import org.json.JSONArray;
import org.json.JSONException;
@ -574,34 +577,92 @@ public class JavaScriptDirectInterface {
}
@JavascriptInterface
public void sendSjrUsingShareDialog(String fileName, String emailSubject,
String emailBody, int shareType, String b64data) {
// Write a temporary file with the project data passed in from JS
File tempFile;
public String createZipForProject(String projectData, String metadataJson, String name) {
// create a temp folder
File tempPath = new File(_activity.getCacheDir() + File.separator + UUID.randomUUID().toString());
tempPath.mkdir();
// save project.json
// Log.d(LOG_TAG, "writing data.json");
File dataFile = new File(tempPath.getAbsolutePath() + File.separator + "data.json");
try {
FileOutputStream outputStream = new FileOutputStream(dataFile);
outputStream.write(projectData.getBytes());
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
return "error";
}
// Log.d(LOG_TAG, "writing data.json done");
// copy assets to target folder
JSONObject metadata;
try {
metadata = new JSONObject(metadataJson);
} catch (JSONException e) {
e.printStackTrace();
return "error";
}
// Log.d(LOG_TAG, "copying assets");
Iterator<String> keys = metadata.keys();
while (keys.hasNext()) {
String key = keys.next();
Log.d(LOG_TAG, key);
File folder = new File(tempPath.getAbsolutePath() + File.separator + key);
if (!folder.exists()) {
folder.mkdir();
}
JSONArray files = metadata.optJSONArray(key);
if (files == null) {
continue;
}
for (int i = 0; i < files.length(); i++) {
String file = files.optString(i);
if (file == null) {
continue;
}
File srcFile = new File(_activity.getFilesDir() + File.separator + file);
if (!srcFile.exists()) {
Log.e(LOG_TAG, "src file not exists" + file);
continue;
}
File targetFile = new File(folder.getAbsolutePath() + File.separator + file);
// Log.d(LOG_TAG, "copying assets" + file);
try {
ScratchJrUtil.copyFile(srcFile, targetFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Log.d(LOG_TAG, "copy assets done");
// create zip file
String extension;
String mimetype;
if (BuildConfig.APPLICATION_ID.equals("org.pbskids.scratchjr")) {
extension = ".psjr";
mimetype = "application/x-pbskids-scratchjr-project";
} else {
extension = ".sjr";
}
String fullName = name + extension;
File file = new File(_activity.getCacheDir() + File.separator + fullName);
file.deleteOnExit();
// Log.d(LOG_TAG, "creating zip");
ScratchJrUtil.zipFileAtPath(tempPath.getAbsolutePath(), file.getAbsolutePath());
// remove the temp folder
tempPath.deleteOnExit();
return fullName;
}
@JavascriptInterface
public void sendSjrUsingShareDialog(String fileName, String emailSubject,
String emailBody, int shareType) {
// Write a temporary file with the project data passed in from JS
String mimetype;
if (BuildConfig.APPLICATION_ID.equals("org.pbskids.scratchjr")) {
mimetype = "application/x-pbskids-scratchjr-project";
} else {
mimetype = "application/x-scratchjr-project";
}
try {
fileName = fileName + extension;
tempFile = new File(_activity.getCacheDir() + File.separator + fileName);
tempFile.createNewFile();
BufferedOutputStream bw = new BufferedOutputStream(new FileOutputStream(tempFile));
// Decode and write the data
bw.write(Base64.decode(b64data, Base64.DEFAULT));
bw.flush();
bw.close();
} catch (IOException e) {
return;
}
File file = new File(_activity.getCacheDir() + File.separator + fileName);
Log.d(LOG_TAG, file.getAbsolutePath());
final Intent it = new Intent(Intent.ACTION_SEND);
it.setType(mimetype);
it.putExtra(android.content.Intent.EXTRA_EMAIL, new String[] {});

View file

@ -3,12 +3,26 @@ package org.scratchjr.android;
import org.json.JSONArray;
import org.json.JSONException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* General utility class with static utility methods.
*
*
* @author markroth8
*/
public class ScratchJrUtil {
private static final int BUFFER = 2048;
/** Utility class private constructor so nobody creates an instance of this class */
private ScratchJrUtil() {
}
@ -16,7 +30,7 @@ public class ScratchJrUtil {
/**
* Convert the given JSONArray to an array of Strings.
*/
public static String[] jsonArrayToStringArray(JSONArray values)
public static String[] jsonArrayToStringArray(JSONArray values)
throws JSONException
{
String[] result = new String[values.length()];
@ -25,4 +39,95 @@ public class ScratchJrUtil {
}
return result;
}
}
public static void copyFile(File sourceLocation, File targetLocation)
throws IOException
{
InputStream in = new FileInputStream(sourceLocation);
OutputStream out = new FileOutputStream(targetLocation);
// Copy the bits from instream to outstream
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
out.close();
}
public static boolean zipFileAtPath(String sourcePath, String toLocation) {
File sourceFile = new File(sourcePath);
try {
BufferedInputStream origin;
FileOutputStream dest = new FileOutputStream(toLocation);
ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(
dest));
if (sourceFile.isDirectory()) {
zipSubFolder(out, sourceFile, sourceFile.getParent().length());
} else {
byte[] data = new byte[BUFFER];
FileInputStream fi = new FileInputStream(sourcePath);
origin = new BufferedInputStream(fi, BUFFER);
ZipEntry entry = new ZipEntry(getLastPathComponent(sourcePath));
entry.setTime(sourceFile.lastModified()); // to keep modification time after unzipping
out.putNextEntry(entry);
int count;
while ((count = origin.read(data, 0, BUFFER)) != -1) {
out.write(data, 0, count);
}
}
out.close();
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/*
*
* Zips a subfolder
*
*/
private static void zipSubFolder(ZipOutputStream out, File folder, int basePathLength)
throws IOException
{
File[] fileList = folder.listFiles();
BufferedInputStream origin;
for (File file : fileList) {
if (file.isDirectory()) {
zipSubFolder(out, file, basePathLength);
} else {
byte[] data = new byte[BUFFER];
String unmodifiedFilePath = file.getPath();
String relativePath = unmodifiedFilePath
.substring(basePathLength);
FileInputStream fi = new FileInputStream(unmodifiedFilePath);
origin = new BufferedInputStream(fi, BUFFER);
ZipEntry entry = new ZipEntry(relativePath);
entry.setTime(file.lastModified()); // to keep modification time after unzipping
out.putNextEntry(entry);
int count;
while ((count = origin.read(data, 0, BUFFER)) != -1) {
out.write(data, 0, count);
}
origin.close();
}
}
}
/*
* gets the last path component
*
* Example: getLastPathComponent("downloads/example/fileToZip");
* Result: "fileToZip"
*/
private static String getLastPathComponent(String filePath) {
String[] segments = filePath.split(File.separator);
if (segments.length == 0) {
return "";
}
return segments[segments.length - 1];
}
}

View file

@ -3,6 +3,7 @@ platform :ios, '8.0'
# add the Firebase pod for Google Analytics
pod 'Firebase/Analytics'
pod 'SSZipArchive', '~> 2.1.4'
target 'ScratchJr Free' do
end

View file

@ -64,9 +64,11 @@ PODS:
- nanopb/encode (= 0.3.901)
- nanopb/decode (0.3.901)
- nanopb/encode (0.3.901)
- SSZipArchive (2.1.5)
DEPENDENCIES:
- Firebase/Analytics
- SSZipArchive (~> 2.1.4)
SPEC REPOS:
trunk:
@ -81,6 +83,7 @@ SPEC REPOS:
- GoogleDataTransportCCTSupport
- GoogleUtilities
- nanopb
- SSZipArchive
SPEC CHECKSUMS:
Firebase: 5e6b7b12bf9adb90986688edc06b156c37e109cd
@ -94,7 +97,8 @@ SPEC CHECKSUMS:
GoogleDataTransportCCTSupport: f6ab1962e9dc05ab1fb938b795e5b310209edeec
GoogleUtilities: f895fde57977df4e0233edda0dbeac490e3703b6
nanopb: 2901f78ea1b7b4015c860c2fdd1ea2fee1a18d48
SSZipArchive: cefe1364104a0231268a5deb8495bdf2861f52f0
PODFILE CHECKSUM: d5c223883d3d06483e7e9d419cc9560fba5781d5
PODFILE CHECKSUM: 43eaf7714849e36c6216d378a51f3dc168eda736
COCOAPODS: 1.8.4
COCOAPODS: 1.9.3

View file

@ -1,5 +1,6 @@
#import "ScratchJr.h"
#import <CommonCrypto/CommonDigest.h>
#import <ZipArchive.h>
ViewController* HTML;
MFMailComposeViewController *emailDialog;
NSMutableDictionary *mediastrings;
@ -202,29 +203,57 @@ NSMutableDictionary *soundtimers;
return @"1";
}
// Receive a .sjr file from inside the app. Send using native UI - Airdrop or Email
+ (NSString*) sendSjrUsingShareDialog:(NSString *)fullname :(NSString*)emailSubject :(NSString*)emailBody :(int)shareType :(NSString*)contents {
+ (NSString *) createZipForProject: (NSString *) projectData :(NSDictionary *) metadata :(NSString *) zipName {
// create a temperary folder for project
NSString *tempDir = [[[NSString alloc] initWithString:NSTemporaryDirectory()] stringByAppendingPathComponent: [[NSUUID alloc] init].UUIDString];
// NSLog(@"%@", tempDir);
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager createDirectoryAtPath:tempDir withIntermediateDirectories:true attributes:nil error:nil];
// save project.json
NSString *dataPath = [tempDir stringByAppendingPathComponent:@"data.json"];
[[[NSData alloc] initWithData: [projectData dataUsingEncoding:NSUTF8StringEncoding]] writeToFile:dataPath atomically:YES];
// copy assets to target temp folder
for (NSString *key in [metadata allKeys]) {
NSString *subDir = [tempDir stringByAppendingPathComponent:key];
[fileManager createDirectoryAtPath:subDir withIntermediateDirectories:true attributes:nil error:nil];
for (NSString *file in [metadata valueForKey:key]) {
// copy file to target folder
// NSLog(@"%@ %@", key, file);
NSString *srcPath = [[IO getpath] stringByAppendingPathComponent:file];
NSString *toPath = [subDir stringByAppendingPathComponent:file];
if ([fileManager fileExistsAtPath:srcPath]) {
[fileManager copyItemAtPath:srcPath toPath:toPath error:nil];
}
}
}
NSString* extensionFormat = @"%@.sjr";
#if PBS
extensionFormat = @"%@.psjr";
#endif
NSString *fullName = [NSString stringWithFormat:extensionFormat, zipName];
NSURL *url = [self getDocumentPath:fullName];
NSString *zipPath = url.path;
NSLog(@"target zip path %@", zipPath);
if ([fileManager fileExistsAtPath:zipPath]) {
[fileManager removeItemAtPath:zipPath error:nil];
}
[SSZipArchive createZipFileAtPath:zipPath withContentsOfDirectory: tempDir];
// delete temp folder
[fileManager removeItemAtPath:tempDir error:nil];
return fullName;
}
NSString *filename = [NSString stringWithFormat:extensionFormat, fullname];
NSURL *url = [self getDocumentPath:filename];
NSData *plaindata = [IO decodeBase64:contents];
BOOL ok = [plaindata writeToURL:url atomically:NO];
// Receive a .sjr file from inside the app. Send using native UI - Airdrop or Email
if (ok) {
if (shareType == 0) {
[HTML showShareEmail:url withName:filename withSubject:emailSubject withBody:emailBody];
} else {
[HTML showShareAirdrop:url];
}
+ (NSString*) sendSjrUsingShareDialog:(NSString *)fileName :(NSString*)emailSubject :(NSString*)emailBody :(int)shareType {
NSURL *url = [self getDocumentPath:fileName];
if (shareType == 0) {
[HTML showShareEmail:url withName:fileName withSubject:emailSubject withBody:emailBody];
} else {
NSLog(@"couldn't save file");
[HTML showShareAirdrop:url];
}
return @"1";
}

View file

@ -158,9 +158,17 @@
[request callback:[ScratchJr captureimage:request.params[0]]];
}
-(void) createZipForProject: (JsRequest *) request {
NSString *projectData = request.params[0];
NSDictionary* metadata = request.params[1];
NSString* name = request.params[2];
NSString * fullName = [IO createZipForProject:projectData :metadata :name];
[request callback:fullName];
}
-(void) sendSjrUsingShareDialog: (JsRequest *) request {
int shareType = [request.params[3] intValue];
NSString *res = [IO sendSjrUsingShareDialog:request.params[0] :request.params[1] :request.params[2] :shareType : request.params[4]];
NSString *res = [IO sendSjrUsingShareDialog:request.params[0] :request.params[1] :request.params[2] :shareType];
[request callback:res];
}

View file

@ -140,11 +140,11 @@
+ (NSString *)getmedialen:(NSString *)file :(NSString *)key;
+ (NSString *)getmediadone:(NSString *)filename;
+ (NSString *)remove:(NSString *)filename;
+ (NSString *)createZipForProject: (NSString *)projectData :(NSDictionary *)metadata :(NSString *)name;
+ (NSString *)sendSjrUsingShareDialog:(NSString *)fileName
:(NSString *)emailSubject
:(NSString *)emailBody
:(int)shareType
:(NSString *)b64data;
:(int)shareType;
+ (NSString *)registerSound:(NSString *)dir :(NSString *)name;
+ (NSString *)playSound:(NSString *)name;
+ (NSString *)stopSound:(NSString *)name;

View file

@ -272,13 +272,17 @@ export default class UI {
Alert.close();
// Package the project as a .sjr file
IO.zipProject(ScratchJr.currentProject, function (contents) {
IO.compressProject(ScratchJr.currentProject, function (fullName) {
ScratchJr.onHold = false; // Unfreeze the editing UI
var emailSubject = Localization.localize('SHARING_EMAIL_SUBJECT', {
PROJECT_NAME: IO.shareName
});
OS.sendSjrToShareDialog(IO.zipFileName, emailSubject, Localization.localize('SHARING_EMAIL_TEXT'),
shareType, contents);
OS.sendSjrToShareDialog(
fullName,
emailSubject,
Localization.localize('SHARING_EMAIL_TEXT'),
shareType
);
shareLoadingGif.style.visibility = 'hidden';
});

View file

@ -221,6 +221,13 @@ export default class Android {
// Sharing
///////////////
static createZipForProject(projectData, metadata, name, fcn) {
const fullName = AndroidInterface.createZipForProject(projectData, JSON.stringify(metadata), name);
console.log(fullName);
if (fcn) {
fcn(fullName);
}
}
// Called on the JS side to trigger native UI for project sharing.
// fileName: name for the file to share
@ -229,8 +236,8 @@ export default class Android {
// shareType: 0 for Email; 1 for Airdrop
// b64data: base-64 encoded .SJR file to share
static sendSjrToShareDialog (fileName, emailSubject, emailBody, shareType, b64data) {
AndroidInterface.sendSjrUsingShareDialog(fileName, emailSubject, emailBody, shareType, b64data);
static sendSjrToShareDialog (fileName, emailSubject, emailBody, shareType) {
AndroidInterface.sendSjrUsingShareDialog(fileName, emailSubject, emailBody, shareType);
}
// // Called on the Objective-C side. The argument is a base64-encoded .SJR file,

View file

@ -298,7 +298,7 @@ export default class IO {
// Sharing
////////////////////
static zipProject (projectReference, finished) {
static compressProject (projectReference, finished) {
IO.getObject(projectReference, function (projectFromDB) {
var projectMetadata = {
'thumbnails': [],
@ -372,59 +372,6 @@ export default class IO {
}
}
// Get the media in projectMetadata and add it to a zip file
zipFile = new JSZip();
zipFile.folder('project');
var projectDataForZip = JSON.stringify(jsonData);
zipFile.file('project/data.json', projectDataForZip, {});
zipAssetsExpected = 0;
zipAssetsActual = 0;
// Generic function for adding media to the zip file
var addMediaToZip = function (folder, md5) {
var addB64ToZip = function (b64data) {
zipFile.file('project/' + folder + '/' + md5, b64data, {
base64: true,
createFolders: true
});
zipAssetsActual++;
};
// Determine if the md5 is a MediaLib file or a user one, and download it appropriately
// See also, Sprite.getAsset
if (md5 in MediaLib.keys) {
// Library character
IO.requestFromServer(MediaLib.path + md5, function (raw) {
addB64ToZip(btoa(raw));
});
} else {
// User file
OS.getmedia(md5, addB64ToZip);
}
};
// Add each type of media
for (var j = 0; j < projectMetadata.thumbnails.length; j++) {
addMediaToZip('thumbnails', projectMetadata.thumbnails[j]);
zipAssetsExpected++;
}
for (var k = 0; k < projectMetadata.characters.length; k++) {
addMediaToZip('characters', projectMetadata.characters[k]);
zipAssetsExpected++;
}
for (var l = 0; l < projectMetadata.backgrounds.length; l++) {
addMediaToZip('backgrounds', projectMetadata.backgrounds[l]);
zipAssetsExpected++;
}
for (var m = 0; m < projectMetadata.sounds.length; m++) {
addMediaToZip('sounds', projectMetadata.sounds[m]);
zipAssetsExpected++;
}
// Now the UI should wait for actual media count to equal expected media count
// This could pause if getmedia takes a long time, for example,
// if we have many large sprites or large sounds
@ -446,16 +393,10 @@ export default class IO {
.replace(windowsTrailingRe, '_');
shareName = jsonData.name;
function checkStatus () {
if ((zipAssetsActual / zipAssetsExpected) == 1) {
finished(zipFile.generate({
'compression': 'STORE'
}));
} else {
setTimeout(checkStatus, 200);
}
}
checkStatus();
// create zip natively
OS.createZipForProject(JSON.stringify(jsonData), projectMetadata, zipFileName, function (name) {
finished(name);
});
});
}

View file

@ -217,6 +217,10 @@ export default class OS {
// Sharing
///////////////
static createZipForProject(projectData, metadata, name, fcn) {
tabletInterface.createZipForProject(projectData, metadata, name, fcn);
}
// Called on the JS side to trigger native UI for project sharing.
// fileName: name for the file to share

View file

@ -347,6 +347,14 @@ export default class iOS {
// Sharing
///////////////
static createZipForProject(projectData, metadata, name, fcn) {
(async () => {
const fullName = await iOS.call('createZipForProject', projectData, metadata, name);
if (fcn) {
fcn(fullName);
}
})();
}
// Called on the JS side to trigger native UI for project sharing.
// fileName: name for the file to share
@ -355,8 +363,8 @@ export default class iOS {
// shareType: 0 for Email; 1 for Airdrop
// b64data: base-64 encoded .SJR file to share
static sendSjrToShareDialog (fileName, emailSubject, emailBody, shareType, b64data) {
iOS.call('sendSjrUsingShareDialog', fileName, emailSubject, emailBody, shareType, b64data);
static sendSjrToShareDialog (fileName, emailSubject, emailBody, shareType) {
iOS.call('sendSjrUsingShareDialog', fileName, emailSubject, emailBody, shareType);
}
// Name of the device/iPad to display on the sharing dialog page