Initial commit

This commit is contained in:
Tim Mickel 2016-01-08 14:31:04 -05:00
commit e88af5b6df
1558 changed files with 110581 additions and 0 deletions
.esformatter.eslintignore.eslintrc.gitignore
.idea
.project
.settings
README.md
android
bin
doc
editions/free/Free-Images.xcassets

26
.esformatter Normal file
View file

@ -0,0 +1,26 @@
{
"indent": {
"value": " "
},
"whiteSpace": {
"before": {
"FunctionExpressionOpeningBrace": 1,
"FunctionExpressionClosingBrace": 0
},
"after": {
"FunctionExpressionOpeningBrace": 1,
"FunctionExpressionClosingBrace": -1,
"FunctionReservedWord": 1
}
},
"plugins": [
"esformatter-quotes",
"esformatter-semicolons",
"esformatter-dot-notation",
"esformatter-braces"
],
"quotes": {
"type": "single",
"avoidEscape": false
}
}

1
.eslintignore Normal file
View file

@ -0,0 +1 @@
src/external/

124
.eslintrc Normal file
View file

@ -0,0 +1,124 @@
{
"rules": {
"curly": [2, "multi-line"],
"eol-last": [2],
"indent": [2, 4],
"linebreak-style": [2, "unix"],
"max-len": [2, 120, 4],
"no-trailing-spaces": [2, { "skipBlankLines": true }],
"no-unused-vars": [2, {"args": "after-used", "varsIgnorePattern": "^_"}],
"quotes": [2, "single"],
"semi": [2, "always"],
"space-before-function-paren": [2, "always"],
"strict": [2, "never"],
"brace-style": [2, "1tbs", { "allowSingleLine": false }]
},
"env": {
"browser": true
},
"globals": {
"AndroidInterface": true,
"window": true,
"Cookie": true,
"getUrlVars": true,
"libInit": true,
"vlen": true,
"gn": true,
"CSSTransition": true,
"CSSTransition3D": true,
"WebKitCSSMatrix": true,
"globalx": true,
"globaly": true,
"localx": true,
"localy": true,
"getIdFor": true,
"hitRect": true,
"hit3DRect": true,
"rgb2hsb": true,
"colorToRGBA": true,
"drawScaled": true,
"getDocumentHeight": true,
"getDocumentWidth": true,
"setCanvasSizeScaledToWindowDocumentHeight": true,
"newHTML": true,
"newCanvas": true,
"newDiv": true,
"newP": true,
"newTextInput": true,
"newImage": true,
"getStringSize": true,
"setCanvasSize": true,
"setProps": true,
"frame": true,
"writeText": true,
"fitInRect": true,
"rgbToHex": true,
"Vector": true,
"DrawPath": true,
"drawThumbnail": true,
"ScratchJr": true,
"Runtime": true,
"Localization": true,
"iOS": true,
"Settings": true,
"BlockSpecs": true,
"Block": true,
"BlockArg": true,
"Stage": true,
"Project": true,
"Scroll": true,
"Thread": true,
"Scripts": true,
"ScratchAudio": true,
"Library": true,
"Paint": true,
"Record": true,
"Prims": true,
"Undo": true,
"Events": true,
"Menu": true,
"IO": true,
"UI": true,
"Thumbs": true,
"Grid": true,
"Alert": true,
"Palette": true,
"ScriptsPane": true,
"MediaLib": true,
"Sprite": true,
"Rectangle": true,
"Matrix": true,
"Page": true,
"Home": true,
"Lobby": true,
"Samples": true,
"Camera": true,
"getIdForCamera": true,
"Ghost": true,
"Layers": true,
"Paint": true,
"PaintAction": true,
"PaintIO": true,
"PaintLayout": true,
"PaintUndo": true,
"SVGTools": true,
"SVGImage": true,
"Transform": true,
"Layer": true,
"Path": true,
"DEGTOR": true,
"xform": true,
"selxform": true,
"SVG2Canvas": true,
"isTablet": true,
"isiOS": true,
"isAndroid": true,
"scaleMultiplier": true,
"JSZip": true,
"Snap": true,
"IntlMessageFormat": true,
"Sound": true,
"webkitAudioContext": true
},
"extends": "eslint:recommended"
}

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/android/ScratchJr/app/src/main/gen/
/node_modules
.DS_Store

22
.idea/compiler.xml generated Normal file
View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>

3
.idea/copyright/profiles_settings.xml generated Normal file
View file

@ -0,0 +1,3 @@
<component name="CopyrightManager">
<settings default="" />
</component>

9
.idea/libraries/Size_Pngs.xml generated Normal file
View file

@ -0,0 +1,9 @@
<component name="libraryTable">
<library name="Size Pngs">
<CLASSES>
<root url="jar://$PROJECT_DIR$/assets/Master Illustrator Files/All Icons/Size Pngs.zip!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

9
.idea/libraries/gradle_wrapper.xml generated Normal file
View file

@ -0,0 +1,9 @@
<component name="libraryTable">
<library name="gradle-wrapper">
<CLASSES>
<root url="jar://$PROJECT_DIR$/android/ScratchJr/gradle/wrapper/gradle-wrapper.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

32
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
<OptionsSetting value="true" id="Add" />
<OptionsSetting value="true" id="Remove" />
<OptionsSetting value="true" id="Checkout" />
<OptionsSetting value="true" id="Update" />
<OptionsSetting value="true" id="Status" />
<OptionsSetting value="true" id="Edit" />
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.7" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="masterDetails">
<states>
<state key="ProjectJDKs.UI">
<settings>
<last-edited>1.7</last-edited>
<splitter-proportions>
<option name="proportions">
<list>
<option value="0.2" />
</list>
</option>
</splitter-proportions>
</settings>
</state>
</states>
</component>
</project>

10
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/android/ScratchJrTest/ScratchJrTest.iml" filepath="$PROJECT_DIR$/android/ScratchJrTest/ScratchJrTest.iml" />
<module fileurl="file://$PROJECT_DIR$/android/ScratchJr/app/src/androidTest/androidTest.iml" filepath="$PROJECT_DIR$/android/ScratchJr/app/src/androidTest/androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/android/ScratchJr/app/src/main/main.iml" filepath="$PROJECT_DIR$/android/ScratchJr/app/src/main/main.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

264
.idea/workspace.xml generated Normal file
View file

@ -0,0 +1,264 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="01c0a7a9-181f-4009-b7e0-1c2c02071cd8" name="Default" comment="">
<change type="MODIFICATION" beforePath="$PROJECT_DIR$/.idea/workspace.xml" afterPath="$PROJECT_DIR$/.idea/workspace.xml" />
</list>
<ignored path="scratchjr.iws" />
<ignored path=".idea/workspace.xml" />
<ignored path="$PROJECT_DIR$/out/" />
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
<option name="TRACKING_ENABLED" value="true" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ChangesViewManager" flattened_view="true" show_ignored="false" />
<component name="CreatePatchCommitExecutor">
<option name="PATCH_PATH" value="" />
</component>
<component name="ExecutionTargetManager" SELECTED_TARGET="default_target" />
<component name="FavoritesManager">
<favorites_list name="scratchjr" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GradleLocalSettings">
<option name="externalProjectsViewState">
<projects_view />
</option>
</component>
<component name="NamedScopeManager">
<order />
</component>
<component name="ProjectFrameBounds">
<option name="x" value="1" />
<option name="y" value="23" />
<option name="width" value="1260" />
<option name="height" value="733" />
</component>
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
<OptionsSetting value="true" id="Add" />
<OptionsSetting value="true" id="Remove" />
<OptionsSetting value="true" id="Checkout" />
<OptionsSetting value="true" id="Update" />
<OptionsSetting value="true" id="Status" />
<OptionsSetting value="true" id="Edit" />
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectView">
<navigator currentView="AndroidView" proportions="" version="1">
<flattenPackages />
<showMembers />
<showModules />
<showLibraryContents />
<hideEmptyPackages />
<abbreviatePackageNames />
<autoscrollToSource />
<autoscrollFromSource />
<sortByType />
</navigator>
<panes>
<pane id="ProjectPane" />
<pane id="Scope" />
<pane id="PackagesPane" />
<pane id="AndroidView">
<subPane />
</pane>
<pane id="Scratches" />
</panes>
</component>
<component name="PropertiesComponent">
<property name="recentsLimit" value="5" />
<property name="restartRequiresConfirmation" value="true" />
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="external.system.task.project.file.to.start" value="$PROJECT_DIR$/android/ScratchJr/build.gradle" />
<property name="FullScreen" value="false" />
</component>
<component name="RunManager">
<configuration default="true" type="AndroidRunConfigurationType" factoryName="Android Application">
<module name="main" />
<option name="DEPLOY" value="true" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="TARGET_SELECTION_MODE" value="EMULATOR" />
<option name="PREFERRED_AVD" value="" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="true" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="USE_LAST_SELECTED_DEVICE" value="false" />
<option name="PREFERRED_AVD" value="" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY_CLASS" value="" />
<method />
</configuration>
<configuration default="true" type="Application" factoryName="Application">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" />
<option name="VM_PARAMETERS" />
<option name="PROGRAM_PARAMETERS" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="ENABLE_SWING_INSPECTOR" value="false" />
<option name="ENV_VARIABLES" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="" />
<envs />
<method />
</configuration>
<configuration default="true" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list />
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<method />
</configuration>
<configuration default="true" type="JUnit" factoryName="JUnit">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<module name="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PACKAGE_NAME" />
<option name="MAIN_CLASS_NAME" />
<option name="METHOD_NAME" />
<option name="TEST_OBJECT" value="class" />
<option name="VM_PARAMETERS" value="-ea" />
<option name="PARAMETERS" />
<option name="WORKING_DIRECTORY" value="$MODULE_DIR$" />
<option name="ENV_VARIABLES" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="singleModule" />
</option>
<envs />
<patterns />
<method />
</configuration>
<configuration default="true" type="Remote" factoryName="Remote">
<option name="USE_SOCKET_TRANSPORT" value="true" />
<option name="SERVER_MODE" value="false" />
<option name="SHMEM_ADDRESS" value="javadebug" />
<option name="HOST" value="localhost" />
<option name="PORT" value="5005" />
<method />
</configuration>
<configuration default="true" type="TestNG" factoryName="TestNG">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<module name="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="SUITE_NAME" />
<option name="PACKAGE_NAME" />
<option name="MAIN_CLASS_NAME" />
<option name="METHOD_NAME" />
<option name="GROUP_NAME" />
<option name="TEST_OBJECT" value="CLASS" />
<option name="VM_PARAMETERS" value="-ea" />
<option name="PARAMETERS" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="OUTPUT_DIRECTORY" />
<option name="ANNOTATION_TYPE" />
<option name="ENV_VARIABLES" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="singleModule" />
</option>
<option name="USE_DEFAULT_REPORTERS" value="false" />
<option name="PROPERTIES_FILE" />
<envs />
<properties />
<listeners />
<method />
</configuration>
<configuration name="&lt;template&gt;" type="Applet" default="true" selected="false">
<option name="MAIN_CLASS_NAME" />
<option name="HTML_FILE_NAME" />
<option name="HTML_USED" value="false" />
<option name="WIDTH" value="400" />
<option name="HEIGHT" value="300" />
<option name="POLICY_FILE" value="$APPLICATION_HOME_DIR$/bin/appletviewer.policy" />
<option name="VM_PARAMETERS" />
</configuration>
<configuration name="&lt;template&gt;" type="#org.jetbrains.idea.devkit.run.PluginConfigurationType" default="true" selected="false">
<option name="VM_PARAMETERS" value="-Xmx512m -Xms256m -XX:MaxPermSize=250m -ea" />
</configuration>
</component>
<component name="ShelveChangesManager" show_recycled="false" />
<component name="SvnConfiguration">
<configuration />
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="01c0a7a9-181f-4009-b7e0-1c2c02071cd8" name="Default" comment="" />
<created>1434859049570</created>
<option name="number" value="Default" />
<updated>1434859049570</updated>
</task>
<servers />
</component>
<component name="ToolWindowManager">
<frame x="1" y="23" width="1260" height="733" extended-state="0" />
<editor active="false" />
<layout>
<window_info id="Palette&#9;" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
<window_info id="Designer" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" />
<window_info id="Terminal" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
<window_info id="TODO" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="6" side_tool="false" content_ui="tabs" />
<window_info id="Capture Analysis" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" />
<window_info id="Structure" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
<window_info id="Maven Projects" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" />
<window_info id="Application Servers" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
<window_info id="Project" active="true" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" weight="0.24958949" sideWeight="0.5" order="0" side_tool="false" content_ui="combo" />
<window_info id="Favorites" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="2" side_tool="true" content_ui="tabs" />
<window_info id="Event Log" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" weight="0.3284553" sideWeight="0.5" order="7" side_tool="true" content_ui="tabs" />
<window_info id="Capture Tool" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" />
<window_info id="Version Control" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
<window_info id="Ant Build" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
<window_info id="Gradle Console" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="7" side_tool="true" content_ui="tabs" />
<window_info id="Build Variants" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="2" side_tool="true" content_ui="tabs" />
<window_info id="Run" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
<window_info id="Hierarchy" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.25" sideWeight="0.5" order="2" side_tool="false" content_ui="combo" />
<window_info id="Cvs" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.25" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" />
<window_info id="Message" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
<window_info id="Find" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
<window_info id="Captures" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.32987967" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
<window_info id="Debug" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.4" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" />
<window_info id="Android" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
<window_info id="Commander" active="false" anchor="right" auto_hide="false" internal_type="SLIDING" type="SLIDING" visible="false" weight="0.4" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
<window_info id="Inspection" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.4" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" />
</layout>
</component>
<component name="Vcs.Log.UiProperties">
<option name="RECENTLY_FILTERED_USER_GROUPS">
<collection />
</option>
<option name="RECENTLY_FILTERED_BRANCH_GROUPS">
<collection />
</option>
</component>
<component name="VcsContentAnnotationSettings">
<option name="myLimit" value="2678400000" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager />
<watches-manager />
</component>
</project>

11
.project Normal file
View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>scratchjr_git</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
</buildSpec>
<natures>
</natures>
</projectDescription>

33
.settings/launch.json Normal file
View file

@ -0,0 +1,33 @@
{
"version": "0.1.0",
// List of configurations. Add new configurations or edit existing ones.
// ONLY "node" and "mono" are supported, change "type" to switch.
"configurations": [
{
// Name of configuration; appears in the launch configuration drop down menu.
"name": "Launch app.js",
// Type of configuration. Possible values: "node", "mono".
"type": "node",
// Workspace relative or absolute path to the program.
"program": "app.js",
// Automatically stop program after launch.
"stopOnEntry": true,
// Command line arguments passed to the program.
"args": [],
// Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace.
"cwd": ".",
// Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH.
"runtimeExecutable": null,
// Environment variables passed to the program.
"env": { }
},
{
"name": "Attach",
"type": "node",
// TCP/IP address. Default is "localhost".
"address": "localhost",
// Port to attach to.
"port": 5858
}
]
}

76
README.md Normal file
View file

