Initial commit
26
.esformatter
Normal 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
|
@ -0,0 +1 @@
|
|||
src/external/
|
124
.eslintrc
Normal 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
|
@ -0,0 +1,3 @@
|
|||
/android/ScratchJr/app/src/main/gen/
|
||||
/node_modules
|
||||
.DS_Store
|
22
.idea/compiler.xml
generated
Normal 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
|
@ -0,0 +1,3 @@
|
|||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
9
.idea/libraries/Size_Pngs.xml
generated
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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="<template>" 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="<template>" 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	" 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
|
@ -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
|
@ -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
|
@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
## 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
|
@ -0,0 +1 @@
|
|||
/releases
|
1
android/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
ScratchJr - Android Studio project containing Android build of Scratch Jr.
|
7
android/ScratchJr/.gitignore
vendored
Normal 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
|
@ -0,0 +1 @@
|
|||
ScratchJr
|
22
android/ScratchJr/.idea/compiler.xml
generated
Normal 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
android/ScratchJr/.idea/copyright/profiles_settings.xml
generated
Normal file
|
@ -0,0 +1,3 @@
|
|||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
19
android/ScratchJr/.idea/gradle.xml
generated
Normal 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
|
@ -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
|
@ -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>
|
12
android/ScratchJr/.idea/runConfigurations.xml
generated
Normal 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
|
@ -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>
|
19
android/ScratchJr/ScratchJr.iml
Normal 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
|
@ -0,0 +1,2 @@
|
|||
/build
|
||||
/google-services.json
|
118
android/ScratchJr/app/app.iml
Normal 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>
|
99
android/ScratchJr/app/build.gradle
Normal 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')
|
17
android/ScratchJr/app/proguard-rules.pro
vendored
Normal 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 *;
|
||||
#}
|
7
android/ScratchJr/app/src/androidTest/androidTest.iml
Normal 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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
1
android/ScratchJr/app/src/main/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/assets
|
62
android/ScratchJr/app/src/main/AndroidManifest.xml
Normal 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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
7
android/ScratchJr/app/src/main/main.iml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
BIN
android/ScratchJr/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 1.4 KiB |
BIN
android/ScratchJr/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 1.3 KiB |
0
android/ScratchJr/app/src/main/res/mipmap-xhdpi/.gitignore
vendored
Normal file
BIN
android/ScratchJr/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 1.5 KiB |
BIN
android/ScratchJr/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 1.9 KiB |
After ![]() (image error) Size: 2.3 KiB |
12
android/ScratchJr/app/src/main/res/values/attrs.xml
Normal 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>
|
5
android/ScratchJr/app/src/main/res/values/colors.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<resources>
|
||||
|
||||
<color name="black_overlay">#66000000</color>
|
||||
|
||||
</resources>
|
2
android/ScratchJr/app/src/main/res/values/dimens.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<resources>
|
||||
</resources>
|
44
android/ScratchJr/app/src/main/res/values/sql.xml
Normal 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>
|
7
android/ScratchJr/app/src/main/res/values/strings.xml
Normal 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>
|
27
android/ScratchJr/app/src/main/res/values/styles.xml
Normal 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>
|
20
android/ScratchJr/build.gradle
Normal 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()
|
||||
}
|
||||
}
|
18
android/ScratchJr/gradle.properties
Normal 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
|
BIN
android/ScratchJr/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
android/ScratchJr/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
@ -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
|
@ -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
|
1
android/ScratchJr/settings.gradle
Normal file
|
@ -0,0 +1 @@
|
|||
include ':app'
|
10
android/ScratchJrTest/.classpath
Normal 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
|
@ -0,0 +1,3 @@
|
|||
/bin
|
||||
/gen
|
||||
/assets
|
34
android/ScratchJrTest/.project
Normal 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>
|
19
android/ScratchJrTest/AndroidManifest.xml
Normal 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>
|
7
android/ScratchJrTest/ScratchJrTest.iml
Normal 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>
|
20
android/ScratchJrTest/proguard-project.txt
Normal 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 *;
|
||||
#}
|
14
android/ScratchJrTest/project.properties
Normal 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
|
BIN
android/ScratchJrTest/res/drawable-hdpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 9.2 KiB |
BIN
android/ScratchJrTest/res/drawable-ldpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 2.7 KiB |
BIN
android/ScratchJrTest/res/drawable-mdpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 5.1 KiB |
BIN
android/ScratchJrTest/res/drawable-xhdpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 14 KiB |
6
android/ScratchJrTest/res/values/strings.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">ScratchJrTest</string>
|
||||
|
||||
</resources>
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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:])
|
BIN
doc/scratchjr_architecture.dia
Normal file
BIN
doc/scratchjr_architecture.png
Normal file
After ![]() (image error) Size: 98 KiB |
176
doc/scratchjr_testplan.csv
Normal 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"
|
|
BIN
doc/scratchjr_testplan.docx
Normal file
BIN
doc/scratchjr_testplan.pdf
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
6
editions/free/Free-Images.xcassets/Contents.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
After ![]() (image error) Size: 581 KiB |
After ![]() (image error) Size: 2.1 KiB |
After ![]() (image error) Size: 1.3 KiB |
After ![]() (image error) Size: 1.3 KiB |