Java tutorial
/* * Copyright 2017 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.blockly.android; import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import android.content.res.AssetManager; import android.support.annotation.Nullable; import android.support.v4.app.FragmentManager; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.widget.Toast; import com.google.blockly.android.clipboard.BlockClipDataHelper; import com.google.blockly.android.clipboard.SingleMimeTypeClipDataHelper; import com.google.blockly.android.codegen.CodeGenerationRequest; import com.google.blockly.android.codegen.CodeGeneratorManager; import com.google.blockly.android.control.BlocklyController; import com.google.blockly.android.ui.BlockListUI; import com.google.blockly.android.ui.BlockViewFactory; import com.google.blockly.android.ui.DefaultVariableCallback; import com.google.blockly.android.ui.WorkspaceHelper; import com.google.blockly.model.BlockExtension; import com.google.blockly.model.BlockFactory; import com.google.blockly.model.BlocklySerializerException; import com.google.blockly.model.Workspace; import com.google.blockly.utils.BlockLoadingException; import com.google.blockly.utils.StringOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.List; import java.util.Map; /** * Class to facilitate Blockly setup on an Activity. * * {@link BlocklyActivityHelper#onCreateFragments()} looks for * the {@link WorkspaceFragment}, the toolbox's {@link BlockListUI}, and the trash's * {@link BlockListUI} via fragment ids {@link R.id#blockly_workspace}, * {@link R.id#blockly_toolbox_ui}, and {@link R.id#blockly_trash_ui}, respectively. * <p/> * The activity can also contain a few buttons to control the workspace. * {@link R.id#blockly_zoom_in_button} and {@link R.id#blockly_zoom_out_button} control the * workspace zoom scale, and {@link R.id#blockly_center_view_button} will reset it. * {@link R.id#blockly_trash_icon} will toggle a closeable {@link R.id#blockly_trash_ui} * {@link BlockListUI} (such as {@link FlyoutFragment}), and also act as a block drop target to * delete blocks. The methods {@link #onConfigureTrashIcon()}, {@link #onConfigureZoomInButton()}, * {@link #onConfigureZoomOutButton()}, and {@link #onConfigureCenterViewButton()} will search for * these views and set the respective behavior. */ public class BlocklyActivityHelper { private static final String TAG = "BlocklyActivityHelper"; protected AppCompatActivity mActivity; protected WorkspaceHelper mWorkspaceHelper; protected BlockViewFactory mBlockViewFactory; protected BlockClipDataHelper mClipDataHelper; protected WorkspaceFragment mWorkspaceFragment; protected BlockListUI mToolboxBlockList; protected BlockListUI mTrashBlockList; protected CategorySelectorFragment mCategoryFragment; protected BlocklyController mController; protected CodeGeneratorManager mCodeGeneratorManager; /** * Creates the activity helper and initializes Blockly. Must be called during * {@link Activity#onCreate}. Executes the following sequence of calls during initialization: * <ul> * <li>{@link #onCreateFragments} to find fragments</li> * <li>{@link #onCreateBlockViewFactory}</li> * </ul> * Subclasses should override those methods to configure the Blockly environment. * * @throws IllegalStateException If error occurs during initialization. Assumes all initial * compile-time assets are known to be valid. */ public BlocklyActivityHelper(AppCompatActivity activity) { mActivity = activity; onCreateFragments(); if (mWorkspaceFragment == null) { throw new IllegalStateException("mWorkspaceFragment is null"); } mWorkspaceHelper = new WorkspaceHelper(activity); mBlockViewFactory = onCreateBlockViewFactory(mWorkspaceHelper); mClipDataHelper = onCreateClipDataHelper(); mCodeGeneratorManager = new CodeGeneratorManager(activity); BlocklyController.Builder builder = new BlocklyController.Builder(activity) .setClipDataHelper(mClipDataHelper).setWorkspaceHelper(mWorkspaceHelper) .setBlockViewFactory(mBlockViewFactory).setWorkspaceFragment(mWorkspaceFragment) .setTrashUi(mTrashBlockList).setToolboxUi(mToolboxBlockList, mCategoryFragment); mController = builder.build(); onCreateVariableCallback(); onConfigureTrashIcon(); onConfigureZoomInButton(); onConfigureZoomOutButton(); onConfigureCenterViewButton(); } /** * Lifecycle hook that must be called from {@link Activity#onStart()}. * Does nothing yet, but required for future compatibility. */ public void onStart() { // Do nothing. } /** * Lifecycle hook that must be called from {@link Activity#onResume()}. */ public void onResume() { mCodeGeneratorManager.onResume(); } /** * @return the {@link BlocklyController} for this activity. */ public BlocklyController getController() { return mController; } /** * Save the workspace to the given file in the application's private data directory, and show a * status toast. If the save fails, the error is logged. */ public void saveWorkspaceToAppDir(String filename) throws FileNotFoundException, BlocklySerializerException { Workspace workspace = mWorkspaceFragment.getWorkspace(); workspace.serializeToXml(mActivity.openFileOutput(filename, Context.MODE_PRIVATE)); } /** * Save the workspace to the given file in the application's private data directory, and show a * status toast. If the save fails, the error is logged. * @return True if the save was successful. Otherwise false. */ public boolean saveWorkspaceToAppDirSafely(String filename) { try { saveWorkspaceToAppDir(filename); Toast.makeText(mActivity, R.string.toast_workspace_saved, Toast.LENGTH_LONG).show(); return true; } catch (FileNotFoundException | BlocklySerializerException e) { Toast.makeText(mActivity, R.string.toast_workspace_not_saved, Toast.LENGTH_LONG).show(); Log.e(TAG, "Failed to save workspace to " + filename, e); return false; } } /** * Loads the workspace from the given file in the application's private data directory. * @param filename The path to the file, in the application's local storage. * @throws IOException If there is a underlying problem with the input. * @throws BlockLoadingException If there is a error with the workspace XML format or blocks. */ public void loadWorkspaceFromAppDir(String filename) throws IOException, BlockLoadingException { mController.loadWorkspaceContents(mActivity.openFileInput(filename)); } /** * Loads the workspace from the given file in the application's private data directory. If it * fails to load, a toast will be shown and the error will be logged. * @param filename The path to the file, in the application's local storage. * @return True if loading the workspace succeeds. Otherwise, false and the error will be * logged. */ public boolean loadWorkspaceFromAppDirSafely(String filename) { try { loadWorkspaceFromAppDir(filename); return true; } catch (FileNotFoundException e) { Toast.makeText(mActivity, R.string.toast_workspace_file_not_found, Toast.LENGTH_LONG).show(); Log.e(TAG, "Failed to load workspace", e); } catch (IOException | BlockLoadingException e) { Toast.makeText(mActivity, R.string.toast_workspace_load_failed, Toast.LENGTH_LONG).show(); Log.e(TAG, "Failed to load workspace", e); } return false; } /** * @return True if the action was handled to close a previously open (and closable) toolbox or * trash UI. Otherwise false. */ public boolean onBackToCloseFlyouts() { return mController.closeFlyouts(); } /** * Requests code generation using the blocks in the {@link Workspace}/{@link WorkspaceFragment}. * * @param blockDefinitionsJsonPaths The asset path to the JSON block definitions. * @param generatorsJsPaths The asset paths to the JavaScript generators, and optionally the * JavaScript block extension/mutator sources. * @param codeGenerationCallback The {@link CodeGenerationRequest.CodeGeneratorCallback} to use * upon completion. */ public void requestCodeGeneration(List<String> blockDefinitionsJsonPaths, List<String> generatorsJsPaths, CodeGenerationRequest.CodeGeneratorCallback codeGenerationCallback) { // final StringOutputStream serialized = new StringOutputStream(); // try { // mController.getWorkspace().serializeToXml(serialized); // } catch (BlocklySerializerException e) { // // Not using a string resource because no non-developer should see this. // String msg = "Failed to serialize workspace during code generation."; // Log.wtf(TAG, msg, e); // Toast.makeText(mActivity, msg, Toast.LENGTH_LONG).show(); // throw new IllegalStateException(msg, e); // } // // mCodeGeneratorManager.requestCodeGeneration( // new CodeGenerationRequest( // serialized.toString(), // codeGenerationCallback, // blockDefinitionsJsonPaths, // generatorsJsPaths)); // try { // serialized.close(); // } catch (IOException e) { // // Ignore error on close(). // } } /** * Lifecycle hook that must be called from {@link Activity#onPause()}. */ public void onPause() { mCodeGeneratorManager.onPause(); } /** * Lifecycle hook that must be called from {@link Activity#onStop()}. * Does nothing yet, but required for future compatibility. */ public void onStop() { // Do nothing. } /** * Lifecycle hook that must be called from {@link Activity#onRestart()}. * Does nothing yet, but required for future compatibility. */ public void onRestart() { // Do nothing. } /** * Lifecycle hook that must be called from {@link Activity#onDestroy()}. * Does nothing yet, but required for future compatibility. */ public void onDestroy() { // Do nothing. } /** * Creates the Views and Fragments before the BlocklyController is constructed. Override to * load a custom View hierarchy. Responsible for assigning {@link #mWorkspaceFragment}, and * optionally, {@link #mToolboxBlockList} and {@link #mTrashBlockList}. This base * implementation attempts to acquire references to: * <ul> * <li>the {@link WorkspaceFragment} with id {@link R.id#blockly_workspace}, assigned to * {@link #mWorkspaceFragment}.</li> * <li>the toolbox {@link CategorySelectorFragment} with id {@link R.id#blockly_categories}, * assigned to {@link #mCategoryFragment}.</li> * <li>the toolbox {@link FlyoutFragment} with id {@link R.id#blockly_toolbox_ui}, * assigned to {@link #mToolboxBlockList}.</li> * <li>the trash {@link FlyoutFragment} with id {@link R.id#blockly_trash_ui}, assigned to * {@link #mTrashBlockList}.</li> * </ul> * Only the workspace fragment is required. The activity layout can choose not to include the * other fragments, and subclasses that override this method can leave the field null if that * are not used. * <p/> * This methods is always called once from the constructor before {@link #mController} is * instantiated. */ protected void onCreateFragments() { FragmentManager fragmentManager = mActivity.getSupportFragmentManager(); mWorkspaceFragment = (WorkspaceFragment) fragmentManager.findFragmentById(R.id.blockly_workspace); mToolboxBlockList = (BlockListUI) fragmentManager.findFragmentById(R.id.blockly_toolbox_ui); mCategoryFragment = (CategorySelectorFragment) fragmentManager.findFragmentById(R.id.blockly_categories); mTrashBlockList = (BlockListUI) fragmentManager.findFragmentById(R.id.blockly_trash_ui); if (mTrashBlockList != null) { // TODO(#14): Make trash list a drop location. } } /** * Constructs the {@link BlockViewFactory} used by all fragments in this activity. The Blockly * core library does not include a factory implementation, and the app developer will need to * include blockly vertical or another block rendering implementation. * <p> * The default implementation attempts to instantiates a VerticalBlockViewFactory, which is * included in the blocklylib-vertical library. An error will be thrown unless * blocklylib-vertical is included or this method is overridden to provide a custom * BlockViewFactory. * * @param helper The Workspace helper for this activity. * @return The {@link BlockViewFactory} used by all fragments in this activity. */ public BlockViewFactory onCreateBlockViewFactory(WorkspaceHelper helper) { try { @SuppressWarnings("unchecked") Class<? extends BlockViewFactory> clazz = (Class<? extends BlockViewFactory>) Class .forName("com.google.blockly.android.ui.vertical.VerticalBlockViewFactory"); return clazz.getConstructor(Context.class, WorkspaceHelper.class).newInstance(mActivity, helper); } catch (ClassNotFoundException e) { throw new RuntimeException("Default BlockViewFactory not found. Did you include blocklylib-vertical?", e); } catch (NoSuchMethodException e) { throw new RuntimeException("Unable to instantiate VerticalBlockViewFactory", e); } catch (InstantiationException e) { throw new RuntimeException("Unable to instantiate VerticalBlockViewFactory", e); } catch (IllegalAccessException e) { throw new RuntimeException("Unable to instantiate VerticalBlockViewFactory", e); } catch (InvocationTargetException e) { throw new RuntimeException("Unable to instantiate VerticalBlockViewFactory", e); } } /** * Constructs the {@link BlockClipDataHelper} for use by all Blockly components of * {@link #mActivity}. The instance will be passed to the controller and available via * {@link BlocklyController#getClipDataHelper()}. * <p/> * By default, it constructs a {@link SingleMimeTypeClipDataHelper} with a MIME type derived * from the application's package name. This assumes all Blockly workspaces in an app work with * the same shared set of blocks, and blocks can be dragged/copied/pasted between them, even if * they are in different Activities. It also ensures blocks from other applications will be * rejected. * <p/> * If your app uses different block sets for different workspaces, or you intend to interoperate * with other applications, you will need to override this method with your own implementation. * * @return A new {@link BlockClipDataHelper}. */ protected BlockClipDataHelper onCreateClipDataHelper() { return SingleMimeTypeClipDataHelper.getDefault(mActivity); } /** * This method finds and configures {@link R.id#blockly_trash_icon} from the view hierarchy as * the button to open and close the trash, and a drop location for deleting * blocks. If {@link R.id#blockly_trash_icon} is not found, it does nothing. * <p/> * This is called from the constructor after {@link #mController} is initialized. */ protected void onConfigureTrashIcon() { View trashIcon = mActivity.findViewById(R.id.blockly_trash_icon); if (mController != null && trashIcon != null) { mController.setTrashIcon(trashIcon); } } /** * This method finds and configures {@link R.id#blockly_zoom_in_button} from the view hierarchy * as the button to zoom in (e.g., enlarge) the workspace view. If * {@link R.id#blockly_zoom_in_button} is not found, it does nothing. * <p/> * This is called from the constructor after {@link #mController} is initialized. */ protected void onConfigureZoomInButton() { View zoomInButton = mActivity.findViewById(R.id.blockly_zoom_in_button); if (zoomInButton != null) { zoomInButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mController.zoomIn(); } }); ZoomBehavior zoomBehavior = mWorkspaceHelper.getZoomBehavior(); zoomInButton.setVisibility(zoomBehavior.isButtonEnabled() ? View.VISIBLE : View.GONE); } } /** * This method finds and configures {@link R.id#blockly_zoom_out_button} from the view hierarchy * as the button to zoom out (e.g., shrink) the workspace view. If * {@link R.id#blockly_zoom_out_button} is not found, it does nothing. * <p/> * This is called from the constructor after {@link #mController} is initialized. */ protected void onConfigureZoomOutButton() { View zoomOutButton = mActivity.findViewById(R.id.blockly_zoom_out_button); if (zoomOutButton != null) { zoomOutButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mController.zoomOut(); } }); ZoomBehavior zoomBehavior = mWorkspaceHelper.getZoomBehavior(); zoomOutButton.setVisibility(zoomBehavior.isButtonEnabled() ? View.VISIBLE : View.GONE); } } /** * This method finds and configures {@link R.id#blockly_center_view_button} from the view * hierarchy as the button to reset the workspace view. If * {@link R.id#blockly_center_view_button} is not found, it does nothing. * <p/> * This is called from the constructor after {@link #mController} is initialized. */ protected void onConfigureCenterViewButton() { View recenterButton = mActivity.findViewById(R.id.blockly_center_view_button); if (recenterButton != null) { recenterButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mController.recenterWorkspace(); } }); ZoomBehavior zoomBehavior = mWorkspaceHelper.getZoomBehavior(); recenterButton.setVisibility(zoomBehavior.isFixed() ? View.GONE : View.VISIBLE); } } /** * Constructs a {@link BlocklyController.VariableCallback} for handling user requests to change * the list of variables (create, rename, delete). This can be used to provide UI for confirming * a deletion or renaming a variable. * <p/> * By default, this method constructs a {@link DefaultVariableCallback}. Apps can override this * to provide an alternative implementation, or optionally override the method to do nothing (no * confirmation UI). * <p/> * This method is responsible for calling {@link BlocklyController#setVariableCallback}. * * @return A {@link com.google.blockly.android.control.BlocklyController.VariableCallback} for * handling variable updates from the controller. */ protected void onCreateVariableCallback() { BlocklyController.VariableCallback variableCb = new DefaultVariableCallback(mActivity, mController); mController.setVariableCallback(variableCb); } /** * Resets the {@link BlockFactory} with the provided block definitions and extensions. * @param blockDefinitionsJsonPaths The list of definition asset paths. * @param blockExtensions The extensions supporting the block definitions. * @throws IllegalStateException On any issues with the input. */ public void resetBlockFactory(@Nullable List<String> blockDefinitionsJsonPaths, @Nullable Map<String, BlockExtension> blockExtensions) { AssetManager assets = mActivity.getAssets(); BlockFactory factory = mController.getBlockFactory(); factory.clear(); String assetPath = null; try { if (blockExtensions != null) { for (String id : blockExtensions.keySet()) { factory.registerExtension(id, blockExtensions.get(id)); } } if (blockDefinitionsJsonPaths != null) { for (String path : blockDefinitionsJsonPaths) { assetPath = path; factory.addJsonDefinitions(assets.open(path)); } } } catch (IOException | BlockLoadingException e) { throw new IllegalStateException("Failed to load block definition asset file: " + assetPath, e); } } /** * Reloads the toolbox from assets. * @param toolboxContentsXmlPath The asset path to the toolbox XML * @throws IllegalStateException If error occurs during loading. */ public void reloadToolbox(String toolboxContentsXmlPath) { AssetManager assetManager = mActivity.getAssets(); BlocklyController controller = getController(); try { controller.loadToolboxContents(assetManager.open(toolboxContentsXmlPath)); } catch (IOException | BlockLoadingException e) { // compile time assets such as assets are assumed to be good. throw new IllegalStateException("Failed to load toolbox XML.", e); } } }