@ -0,0 +1,76 @@
## Overview
This is the official git repository hosting the source code for the
[ScratchJr](http://scratchjr.org/) project.
ScratchJr cannot be copied and/or distributed without the express
permission of the Massachusetts Institute of Technology (MIT). You should
only access this repository if you have been given explicit permission
from MIT.
ScratchJr can be built both on iOS and Android.
A pure-web version or Chrome-app version is planned to follow at some point in the future.
Platform | Status
-------- | -------------
iOS | Released in App Store
Android | Released in Google Play
## Release Schedule
As of this writing, the Android version now supports Android 4.2
and above.
## Architecture Overview
The diagram below illustrates the architecture of ScratchJr and
how the iOS (functional), Android (functional) and pure HTML5 (future)
versions share a common client.
![Scratch Jr. Architecture Diagram](doc/scratchjr_architecture.png)
## Directory Structure and Projects
This repository has the following directory structure:
* <tt>src/</tt> - Shared Javasript code for iOS and Android common client. This is where most changes should be made for features, bug fixes, UI, etc.
* <tt>editions/</tt> - Assembly directories for each "flavor" of ScratchJr. These symlink to src for common code, and could diverge in settings and assets.
* <tt>free/</tt> - Free edition JavaScript, including all shared code for all releases
* <tt>android/</tt> - Android port of Scratch Jr. (Java, Android Studio Projects)
* <tt>ScratchJr/</tt> - Android Studio Project for ScratchJr Android Application
* <tt>bin/</tt> - Build scripts and other executables
* <tt>doc/</tt> - Developer Documentation
* <tt>ios/</tt> - XCode project for iOS build.
## Building ScratchJr
To build the Android version, you need to have a system equipped with Android Studio. To build the iOS version, you need to have a Mac with XCode.
The build caches .png files out of the .svg files to improve performance. To enable this build step, you need to install a few dependencies.
On Ubuntu:
* Run <tt>sudo easy_install pysvg</tt> to install python svg libraries
* Run <tt>sudo apt-get install librsvg2-bin</tt> to install rsvg-convert
* Run <tt>sudo apt-get install imagemagick</tt> to install ImageMagick
On OS X:
* Install [Homebrew](http://brew.sh).
* Run <tt>sudo easy_install pysvg</tt> to install python svg libraries
* Run <tt>brew install librsvg</tt> to install rsvg-convert
* Run <tt>brew install imagemagick</tt> to install ImageMagick
Once these are installed, select the appropriate target in XCode or the appropriate flavor/build variant in Android Studio.
## Code credits
ScratchJr would not be possible without free and open source libraries, including:
* [Snap.svg](https://github.com/adobe-webplatform/Snap.svg/)
* [JSZip](https://github.com/Stuk/jszip)
* [Intl.js](https://github.com/andyearnshaw/Intl.js)
* [Yahoo intl-messageformat](https://github.com/yahoo/intl-messageformat)
## Acknowledgments
ScratchJr is a collaborative effort between:
* [Tufts DevTech Research Group](http://ase.tufts.edu/devtech/)
* [Lifelong Kindergarten group at MIT Media Lab](http://llk.media.mit.edu/)
* [Playful Invention Company](http://www.playfulinvention.com/)
* [Two Sigma Investments](http://twosigma.com)

1
android/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/releases

1
android/README.md Normal file
View file

@ -0,0 +1 @@
ScratchJr - Android Studio project containing Android build of Scratch Jr.

7
android/ScratchJr/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/assets

1
android/ScratchJr/.idea/.name generated Normal file
View file

@ -0,0 +1 @@
ScratchJr

22
android/ScratchJr/.idea/compiler.xml generated Normal file
View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>

View file

@ -0,0 +1,3 @@
<component name="CopyrightManager">
<settings default="" />
</component>

19
android/ScratchJr/.idea/gradle.xml generated Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="LOCAL" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="$APPLICATION_HOME_DIR$/gradle/gradle-2.8" />
<option name="gradleJvm" value="1.7" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

46
android/ScratchJr/.idea/misc.xml generated Normal file
View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<entry_points version="2.0" />
</component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
<OptionsSetting value="true" id="Add" />
<OptionsSetting value="true" id="Remove" />
<OptionsSetting value="true" id="Checkout" />
<OptionsSetting value="true" id="Update" />
<OptionsSetting value="true" id="Status" />
<OptionsSetting value="true" id="Edit" />
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.7" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

9
android/ScratchJr/.idea/modules.xml generated Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/ScratchJr.iml" filepath="$PROJECT_DIR$/ScratchJr.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

6
android/ScratchJr/.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="ScratchJr" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle">
<configuration>
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
<option name="BUILDABLE" value="false" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

2
android/ScratchJr/app/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/build
/google-services.json

View file

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="ScratchJr" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="freeDebug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleFreeDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileFreeDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleFreeDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileFreeDebugAndroidTestSources" />
<afterSyncTasks>
<task>generateFreeDebugAndroidTestSources</task>
<task>generateFreeDebugSources</task>
</afterSyncTasks>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/free/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/free/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/free/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/free/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/free/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/free/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/google-services/free/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/free/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/free/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/freeDebug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/freeDebug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/freeDebug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/freeDebug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/freeDebug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/freeDebug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/freeDebug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/free/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/free/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/free/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/free/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/free/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/free/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/free/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/free/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/free/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/free/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/free/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/free/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/free/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestFree/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestFree/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestFree/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestFree/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestFree/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestFree/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestFree/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/23.1.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/23.1.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.google.android.gms/play-services-analytics/8.3.0/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.google.android.gms/play-services-basement/8.3.0/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.google.android.gms/play-services-measurement/8.3.0/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="support-annotations-23.1.1" level="project" />
<orderEntry type="library" exported="" name="support-v4-23.1.1" level="project" />
<orderEntry type="library" exported="" name="appcompat-v7-23.1.1" level="project" />
<orderEntry type="library" exported="" name="play-services-analytics-8.3.0" level="project" />
<orderEntry type="library" exported="" name="play-services-measurement-8.3.0" level="project" />
<orderEntry type="library" exported="" name="play-services-basement-8.3.0" level="project" />
</component>
</module>

View file

@ -0,0 +1,99 @@
apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'
android {
compileSdkVersion 23
buildToolsVersion "21.1.2"
defaultConfig {
applicationId "org.scratchjr.android"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
productFlavors {
free {
applicationId "org.scratchjr.androidfree"
minSdkVersion 17
targetSdkVersion 23
versionCode 20
versionName "1.1"
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.google.android.gms:play-services-analytics:8.3.0'
}
def appModuleRootFolder = '.'
def srcDir = 'src'
def googleServicesJson = 'google-services.json'
def commonHome = '../../..'
def html5Assets = 'src/main/assets/HTML5'
task switchToFree(type: Copy) {
def buildType = 'free'
from "${commonHome}/editions/${buildType}/src"
into html5Assets
}
task switchToFreeGA(type: Copy) {
def buildType = 'free'
from "${commonHome}/editions/${buildType}/android-resources"
include "$googleServicesJson"
into "$appModuleRootFolder"
}
class GenerateScratchJrPNGsTask extends DefaultTask {
File commonHome = project.file("../../..")
File binDir = project.file("${commonHome}/bin")
@OutputDirectory
File generatedAssets = project.file("src/main/assets")
File generatedHtml5Assets = project.file("${generatedAssets}/HTML5")
File pngLibrary = project.file("${generatedHtml5Assets}/pnglibrary")
File svgLibrary = project.file("${generatedHtml5Assets}/svglibrary")
@TaskAction
def go() {
pngLibrary.mkdirs()
ant.exec(executable: "${binDir}/convert-svg-to-png.py",
failOnError: true)
{
arg(value: "-i ${svgLibrary}/")
arg(value: "-o ${pngLibrary}/")
}
}
}
class CleanScratchJrResourcesTask extends DefaultTask {
File generatedAssets = project.file('src/main/assets')
@TaskAction
def go() {
generatedAssets.deleteDir()
}
}
task generateScratchJrPNGs(type: GenerateScratchJrPNGsTask)
task cleanScratchJrResources(type: CleanScratchJrResourcesTask)
afterEvaluate {
processFreeDebugGoogleServices.dependsOn switchToFreeGA
processFreeReleaseGoogleServices.dependsOn switchToFreeGA
prepareFreeDebugDependencies.dependsOn switchToFree
prepareFreeReleaseDependencies.dependsOn switchToFree
generateFreeReleaseResources.dependsOn generateScratchJrPNGs
generateFreeDebugResources.dependsOn generateScratchJrPNGs
generateScratchJrPNGs.mustRunAfter switchToFree
}
clean.dependsOn('cleanScratchJrResources')

View file

@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/mroth/programs/adt-bundle-linux-x86_64-20140702/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -0,0 +1,79 @@
package org.scratchjr.android;
import junit.framework.Assert;
import java.util.concurrent.Callable;
/**
* Common utilities for unit tests
*/
public final class TestUtils {
static final int TIMEOUT = 2000;
/** Utility class constructor */
private TestUtils() {
}
/**
* Waits until the given callable returns true, or if a timeout occurrs, fail the test.
*/
static void waitUntilTrue(final Callable<Boolean> callable)
throws Exception
{
waitForCondition(new Condition<Boolean>() {
@Override
public Boolean call() throws Exception {
return callable.call();
}
@Override
boolean conditionReady(Boolean value) {
return (value != null) && value;
}
});
}
static <V> V waitForCondition(Condition<V> condition)
throws Exception
{
long timeout = System.currentTimeMillis() + TIMEOUT;
V result;
result = condition.call();
while (!condition.conditionReady(result) && (System.currentTimeMillis() < timeout)) {
Thread.sleep(10);
result = condition.call();
}
if (!condition.conditionReady(result)) {
Assert.fail("Timed out waiting for condition");
}
return result;
}
static <V> V ensureConditionDoesNotHappenWithinTimeout(Condition<V> condition)
throws Exception
{
long timeout = System.currentTimeMillis() + TIMEOUT;
V result;
result = condition.call();
while (!condition.conditionReady(result) && (System.currentTimeMillis() < timeout)) {
Thread.sleep(10);
result = condition.call();
}
if (condition.conditionReady(result)) {
Assert.fail("Unexpected condition occurred within timeout");
}
return result;
}
abstract static class Condition<V>
implements Callable<V>
{
/**
* Return true if the condition is ready to be analyzed.
* Defaults to returning true when the value is not null but can be overridden.
*/
boolean conditionReady(V value) {
return value != null;
}
}
}

View file

@ -0,0 +1 @@
/assets

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.scratchjr.android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-feature android:name="android.hardware.camera" android:required="false" />
<supports-screens android:smallScreens="false"
android:normalScreens="false"
android:largeScreens="true"
android:xlargeScreens="true"
android:requiresSmallestWidthDp="600" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:name="ScratchJrApplication"
android:hardwareAccelerated="true">
<provider android:name="ShareContentProvider"
android:grantUriPermissions="true"
android:authorities="${applicationId}.ShareContentProvider">
</provider>
<activity
android:name=".ScratchJrActivity"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize"
android:configChanges="keyboard|keyboardHidden"
android:theme="@style/FullscreenTheme"
android:launchMode="singleTask" >
<intent-filter>
<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:mimeType="@string/share_mimetype" />
<data android:pathPattern="@string/share_extension_filter" />
<data android:host="*" />
</intent-filter>
</activity>
<receiver android:name="com.google.android.gms.analytics.AnalyticsReceiver"
android:enabled="true">
<intent-filter>
<action android:name="com.google.android.gms.analytics.ANALYTICS_DISPATCH" />
</intent-filter>
</receiver>
<service android:name="com.google.android.gms.analytics.AnalyticsService"
android:enabled="true"
android:exported="false"/>
</application>
</manifest>

View file

@ -0,0 +1,390 @@
package org.scratchjr.android;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.PictureCallback;
import android.hardware.Camera.Size;
import android.hardware.SensorManager;
import android.util.Log;
import android.view.Display;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.ScrollView;
/**
* Creates a camera view that hovers at a particular location and has a mask.
*
* We use a ScrollView because the camera will rescale (squish) the preview to whatever size the
* SurfaceView is and we want to keep the aspect ratio. So the ScrollView is of the desired
* size and then we add a SurfaceView to it with the camera preview.
*
* @author markroth8
*/
public class CameraView
extends ScrollView
{
private static final String LOG_TAG = "ScratchJr.CameraView";
private CameraPreviewView _cameraPreview;
private Camera _camera;
private final RectF _rect;
private boolean _currentFacingFront;
private int _cameraId;
private CameraOrientationListener _orientationListener;
private float _scale;
public CameraView(Context context, RectF rect, float scale, boolean facingFront) {
super(context);
_currentFacingFront = facingFront;
_rect = rect;
_scale = scale;
_camera = safeOpenCamera(facingFront);
if (_camera != null) {
_cameraPreview = new CameraPreviewView(context);
Size previewSize = _camera.getParameters().getPreviewSize();
float previewWidth = _rect.width();
float previewHeight = previewSize.height * rect.width() / previewSize.width;
float centerScrollY = (previewHeight - rect.height()) / 2;
float centerScrollX = 0.0f;
if (previewHeight < rect.height()) {
previewHeight = rect.height();
previewWidth = previewSize.width * rect.height() / previewSize.height;
centerScrollX = (previewWidth - rect.width()) / 2;
centerScrollY = 0.0f;
}
LinearLayout linearLayout = new LinearLayout(context);
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams((int) previewWidth, (int) previewHeight);
addView(linearLayout, layoutParams);
linearLayout.addView(_cameraPreview, layoutParams);
final float cx = centerScrollX;
final float cy = centerScrollY;
post(new Runnable() {
@Override
public void run() {
scrollTo((int) cx, (int) cy);
}
});
}
_orientationListener = new CameraOrientationListener(context, SensorManager.SENSOR_DELAY_NORMAL);
enableOrientationListener();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// Disabling scrolling in this ScrollView
return false;
}
public void captureStillImage(PictureCallback pictureCallback, Runnable failed) {
if (_camera != null) {
final Parameters params = _camera.getParameters();
params.setRotation(0);
// Set picture size to the maximum supported resolution.
List<Size> supportedPictureSizes = params.getSupportedPictureSizes();
int maxHeight = 0;
for (Size size : supportedPictureSizes) {
if (size.height > maxHeight) {
params.setPictureSize(size.width, size.height);
maxHeight = size.height;
}
}
_camera.setParameters(params);
_camera.takePicture(null, null, pictureCallback);
} else {
failed.run();
}
}
private Camera safeOpenCamera(boolean facingFront) {
Camera result = null;
try {
_cameraId = findFirstCameraId(facingFront);
if (_cameraId != -1) {
result = Camera.open(_cameraId);
}
} catch (RuntimeException e) {
Log.e(LOG_TAG, "Failed to open camera", e);
}
return result;
}
private int findFirstCameraId(boolean facingFront) {
int result = -1;
int facingTarget = facingFront ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK;
int count = Camera.getNumberOfCameras();
CameraInfo cameraInfo = new CameraInfo();
for (int i = 0; i < count; i++) {
Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == facingTarget) {
result = i;
break;
}
}
if (result == -1) {
Log.w(LOG_TAG, "No " + (facingFront ? "front" : "back") + " -facing camera detected on this device.");
}
return result;
}
public boolean setCameraFacing(boolean facingFront) {
boolean result;
if (_currentFacingFront != facingFront) {
// switch cameras
int id = findFirstCameraId(facingFront);
if (id == -1) {
result = false;
} else {
result = true;
_currentFacingFront = facingFront;
if (_camera != null) {
disableOrientationListener();
_camera.release();
_camera = null;
}
_camera = safeOpenCamera(facingFront);
_cameraPreview.startPreview();
enableOrientationListener();
}
} else {
result = true;
}
return result;
}
public RectF getRect() {
return _rect;
}
/**
* Take the given bitmap image from the camera and transform it to the correct
* aspect ratio and size.
*
* @return jpeg-encoded data of the transformed image.
*/
public byte[] getTransformedImage(Bitmap originalImage) {
Bitmap cropped = cropAndResize(originalImage);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
cropped.compress(CompressFormat.JPEG, 90, bos);
try {
bos.close();
} catch (IOException e) {
// will not happen - this is a ByteArrayOutputStream
Log.e(LOG_TAG, "IOException while closing byte array stream", e);
}
byte[] jpegData = bos.toByteArray();
return jpegData;
}
/**
* Crop and resize the given image to the dimensions of the rectangle for this camera view.
*
* If the image was front-facing, also mirror horizontally.
*/
private Bitmap cropAndResize(Bitmap image) {
int imageWidth = image.getWidth();
int imageHeight = image.getHeight();
float rectWidth = _rect.width();
float rectHeight = _rect.height();
float newHeight = rectWidth * imageHeight / imageWidth;
float scale = rectWidth / imageWidth;
int offsetX = 0;
int offsetY = (int) ((newHeight - rectHeight) / 2 * imageHeight / newHeight);
if (newHeight < rectHeight) {
float newWidth = rectHeight * imageWidth / imageHeight;
scale = rectHeight / imageHeight;
offsetY = 0;
offsetX = (int) ((newWidth - rectWidth) / 2 * imageWidth / newWidth);
}
Matrix m = new Matrix();
if (_currentFacingFront) {
// flip bitmap horizontally since front-facing camera is mirrored
m.preScale(-1.0f, 1.0f);
}
CameraInfo cameraInfo = new CameraInfo();
Camera.getCameraInfo(_cameraId, cameraInfo);
int rotation = findDisplayRotation(getContext(), cameraInfo.facing);
if (rotation == 180) {
m.preScale(-1.0f, -1.0f);
}
m.postScale(scale / _scale, scale / _scale);
Bitmap newBitmap = Bitmap.createBitmap(image, offsetX, offsetY, imageWidth - offsetX * 2, imageHeight - offsetY * 2, m, true);
return newBitmap;
}
private void enableOrientationListener() {
synchronized(_orientationListener) {
if (_orientationListener.canDetectOrientation()) {
_orientationListener.setCameraInfo(_camera, _cameraId);
_orientationListener.enable();
}
}
}
private void disableOrientationListener() {
synchronized(_orientationListener) {
if (_orientationListener != null) {
_orientationListener.disable();
_orientationListener.clearCameraInfo();
}
}
}
private static Display findDisplay(Context context) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
return windowManager.getDefaultDisplay();
}
private static int findDisplayRotation(Context context, int facing) {
Display display = findDisplay(context);
int r = display.getRotation();
int rotation = (r == Surface.ROTATION_180) ? 180 : 0;
if (facing == CameraInfo.CAMERA_FACING_FRONT) {
rotation = (rotation + 360) % 360;
}
return rotation;
}
private class CameraPreviewView
extends SurfaceView
implements SurfaceHolder.Callback
{
private final SurfaceHolder _holder;
public CameraPreviewView(Context context) {
super(context);
_holder = getHolder();
_holder.addCallback(CameraPreviewView.this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
if (_camera != null) {
_camera.setPreviewDisplay(holder);
_camera.startPreview();
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error creating surface", e);
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (_holder.getSurface() != null) {
if (_camera != null) {
try {
_camera.stopPreview();
} catch (Exception e) {
Log.e(LOG_TAG, "Error releasing camera", e);
}
startPreview();
}
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (_camera != null) {
Log.i(LOG_TAG, "Releasing camera");
disableOrientationListener();
_camera.release();
_camera = null;
}
}
public void startPreview() {
if (_camera != null) {
try {
_camera.setPreviewDisplay(_holder);
Size previewSize = _camera.getParameters().getPreviewSize();
Log.i(LOG_TAG, "Preview size: " + previewSize.width + " x " + previewSize.height);
_camera.startPreview();
} catch (IOException e) {
Log.e(LOG_TAG, "Error in starting preview", e);
} catch (RuntimeException e) {
Log.e(LOG_TAG, "Error in starting preview", e);
}
}
}
}
/**
* An {@link OrientationEventListener} which updates the camera preview
* based on the device's orientation.
* @author khu
*
*/
private static class CameraOrientationListener extends OrientationEventListener {
private Camera _observedCamera;
private int _observedCameraId;
private Display _display;
private int _previousRotation = -1;
private Context _context;
public CameraOrientationListener(Context context) {
super(context);
_context = context;
_display = findDisplay(context);
}
public CameraOrientationListener(Context context, int rate) {
super(context, rate);
_context = context;
_display = findDisplay(context);
}
public synchronized void setCameraInfo(Camera camera, int cameraId) {
_observedCamera = camera;
_observedCameraId = cameraId;
}
public synchronized void clearCameraInfo() {
_observedCamera = null;
_observedCameraId = -1;
}
@Override
public synchronized void onOrientationChanged(int orientation) {
if (orientation == ORIENTATION_UNKNOWN || _observedCamera == null
|| _observedCameraId == -1)
{
return;
}
CameraInfo cameraInfo = new CameraInfo();
Camera.getCameraInfo(_observedCameraId, cameraInfo);
int rotation = findDisplayRotation(_context, cameraInfo.facing);
if (rotation != _previousRotation) {
// Update the preview
_observedCamera.setDisplayOrientation(rotation);
_previousRotation = rotation;
}
}
}
}

View file

@ -0,0 +1,25 @@
package org.scratchjr.android;
/**
* Exception thrown when there was a problem connecting to or accessing the database.
*
* @author markroth8
*/
public class DatabaseException
extends Exception
{
private static final long serialVersionUID = 2762109849013951310L;
public DatabaseException(String message) {
super(message);
}
public DatabaseException(String message, Throwable t) {
super(message, t);
}
public DatabaseException(Throwable t) {
super(t);
}
}

View file

@ -0,0 +1,217 @@
package org.scratchjr.android;
import java.util.Arrays;
import java.util.Locale;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
/**
* Manages the database connection for Scratch Jr
*
* @author markroth8
*/
public class DatabaseManager {
private static final String LOG_TAG = "ScratchJr.DBManager";
private static final String DB_NAME = "ScratchJr";
private static final int DB_VERSION = 1;
private boolean _open = false;
private Context _applicationContext;
private DatabaseHelper _databaseHelper;
private SQLiteDatabase _database;
public DatabaseManager(Context applicationContext) {
_applicationContext = applicationContext;
}
/**
* Open the database, creating it if it does not yet exist.
*
* @throws SQLException
* if the database could be neither opened or created
*/
public void open()
throws SQLException
{
_databaseHelper = new DatabaseHelper(_applicationContext, DB_NAME, null, DB_VERSION);
_database = _databaseHelper.getWritableDatabase();
// Migrations
try {
_database.execSQL(_applicationContext.getString(R.string.sql_add_gift));
} catch (SQLException e) {
// isgift field already exists
}
_open = true;
}
public void close() {
_databaseHelper.close();
_open = false;
}
public boolean isOpen() {
return _open;
}
public void clearTables()
throws DatabaseException
{
exec("DELETE FROM PROJECTS");
exec("DELETE FROM USERSHAPES");
exec("DELETE FROM USERBKGS");
}
private static class DatabaseHelper
extends SQLiteOpenHelper
{
private Context _context;
public DatabaseHelper(Context context, String name, CursorFactory factory, int version) {
super(context, name, factory, version);
_context = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(_context.getString(R.string.sql_create_projects));
Log.i(LOG_TAG, "Created table projects");
db.execSQL(_context.getString(R.string.sql_create_usershapes));
Log.i(LOG_TAG, "Created table usershapes");
db.execSQL(_context.getString(R.string.sql_create_userbkgs));
Log.i(LOG_TAG, "Created table userbkgs");
db.execSQL(_context.getString(R.string.sql_add_gift));
Log.i(LOG_TAG, "Created project gift field");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(LOG_TAG, "Upgrading database from version " + oldVersion + " to " + newVersion +
", which currently does nothing.");
}
}
/**
* Execute a statement on the database and return "success" if successful or the error message if not.
*
* @param stmt
* The statement to execute.
* @return "success" if the statement was executed successfully, or the error message if not.
* @throws DatabaseException
* If there was an error in accessing the database.
*/
public String exec(String stmt)
throws DatabaseException
{
Log.d(LOG_TAG, "exec '" + stmt + "'");
String result;
try {
_database.execSQL(stmt);
result = "success";
} catch (SQLException e) {
Log.e(LOG_TAG, "Error while executing statement '" + stmt + "'", e);
result = e.getMessage();
}
return result;
}
/**
* Perform a query on the database and return the results as a JSON-encoded array.
*
* @param statement
* The SQL statement to query
* @param values
* Ordered list of arguments to substitute
* @return A JSONArray that contains the results of the query. Each element of the array is a JSON object with the keys as
* column names and the values as column values (Strings).
* @throws JSONException
* If there was an error encoding or decoding JSON
* @throws DatabaseException
* If there was an error in accessing the database.
*/
public JSONArray query(String statement, String[] values)
throws JSONException, DatabaseException
{
Log.d(LOG_TAG, "query '" + statement + "', " + Arrays.toString(values));
Cursor cursor;
try {
cursor = _database.rawQuery(statement, values);
} catch (IllegalStateException e) {
// The database is inaccessible - we tried to query in the background
throw new DatabaseException("Query '" + statement + "' failed to run.");
}
if (cursor == null) {
throw new DatabaseException("Query '" + statement + "' returned null cursor.");
}
JSONArray resultArr = new JSONArray();
if (cursor.moveToFirst()) {
do {
resultArr.put(getRowDataAsJsonObject(cursor));
} while (cursor.moveToNext());
}
return resultArr;
}
/**
* Execute a statement on the database and return the id of the last inserted row.
*
* @param stmt
* The SQL statement to execute
* @param values
* Ordered list of arguments to substitute
* @return A string containing the id of the inserted row.
* @throws DatabaseException
* If there was an error in accessing the database.
*/
public String stmt(String stmt, String[] values)
throws DatabaseException
{
Log.d(LOG_TAG, "stmt '" + stmt + "', " + Arrays.toString(values));
try {
_database.execSQL(stmt, values);
} catch (IllegalStateException e) {
// The database is inaccessible - we tried to run a statement in the background
throw new DatabaseException("Query '" + stmt + "' failed to run.");
}
// get last inserted row id
Cursor cursor = _database.rawQuery("SELECT last_insert_rowid()", null);
if (cursor == null) {
throw new DatabaseException("Query '" + stmt + "' returned null cursor.");
}
cursor.moveToFirst();
long id = cursor.getLong(0);
cursor.close();
return Long.toString(id);
}
private JSONObject getRowDataAsJsonObject(Cursor cursor)
throws SQLiteException, JSONException
{
JSONObject result = new JSONObject();
int count = cursor.getColumnCount();
for (int i = 0; i < count; i++) {
if (!cursor.isNull(i)) {
String columnName = cursor.getColumnName(i).toLowerCase(Locale.ENGLISH);
String value = cursor.getString(i);
result.put(columnName, value);
}
}
return result;
}
}

View file

@ -0,0 +1,257 @@
package org.scratchjr.android;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import org.json.JSONArray;
import org.json.JSONException;
import android.content.Context;
import android.util.Base64;
import android.util.Log;
/**
* Manages file storage for ScratchJr.
*
* Also interfaces with the DatabaseManager to clean assets.
*
* @author markroth8
*/
public class IOManager {
private static final String LOG_TAG = "ScratchJr.IOManager";
private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
private final ScratchJrActivity _application;
private final DatabaseManager _databaseManager;
/** Cache of key to base64-encoded media value */
private final Map<String, String> _mediaStrings = new HashMap<String, String>();
public IOManager(ScratchJrActivity application) {
_application = application;
_databaseManager = application.getDatabaseManager();
}
/**
* Clean any assets that are not referenced in the database
*
* @param fileType The extension of the type of file to clean
*/
public void cleanAssets(String fileType)
throws IOException
{
String suffix = "." + fileType;
Log.i(LOG_TAG, "Cleaning files of type '" + fileType + "'");
File dir = _application.getFilesDir();
for (File file : dir.listFiles()) {
String filename = file.getName();
if (filename.endsWith(suffix)) {
try {
String statement = "SELECT ID FROM PROJECTS WHERE JSON LIKE ?";
String[] values = new String[] { "%" + filename + "%" };
JSONArray rows = _databaseManager.query(statement, values);
if (rows.length() > 0) continue;
statement = "SELECT ID FROM USERSHAPES WHERE MD5 = ?";
values = new String[] { filename };
rows = _databaseManager.query(statement, values);
if (rows.length() > 0) continue;
statement = "SELECT ID FROM USERBKGS WHERE MD5 = ?";
rows = _databaseManager.query(statement, values);
if (rows.length() > 0) continue;
Log.i(LOG_TAG, "Deleting because not found anywhere: '" + filename + "'");
file.delete();
} catch (JSONException e) {
// log and continue searching
Log.e(LOG_TAG, "While searching for resources to delete", e);
} catch (DatabaseException e) {
// log and continue searching
Log.e(LOG_TAG, "While searching for resources to delete", e);
}
}
}
}
/** Sets the file with the given name to the given contents */
public String setFile(String filename, String base64ContentStr)
throws IOException
{
byte[] content = Base64.decode(base64ContentStr, Base64.NO_WRAP);
FileOutputStream out = _application.openFileOutput(filename, Context.MODE_PRIVATE);
try {
out.write(content);
} finally {
out.close();
}
return filename;
}
/** Gets a base64-encoded view of the contents of the given file */
public String getFile(String filename)
throws IOException
{
String result;
InputStream in = _application.openFileInput(filename);
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();
byte[] data = bos.toByteArray();
result = Base64.encodeToString(data, Base64.NO_WRAP);
} finally {
in.close();
}
return result;
}
/**
* Returns the media data associated with the given filename and return the result base64-encoded.
*/
public String getMedia(String filename)
throws IOException
{
String result;
InputStream in = _application.openFileInput(filename);
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();
byte[] data = bos.toByteArray();
result = Base64.encodeToString(data, Base64.NO_WRAP);
} finally {
in.close();
}
return result;
}
/**
* Allows incremental loading of large resources
*/
public String getMediaData(String key, int offset, int length) {
return _mediaStrings.get(key).substring(offset, offset + length);
}
public int getMediaLen(String file, String key)
throws IOException
{
String value = getMedia(file);
_mediaStrings.put(key, value);
return value.length();
}
public void getMediaDone(String filename) {
_mediaStrings.remove(filename);
}
/**
* Store the given content in a file whose filename is constructed using the md5 sum of the base64 content string
* followed by the given extension, and return the filename.
*
* @param base64ContentStr Base64-encoded content to store in the file
* @param extension The extension of the filename to store to
* @return The filename of the file that was saved.
* @throws IOException If there was an error saving the file.
*/
public String setMedia(String base64ContentStr, String extension)
throws IOException
{
String md5Sum = md5(base64ContentStr);
String filename = md5Sum + "." + extension;
byte[] content = Base64.decode(base64ContentStr, Base64.NO_WRAP);
FileOutputStream out = _application.openFileOutput(filename, Context.MODE_PRIVATE);
try {
out.write(content);
} finally {
out.close();
}
return filename;
}
/**
* Writes the given base64-encoded content to a filename with the name key.ext.
*/
public String setMediaName(String base64ContentStr, String key, String ext)
throws IOException
{
String md5 = key + "." + ext;
return writeFile(md5, base64ContentStr);
}
/**
* Decodes the given base64-encoded data and writes it to the file with the given filename
* in the application's persistent store.
*/
public String writeFile(String filename, String base64ContentStr)
throws IOException
{
byte[] content = Base64.decode(base64ContentStr, Base64.NO_WRAP);
FileOutputStream out = _application.openFileOutput(filename, Context.MODE_PRIVATE);
try {
out.write(content);
} finally {
out.close();
}
return filename;
}
/**
* Returns the MD5 sum of the provided content.
*
* @param content The content to sum
* @return A string representation of the MD5 sum
*/
public String md5(String content) {
MessageDigest digester;
try {
digester = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
digester.update(content.getBytes());
byte[] digest = digester.digest();
return bytesToHexString(digest);
}
/**
* Removes (deletes) a file with a given file name.
*
* @param filename The file to remove
* @return True if the file was successfully removed; else false
* @throws IOException If there was an error removing the file
*/
public boolean remove(String filename)
throws IOException
{
return _application.deleteFile(filename);
}
/**
* Borrowed from http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java
*/
public static String bytesToHexString(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
}

View file

@ -0,0 +1,627 @@
package org.scratchjr.android;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Locale;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.RectF;
import android.hardware.Camera;
import android.net.Uri;
import android.text.Html;
import android.util.Base64;
import android.util.Log;
import android.view.inputmethod.InputMethodManager;
import android.webkit.JavascriptInterface;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.Tracker;
/**
* The methods in this inner class are exposed directly to JavaScript in the HTML5 pages
* as AndroidInterface.
*
* @author markroth8
*/
public class JavaScriptDirectInterface {
private static final String LOG_TAG = "ScratchJr.JSDirect";
/** Activity hosting the webview running the JavaScript */
private final ScratchJrActivity _activity;
private final ScratchJrApplication _application;
/** Current camera view, if active */
private CameraView _cameraView;
/** Current camera mask, if active */
private ImageView _cameraMask;
/**
* @param scratchJrActivity
*/
JavaScriptDirectInterface(ScratchJrActivity scratchJrActivity, ScratchJrApplication application) {
_activity = scratchJrActivity;
_application = application;
}
@JavascriptInterface
public void log(String message) {
Log.i(LOG_TAG, message);
}
@JavascriptInterface
public void notifySplashDone() {
Log.i(LOG_TAG, "Splash screen done loading");
_activity.setSplashDone(true);
}
@JavascriptInterface
public void notifyDoneLoading() {
Log.i(LOG_TAG, "Application is done loading");
_activity.setAppInitialized(true);
}
@JavascriptInterface
public void notifyEditorDoneLoading() {
Log.i(LOG_TAG, "Editor is done loading");
_activity.setEditorInitialized(true);
}
//////////////////////////////////////////////////////////////////////
// audio_*
@JavascriptInterface
public void audio_sndfx(String file) {
SoundManager soundManager = _activity.getSoundManager();
soundManager.playSoundEffect(file);
}
@JavascriptInterface
public void audio_sndfxwithvolume(String file, float volume) {
SoundManager soundManager = _activity.getSoundManager();
soundManager.playSoundEffectWithVolume(file, volume);
}
@JavascriptInterface
public int audio_play(String file, float volume) {
SoundManager soundManager = _activity.getSoundManager();
return soundManager.playSound(file);
}
@JavascriptInterface
public boolean audio_isplaying(int soundId) {
SoundManager soundManager = _activity.getSoundManager();
return soundManager.isPlaying(soundId);
}
@JavascriptInterface
public void audio_stop(int soundId) {
SoundManager soundManager = _activity.getSoundManager();
soundManager.stopSound(soundId);
}
//////////////////////////////////////////////////////////////////////
// database_*
@JavascriptInterface
public String database_query(String json) {
String result;
try {
JSONObject obj = new JSONObject(json);
String stmt = obj.getString("stmt");
JSONArray valuesJSONArray = obj.getJSONArray("values");
String[] values = ScratchJrUtil.jsonArrayToStringArray(valuesJSONArray);
DatabaseManager databaseManager = _activity.getDatabaseManager();
result = databaseManager.query(stmt, values).toString();
} catch (JSONException e) {
result = "JSON error: " + e.getMessage();
} catch (DatabaseException e) {
result = "SQL error: " + e.getMessage();
}
return result;
}
@JavascriptInterface
public String database_stmt(String json) {
String result;
try {
JSONObject obj = new JSONObject(json);
String stmt = obj.getString("stmt");
JSONArray valuesJSONArray = obj.getJSONArray("values");
String[] values = ScratchJrUtil.jsonArrayToStringArray(valuesJSONArray);
DatabaseManager databaseManager = _activity.getDatabaseManager();
result = databaseManager.stmt(stmt, values).toString();
} catch (JSONException e) {
result = "JSON error: " + e.getMessage();
} catch (DatabaseException e) {
result = "SQL error: " + e.getMessage();
}
return result;
}
//////////////////////////////////////////////////////////////////////
// io_*
@JavascriptInterface
public String io_getmd5(String str) {
IOManager ioManager = _activity.getIOManager();
return ioManager.md5(str);
}
@JavascriptInterface
public String io_getsettings() {
String homeDirectory = "";
String choice = "";
String soundPermission = (_activity.micPermissionResult == PackageManager.PERMISSION_GRANTED) ? "YES" : "NO";
String cameraPermission = (_activity.cameraPermissionResult == PackageManager.PERMISSION_GRANTED) ? "YES" : "NO";
return homeDirectory + "," + choice + "," + soundPermission + "," + cameraPermission;
}
@JavascriptInterface
public void io_cleanassets(String fileType) {
IOManager ioManager = _activity.getIOManager();
try {
ioManager.cleanAssets(fileType);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not clean assets", e);
}
}
@JavascriptInterface
public String io_setfile(String filename, String base64ContentStr) {
String result;
IOManager ioManager = _activity.getIOManager();
try {
result = ioManager.setFile(filename, base64ContentStr);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not set file '" + filename + "'", e);
result = "-1";
}
return result;
}
@JavascriptInterface
public String io_getfile(String filename) {
String result;
IOManager ioManager = _activity.getIOManager();
try {
result = ioManager.getFile(filename);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not get file '" + filename + "'", e);
result = "";
}
return result;
}
@JavascriptInterface
public String io_setmedia(String base64ContentStr, String extension) {
String result;
IOManager ioManager = _activity.getIOManager();
try {
result = ioManager.setMedia(base64ContentStr, extension);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not set media of type '" + extension + "'", e);
result = "-1";
}
return result;
}
@JavascriptInterface
public String io_setmedianame(String contents, String key, String ext) {
String result;
IOManager ioManager = _activity.getIOManager();
try {
result = ioManager.setMediaName(contents, key, ext);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not set media name of key '" + key + "' ext '" + ext + "'", e);
result = "-1";
}
return result;
}
@JavascriptInterface
public String io_getmedia(String filename) {
String result;
IOManager ioManager = _activity.getIOManager();
try {
result = ioManager.getMedia(filename);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not get media with filename '" + filename + "'", e);
result = "-1";
}
return result;
}
@JavascriptInterface
public String io_getmediadata(String filename, int offset, int length) {
IOManager ioManager = _activity.getIOManager();
return ioManager.getMediaData(filename, offset, length);
}
@JavascriptInterface
public int io_getmedialen(String file, String key) {
int result;
IOManager ioManager = _activity.getIOManager();
try {
result = ioManager.getMediaLen(file, key);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not get media len for file '" + file + "' key '" + key + "'", e);
result = 0;
}
return result;
}
@JavascriptInterface
public String io_getmediadone(String filename) {
IOManager ioManager = _activity.getIOManager();
ioManager.getMediaDone(filename);
return "1";
}
@JavascriptInterface
public String io_remove(String filename) {
String result;
IOManager ioManager = _activity.getIOManager();
Log.d(LOG_TAG, "Trying to remove filename '" + filename + "'");
try {
result = ioManager.remove(filename) ? "1" : "-1";
} catch (IOException e) {
Log.e(LOG_TAG, "Could not remove file '" + filename + "'", e);
result = "-1";
}
return result;
}
//////////////////////////////////////////////////////////////////////
// recordsound_*
@JavascriptInterface
public String recordsound_recordstart() {
SoundRecorderManager soundRecorderManager = _activity.getSoundRecorderManager();
String soundFile = soundRecorderManager.startRecord();
return (soundFile == null) ? "-1" : soundFile;
}
@JavascriptInterface
public String recordsound_recordstop() {
SoundRecorderManager soundRecorderManager = _activity.getSoundRecorderManager();
try {
return soundRecorderManager.stopRecord() ? "1" : "-1";
} catch (Throwable t) {
Log.e(LOG_TAG, "Error stopping recording", t);
return "-1";
}
}
@JavascriptInterface
public double recordsound_volume() {
SoundRecorderManager soundRecorderManager = _activity.getSoundRecorderManager();
return soundRecorderManager.getVolume();
}
@JavascriptInterface
public String recordsound_startplay() {
SoundRecorderManager soundRecorderManager = _activity.getSoundRecorderManager();
try {
return Double.toString(soundRecorderManager.startPlay());
} catch (IllegalStateException e) {
Log.e(LOG_TAG, "Error starting play", e);
return "ERROR: " + e.getMessage();
}
}
@JavascriptInterface
public String recordsound_stopplay() {
SoundRecorderManager soundRecorderManager = _activity.getSoundRecorderManager();
try {
soundRecorderManager.stopPlay();
return "1";
} catch (IllegalStateException e) {
Log.e(LOG_TAG, "Error stopping play", e);
return "-1";
}
}
@JavascriptInterface
public String recordsound_recordclose(String keep) {
boolean keepBoolean = !keep.toLowerCase(Locale.US).equals("no");
SoundRecorderManager soundRecorderManager = _activity.getSoundRecorderManager();
soundRecorderManager.recordClose(keepBoolean);
return keepBoolean ? "1" : "-1";
}
//////////////////////////////////////////////////////////////////////
// scratchjr_*
@JavascriptInterface
public String scratchjr_cameracheck() {
return _activity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) ? "1" : "0";
}
@JavascriptInterface
public boolean scratchjr_has_multiple_cameras() {
return Camera.getNumberOfCameras() > 1;
}
@JavascriptInterface
public String scratchjr_startfeed(String str) {
try {
JSONObject obj = new JSONObject(str);
String imageDataStr = obj.getString("image");
if (!imageDataStr.startsWith("data:image/png")) {
Log.e(LOG_TAG, "Expecting data URL for image data but got '" + imageDataStr + "'");
return "-1";
}
String base64ImageData = imageDataStr.substring(imageDataStr.indexOf(";base64,") + 8);
byte[] imageContent = Base64.decode(base64ImageData, Base64.NO_WRAP);
float x = (float) obj.getDouble("x");
float y = (float) obj.getDouble("y");
float width = (float) obj.getDouble("width");
float height = (float) obj.getDouble("height");
RectF r = new RectF(x, y, x + width, y + height);
float mx = (float) obj.getDouble("mx");
float my = (float) obj.getDouble("my");
float mw = (float) obj.getDouble("mw");
float mh = (float) obj.getDouble("mh");
RectF r2 = new RectF(mx, my, mx + mw, my + mh);
float scale = (float) obj.getDouble("scale");
float devicePixelRatio = (float) obj.getDouble("devicePixelRatio");
openFeed(r, scale, devicePixelRatio, imageContent, r2);
} catch (JSONException e) {
Log.e(LOG_TAG, "Could not decode json: '" + str + "'", e);
return "-1";
}
return "1";
}
@JavascriptInterface
public String scratchjr_stopfeed() {
closeFeed();
return "1";
}
@JavascriptInterface
public String scratchjr_choosecamera(String facing) {
String result = "-1";
if (_cameraView != null) {
if (_cameraView.setCameraFacing(facing.equals("front"))) {
result = "1";
} else {
result = "-1";
}
}
return result;
}
@JavascriptInterface
public void scratchjr_captureimage(final String onCameraCaptureComplete) {
_cameraView.captureStillImage(
new Camera.PictureCallback() {
public void onPictureTaken(byte[] jpegData, Camera camera) {
sendBase64Image(onCameraCaptureComplete, jpegData);
}
},
new Runnable() {
public void run() {
Log.e(LOG_TAG, "Could not capture picture");
reportImageError(onCameraCaptureComplete);
}
}
);
}
@JavascriptInterface
public String scratchjr_getgettingstartedvideopath() {
File cacheDir = _activity.getCacheDir();
File videoFile = new File(cacheDir, "intro.mp4");
if (!videoFile.exists()) {
copyVideoToCacheDir();
}
if (!videoFile.exists()) {
Log.w(LOG_TAG, "Video file does not exist after copying: '" + videoFile.getPath() + "'");
}
return videoFile.getPath();
}
@JavascriptInterface
public String scratchjr_stopserver() {
// On Android, we don't use an HTTP server - everything gets invoked directly.
return "1";
}
@JavascriptInterface
public void scratchjr_setsoftkeyboardscrolllocation(int topYPx, int bottomYPx) {
_activity.setSoftKeyboardScrollLocation(topYPx, bottomYPx);
}
/**
* Pop up the soft keyboard if this is not a hardware-keyboard device.
*/
@JavascriptInterface
public void scratchjr_forceShowKeyboard() {
InputMethodManager mgr = (InputMethodManager) _activity.getSystemService(Context.INPUT_METHOD_SERVICE);
mgr.showSoftInput(_activity.getCurrentFocus(), InputMethodManager.SHOW_IMPLICIT);
}
/**
* Hide the soft keyboard if this is not a hardware-keyboard device.
*/
@JavascriptInterface
public void scratchjr_forceHideKeyboard() {
InputMethodManager mgr = (InputMethodManager) _activity.getSystemService(Context.INPUT_METHOD_SERVICE);
mgr.hideSoftInputFromWindow(_activity.getCurrentFocus().getWindowToken(), 0);
}
private void sendBase64Image(String onCameraCaptureComplete, byte[] jpegData) {
Bitmap bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
Log.i(LOG_TAG, "Picture size: " + bitmap.getWidth() + " x " + bitmap.getHeight());
byte[] translatedJpegData = _cameraView.getTransformedImage(bitmap);
String base64Data = Base64.encodeToString(translatedJpegData, Base64.NO_WRAP);
closeFeed();
_activity.runJavaScript(onCameraCaptureComplete + "('" + base64Data + "');");
}
private void reportImageError(String onCameraCaptureComplete) {
_activity.runJavaScript(onCameraCaptureComplete + "('error getting a still');");
}
private void openFeed(final RectF rect, final float scale, final float devicePixelRatio, final byte[] maskImageData,
final RectF maskRect)
{
_activity.runOnUiThread(new Runnable() {
public void run() {
_activity.translateAndScaleRectToContainerCoords(rect, devicePixelRatio);
_activity.translateAndScaleRectToContainerCoords(maskRect, devicePixelRatio);
scaleRectFromCenter(rect, scale);
scaleRectFromCenter(maskRect, scale);
RelativeLayout container = _activity.getContainer();
_cameraView = new CameraView(_activity, rect, scale * devicePixelRatio, true); // always start with front-facing camera
container.addView(_cameraView, new RelativeLayout.LayoutParams((int) (rect.width()), (int) (rect.height())));
_cameraView.setX(rect.left);
_cameraView.setY(rect.top);
_cameraMask = new ImageView(_activity);
Bitmap bitmap = BitmapFactory.decodeByteArray(maskImageData, 0, maskImageData.length);
_cameraMask.setImageBitmap(bitmap);
container.addView(_cameraMask, new RelativeLayout.LayoutParams((int) (maskRect.width()), (int) (maskRect.height())));
_cameraMask.setX(maskRect.left);
_cameraMask.setY(maskRect.top);
}
});
}
private void scaleRectFromCenter(RectF rect, float scale) {
float deltaWidth = rect.width() * scale - rect.width();
float deltaHeight = rect.height() * scale - rect.height();
rect.left -= deltaWidth / 2;
rect.top -= deltaHeight / 2;
rect.right += deltaWidth / 2;
rect.bottom += deltaHeight / 2;
}
private void closeFeed() {
_activity.runOnUiThread(new Runnable() {
public void run() {
RelativeLayout container = _activity.getContainer();
if (_cameraView != null) {
container.removeView(_cameraView);
_cameraView = null;
}
if (_cameraMask != null) {
container.removeView(_cameraMask);
_cameraMask = null;
}
}
});
}
private void copyVideoToCacheDir() {
InputStream in = null;
OutputStream out = null;
try {
File cacheDir = _activity.getCacheDir();
File videoFile = new File(cacheDir, "intro.mp4");
byte[] buffer = new byte[1024];
int len;
in = _activity.getAssets().open("HTML5/assets/lobby/intro.mp4");
out = new FileOutputStream(videoFile);
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
} catch (IOException e) {
Log.e(LOG_TAG, "Could not copy video to cache dir", e);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Could not close input stream while copying video file", e);
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Could not close output stream while copying video file", e);
}
}
}
}
//////////////////////////////////////////////////////////////////////
// Sharing
@JavascriptInterface
public String deviceName() {
return android.os.Build.MODEL;
}
@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;
String extension;
if (BuildConfig.APPLICATION_ID.equals("org.pbskids.scratchjr")) {
extension = ".psjr";
} else {
extension = ".sjr";
}
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;
}
final Intent it = new Intent(Intent.ACTION_SEND);
it.setType("message/rfc822");
it.putExtra(android.content.Intent.EXTRA_EMAIL, new String[] {});
it.putExtra(android.content.Intent.EXTRA_SUBJECT, emailSubject);
it.putExtra(android.content.Intent.EXTRA_TEXT, Html.fromHtml(emailBody));
// The stream data is a reference to the temporary file provided by our contentprovider
it.putExtra(Intent.EXTRA_STREAM,
Uri.parse("content://" + ShareContentProvider.AUTHORITY + "/"
+ fileName));
_activity.startActivity(it);
}
// Analytics
@JavascriptInterface
public void analyticsEvent(String category, String action, String label, long value) {
_application.getDefaultTracker().send(new HitBuilders.EventBuilder()
.setCategory(category).setAction(action).setLabel(label).setValue(value).build());
}
}

View file

@ -0,0 +1,578 @@
package org.scratchjr.android;
import android.Manifest;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.graphics.Rect;
import android.graphics.RectF;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Base64;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnSystemUiVisibilityChangeListener;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.Window;
import android.view.WindowManager;
import android.webkit.ConsoleMessage;
import android.webkit.CookieManager;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.RelativeLayout;
import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.Tracker;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
/**
* Main activity for Scratch Jr., consisting of a full-screen landscape WebView.
*
* This activity creates an embedded WebView, which runs the HTML5 app containing the majority of the source code.
*
* Special thanks to Benesse Corp. for providing access to their Android port, which helped inspire some of the source code here.
*
* @author markroth8
*/
public class ScratchJrActivity
extends Activity
{
/** Milliseconds to pan when showing the soft keyboard */
private static final int SOFT_KEYBOARD_PAN_MS = 250;
/** Log tag for Scratch Jr. app */
private static final String LOG_TAG = "ScratchJr";
/** Bundle key in which the current url is stored */
private static final String BUNDLE_KEY_URL = "url";
/** The url of the index page */
private static final String INDEX_PAGE_URL = "file:///android_asset/HTML5/index.html";
/** Container containing the web view */
private RelativeLayout _container;
/** Web browser containing the Scratch Jr. HTML5 webapp */
private WebView _webView;
/** Maintains connection to database */
private DatabaseManager _databaseManager;
/** Performs file IO */
private IOManager _ioManager;
/** Manages sounds */
private SoundManager _soundManager;
/** Manages recording of new sounds */
private SoundRecorderManager _soundRecorderManager;
/** Set to true when the app is initialized. This is used for unit testing. */
private boolean _appInitialized = false;
/** Set to true when the editor is initialized. This is used for unit testing. */
private boolean _editorInitialized = false;
/** Set to true when the splash screen is done loading. This is used for unit testing. */
private boolean _splashDone = false;
/** Y starting and ending coordinate for soft keyboard scroll position */
private int _softKeyboardScrollPosY0;
private int _softKeyboardScrollPosY1;
/** Handler for posting delayed updates */
private final Handler _handler = new Handler();
/** Run-time Permissions */
private final int SCRATCHJR_CAMERA_MIC_PERMISSION = 1;
public int cameraPermissionResult = PackageManager.PERMISSION_DENIED;
public int micPermissionResult = PackageManager.PERMISSION_DENIED;
/** Analytics tracker */
private Tracker _tracker;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
_databaseManager = new DatabaseManager(this);
_ioManager = new IOManager(this);
_soundManager = new SoundManager(this);
_soundRecorderManager = new SoundRecorderManager(this);
setContentView(R.layout.activity_scratch_jr);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
_container = (RelativeLayout) findViewById(R.id.container);
_webView = (WebView) findViewById(R.id.webview);
_webView.setBackgroundColor(0x00000000);
_webView.clearCache(true);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
Log.i(LOG_TAG, "Setting non-immersive full screen");
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else {
setImmersiveMode();
}
configureWebView();
registerSoftKeyboardPanner();
/* URL to load once ready */
String urlToLoad;
if ((savedInstanceState != null) && savedInstanceState.containsKey(BUNDLE_KEY_URL)) {
Log.i(LOG_TAG, "Restoring bundle state...");
urlToLoad = savedInstanceState.getString(BUNDLE_KEY_URL);
if (urlToLoad == null) {
urlToLoad = INDEX_PAGE_URL;
}
} else {
urlToLoad = INDEX_PAGE_URL;
}
_webView.loadUrl(urlToLoad);
CookieManager.getInstance().setAcceptCookie(true);
CookieManager.setAcceptFileSchemeCookies(true);
String PROJECT_MIMETYPE = getApplicationContext().getString(R.string.share_mimetype);
Intent it = getIntent();
if (it != null && it.getType() != null && it.getType().equals(PROJECT_MIMETYPE)) {
receiveProject(it.getData());
}
ScratchJrApplication application = (ScratchJrApplication) getApplication();
_tracker = application.getDefaultTracker();
// When System UI bar is displayed, wait one second and then re-assert immersive mode.
getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(new OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int visibility) {
_handler.postDelayed(new Runnable() {
public void run() {
runOnUiThread(new Runnable() {
public void run() {
setImmersiveMode();
}
});
}
}, 1000);
}
});
requestPermissions();
}
public void requestPermissions() {
cameraPermissionResult = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA);
micPermissionResult = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
String[] desiredPermissions;
if (cameraPermissionResult != PackageManager.PERMISSION_GRANTED
&& micPermissionResult != PackageManager.PERMISSION_GRANTED) {
desiredPermissions = new String[]{
Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO
};
} else if (cameraPermissionResult != PackageManager.PERMISSION_GRANTED) {
desiredPermissions = new String[]{Manifest.permission.CAMERA};
} else if (micPermissionResult != PackageManager.PERMISSION_GRANTED) {
desiredPermissions = new String[]{Manifest.permission.RECORD_AUDIO};
} else {
return;
}
ActivityCompat.requestPermissions(this,
desiredPermissions,
SCRATCHJR_CAMERA_MIC_PERMISSION);
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
if (requestCode == SCRATCHJR_CAMERA_MIC_PERMISSION) {
int permissionId = 0;
for (String permission : permissions) {
if (permission.equals(Manifest.permission.CAMERA)) {
cameraPermissionResult = grantResults[permissionId];
}
if (permission.equals(Manifest.permission.RECORD_AUDIO)) {
micPermissionResult = grantResults[permissionId];
}
permissionId++;
}
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && hasFocus) {
setImmersiveMode();
}
}
@Override
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
// Check the WebView to see if we're on the editor page.
// If so, call the JavaScript to save the current project
// and return to the lobby.
final String url = _webView.getUrl();
if (url != null) {
Log.i(LOG_TAG, url);
if (url.contains("home.html")) {
runJavaScript("Lobby.goHome()");
} else if (url.contains("gettingstarted.html")) {
runJavaScript("closeme()");
} else if (url.contains("index.html")) {
finish();
} else if (url.contains("editor.html")) {
runJavaScript("ScratchJr.goBack()");
} else if (_webView.canGoBack()) {
_webView.goBack();
}
}
return true;
}
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onResume() {
super.onResume();
_databaseManager.open();
_soundManager.open();
_soundRecorderManager.open();
runOnUiThread(new Runnable() {
@Override
public void run() {
_webView.onResume();
}
});
runJavaScript("if (typeof(ScratchJr) !== 'undefined') ScratchJr.onResume();");
}
@Override
protected void onPause() {
super.onPause();
runJavaScript("if (typeof(ScratchJr) !== 'undefined') ScratchJr.onPause();");
runOnUiThread(new Runnable() {
@Override
public void run() {
_webView.onPause();
}
});
_databaseManager.close();
_soundManager.close();
_soundRecorderManager.close();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(BUNDLE_KEY_URL, _webView.getUrl());
}
@Override
protected void onNewIntent(Intent it) {
super.onNewIntent(it);
String PROJECT_MIMETYPE = getApplicationContext().getString(R.string.share_mimetype);
if (it != null && it.getType() != null && it.getType().equals(PROJECT_MIMETYPE)) {
receiveProject(it.getData());
}
}
private void receiveProject(Uri projectUri) {
// 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]);
}
} 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("iOS.loadProjectFromSjr('" + base64Project + "');");
}
public RelativeLayout getContainer() {
return _container;
}
public DatabaseManager getDatabaseManager() {
return _databaseManager;
}
public IOManager getIOManager() {
return _ioManager;
}
public SoundManager getSoundManager() {
return _soundManager;
}
public SoundRecorderManager getSoundRecorderManager() {
return _soundRecorderManager;
}
private void setImmersiveMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.i(LOG_TAG, "Setting immersive mode");
int immersiveStickyFlag = 0;
try {
immersiveStickyFlag = View.class.getField("SYSTEM_UI_FLAG_IMMERSIVE_STICKY").getInt(null);
} catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException e) {
Log.e(LOG_TAG, "Reflection fail", e);
}
_webView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| immersiveStickyFlag);
}
}
@SuppressLint("SetJavaScriptEnabled")
private void configureWebView() {
WebSettings webSettings = _webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setBuiltInZoomControls(false);
webSettings.setDisplayZoomControls(false);
webSettings.setLoadWithOverviewMode(false);
webSettings.setUseWideViewPort(false);
// Uncomment to enable remote Chrome debugging on a physical Android device
//WebView.setWebContentsDebuggingEnabled(true);
/* Object exposed to the JavaScript that makes it easy to bridge JavaScript and Java */
JavaScriptDirectInterface javaScriptDirectInterface = new JavaScriptDirectInterface(this, (ScratchJrApplication) getApplication());
_webView.addJavascriptInterface(javaScriptDirectInterface, "AndroidInterface");
_webView.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
Log.e(LOG_TAG, description);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// Filter out Internet links and open those with the Android browser
if (url != null && (url.startsWith("http://") || url.startsWith("https://"))) {
view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
return false; // Allow WebView to load url
}
@Override
public void onPageFinished(WebView view, String url) {
String[] parts = url.split("/");
_tracker.setScreenName(parts[parts.length - 1]);
_tracker.send(new HitBuilders.ScreenViewBuilder().build());
}
});
_webView.requestFocus(View.FOCUS_DOWN);
webSettings.setAllowFileAccess(true);
webSettings.setAllowFileAccessFromFileURLs(true);
webSettings.setAllowUniversalAccessFromFileURLs(true);
webSettings.setAllowContentAccess(true);
// Configure the web chrome client to consume console.log
// calls from JavaScript.
Log.i(LOG_TAG, "Configurating webChromeClient");
_webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onConsoleMessage(@NonNull ConsoleMessage cm) {
Log.e(LOG_TAG, "JavaScript log, " + cm.sourceId() + ":" + cm.lineNumber() + ", " + cm.message());
return true;
}
});
}
public void createNewProject() {
runJavaScript("Home.createNewProject()");
}
/**
* Returns true when all resources are loaded and the app is initialized.
*/
public boolean isAppInitialized() {
return _appInitialized;
}
public void setAppInitialized(boolean initialized) {
_appInitialized = initialized;
}
public boolean isEditorInitialized() {
return _editorInitialized;
}
public void setEditorInitialized(boolean initialized) {
_editorInitialized = initialized;
}
public boolean isSplashDone() {
return _splashDone;
}
public void setSplashDone(boolean done) {
_splashDone = done;
}
/**
* Click the "Home" button on the title screen (for unit testing).
*/
public void goHome() {
runJavaScript("gohome()");
}
/**
* Run the given JavaScript in the web view.
*/
public void runJavaScript(final String js) {
runOnUiThread(new Runnable() {
@Override
public void run() {
_webView.loadUrl("javascript:" + js);
}
});
}
public void translateAndScaleRectToContainerCoords(RectF rect, float devicePixelRatio) {
float wx = _webView.getX();
float wy = _webView.getY();
rect.set(wx + rect.left * devicePixelRatio, wy + rect.top * devicePixelRatio,
wx + rect.right * devicePixelRatio, wy + rect.bottom * devicePixelRatio);
}
public void setSoftKeyboardScrollLocation(int topYPx, int bottomYPx) {
_softKeyboardScrollPosY0 = topYPx;
_softKeyboardScrollPosY1 = bottomYPx;
}
/**
* Height of the status bar at the top of the screen
*/
private int getStatusBarHeight() {
Rect rectangle= new Rect();
Window window= getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
int result = rectangle.top;
return result;
}
/**
* Android does not properly pan to the text fields in full screen mode, so
* here we introduce some custom logic to pan when the soft keyboard appears.
*
* The technique used here was inspired by http://stackoverflow.com/questions/7417123/
* android-how-to-adjust-layout-in-full-screen-mode-when-softkeyboard-is-visible
*/
private void registerSoftKeyboardPanner() {
_container.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
private int _priorVisibleHeight;
private ObjectAnimator _currentAnimator;
@Override
public void onGlobalLayout() {
Rect r = new Rect();
_container.getWindowVisibleDisplayFrame(r);
int currentVisibleHeight = r.bottom - r.top;
// Determine if visible height changed
if (currentVisibleHeight != _priorVisibleHeight) {
// Determine if keyboard visibility changed
int screenHeight = _container.getRootView().getHeight();
int coveredHeight = screenHeight - currentVisibleHeight;
if((currentVisibleHeight < _priorVisibleHeight) && (coveredHeight > (screenHeight / 4))) {
// Keyboard probably just became visible
// Get the current focus elements top & bottom using a ratio to convert the values
// to the native scale.
int elTop = _softKeyboardScrollPosY0;
int elBottom = _softKeyboardScrollPosY1;
// Determine the amount of the focus element covered by the keyboard
int elPixelsCovered = elBottom - currentVisibleHeight;
// If any amount is covered
if (elPixelsCovered > 0) {
// Pan by the amount of coverage
int panUpPixels = elPixelsCovered;
// Prevent panning so much the top of the element becomes hidden
panUpPixels = panUpPixels > elTop ? elTop : panUpPixels;
// Prevent panning more than the keyboard height (which produces an empty gap in the screen)
int statusBarHeight = getStatusBarHeight();
panUpPixels = panUpPixels > (coveredHeight - statusBarHeight) ?
(coveredHeight - statusBarHeight) : panUpPixels;
// Pan up
cancelAnimator();
ObjectAnimator animator = ObjectAnimator.ofFloat(_container, "y", _container.getY(), -panUpPixels).
setDuration(SOFT_KEYBOARD_PAN_MS);
animator.start();
_currentAnimator = animator;
}
else {
cancelAnimator();
ObjectAnimator animator = ObjectAnimator.ofFloat(_container, "y", _container.getY(),
getStatusBarHeight()).setDuration(SOFT_KEYBOARD_PAN_MS);
animator.start();
_currentAnimator = animator;
}
} else if (currentVisibleHeight > _priorVisibleHeight) {
// Keyboard probably just became hidden
// Reset pan
cancelAnimator();
ObjectAnimator animator = ObjectAnimator.ofFloat(_container, "y", _container.getY(), 0).
setDuration(SOFT_KEYBOARD_PAN_MS);
animator.start();
_currentAnimator = animator;
setImmersiveMode();
runJavaScript("if (typeof(ScratchJr) !== 'undefined') ScratchJr.editDone();");
}
// Save usable height for the next comparison
_priorVisibleHeight = currentVisibleHeight;
}
}
private void cancelAnimator() {
ObjectAnimator animator = _currentAnimator;
if (animator != null) {
if (animator.isStarted()) {
animator.cancel();
}
_currentAnimator = null;
}
}
});
}
}

View file

@ -0,0 +1,24 @@
package org.scratchjr.android;
import android.app.Application;
import com.google.android.gms.analytics.GoogleAnalytics;
import com.google.android.gms.analytics.Logger;
import com.google.android.gms.analytics.Tracker;
public class ScratchJrApplication extends Application {
private Tracker mTracker;
/**
* Gets the default {@link Tracker} for this {@link Application}.
* @return tracker
*/
synchronized public Tracker getDefaultTracker() {
if (mTracker == null) {
GoogleAnalytics analytics = GoogleAnalytics.getInstance(this);
// To enable debug logging use: adb shell setprop log.tag.GAv4 DEBUG
mTracker = analytics.newTracker(R.xml.global_tracker);
}
return mTracker;
}
}

View file

@ -0,0 +1,28 @@
package org.scratchjr.android;
import org.json.JSONArray;
import org.json.JSONException;
/**
* General utility class with static utility methods.
*
* @author markroth8
*/
public class ScratchJrUtil {
/** Utility class private constructor so nobody creates an instance of this class */
private ScratchJrUtil() {
}
/**
* Convert the given JSONArray to an array of Strings.
*/
public static String[] jsonArrayToStringArray(JSONArray values)
throws JSONException
{
String[] result = new String[values.length()];
for (int i = 0; i < values.length(); i++) {
result[i] = values.getString(i);
}
return result;
}
}

View file

@ -0,0 +1,64 @@
package org.scratchjr.android;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import java.io.File;
import java.io.FileNotFoundException;
// Special thanks to stephendnicholas.com for a reference implementation
public class ShareContentProvider extends ContentProvider {
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".ShareContentProvider";
public ShareContentProvider() {
}
@Override
public String getType(Uri uri) {
if (BuildConfig.APPLICATION_ID.equals("org.pbskids.scratchjr")) {
return "application/x-pbskids-scratchjr-project";
}
return "application/x-scratchjr-project";
}
@Override
public boolean onCreate() {
return true;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
// Provide a read-only file descriptor for the shared file
String fileLocation = getContext().getCacheDir() + File.separator
+ uri.getLastPathSegment();
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(
fileLocation), ParcelFileDescriptor.MODE_READ_ONLY);
return pfd;
}
// Unimplemented methods since we're only providing a file reference
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return -1;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return -1;
}
}

View file

@ -0,0 +1,285 @@
package org.scratchjr.android;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.SoundPool;
import android.util.Log;
import android.util.SparseArray;
/**
* Manages sound playing for ScratchJr.
*
* @author markroth8
*/
public class SoundManager {
private static final String LOG_TAG = "ScratchJr.SoundManager";
/** Reference to the activity */
private ScratchJrActivity _application;
/** Pool of pre-loaded sound effects */
private SoundPool _soundEffectPool;
/** Maps filename to sound id in the sound effect pool */
private Map<String, Integer> _soundEffectMap = new HashMap<String, Integer>();
/** Active sounds playing currently */
private SparseArray<MediaPlayer> _activeSoundMap = new SparseArray<MediaPlayer>();
/** Running count of active sounds, so each one has a unique id */
private int _activeSoundCount = 0;
/** Set of assets in the HTML5 directory (cached for performance) */
private final Set<String> _html5AssetList;
public SoundManager(ScratchJrActivity application) {
_application = application;
Set<String> assetList;
try {
assetList = new HashSet<String>(listHTML5Assets(application));
} catch (IOException e) {
Log.e(LOG_TAG, "Could not retrieve list of assets from application", e);
assetList = Collections.emptySet();
}
_html5AssetList = assetList;
loadSoundEffects();
}
/**
* Play the sound at the given path, interrupting any current sound.
*/
public synchronized void playSoundEffect(String name) {
playSoundEffectWithVolume(name, 1.0f);
}
/**
* Play the sound at the given path, with the given volume, interrupting any current sound.
*/
public synchronized void playSoundEffectWithVolume(String name, float volume) {
if (_soundEffectPool == null) {
Log.e(LOG_TAG, "Sound effect pool is closed. Cannot play '" + name + "' right now.");
} else {
Integer soundId = _soundEffectMap.get(name);
if (soundId == null) {
Log.e(LOG_TAG, "Could not find sound effect '" + name + "'");
} else {
_soundEffectPool.play(soundId, volume, volume, 0, 0, 1.0f);
}
}
}
/**
* Play the given sound and return an id that can be used to stop the sound later.
*
* @param file Path to sound to play. If relative, sound will come from assets HTML5/ directory, else sound
* comes from an absolute path.
* @return An id which can be used to stop the sound later.
*/
public synchronized int playSound(String file) {
if (file.equals("pop.mp3")) {
// We special-case pop.mp3 because it is easier to get it to play as a sound effect than a sound resource
playSoundEffect(file);
return -1;
}
final int result = _activeSoundCount++;
MediaPlayer player = new MediaPlayer();
_activeSoundMap.put(result, player);
try {
// If file starts with / it is an absolute path and use it.
// Otherwise, path is relative, and find path relative to assets HTML5 directory.
FileDescriptor fd;
long startOffset;
long length;
final Runnable closeTask;
if (file.startsWith("/")) {
final FileInputStream fis = new FileInputStream(file);
fd = fis.getFD();
startOffset = 0;
length = new File(file).length();
final String finalFile = file;
closeTask = new Runnable() {
public void run() {
try {
fis.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Could not close file '" + finalFile + "'", e);
}
}
};
} else if (_html5AssetList.contains(file)) {
final String path = "HTML5/" + file;
final AssetFileDescriptor afd = _application.getAssets().openFd(path);
fd = afd.getFileDescriptor();
startOffset = afd.getStartOffset();
length = afd.getLength();
closeTask = new Runnable() {
public void run() {
try {
afd.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Could not close asset '" + path + "'", e);
}
}
};
} else {
final File soundFile = new File(_application.getFilesDir(), file);
final FileInputStream in = new FileInputStream(soundFile);
fd = in.getFD();
startOffset = 0;
length = soundFile.length();
closeTask = new Runnable() {
public void run() {
try {
in.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Could not close asset '" + soundFile + "'", e);
}
}
};
}
player.setDataSource(fd, startOffset, length);
player.prepare();
player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
public void onCompletion(MediaPlayer mp) {
synchronized (SoundManager.this) {
mp.release();
_activeSoundMap.remove(result);
closeTask.run();
}
}
});
player.start();
} catch (IllegalArgumentException e) {
Log.e(LOG_TAG, "Could not play sound '" + file + "'", e);
} catch (IllegalStateException e) {
Log.e(LOG_TAG, "Could not play sound '" + file + "'", e);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not play sound '" + file + "'", e);
}
return result;
}
/**
* Returns true if the sound for the given id is playing, or false if not.
*/
public synchronized boolean isPlaying(int soundId) {
boolean result = false;
MediaPlayer player = _activeSoundMap.get(soundId);
if (player != null) {
try {
result = player.isPlaying();
} catch (IllegalStateException e) {
result = false;
}
}
return result;
}
/**
* Returns the number of milliseconds long the given sound is, in duration.
*
* @throws IllegalArgumentException If there was no sound with the provided sound id.
*/
public synchronized int soundDuration(int soundId)
throws IllegalArgumentException
{
int result;
MediaPlayer player = _activeSoundMap.get(soundId);
if (player != null) {
result = player.getDuration();
} else {
throw new IllegalArgumentException("No sound found for id '" + soundId + "'");
}
return result;
}
/**
* Stop the sound with the given id.
*
* @param id The id of the sound to stop. If already stopped, does nothing.
*/
public synchronized void stopSound(int soundId) {
MediaPlayer player = _activeSoundMap.get(soundId);
if (player != null) {
player.stop();
}
}
/**
* Load all sound effects
*/
public synchronized void open() {
loadSoundEffects();
}
/**
* Release all resources
*/
public synchronized void close() {
releaseSoundEffects();
}
/**
* Release the media player if it already exists.
*/
private void releaseSoundEffects() {
if (_soundEffectPool != null) {
_soundEffectPool.release();
_soundEffectPool = null;
_soundEffectMap.clear();
}
_activeSoundMap.clear();
}
private void loadSoundEffects() {
if (_soundEffectPool == null) {
_soundEffectPool = new SoundPool(11, AudioManager.STREAM_MUSIC, 0);
// Load all sound effects into memory
AssetManager assetManager = _application.getAssets();
try {
String[] soundEffects = assetManager.list("HTML5/sounds");
loadSoundEffects(assetManager, "HTML5/sounds/", soundEffects);
loadSoundEffects(assetManager, "HTML5/", "pop.mp3");
} catch (IOException e) {
Log.e(LOG_TAG, "Could not list sound assets", e);
}
}
}
private void loadSoundEffects(AssetManager assetManager, String basePath, String... soundEffects)
throws IOException
{
for (String filename : soundEffects) {
AssetFileDescriptor fd = assetManager.openFd(basePath + filename);
int soundId = _soundEffectPool.load(fd, 1);
_soundEffectMap.put(filename, soundId);
}
}
private List<String> listHTML5Assets(ScratchJrActivity application)
throws IOException
{
ArrayList<String> result = new ArrayList<String>();
result.addAll(Arrays.asList(application.getAssets().list("HTML5")));
for (String path : application.getAssets().list("HTML5/samples")) {
result.add("samples/" + path);
}
return result;
}
}

View file

@ -0,0 +1,334 @@
package org.scratchjr.android;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.nio.channels.FileChannel;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Log;
/**
* Manages sound recording for ScratchJr.
*
* @author markroth8
*/
public class SoundRecorderManager {
private static final String LOG_TAG = "ScratchJr.SoundRecorderManager";
// Recording parameters
private static final int SAMPLE_RATE_IN_HZ_DEVICE = 22050;
private static final int SAMPLE_RATE_IN_HZ_EMULATOR = 8000; // Emulator only supports 8Khz
private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
/** Reference to the activity */
private ScratchJrActivity _application;
/** True if there is a microphone present on this device, false if not. */
private final boolean _hasMicrophone;
/** Android AudioRecorder */
private AudioRecord _audioRecorder = null;
/** True if running in emulator (and only 8000 Hz supported) or false if not */
private final boolean _runningInEmulator = Build.PRODUCT.startsWith("sdk");
/** Sample rate chosen based on whether running in emulation */
private final int _sampleRateHz = _runningInEmulator ? SAMPLE_RATE_IN_HZ_EMULATOR : SAMPLE_RATE_IN_HZ_DEVICE;
/** Minimum buffer size, based on sample rate */
private final int _minBufferSize = Math.max(640, AudioRecord.getMinBufferSize(_sampleRateHz, CHANNEL_CONFIG, AUDIO_FORMAT));
/** Current file being recorded to */
private File _soundFile;
/** RandomAccessFile for the file being recorded to */
private RandomAccessFile _soundRandomAccessFile;
/** Channel pointing to the file to be written to */
private FileChannel _soundFileChannel;
/** Buffer into which to read data */
private final ByteBuffer _audioBuffer = ByteBuffer.allocateDirect(_minBufferSize).order(ByteOrder.LITTLE_ENDIAN);
/** Short view into audio buffer */
private final ShortBuffer _audioBufferShort = _audioBuffer.asShortBuffer();
/** Thread that is recording audio */
private final ExecutorService _audioRecordExecutor = Executors.newSingleThreadExecutor();
/** Future of the audio thread in progress */
private Future<Void> _audioWriterTask;
/** Buffer for WAV header */
private final ByteBuffer _wavHeaderBuffer = ByteBuffer.allocate(44).order(ByteOrder.LITTLE_ENDIAN);
/** Id of sound currently playing */
private Integer _soundPlayingId;
/** Current volume level detected during recording, with slow decay */
private volatile double _slowDecayVolumeLevel = 0.0;
public SoundRecorderManager(ScratchJrActivity application) {
Log.i(LOG_TAG, Build.PRODUCT + " Using audio sample rate " + _sampleRateHz + " Hz");
_application = application;
_hasMicrophone = _application.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_MICROPHONE);
if (!_hasMicrophone) {
Log.i(LOG_TAG, "No microphone detected. Sound recording will be disabled.");
}
}
public boolean hasMicrophone() {
return _hasMicrophone;
}
/** Called when application starts / resumes */
public synchronized void open() {
}
/** Called when application sleeps */
public synchronized void close() {
releaseAudioRecorder();
stopPlayingSound();
}
/**
* Returns the sound name or null if error.
*/
public synchronized String startRecord() {
if (!_hasMicrophone) return null;
String result;
releaseAudioRecorder();
stopPlayingSound();
_audioRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC, _sampleRateHz,
CHANNEL_CONFIG, AUDIO_FORMAT, _minBufferSize * 16);
// Mimic filename from iOS: time in seconds since 1970 as a double. Name is the md5 of the time.
String now = String.format(Locale.US, "%f", System.currentTimeMillis() / 1000.0);
String filename = String.format("SND%s.wav", _application.getIOManager().md5(now));
_soundFile = new File(_application.getFilesDir(), filename);
File parentDir = _soundFile.getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}
Log.i(LOG_TAG, "Saving audio to file '" + _soundFile.getPath() + "'");
try {
_soundRandomAccessFile = new RandomAccessFile(_soundFile, "rw");
_soundFileChannel = _soundRandomAccessFile.getChannel();
writeWAVHeader(_soundFileChannel, _sampleRateHz);
_audioRecorder.startRecording();
_audioWriterTask = _audioRecordExecutor.submit(new Runnable() {
public void run() {
String filename = _soundFile.getPath();
long totalBytesWritten = 0;
try {
AudioRecord ar = _audioRecorder;
RandomAccessFile raf = _soundRandomAccessFile;
ByteBuffer buffer = _audioBuffer;
ShortBuffer shortBuffer = _audioBufferShort; // Little-endian buffer
FileChannel c = _soundFileChannel;
while (true) {
// Read from buffer
buffer.rewind().limit(buffer.capacity());
int len = ar.read(buffer, _minBufferSize);
if ((len == -1) || ((len == 0) && (ar.getRecordingState() == AudioRecord.RECORDSTATE_STOPPED))) {
break;
}
if (len == AudioRecord.ERROR_BAD_VALUE) {
Log.e(LOG_TAG, "AudioRecord.read() returned BAD_VALUE");
break;
}
if (len == AudioRecord.ERROR_INVALID_OPERATION) {
Log.e(LOG_TAG, "AudioRecord.read() returned INVALID_OPERATION");
break;
}
// Write to file
buffer.rewind().limit(len);
c.write(buffer);
// Get current volume level (max of all samples taken this period)
shortBuffer.rewind();
int max = 0;
while (shortBuffer.hasRemaining()) {
int s = Math.abs(shortBuffer.get());
if (s > max) {
max = s;
}
}
_slowDecayVolumeLevel = Math.max(1.0 * max / Short.MAX_VALUE, _slowDecayVolumeLevel * 0.85);
totalBytesWritten += len;
}
updateWAVHeader(raf, _soundFileChannel, totalBytesWritten);
c.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Error writing wav file '" + filename + "'", e);
}
}
}, null);
result = filename;
} catch (IOException e) {
Log.e(LOG_TAG, "Error opening wav file '" + _soundFile.getPath() + "'", e);
result = null;
}
return result;
}
private void writeWAVHeader(FileChannel fileChannel, int sampleRateHz)
throws IOException
{
ByteBuffer b = _wavHeaderBuffer;
b.rewind().limit(b.capacity());
b.put("RIFF".getBytes());
long totalDataLen = 0; // Placeholder until all audio is recorded
b.putInt((int) totalDataLen);
b.put("WAVE".getBytes());
b.put("fmt ".getBytes());
b.putInt(16); // size of fmt chunk
b.putShort((short) 1); // format = 1 (PCM)
short channels = (short) 1;
b.putShort(channels);
b.putInt(sampleRateHz);
short bitsPerSample = 16;
short bytesPerSample = (short) (bitsPerSample / 8);
b.putInt(sampleRateHz * channels * bytesPerSample);
b.putShort((short) (channels * bytesPerSample));
b.putShort(bitsPerSample);
b.put("data".getBytes());
b.putInt(0); // Placeholder until all audio is recorded
b.limit(b.position()).rewind();
fileChannel.write(b);
}
private void updateWAVHeader(RandomAccessFile randomAccessFile, FileChannel fileChannel, long totalBytesWritten)
throws IOException
{
ByteBuffer b = _wavHeaderBuffer;
// Write totalDataLen
randomAccessFile.seek(4);
b.limit(8).position(4).mark();
b.putInt((int) (totalBytesWritten + 36));
b.reset();
fileChannel.write(b);
// Write data chunk size
randomAccessFile.seek(40);
b.limit(44).position(40).mark();
b.putInt((int) (totalBytesWritten));
b.reset();
fileChannel.write(b);
}
public synchronized boolean stopRecord()
throws IllegalStateException
{
if (!_hasMicrophone) return false;
boolean result;
if (_audioRecorder == null) {
Log.e(LOG_TAG, "Attempt to stop recording when no recording is taking place");
result = false;
} else {
_audioRecorder.stop();
try {
_audioWriterTask.get(5, TimeUnit.SECONDS);
result = true;
Log.i(LOG_TAG, "Stopped recording. File is " + _soundFile.length() + " bytes");
} catch (InterruptedException e) {
Log.e(LOG_TAG, "Interrupted while waiting for audio writer to complete", e);
result = false;
} catch (ExecutionException e) {
Log.e(LOG_TAG, "Execution exception while waiting for audio writer to complete", e);
result = false;
} catch (TimeoutException e) {
Log.e(LOG_TAG, "Timeout while waiting for audio writer to complete", e);
result = false;
} finally {
releaseAudioRecorder();
}
}
return result;
}
/**
* @return The number of seconds for the sound to play
*/
public synchronized double startPlay()
throws IllegalStateException
{
if (_soundFile == null) {
throw new IllegalArgumentException("No sound available.");
}
stopPlayingSound();
SoundManager soundManager = _application.getSoundManager();
_soundPlayingId = soundManager.playSound(_soundFile.getPath());
Log.i(LOG_TAG, "Sound id: " + _soundPlayingId);
return soundManager.soundDuration(_soundPlayingId) / 1000.0;
}
public synchronized void stopPlay() {
stopPlayingSound();
}
public synchronized void recordClose(boolean keep) {
stopPlayingSound();
if (!keep) {
if (_soundFile != null) {
_soundFile.delete();
}
}
_soundFile = null;
}
/**
* Return the volume level, from 0.0 to 1.0
*/
public double getVolume() {
if (!_hasMicrophone) return 0.0;
return _slowDecayVolumeLevel;
}
private void releaseAudioRecorder() {
if (_audioRecorder != null) {
_audioRecorder.release();
_audioRecorder = null;
}
}
private void stopPlayingSound() {
SoundManager soundManager = _application.getSoundManager();
if (_soundPlayingId != null) {
soundManager.stopSound(_soundPlayingId);
_soundPlayingId = null;
}
}
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#ff000000">
</LinearLayout>

View file

@ -0,0 +1,16 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
tools:context="org.scratchjr.android.ScratchJrActivity" >
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
/>
</RelativeLayout>

Binary file not shown.

After

(image error) Size: 1.4 KiB

Binary file not shown.

After

(image error) Size: 1.3 KiB

Binary file not shown.

After

(image error) Size: 1.5 KiB

Binary file not shown.

After

(image error) Size: 1.9 KiB

Binary file not shown.

After

(image error) Size: 2.3 KiB

View file

@ -0,0 +1,12 @@
<resources>
<!-- Declare custom theme attributes that allow changing which styles are
used for button bars depending on the API level.
?android:attr/buttonBarStyle is new as of API 11 so this is
necessary to support previous API levels. -->
<declare-styleable name="ButtonBarContainerTheme">
<attr name="metaButtonBarStyle" format="reference" />
<attr name="metaButtonBarButtonStyle" format="reference" />
</declare-styleable>
</resources>

View file

@ -0,0 +1,5 @@
<resources>
<color name="black_overlay">#66000000</color>
</resources>

View file

@ -0,0 +1,2 @@
<resources>
</resources>

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="sql_create_projects">CREATE TABLE IF NOT EXISTS PROJECTS (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
CTIME DATETIME DEFAULT CURRENT_TIMESTAMP,
MTIME DATETIME,
ALTMD5 TEXT,
POS INTEGER,
NAME TEXT,
JSON TEXT,
THUMBNAIL TEXT,
OWNER TEXT,
GALLERY TEXT,
DELETED TEXT,
VERSION TEXT
)</string>
<string name="sql_create_usershapes">CREATE TABLE IF NOT EXISTS USERSHAPES (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
CTIME DATETIME DEFAULT CURRENT_TIMESTAMP,
MD5 TEXT,
ALTMD5 TEXT,
WIDTH TEXT,
HEIGHT TEXT,
EXT TEXT,
NAME TEXT,
OWNER TEXT,
SCALE TEXT,
VERSION TEXT
)</string>
<string name="sql_create_userbkgs">CREATE TABLE IF NOT EXISTS USERBKGS (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
CTIME DATETIME DEFAULT CURRENT_TIMESTAMP,
MD5 TEXT,
ALTMD5 TEXT,
WIDTH TEXT,
HEIGHT TEXT,
EXT TEXT,
OWNER TEXT,
VERSION TEXT
)</string>
<string name="sql_add_gift">
ALTER TABLE PROJECTS ADD COLUMN ISGIFT INTEGER DEFAULT 0;
</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name">ScratchJr Free</string>
<string name="app_version">1.1</string>
<string name="share_extension_filter">.*\\.sjr</string>
<string name="share_mimetype">application/x-scratchjr-project</string>
</resources>

View file

@ -0,0 +1,27 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
</style>
<style name="FullscreenTheme" parent="android:Theme.NoTitleBar">
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@null</item>
<item name="metaButtonBarStyle">@style/ButtonBar</item>
<item name="metaButtonBarButtonStyle">@style/ButtonBarButton</item>
</style>
<!-- Backward-compatible version of ?android:attr/buttonBarStyle -->
<style name="ButtonBar">
<item name="android:paddingLeft">2dp</item>
<item name="android:paddingTop">5dp</item>
<item name="android:paddingRight">2dp</item>
<item name="android:paddingBottom">0dp</item>
<item name="android:background">@android:drawable/bottom_bar</item>
</style>
<!-- Backward-compatible version of ?android:attr/buttonBarButtonStyle -->
<style name="ButtonBarButton" />
</resources>

View file

@ -0,0 +1,20 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0'
classpath 'com.google.gms:google-services:1.5.0-beta2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}

View file

@ -0,0 +1,18 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip

164
android/ScratchJr/gradlew vendored Executable file
View file

@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
android/ScratchJr/gradlew.bat vendored Normal file
View file

@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1 @@
include ':app'

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry combineaccessrules="false" kind="src" path="/ScratchJrAndroid"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

3
android/ScratchJrTest/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/bin
/gen
/assets

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ScratchJrAndroidTest</name>
<comment></comment>
<projects>
<project>ScratchJrAndroid</project>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.scratchjr.android.test"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="16" />
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="org.scratchjr.android" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<uses-library android:name="android.test.runner" />
</application>
</manifest>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -0,0 +1,20 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View file

@ -0,0 +1,14 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-19

Binary file not shown.

After

(image error) Size: 9.2 KiB

Binary file not shown.

After

(image error) Size: 2.7 KiB

Binary file not shown.

After

(image error) Size: 5.1 KiB

Binary file not shown.

After

(image error) Size: 14 KiB

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ScratchJrTest</string>
</resources>

View file

@ -0,0 +1,136 @@
/*
ScratchJr © Massachusetts Institute of Technology and Tufts University - 2014, All Rights Reserved.
*/
package org.scratchjr.android.test;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import junit.framework.Assert;
import org.json.JSONArray;
import org.json.JSONObject;
import org.scratchjr.android.DatabaseManager;
import org.scratchjr.android.ScratchJrActivity;
import android.test.ActivityInstrumentationTestCase2;
import android.webkit.WebView;
/**
* Tests the DatabaseManager class
*
* @author markroth8
*/
public class DatabaseManagerTest
extends ActivityInstrumentationTestCase2<ScratchJrActivity>
{
private ScratchJrActivity _activity;
private DatabaseManager _databaseManager;
public DatabaseManagerTest() {
super(ScratchJrActivity.class);
}
@Override
protected void setUp()
throws Exception
{
super.setUp();
setActivityInitialTouchMode(false);
_activity = getActivity();
_databaseManager = _activity.getDatabaseManager();
_databaseManager.clearTables();
}
@Override
protected void tearDown()
throws Exception
{
super.tearDown();
}
public void testProjectsTable()
throws Exception
{
String[] columns = new String[] { "MTIME", "ALTMD5", "POS", "NAME", "JSON", "THUMBNAIL", "OWNER", "GALLERY", "DELETED",
"VERSION" };
String[] values = new String[] { "2014-05-26", "0123456789abcdef", "pos_value", "name_value", "json_value",
"thumbnail_value", "owner_value", "gallery_value", "false", "1" };
testTable("PROJECTS", columns, values);
}
public void testUserShapesTable()
throws Exception
{
String[] columns = new String[] { "MD5", "ALTMD5", "WIDTH", "HEIGHT", "EXT", "NAME", "OWNER", "SCALE", "VERSION" };
String[] values = new String[] { "123456789abcdef0", "23456789abcdef01", "1024", "768", "ext_value", "name_value",
"owner_value2", "scale_value", "1" };
testTable("USERSHAPES", columns, values);
}
public void testUserBkgsTable()
throws Exception
{
String[] columns = new String[] { "MD5", "ALTMD5", "WIDTH", "HEIGHT", "EXT", "OWNER", "VERSION" };
String[] values = new String[] { "3456789abcdef012", "456789abcdef0123", "1920", "1080", "ext_value2", "owner_value3", "1" };
testTable("USERBKGS", columns, values);
}
/**
* Tests the functionality of exec, the expected structure of the database tables and the functionality of query for the given
* table.
*/
private void testTable(String tableName, String[] columns, String[] values)
throws Exception
{
Assert.assertNotNull(_databaseManager);
Assert.assertTrue(_databaseManager.isOpen());
StringBuilder columnsStr = new StringBuilder();
boolean first = true;
for (String columnName : columns) {
if (!first) {
columnsStr.append(", ");
}
columnsStr.append(columnName);
first = false;
}
StringBuilder valuesStr = new StringBuilder();
StringBuilder questionStr = new StringBuilder();
first = true;
for (String value : values) {
if (!first) {
valuesStr.append(", ");
questionStr.append(", ");
}
valuesStr.append("'").append(value).append("'");
questionStr.append("?");
first = false;
}
JSONArray results = _databaseManager.query("SELECT " + columnsStr + " FROM " + tableName, new String[] {});
Assert.assertEquals(results.toString(), 0, results.length());
String idStr = _databaseManager.stmt("INSERT INTO " + tableName + " (" + columnsStr + ") VALUES (" + questionStr + ")", values);
Assert.assertTrue(Integer.parseInt(idStr) > 0);
JSONArray rows = _databaseManager.query("SELECT " + columnsStr + " FROM " + tableName, new String[] {});
Assert.assertEquals(1, rows.length());
JSONObject row = rows.getJSONObject(0);
for (int i = 0; i < columns.length; i++) {
String column = columns[i];
String expectedValue = values[i];
String actualValue = row.getString(column.toLowerCase());
Assert.assertEquals(expectedValue, actualValue);
}
// Now, use exec to delete it
_databaseManager.exec("DELETE FROM " + tableName + " WHERE ID = " + idStr);
results = _databaseManager.query("SELECT " + columnsStr + " FROM " + tableName, new String[] {});
Assert.assertEquals(results.toString(), 0, results.length());
}
}

View file

@ -0,0 +1,123 @@
package org.scratchjr.android.test;
import java.io.IOException;
import junit.framework.Assert;
import org.scratchjr.android.IOManager;
import org.scratchjr.android.ScratchJrActivity;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Base64;
/**
* Unit tests for IOManager
*
* @author markroth8
*/
public class IOManagerTest
extends ActivityInstrumentationTestCase2<ScratchJrActivity>
{
private ScratchJrActivity _activity;
private IOManager _ioManager;
public IOManagerTest() {
super(ScratchJrActivity.class);
}
@Override
protected void setUp()
throws Exception
{
super.setUp();
setActivityInitialTouchMode(false);
_activity = getActivity();
_ioManager = _activity.getIOManager();
}
@Override
protected void tearDown()
throws Exception
{
super.tearDown();
}
public void testBytesToHexString() {
byte[] data = new byte[] {};
String expectedValue = "";
Assert.assertEquals(expectedValue, IOManager.bytesToHexString(data));
data = new byte[] { (byte)0x10, (byte)0x70, (byte)0x80, (byte)0x85, (byte)0xFF };
expectedValue = "10708085ff";
Assert.assertEquals(expectedValue, IOManager.bytesToHexString(data));
}
public void testMD5() {
Assert.assertEquals("bae941e0d1cdf42b75d6d0ef6bd7d25a", _ioManager.md5("testContent"));
}
public void testGetSetCleanMedia()
throws Exception
{
byte[] testContent = "testContent".getBytes();
String testContentBase64 = Base64.encodeToString(testContent, Base64.NO_WRAP);
String testContentMD5 = _ioManager.md5(testContentBase64);
String filename = "testfile";
try {
_ioManager.getMedia(filename);
Assert.fail("Should have thrown IOException");
} catch (IOException e) {
// pass
}
_ioManager.setMedia(testContentBase64, "bin");
String result = _ioManager.getMedia(testContentMD5 + ".bin");
Assert.assertEquals(testContentBase64, result);
_ioManager.cleanAssets("bar");
result = _ioManager.getMedia(testContentMD5 + ".bin");
Assert.assertEquals(testContentBase64, result);
_ioManager.cleanAssets("bin");
try {
_ioManager.getMedia(testContentMD5 + ".bin");
Assert.fail("Should have thrown IOException");
} catch (IOException e) {
// pass
}
_ioManager.setMediaName(testContentBase64, filename, "bin");
result = _ioManager.getMedia(filename + ".bin");
Assert.assertEquals(testContentBase64, result);
String key = "1";
Assert.assertEquals(testContentBase64.length(), _ioManager.getMediaLen(filename + ".bin", key));
Assert.assertEquals(testContentBase64.substring(0, 4), _ioManager.getMediaData(key, 0, 4));
Assert.assertEquals(testContentBase64.substring(4, 8), _ioManager.getMediaData(key, 4, 4));
_ioManager.getMediaDone(key);
}
public void testRemove()
throws Exception
{
byte[] testContent = "testContent".getBytes();
String testContentBase64 = Base64.encodeToString(testContent, Base64.NO_WRAP);
String testFilename = "testfile";
_ioManager.setFile(testFilename, testContentBase64);
Assert.assertEquals(testContentBase64, _ioManager.getFile(testFilename));
Assert.assertTrue(_ioManager.remove(testFilename));
Assert.assertFalse(_ioManager.remove(testFilename));
try {
_ioManager.getFile(testFilename);
Assert.fail("Should have thrown IOException");
} catch (IOException e) {
// pass
}
}
}

View file

@ -0,0 +1,191 @@
/*
ScratchJr © Massachusetts Institute of Technology and Tufts University - 2014, All Rights Reserved.
*/
package org.scratchjr.android.test;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import junit.framework.Assert;
import org.scratchjr.android.ScratchJrActivity;
import android.test.ActivityInstrumentationTestCase2;
import android.view.KeyEvent;
import android.webkit.WebView;
/**
* Main test entry point for Scratchjr Activity testing.
*
* @author markroth8
*/
public class ScratchJrActivityTest
extends ActivityInstrumentationTestCase2<ScratchJrActivity>
{
private static final String URL_EDITOR_PAGE = "file:///android_asset/HTML5/editor.html";
private static final String EDITOR_HTML = "editor.html";
private static final String URL_HOME_PAGE = "file:///android_asset/HTML5/home.html";
private static final String HOME_HTML = "home.html";
private static final String URL_SPLASH_PAGE = "file:///android_asset/HTML5/index.html";
private static final int WAIT_PAGE_TIMEOUT = 5000;
private ScratchJrActivity _activity;
private WebView _webView;
public ScratchJrActivityTest() {
super(ScratchJrActivity.class);
}
@Override
protected void setUp() throws Exception
{
super.setUp();
setActivityInitialTouchMode(false);
_activity = getActivity();
_webView = (WebView) _activity.findViewById(org.scratchjr.android.R.id.webview);
Assert.assertNotNull(_webView);
}
/**
* Tests that the splash screen is shown for at least one second and then the main title screen is shown.
*/
public void testSplashScreen()
throws Exception
{
Assert.assertEquals(URL_SPLASH_PAGE, getCurrentURL());
waitForHomePage(); // Will throw an exception if it takes too long
Assert.assertEquals(URL_HOME_PAGE, getCurrentURL());
}
/**
* Tests that the back button works as expected
*/
public void testBackButtonFromHome()
throws Exception
{
waitForHomePage();
Assert.assertTrue(!_activity.isFinishing());
sendKeys(KeyEvent.KEYCODE_BACK);
Assert.assertTrue(_activity.isFinishing());
}
/**
* Tests that the back button works as expected
*/
public void testBackButtonFromEditor()
throws Exception
{
waitForHomePage();
_activity.createNewProject();
waitForLeavePage(URL_HOME_PAGE);
String editorUrl = getCurrentURL();
Assert.assertTrue(editorUrl.contains(EDITOR_HTML));
waitForEditorPageReady();
Assert.assertTrue(!_activity.isFinishing());
sendKeys(KeyEvent.KEYCODE_BACK);
Assert.assertTrue(!_activity.isFinishing());
waitForLeavePage(editorUrl);
String homeUrl = getCurrentURL();
Assert.assertTrue(homeUrl.contains(HOME_HTML));
}
/**
* Returns the URL of the current page in the web view
* @throws ExecutionException
* @throws InterruptedException
*/
private String getCurrentURL()
throws InterruptedException, ExecutionException
{
GetWebViewUrlTask queryUrlTask = new GetWebViewUrlTask();
_activity.runOnUiThread(queryUrlTask);
return queryUrlTask.get();
}
private void waitForHomePage()
throws Exception
{
waitForSplashDone();
Assert.assertEquals(URL_SPLASH_PAGE, getCurrentURL());
_activity.goHome();
// Now, wait for all the resources to load
long timeout = System.currentTimeMillis() + WAIT_PAGE_TIMEOUT;
long time = System.currentTimeMillis();
while (!_activity.isAppInitialized() && ((time = System.currentTimeMillis()) < timeout)) {
Thread.sleep(100);
}
if (time >= timeout) {
Assert.fail("Took too long to wait for app to initialize.");
}
}
private void waitForEditorPageReady()
throws Exception
{
// Now, wait for all the resources to load
long timeout = System.currentTimeMillis() + WAIT_PAGE_TIMEOUT;
long time = System.currentTimeMillis();
while (!_activity.isEditorInitialized() && ((time = System.currentTimeMillis()) < timeout)) {
Thread.sleep(100);
}
if (time >= timeout) {
Assert.fail("Took too long to wait for editor to initialize.");
}
}
private void waitForSplashDone()
throws Exception
{
long timeout = System.currentTimeMillis() + WAIT_PAGE_TIMEOUT;
long time = System.currentTimeMillis();
while (!_activity.isSplashDone() && ((time = System.currentTimeMillis()) < timeout)) {
Thread.sleep(100);
}
if (time >= timeout) {
Assert.fail("Took too long to wait for splash screen to be done.");
}
}
/**
* Wait for the projects page to disappear.
*
* If it takes longer than 10 seconds to disappear, throw an exception.
*/
private void waitForLeavePage(String page)
throws Exception
{
long timeout = System.currentTimeMillis() + WAIT_PAGE_TIMEOUT;
String url = null;
long lastTime = System.currentTimeMillis();
while (lastTime < timeout) {
url = getCurrentURL();
if (!page.equals(url)) {
break;
}
Thread.sleep(100);
lastTime = System.currentTimeMillis();
}
Assert.assertTrue("Took too long to advance from '" + page + "'", lastTime < timeout);
}
/**
* Task that runs on the UI Thread and retrieves the current url of the web view.
*/
private class GetWebViewUrlTask
extends FutureTask<String>
{
public GetWebViewUrlTask() {
super(new Callable<String>() {
public String call()
throws Exception
{
return _webView.getUrl();
}
});
}
};
}

View file

@ -0,0 +1,47 @@
/*
ScratchJr © Massachusetts Institute of Technology and Tufts University - 2014, All Rights Reserved.
*/
package org.scratchjr.android.test;
import java.util.Arrays;
import org.json.JSONArray;
import org.json.JSONException;
import org.scratchjr.android.ScratchJrUtil;
import junit.framework.Assert;
import junit.framework.TestCase;
/**
* Unit tests for ScratchJrUtil
*
* @author markroth8
*/
public class ScratchJrUtilTest
extends TestCase
{
public void testJsonArrayToStringArray()
throws Exception
{
JSONArray emptyJsonArray = new JSONArray();
String[] result = ScratchJrUtil.jsonArrayToStringArray(emptyJsonArray);
Assert.assertTrue(Arrays.toString(result), Arrays.equals(new String[] {}, result));
JSONArray singleElementJsonArray = new JSONArray();
singleElementJsonArray.put("item 1");
String[] expected = new String[] { "item 1" };
result = ScratchJrUtil.jsonArrayToStringArray(singleElementJsonArray);
Assert.assertTrue(Arrays.toString(result), Arrays.equals(expected, result));
JSONArray multiElementJsonArray = new JSONArray();
multiElementJsonArray.put("item 1");
multiElementJsonArray.put("item 2");
multiElementJsonArray.put("item 3");
expected = new String[] { "item 1", "item 2", "item 3" };
result = ScratchJrUtil.jsonArrayToStringArray(multiElementJsonArray);
Assert.assertTrue(Arrays.toString(result), Arrays.equals(expected, result));
}
}

View file

@ -0,0 +1,76 @@
/*
ScratchJr © Massachusetts Institute of Technology and Tufts University - 2014, All Rights Reserved.
*/
package org.scratchjr.android.test;
import org.scratchjr.android.ScratchJrActivity;
import org.scratchjr.android.SoundManager;
import android.test.ActivityInstrumentationTestCase2;
/**
* Tests the SoundManager class
*
* @author markroth8
*/
public class SoundManagerTest
extends ActivityInstrumentationTestCase2<ScratchJrActivity>
{
private ScratchJrActivity _activity;
private SoundManager _soundManager;
public SoundManagerTest() {
super(ScratchJrActivity.class);
}
@Override
protected void setUp()
throws Exception
{
super.setUp();
setActivityInitialTouchMode(false);
_activity = getActivity();
_soundManager = _activity.getSoundManager();
}
@Override
protected void tearDown()
throws Exception
{
super.tearDown();
}
public void testPlaySoundEffect()
throws Exception
{
String[] soundFiles = _activity.getAssets().list("HTML5/sounds");
assertTrue(soundFiles.length > 0);
for (String name : soundFiles) {
_soundManager.playSoundEffect(name);
}
Thread.sleep(100);
for (String name : soundFiles) {
_soundManager.playSoundEffect(name);
}
}
public void testPlaySound()
throws Exception
{
int id = _soundManager.playSound("pop.mp3");
assertTrue("" + id, id >= 0);
assertTrue(_soundManager.isPlaying(id));
int duration = _soundManager.soundDuration(id);
assertTrue("" + duration, duration > 0);
while (_soundManager.isPlaying(id)) {
Thread.sleep(100);
}
int id2 = _soundManager.playSound("pop.mp3");
assertTrue(id + " vs " + id2, id2 != id);
Thread.sleep(10);
_soundManager.stopSound(id2);
assertFalse(_soundManager.isPlaying(id2));
}
}

View file

@ -0,0 +1,78 @@
/*
ScratchJr © Massachusetts Institute of Technology and Tufts University - 2014, All Rights Reserved.
*/
package org.scratchjr.android.test;
import org.scratchjr.android.ScratchJrActivity;
import org.scratchjr.android.SoundManager;
import org.scratchjr.android.SoundRecorderManager;
import android.test.ActivityInstrumentationTestCase2;
/**
* Tests the SoundRecorderManager class
*
* @author markroth8
*/
public class SoundRecorderManagerTest
extends ActivityInstrumentationTestCase2<ScratchJrActivity>
{
private ScratchJrActivity _activity;
private SoundRecorderManager _soundRecorderManager;
public SoundRecorderManagerTest() {
super(ScratchJrActivity.class);
}
@Override
protected void setUp()
throws Exception
{
super.setUp();
setActivityInitialTouchMode(false);
_activity = getActivity();
_soundRecorderManager = _activity.getSoundRecorderManager();
}
@Override
protected void tearDown()
throws Exception
{
super.tearDown();
}
public void testHasMicrophone()
throws Exception
{
assertTrue(_soundRecorderManager.hasMicrophone());
}
public void testGetVolume()
throws Exception
{
// Can't test much in emulation - microphone is normally dead silent. Just make sure it doesn't throw an exception.
for (int i = 0; i < 50; i++) {
_soundRecorderManager.getVolume();
}
}
public void testRecordAndPlay()
throws Exception
{
// Note this isn't really testing much of anything - on the emulator, no audio is recorded, so this just tests
// that the mechanics of the API are working and not throwing exceptions.
for (int i = 0; i < 2; i++) {
assertNotNull(_soundRecorderManager.startRecord());
Thread.sleep(500);
assertTrue(_soundRecorderManager.stopRecord());
for (int replay = 0; replay < 2; replay++) {
assertTrue(_soundRecorderManager.startPlay() > 0.5);
Thread.sleep(500);
_soundRecorderManager.stopPlay();
}
}
_soundRecorderManager.recordClose(false);
}
}

104
bin/convert-svg-to-png.py Executable file
View file

@ -0,0 +1,104 @@
#!/usr/bin/env python
import getopt
import os
import pysvg.parser
import shutil
from subprocess import Popen, PIPE
import sys
from subprocess import call
# Converts the svglibrary assets to png.
# This script depends on the rsvg-convert to perform the conversion.
def main(argv):
rsvgConvert = "/usr/bin/rsvg-convert"
localRsvgConvert = "/usr/local/bin/rsvg-convert"
imConvert = "/usr/bin/convert"
localImConvert = "/usr/local/bin/convert"
svgDirectory = ''
pngDirectory = ''
if not os.path.isfile(rsvgConvert):
if os.path.isfile(localRsvgConvert):
rsvgConvert = localRsvgConvert
else:
print 'You must install librsvg2-bin to build'
sys.exit(1)
if not os.path.isfile(imConvert):
if os.path.isfile(localImConvert):
imConvert = localImConvert
else:
print 'You must install ImageMagick to build'
sys.exit(1)
try:
opts, args = getopt.getopt(argv,"hi:o:",["input=","output="])
except getopt.GetoptError:
print 'convert-svg-to-png.py -i <svgDirectory> -o <pngDirectory>'
sys.exit(2)
for opt, arg in opts:
if opt == '-h':
print 'convert-svg-to-png.py -i <svgDirectory> -o <pngDirectory>'
sys.exit()
elif opt in ("-i", "--input"):
svgDirectory = arg.lstrip()
elif opt in ("-o", "--output"):
pngDirectory = arg.lstrip()
print 'Input svg directory is ' + svgDirectory
print 'Output png directory is ' + pngDirectory
MAX_WIDTH = 180
MAX_HEIGHT = 140
nullout = open(os.devnull,'wb')
count = 0
for i in os.listdir(svgDirectory):
tokens = i.split(".")
fname = tokens[0]
pngname = fname + ".png"
inputFile = svgDirectory + "/" + i
outputFile = pngDirectory + "/" + pngname
# Handle large PNGs and other files
if tokens[-1] == "png":
# Downscale with ImageMagick
call([imConvert, inputFile, "-resize", '%ix%i' % (MAX_WIDTH, MAX_HEIGHT), outputFile])
continue
elif tokens[-1] != "svg":
# Don't do any processing
continue
# Handle SVGs
# hide stdout because pysvg.parser.parse spits out spurious warnings
temp = sys.stdout
sys.stdout = nullout
svg = pysvg.parser.parse(svgDirectory + i)
sys.stdout = temp
svgHeight = float(svg.get_height()[:-2])
svgWidth = float(svg.get_width()[:-2])
ratio = svgWidth / svgHeight
heightBoundByWidth = MAX_WIDTH / ratio
if heightBoundByWidth > MAX_HEIGHT:
call([rsvgConvert, "-h", str(MAX_HEIGHT), inputFile, "-o", outputFile])
else:
call([rsvgConvert, "-w", str(MAX_WIDTH), inputFile, "-o", outputFile])
count = count + 1
nullout.close()
print 'Converted {0} svgs to png'.format(count)
if __name__ == "__main__":
main(sys.argv[1:])

Binary file not shown.

Binary file not shown.

After

(image error) Size: 98 KiB

176
doc/scratchjr_testplan.csv Normal file
View file

@ -0,0 +1,176 @@
"Section","Test","Android Emulator 4.4.2","Notes"
"Stage","Drag characters around stage","PASS",
"Stage","Press and hold character and delete","PASS",
"Icons above stage","Home saves project","PASS",
"Icons above stage","Home returns to project library","PASS",
"Icons above stage","Presentation Mode expands to full-screen","PASS",
"Icons above stage","Can return back to project page from presentation mode","PASS",
"Icons above stage","Grid toggles on x-y coordinate grid","PASS",
"Icons above stage","Grid toggles off x-y coordinate grid","PASS",
"Icons above stage","Change background goes to background library","PASS",
"Icons above stage","Can return back to project page from background library","PASS",
"Icons above stage","Text editor allows adding text to stage","PASS",
"Icons above stage","Can change text size in text editor","PASS",
"Icons above stage","Can change color in text editor","PASS",
"Icons above stage","Text can be dragged around stage","PASS",
"Icons above stage","Reset resets all characters to their starting positions on stage.","PASS",
"Icons above stage","Tapping reset stops any script.","PASS",
"Icons above stage","Dragging characters after reset sets up new starting positions","PASS",
"Icons above stage","Green flag starts all scripts that begin with a “Start on Green Flag”","PASS",
"Icons above stage","Folded corner in top-right takes you to change title screen","PASS",
"Icons above stage","Change title screen allows you to change the title of the project and save it","PASS",
"Programming blocks","Can drag a block to programming area","PASS",
"Programming blocks","Connecting two blocks works","PASS",
"Programming blocks","Breaking apart two blocks works","PASS",
"Programming blocks","Repeat block connects in the right place","PASS",
"Programming blocks","All six block categories can be selected","PASS",
"Programming blocks","Numerical input feature works (type, delete)","PASS",
"Programming blocks","Negative numbers can be input and work","PASS",
"Programming blocks","Triggering: Green flag block works","PASS",
"Programming blocks","Triggering: Tap block works","PASS",
"Programming blocks","Triggering: Touch block works","PASS",
"Programming blocks","Triggering: Start on message block works","PASS",
"Programming blocks","Triggering: Send message works","PASS",
"Programming blocks","Motion: Move left 1 works","PASS",
"Programming blocks","Motion: Move left 2 works","PASS",
"Programming blocks","Motion: Move left -3 works","PASS",
"Programming blocks","Motion: Move right 1 works","PASS",
"Programming blocks","Motion: Move right 2 works","PASS",
"Programming blocks","Motion: Move right -3 works","PASS",
"Programming blocks","Motion: Move up 1 works","PASS",
"Programming blocks","Motion: Move up 2 works","PASS",
"Programming blocks","Motion: Move up -3 works","PASS",
"Programming blocks","Motion: Move down 1 works","PASS",
"Programming blocks","Motion: Move down 2 works","PASS",
"Programming blocks","Motion: Move down -3 works","PASS",
"Programming blocks","Motion: Turn left 1 works","PASS",
"Programming blocks","Motion: Turn left 2 works","PASS",
"Programming blocks","Motion: Turn left -3 works","PASS",
"Programming blocks","Motion: Turn right 1 works","PASS",
"Programming blocks","Motion: Turn right 2 works","PASS",
"Programming blocks","Motion: Turn right -3 works","PASS",
"Programming blocks","Motion: Hop 1 works","PASS",
"Programming blocks","Motion: Hop 2 works","PASS",
"Programming blocks","Motion: Hop -3 works","PASS",
"Programming blocks","Motion: Go Home works","PASS",
"Programming blocks","Looks: Grow 1 works","PASS",
"Programming blocks","Looks: Grow 2 works","PASS",
"Programming blocks","Looks: Grow -3 works","PASS",
"Programming blocks","Looks: Shrink 1 works","PASS",
"Programming blocks","Looks: Shrink 2 works","PASS",
"Programming blocks","Looks: Shrink -3 works","PASS",
"Programming blocks","Looks: Default size works","PASS",
"Programming blocks","Looks: Hide works","PASS",
"Programming blocks","Looks: Show works","PASS",
"Programming blocks","Looks: Speech bubble works","FAIL","Text box is not visible when keyboard pops up"
"Programming blocks","Sounds: Record sound allows user to record sound",,
"Programming blocks","Sounds: Sound limit is up to 1 minute in sound editor","PASS",
"Programming blocks","Sounds: Can play back recorded sound in sound editor",,
"Programming blocks","Sounds: Sound editor saves on close",,
"Programming blocks","Sounds: Dragging play recorded sound block onto programming area plays",,
"Programming blocks","Sounds: Pre-set “pop” sound works","PASS",
"Programming blocks","Sounds: Possible for user to disable microphone","FAIL","Not possible in this build"
"Programming blocks","Sounds: Can play back recorded sound in sound editor",,
"Programming blocks","Control: Wait for block waits indicated time","PASS",
"Programming blocks","Control: Stop block works","PASS",
"Programming blocks","Control: Slow speed works","PASS",
"Programming blocks","Control: Medium speed works","PASS",
"Programming blocks","Control: Fast speed works","PASS",
"Programming blocks","Control: Repeat 1 works","PASS",
"Programming blocks","Control: Repeat 3 works","PASS",
"Programming blocks","End Blocks: End block works (NOP)","PASS",
"Programming blocks","End Blocks: Repeat block works","PASS",
"Programming blocks","End Blocks: Jump to page works","PASS",
"Programming blocks","Undo reverses a block creation","PASS",
"Programming blocks","Redo reverses a block cration undo","PASS",
"Programming blocks","Undo reverses a changed value","PASS",
"Programming blocks","Redo reverses a changed value undo","PASS",
"Programming blocks","Undo reverses a character creation","PASS",
"Programming blocks","Redo reverses a character creation undo","PASS",
"Programming blocks","Undo reverses a character deletion","PASS",
"Programming blocks","Redo reverses a character deletion undo","PASS",
"Programming blocks","Undo reverses a page creation","PASS",
"Programming blocks","Redo reverses a page creation undo","PASS",
"Programming blocks","Undo reverses a page deletion","PASS",
"Programming blocks","Redo reverses a page deletion undo","PASS",
"Thumbnails","Tapping a character thumbnail selects the character","PASS",
"Thumbnails","Tapping plus adds a new character","PASS",
"Thumbnails","Tapping character's name allows you to rename it","PASS",
"Thumbnails","Tapping paintbrush allows editing character in paint editor","PASS",
"Thumbnails","Press and hold character thumbnail to delete","PASS",
"Thumbnails","Dragging character to a page thumbnail copies it and scripts to page","PASS",
"Thumbnails","Character thumbnails scroll vertically when more than four characters in project","PASS",
"Thumbnails","Tapping a page thumbnail selects the page","PASS",
"Thumbnails","Tapping plus adds a new page","PASS",
"Thumbnails","Each page has its own set of characters and a background","PASS",
"Thumbnails","Press and hold page thumbnail to delete page","PASS",
"Thumbnails","Dragging pages into a new position reorders pages","PASS",
"Thumbnails","Moving a character on the stage should be reflected in the page thumbnail","PASS",
"Libraries","Tapping plus on character thumbnails enters character library","PASS",
"Libraries","Double-tapping character thumbnail adds character to the stage","PASS",
"Libraries","Selecting the character and tapping the check mark adds character to stage","PASS",
"Libraries","Tapping paintbrush icon in top-right of character library opens the paint editor","PASS",
"Libraries","Selecting the blank character at the top-left and tapping paintbrush icon opens the paint editor","PASS",
"Libraries","Selecting a character and tapping the paintbrush icon enters the paint editor to edit selected character","PASS",
"Libraries","When character thumbnail selected, name of character is seen in character library","PASS",
"Libraries","Word “Character” for user-created characters should show at top of library if not named","PASS",
"Libraries","Character library can be scrolled vertically","PASS",
"Libraries","Tapping 'X' in top-right exits character library","PASS",
"Libraries","User-created characters are listed in front of the pre-set characters","PASS",
"Libraries","Double-tapping background thumbnail adds background to the stage","PASS",
"Libraries","Selecting the background and tapping the check mark adds background to stage","PASS",
"Libraries","Tapping paintbrush icon in top-right of background library opens the paint editor","PASS",
"Libraries","Selecting the blank background page at the top-left and tapping paintbrush icon opens the paint editor","PASS",
"Libraries","Selecting a background and tapping the paintbrush icon enters the paint editor to edit selected background","PASS",
"Libraries","When background thumbnail selected, name of background is seen in background library","PASS",
"Libraries","Word “background” for user-created backgrounds should show at top of library if not named","PASS",
"Libraries","Background library can be scrolled vertically","PASS",
"Libraries","Tapping 'X' in top-right exits background library","PASS",
"Libraries","User-created backgrounds are listed in front of the pre-set backgrounds","PASS",
"Libraries","On “My Projects” page, thumbnails show number of pages in project in a 'stack' behind each other","PASS",
"Libraries","On “My Projects” page, projects are ordered numerically if not named by the user","PASS",
"Paint Editor","Line tool draws freehand shapes and lines","PASS",
"Paint Editor","Tapping shape with line tool selected changes outline thickness","PASS",
"Paint Editor","Adjusting line thickness functions for both pre-set shapes and user-created shapes","FAIL","Only for user-created shapes"
"Paint Editor","Ellipse tool creates an ellipse","PASS",
"Paint Editor","Square tool creates a square / rectangle","PASS",
"Paint Editor","Triangle tool creates a triangle","PASS",
"Paint Editor","Arrow tool allows user to freely drag entire shape","PASS",
"Paint Editor","Tapping a shape with the arrow tool selected activates the warp tool and warp tool works","PASS",
"Paint Editor","Tapping on an existing warp tool point deletes the point","PASS",
"Paint Editor","Tapping and holding shape with rotate tool selected allows clockwise and counter-clockwise rotation","PASS",
"Paint Editor","Stamp tool allows creating a copy of a shape","PASS",
"Paint Editor","After copy made, arrow tool selects automatically and copied shape can be moved","PASS",
"Paint Editor","Scissors tool allows user to delete shape","PASS",
"Paint Editor","Tapping a shape with the camera tool activates camera mode","PASS",
"Paint Editor","In camera mode, body of shape acts as a camera lens","PASS",
"Paint Editor","Tapping camera-rotate icon flips perspective of the lens","PASS",
"Paint Editor","Tapping X in top-left exits camera mode","PASS",
"Paint Editor","Tapping camera button in bottom-middle takes photo","PASS",
"Paint Editor","Tapping shape with paint-bucket tool adds or changes color of shape","PASS",
"Paint Editor","Undo reverses a mistake","PASS",
"Paint Editor","Redo reverses the last undo","PASS",
"Paint Editor","Drag 3 fingers in the paint editor to adjust and navigate the perspective",,
"Paint Editor","Selecting a color and a shape/line tool allows the user to create a colored shape / line","PASS",
"Paint Editor","Selecting a color and the paint-bucket tool allows the user to add or change the color of the shape","PASS",
"Help / About","Video tutorial plays","FAIL","Crashes emulator"
"Help / About","Sample project 1 works","PASS",
"Help / About","Sample project 2 works","PASS",
"Help / About","Sample project 3 works","PASS",
"Help / About","Sample project 4 works","PASS",
"Help / About","Sample project 5 works","PASS",
"Help / About","Sample project 6 works","PASS",
"Help / About","Sample project 7 works","PASS",
"Help / About","Sample project 8 works","PASS",
"Help / About","Tapping the question mark at top of screen accesses Help page","PASS",
"Help / About","Tutorials are interactive and can be changed","PASS",
"Help / About","No changes are saved to tutorial after exiting","PASS",
"Help / About","In tutorials, scripts can be deleted but characters cannot be deleted","PASS",
"Help / About","About page shows detailed guide to project editor","PASS",
"Help / About","About page shows detailed guide to paint editor","PASS",
"Help / About","About page shows detailed guide to blocks","PASS",
"Help / About","Book icon selects About page","PASS",
"Help / About","All of the circled numbers in the interface and paint editor guides can be tapped","FAIL","Circles don't select"
"Known Bugs","Encompassing blocks with repeat block if it has a red block attached to it will not work",,
"Known Bugs","Empty shapes created in the paint editor do not separate after being copied",,
"Additional Notes",,,"When there are not two cameras, adjust perspective still shows"
1 Section Test Android Emulator 4.4.2 Notes
2 Stage Drag characters around stage PASS
3 Stage Press and hold character and delete PASS
4 Icons above stage Home saves project PASS
5 Icons above stage Home returns to project library PASS
6 Icons above stage Presentation Mode expands to full-screen PASS
7 Icons above stage Can return back to project page from presentation mode PASS
8 Icons above stage Grid toggles on x-y coordinate grid PASS
9 Icons above stage Grid toggles off x-y coordinate grid PASS
10 Icons above stage Change background goes to background library PASS
11 Icons above stage Can return back to project page from background library PASS
12 Icons above stage Text editor allows adding text to stage PASS
13 Icons above stage Can change text size in text editor PASS
14 Icons above stage Can change color in text editor PASS
15 Icons above stage Text can be dragged around stage PASS
16 Icons above stage Reset resets all characters to their starting positions on stage. PASS
17 Icons above stage Tapping reset stops any script. PASS
18 Icons above stage Dragging characters after reset sets up new starting positions PASS
19 Icons above stage Green flag starts all scripts that begin with a “Start on Green Flag” PASS
20 Icons above stage Folded corner in top-right takes you to change title screen PASS
21 Icons above stage Change title screen allows you to change the title of the project and save it PASS
22 Programming blocks Can drag a block to programming area PASS
23 Programming blocks Connecting two blocks works PASS
24 Programming blocks Breaking apart two blocks works PASS
25 Programming blocks Repeat block connects in the right place PASS
26 Programming blocks All six block categories can be selected PASS
27 Programming blocks Numerical input feature works (type, delete) PASS
28 Programming blocks Negative numbers can be input and work PASS
29 Programming blocks Triggering: Green flag block works PASS
30 Programming blocks Triggering: Tap block works PASS
31 Programming blocks Triggering: Touch block works PASS
32 Programming blocks Triggering: Start on message block works PASS
33 Programming blocks Triggering: Send message works PASS
34 Programming blocks Motion: Move left 1 works PASS
35 Programming blocks Motion: Move left 2 works PASS
36 Programming blocks Motion: Move left -3 works PASS
37 Programming blocks Motion: Move right 1 works PASS
38 Programming blocks Motion: Move right 2 works PASS
39 Programming blocks Motion: Move right -3 works PASS
40 Programming blocks Motion: Move up 1 works PASS
41 Programming blocks Motion: Move up 2 works PASS
42 Programming blocks Motion: Move up -3 works PASS
43 Programming blocks Motion: Move down 1 works PASS
44 Programming blocks Motion: Move down 2 works PASS
45 Programming blocks Motion: Move down -3 works PASS
46 Programming blocks Motion: Turn left 1 works PASS
47 Programming blocks Motion: Turn left 2 works PASS
48 Programming blocks Motion: Turn left -3 works PASS
49 Programming blocks Motion: Turn right 1 works PASS
50 Programming blocks Motion: Turn right 2 works PASS
51 Programming blocks Motion: Turn right -3 works PASS
52 Programming blocks Motion: Hop 1 works PASS
53 Programming blocks Motion: Hop 2 works PASS
54 Programming blocks Motion: Hop -3 works PASS
55 Programming blocks Motion: Go Home works PASS
56 Programming blocks Looks: Grow 1 works PASS
57 Programming blocks Looks: Grow 2 works PASS
58 Programming blocks Looks: Grow -3 works PASS
59 Programming blocks Looks: Shrink 1 works PASS
60 Programming blocks Looks: Shrink 2 works PASS
61 Programming blocks Looks: Shrink -3 works PASS
62 Programming blocks Looks: Default size works PASS
63 Programming blocks Looks: Hide works PASS
64 Programming blocks Looks: Show works PASS
65 Programming blocks Looks: Speech bubble works FAIL Text box is not visible when keyboard pops up
66 Programming blocks Sounds: Record sound allows user to record sound
67 Programming blocks Sounds: Sound limit is up to 1 minute in sound editor PASS
68 Programming blocks Sounds: Can play back recorded sound in sound editor
69 Programming blocks Sounds: Sound editor saves on close
70 Programming blocks Sounds: Dragging play recorded sound block onto programming area plays
71 Programming blocks Sounds: Pre-set “pop” sound works PASS
72 Programming blocks Sounds: Possible for user to disable microphone FAIL Not possible in this build
73 Programming blocks Sounds: Can play back recorded sound in sound editor
74 Programming blocks Control: Wait for block waits indicated time PASS
75 Programming blocks Control: Stop block works PASS
76 Programming blocks Control: Slow speed works PASS
77 Programming blocks Control: Medium speed works PASS
78 Programming blocks Control: Fast speed works PASS
79 Programming blocks Control: Repeat 1 works PASS
80 Programming blocks Control: Repeat 3 works PASS
81 Programming blocks End Blocks: End block works (NOP) PASS
82 Programming blocks End Blocks: Repeat block works PASS
83 Programming blocks End Blocks: Jump to page works PASS
84 Programming blocks Undo reverses a block creation PASS
85 Programming blocks Redo reverses a block cration undo PASS
86 Programming blocks Undo reverses a changed value PASS
87 Programming blocks Redo reverses a changed value undo PASS
88 Programming blocks Undo reverses a character creation PASS
89 Programming blocks Redo reverses a character creation undo PASS
90 Programming blocks Undo reverses a character deletion PASS
91 Programming blocks Redo reverses a character deletion undo PASS
92 Programming blocks Undo reverses a page creation PASS
93 Programming blocks Redo reverses a page creation undo PASS
94 Programming blocks Undo reverses a page deletion PASS
95 Programming blocks Redo reverses a page deletion undo PASS
96 Thumbnails Tapping a character thumbnail selects the character PASS
97 Thumbnails Tapping plus adds a new character PASS
98 Thumbnails Tapping character's name allows you to rename it PASS
99 Thumbnails Tapping paintbrush allows editing character in paint editor PASS
100 Thumbnails Press and hold character thumbnail to delete PASS
101 Thumbnails Dragging character to a page thumbnail copies it and scripts to page PASS
102 Thumbnails Character thumbnails scroll vertically when more than four characters in project PASS
103 Thumbnails Tapping a page thumbnail selects the page PASS
104 Thumbnails Tapping plus adds a new page PASS
105 Thumbnails Each page has its own set of characters and a background PASS
106 Thumbnails Press and hold page thumbnail to delete page PASS
107 Thumbnails Dragging pages into a new position reorders pages PASS
108 Thumbnails Moving a character on the stage should be reflected in the page thumbnail PASS
109 Libraries Tapping plus on character thumbnails enters character library PASS
110 Libraries Double-tapping character thumbnail adds character to the stage PASS
111 Libraries Selecting the character and tapping the check mark adds character to stage PASS
112 Libraries Tapping paintbrush icon in top-right of character library opens the paint editor PASS
113 Libraries Selecting the blank character at the top-left and tapping paintbrush icon opens the paint editor PASS
114 Libraries Selecting a character and tapping the paintbrush icon enters the paint editor to edit selected character PASS
115 Libraries When character thumbnail selected, name of character is seen in character library PASS
116 Libraries Word “Character” for user-created characters should show at top of library if not named PASS
117 Libraries Character library can be scrolled vertically PASS
118 Libraries Tapping 'X' in top-right exits character library PASS
119 Libraries User-created characters are listed in front of the pre-set characters PASS
120 Libraries Double-tapping background thumbnail adds background to the stage PASS
121 Libraries Selecting the background and tapping the check mark adds background to stage PASS
122 Libraries Tapping paintbrush icon in top-right of background library opens the paint editor PASS
123 Libraries Selecting the blank background page at the top-left and tapping paintbrush icon opens the paint editor PASS
124 Libraries Selecting a background and tapping the paintbrush icon enters the paint editor to edit selected background PASS
125 Libraries When background thumbnail selected, name of background is seen in background library PASS
126 Libraries Word “background” for user-created backgrounds should show at top of library if not named PASS
127 Libraries Background library can be scrolled vertically PASS
128 Libraries Tapping 'X' in top-right exits background library PASS
129 Libraries User-created backgrounds are listed in front of the pre-set backgrounds PASS
130 Libraries On “My Projects” page, thumbnails show number of pages in project in a 'stack' behind each other PASS
131 Libraries On “My Projects” page, projects are ordered numerically if not named by the user PASS
132 Paint Editor Line tool draws freehand shapes and lines PASS
133 Paint Editor Tapping shape with line tool selected changes outline thickness PASS
134 Paint Editor Adjusting line thickness functions for both pre-set shapes and user-created shapes FAIL Only for user-created shapes
135 Paint Editor Ellipse tool creates an ellipse PASS
136 Paint Editor Square tool creates a square / rectangle PASS
137 Paint Editor Triangle tool creates a triangle PASS
138 Paint Editor Arrow tool allows user to freely drag entire shape PASS
139 Paint Editor Tapping a shape with the arrow tool selected activates the warp tool and warp tool works PASS
140 Paint Editor Tapping on an existing warp tool point deletes the point PASS
141 Paint Editor Tapping and holding shape with rotate tool selected allows clockwise and counter-clockwise rotation PASS
142 Paint Editor Stamp tool allows creating a copy of a shape PASS
143 Paint Editor After copy made, arrow tool selects automatically and copied shape can be moved PASS
144 Paint Editor Scissors tool allows user to delete shape PASS
145 Paint Editor Tapping a shape with the camera tool activates camera mode PASS
146 Paint Editor In camera mode, body of shape acts as a camera lens PASS
147 Paint Editor Tapping camera-rotate icon flips perspective of the lens PASS
148 Paint Editor Tapping X in top-left exits camera mode PASS
149 Paint Editor Tapping camera button in bottom-middle takes photo PASS
150 Paint Editor Tapping shape with paint-bucket tool adds or changes color of shape PASS
151 Paint Editor Undo reverses a mistake PASS
152 Paint Editor Redo reverses the last undo PASS
153 Paint Editor Drag 3 fingers in the paint editor to adjust and navigate the perspective
154 Paint Editor Selecting a color and a shape/line tool allows the user to create a colored shape / line PASS
155 Paint Editor Selecting a color and the paint-bucket tool allows the user to add or change the color of the shape PASS
156 Help / About Video tutorial plays FAIL Crashes emulator
157 Help / About Sample project 1 works PASS
158 Help / About Sample project 2 works PASS
159 Help / About Sample project 3 works PASS
160 Help / About Sample project 4 works PASS
161 Help / About Sample project 5 works PASS
162 Help / About Sample project 6 works PASS
163 Help / About Sample project 7 works PASS
164 Help / About Sample project 8 works PASS
165 Help / About Tapping the question mark at top of screen accesses Help page PASS
166 Help / About Tutorials are interactive and can be changed PASS
167 Help / About No changes are saved to tutorial after exiting PASS
168 Help / About In tutorials, scripts can be deleted but characters cannot be deleted PASS
169 Help / About About page shows detailed guide to project editor PASS
170 Help / About About page shows detailed guide to paint editor PASS
171 Help / About About page shows detailed guide to blocks PASS
172 Help / About Book icon selects About page PASS
173 Help / About All of the circled numbers in the interface and paint editor guides can be tapped FAIL Circles don't select
174 Known Bugs Encompassing blocks with repeat block if it has a red block attached to it will not work
175 Known Bugs Empty shapes created in the paint editor do not separate after being copied
176 Additional Notes When there are not two cameras, adjust perspective still shows

BIN
doc/scratchjr_testplan.docx Normal file

Binary file not shown.

BIN
doc/scratchjr_testplan.pdf Normal file

Binary file not shown.

View file

@ -0,0 +1,43 @@
{
"images" : [
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,36 @@
{
"images" : [
{
"orientation" : "landscape",
"idiom" : "ipad",
"minimum-system-version" : "7.0",
"extent" : "full-screen",
"scale" : "1x"
},
{
"orientation" : "landscape",
"idiom" : "ipad",
"minimum-system-version" : "7.0",
"extent" : "full-screen",
"scale" : "2x"
},
{
"orientation" : "portrait",
"idiom" : "ipad",
"minimum-system-version" : "7.0",
"extent" : "full-screen",
"scale" : "1x"
},
{
"orientation" : "portrait",
"idiom" : "ipad",
"minimum-system-version" : "7.0",
"extent" : "full-screen",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,50 @@
{
"images" : [
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "KITTEN-29x29.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "KITTEN-58x58.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "KITTEN-40x40.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "KITTEN-80x80.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "KITTEN-76x76.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "KITTEN-152x152.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-167.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

(image error) Size: 581 KiB

Binary file not shown.

After

(image error) Size: 2.1 KiB

Binary file not shown.

After

(image error) Size: 1.3 KiB

Binary file not shown.

After

(image error) Size: 1.3 KiB

Some files were not shown because too many files have changed in this diff Show more