Java tutorial
// -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved // Copyright 2011-2012 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 // *********************************************** // If we're not going to go this route with onDestroy, then at least get rid of the DEBUG flag. package com.google.appinventor.components.runtime; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.json.JSONException; import android.app.Activity; import android.app.Dialog; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.PorterDuff; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.FragmentActivity; import android.text.Html; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.ScrollView; import android.widget.Toast; import com.google.appinventor.components.annotations.DesignerComponent; import com.google.appinventor.components.annotations.DesignerProperty; import com.google.appinventor.components.annotations.PropertyCategory; import com.google.appinventor.components.annotations.SimpleEvent; import com.google.appinventor.components.annotations.SimpleObject; import com.google.appinventor.components.annotations.SimpleProperty; import com.google.appinventor.components.annotations.UsesLibraries; import com.google.appinventor.components.annotations.UsesPermissions; import com.google.appinventor.components.common.ComponentCategory; import com.google.appinventor.components.common.ComponentConstants; import com.google.appinventor.components.common.PropertyTypeConstants; import com.google.appinventor.components.common.YaVersion; import com.google.appinventor.components.runtime.collect.Lists; import com.google.appinventor.components.runtime.collect.Maps; import com.google.appinventor.components.runtime.collect.Sets; import com.google.appinventor.components.runtime.util.AlignmentUtil; import com.google.appinventor.components.runtime.util.AnimationUtil; import com.google.appinventor.components.runtime.util.ErrorMessages; import com.google.appinventor.components.runtime.util.FullScreenVideoUtil; import com.google.appinventor.components.runtime.util.JsonUtil; import com.google.appinventor.components.runtime.util.MediaUtil; import com.google.appinventor.components.runtime.util.OnInitializeListener; import com.google.appinventor.components.runtime.util.SdkLevel; import com.google.appinventor.components.runtime.util.ViewUtil; /** * Component underlying activities and UI apps, not directly accessible to Simple programmers. * * <p>This is the root container of any Android activity and also the * superclass for for Simple/Android UI applications. * * The main form is always named "Screen1". * */ @DesignerComponent(version = YaVersion.FORM_COMPONENT_VERSION, category = ComponentCategory.LAYOUT, description = "Top-level component containing all other components in the program", showOnPalette = false) @SimpleObject @UsesLibraries(libraries = "android-support-v4.jar") @UsesPermissions(permissionNames = "android.permission.INTERNET,android.permission.ACCESS_WIFI_STATE,android.permission.ACCESS_NETWORK_STATE") public class Form extends FragmentActivity implements Component, ComponentContainer, HandlesEventDispatching { private static final String LOG_TAG = "Form"; private static final String RESULT_NAME = "APP_INVENTOR_RESULT"; private static final String ARGUMENT_NAME = "APP_INVENTOR_START"; public static final String APPINVENTOR_URL_SCHEME = "appinventor"; // Keep track of the current form object. // activeForm always holds the Form that is currently handling event dispatching so runtime.scm // can lookup symbols in the correct environment. // There is at least one case where an event can be fired when the activity is not the foreground // activity: if a Clock component's TimerAlwaysFires property is true, the Clock component's // Timer event will still fire, even when the activity is no longer in the foreground. For this // reason, we cannot assume that the activeForm is the foreground activity. protected static Form activeForm; // applicationIsBeingClosed is set to true during closeApplication. private static boolean applicationIsBeingClosed; private final Handler androidUIHandler = new Handler(); protected String formName; private boolean screenInitialized; private static final int SWITCH_FORM_REQUEST_CODE = 1; private static int nextRequestCode = SWITCH_FORM_REQUEST_CODE + 1; // Backing for background color private int backgroundColor; // Information string the app creator can set. It will be shown when // "about this application" menu item is selected. private String aboutScreen; private boolean showStatusBar = true; private boolean showTitle = true; private String backgroundImagePath = ""; private Drawable backgroundDrawable; // Layout private LinearLayout viewLayout; // List of components used to support Iterator interface private List<AndroidViewComponent> components; // translates App Inventor alignment codes to Android gravity private AlignmentUtil alignmentSetter; // the alignment for this component's LinearLayout private int horizontalAlignment; private int verticalAlignment; // String representing the transition animation type private String openAnimType; private String closeAnimType; private FrameLayout frameLayout; private boolean scrollable; // Application lifecycle related fields private final HashMap<Integer, ActivityResultListener> activityResultMap = Maps.newHashMap(); private final Set<OnStopListener> onStopListeners = Sets.newHashSet(); private final Set<OnNewIntentListener> onNewIntentListeners = Sets.newHashSet(); private final Set<OnResumeListener> onResumeListeners = Sets.newHashSet(); private final Set<OnPauseListener> onPauseListeners = Sets.newHashSet(); private final Set<OnDestroyListener> onDestroyListeners = Sets.newHashSet(); // AppInventor lifecycle: listeners for the Initialize Event private final Set<OnInitializeListener> onInitializeListeners = Sets.newHashSet(); // Set to the optional String-valued Extra passed in via an Intent on startup. // This is passed directly in the Repl. protected String startupValue = ""; // To control volume of error complaints private static long minimumToastWait = 10000000000L; // 10 seconds private long lastToastTime = System.nanoTime() - minimumToastWait; // In a multiple screen application, when a secondary screen is opened, nextFormName is set to // the name of the secondary screen. It is saved so that it can be passed to the OtherScreenClosed // event. private String nextFormName; private FullScreenVideoUtil fullScreenVideoUtil; //This is the constant value that will be passed in as the key in the intent's putExtra to find //the component that the Form will pass the intent's extra value to //The convention for naming a extra value that requires the Form to pass along can be: //APP_INVENTOR_XYZ (XYZ = component's name) //The value for component XYZ will be retrieved through form.getXYZStartValues() private static final String ARGUMENT_SURVEY = "APP_INVENTOR_SURVEY"; //Set to the optional String-valued Extra passed in via an Intent on startup.(for Survey component only) private String startupValueForSurvey = ""; //This is the constant value that will be passed in as the key in the intent's putExtra to find //the component that the Form will pass the intent's extra value to //The convention for naming a extra value that requires the Form to pass along can be: //APP_INVENTOR_XYZ (XYZ = component's name) //The value for component XYZ will be retrieved through form.getXYZStartValues() private static final String ARGUMENT_GCM = "APP_INVENTOR_GCM"; //Set to the optional String-valued Extra passed in via an Intent on startup.(for GCM component only) private String startupValueForGCM = ""; private Bundle onCreateBundle = null; private long lastBackPressTime; @Override public void onCreate(Bundle icicle) { // Called when the activity is first created super.onCreate(icicle); Log.i(LOG_TAG, "saveBundle" + icicle); onCreateBundle = icicle; // icicle, (savedInstance == null) if it's not the result of changing orientation // Figure out the name of this form. String className = getClass().getName(); int lastDot = className.lastIndexOf('.'); formName = className.substring(lastDot + 1); Log.d(LOG_TAG, "Form " + formName + " got onCreate"); activeForm = this; Log.i(LOG_TAG, "activeForm is now " + activeForm.formName); viewLayout = new LinearLayout(this, ComponentConstants.LAYOUT_ORIENTATION_VERTICAL); components = new ArrayList<AndroidViewComponent>(); alignmentSetter = new AlignmentUtil(viewLayout); defaultPropertyValues(); // Get startup text if any before adding components Intent startIntent = getIntent(); if (startIntent != null && startIntent.hasExtra(ARGUMENT_NAME)) { startupValue = startIntent.getStringExtra(ARGUMENT_NAME); } fullScreenVideoUtil = new FullScreenVideoUtil(this, androidUIHandler); /* * Add by Fuming. * We can save extras values that are intended for the Survey component, * and later the component can read the value through container$form * TODO: need to think about how to pass intent to a component through notification. * Something like the activity starter: the extra_key will be the component name * the extra_value to pass in will be a json string. * * TODO: We could think of a more general approach that could work for arbitrary AI component that needs * intent data at start up. * */ if (startIntent != null && startIntent.hasExtra(ARGUMENT_SURVEY)) { Log.i(LOG_TAG, "surveyIntentValue:" + startIntent.getStringExtra(ARGUMENT_SURVEY)); startupValueForSurvey = startIntent.getStringExtra(ARGUMENT_SURVEY); } /* * Add by Weihua Li. * We can save extras values that are intended for the GCM component, * and later the component can read the value through container$form * TODO: need to think about how to pass intent to a component through notification. * Something like the activity starter: the extra_key will be the component name * the extra_value to pass in will be a json string. * * TODO: We could think of a more general approach that could work for arbitrary AI component that needs * intent data at start up. * */ if (startIntent != null && startIntent.hasExtra(ARGUMENT_GCM)) { Log.i(LOG_TAG, "GCMIntentValue:" + startIntent.getStringExtra(ARGUMENT_GCM)); startupValueForGCM = startIntent.getStringExtra(ARGUMENT_GCM); } // Set soft keyboard to not cover the focused UI element, e.g., when you are typing // into a textbox near the bottom of the screen. WindowManager.LayoutParams params = getWindow().getAttributes(); int softInputMode = params.softInputMode; getWindow().setSoftInputMode(softInputMode | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); // Add application components to the form $define(); // Special case for Event.Initialize(): all other initialize events are triggered after // completing the constructor. This doesn't work for Android apps though because this method // is called after the constructor completes and therefore the Initialize event would run // before initialization finishes. Instead the compiler suppresses the invocation of the // event and leaves it up to the library implementation. Initialize(); } /** * Getting Bundle (savedInstance) in the onCreate method (this is needed for * Google Map Component to avoid recreating two map layers when changing orientation) * @return */ public Bundle getOnCreateBundle() { return onCreateBundle; } /* * 1) This method is to pass the start value that a Form gets when it is created by some other app using * activityStarter or Fuming's create notification (using Android Intent) * 2) The alternative to pass the intent's extra values could use listeners such as resgisterForXXX in * Form.java. , */ public String getSurveyStartValues() { return startupValueForSurvey; } /* * 1) This method is to pass the start value that a Form gets when it is created by some other app using * activityStarter or Wei's create notification (using Android Intent) * 2) The alternative to pass the intent's extra values could use listeners such as resgisterForXXX in * Form.java. , */ public String getGCMStartValues() { return startupValueForGCM; } private void defaultPropertyValues() { Scrollable(false); // frameLayout is created in Scrollable() AboutScreen(""); BackgroundImage(""); BackgroundColor(Component.COLOR_WHITE); AlignHorizontal(ComponentConstants.GRAVITY_LEFT); AlignVertical(ComponentConstants.GRAVITY_TOP); Title(""); ShowStatusBar(true); TitleVisible(true); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); final int newOrientation = newConfig.orientation; if (newOrientation == Configuration.ORIENTATION_LANDSCAPE || newOrientation == Configuration.ORIENTATION_PORTRAIT) { // At this point, the screen has not be resized to match the new orientation. // We use Handler.post so that we'll dispatch the ScreenOrientationChanged event after the // screen has been resized to match the new orientation. androidUIHandler.post(new Runnable() { public void run() { boolean dispatchEventNow = false; if (frameLayout != null) { if (newOrientation == Configuration.ORIENTATION_LANDSCAPE) { if (frameLayout.getWidth() >= frameLayout.getHeight()) { dispatchEventNow = true; } } else { // Portrait if (frameLayout.getHeight() >= frameLayout.getWidth()) { dispatchEventNow = true; } } } if (dispatchEventNow) { ScreenOrientationChanged(); } else { // Try again later. androidUIHandler.post(this); } } }); } } /* * Here we override the hardware back button, just to make sure * that the closing screen animation is applied. (In API level * 5, we can simply override the onBackPressed method rather * than bothering with onKeyDown) */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { if (!BackPressed()) { boolean handled = super.onKeyDown(keyCode, event); AnimationUtil.ApplyCloseScreenAnimation(this, closeAnimType); return handled; } else { return true; } } return super.onKeyDown(keyCode, event); } @SimpleEvent(description = "Device back button pressed.") public boolean BackPressed() { return EventDispatcher.dispatchEvent(this, "BackPressed"); } // onActivityResult should be triggered in only two cases: // (1) The result is for some other component in the app, not this Form itself // (2) This page started another page, and that page is closing, and passing // its value back as a JSON-encoded string in the intent. @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.i(LOG_TAG, "Form " + formName + " got onActivityResult, requestCode = " + requestCode + ", resultCode = " + resultCode); if (requestCode == SWITCH_FORM_REQUEST_CODE) { // Assume this is a multiple screen application, and a secondary // screen has closed. Process the result as a JSON-encoded string. // This can also happen if the user presses the back button, in which case // there's no data. String resultString; if (data != null && data.hasExtra(RESULT_NAME)) { resultString = data.getStringExtra(RESULT_NAME); } else { resultString = ""; } Object decodedResult = decodeJSONStringForForm(resultString, "other screen closed"); // nextFormName was set when this screen opened the secondary screen OtherScreenClosed(nextFormName, decodedResult); } else { // Another component (such as a ListPicker, ActivityStarter, etc) is expecting this result. ActivityResultListener component = activityResultMap.get(requestCode); if (component != null) { component.resultReturned(requestCode, resultCode, data); } } } // functionName is a string to include in the error message that will be shown // if the JSON decoding fails private static Object decodeJSONStringForForm(String jsonString, String functionName) { Log.i(LOG_TAG, "decodeJSONStringForForm -- decoding JSON representation:" + jsonString); Object valueFromJSON = ""; try { valueFromJSON = JsonUtil.getObjectFromJson(jsonString); Log.i(LOG_TAG, "decodeJSONStringForForm -- got decoded JSON:" + valueFromJSON.toString()); } catch (JSONException e) { activeForm.dispatchErrorOccurredEvent(activeForm, functionName, // showing the start value here will produce an ugly error on the phone, but it's // more useful than not showing the value ErrorMessages.ERROR_SCREEN_BAD_VALUE_RECEIVED, jsonString); } return valueFromJSON; } public int registerForActivityResult(ActivityResultListener listener) { int requestCode = generateNewRequestCode(); activityResultMap.put(requestCode, listener); return requestCode; } public void unregisterForActivityResult(ActivityResultListener listener) { List<Integer> keysToDelete = Lists.newArrayList(); for (Map.Entry<Integer, ActivityResultListener> mapEntry : activityResultMap.entrySet()) { if (listener.equals(mapEntry.getValue())) { keysToDelete.add(mapEntry.getKey()); } } for (Integer key : keysToDelete) { activityResultMap.remove(key); } } private static int generateNewRequestCode() { return nextRequestCode++; } @Override protected void onResume() { super.onResume(); Log.i(LOG_TAG, "Form " + formName + " got onResume"); activeForm = this; // If applicationIsBeingClosed is true, call closeApplication() immediately to continue // unwinding through all forms of a multi-screen application. if (applicationIsBeingClosed) { closeApplication(); return; } for (OnResumeListener onResumeListener : onResumeListeners) { onResumeListener.onResume(); } } public void registerForOnResume(OnResumeListener component) { onResumeListeners.add(component); } /** * An app can register to be notified when App Inventor's Initialize * block has fired. They will be called in Initialize(). * * @param component */ public void registerForOnInitialize(OnInitializeListener component) { onInitializeListeners.add(component); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); Log.d(LOG_TAG, "Form " + formName + " got onNewIntent " + intent); for (OnNewIntentListener onNewIntentListener : onNewIntentListeners) { onNewIntentListener.onNewIntent(intent); } } public void registerForOnNewIntent(OnNewIntentListener component) { onNewIntentListeners.add(component); } @Override protected void onPause() { super.onPause(); Log.i(LOG_TAG, "Form " + formName + " got onPause"); for (OnPauseListener onPauseListener : onPauseListeners) { onPauseListener.onPause(); } } public void registerForOnPause(OnPauseListener component) { onPauseListeners.add(component); } @Override protected void onStop() { super.onStop(); Log.i(LOG_TAG, "Form " + formName + " got onStop"); for (OnStopListener onStopListener : onStopListeners) { onStopListener.onStop(); } } public void registerForOnStop(OnStopListener component) { onStopListeners.add(component); } @Override protected void onDestroy() { super.onDestroy(); // for debugging and future growth Log.i(LOG_TAG, "Form " + formName + " got onDestroy"); // Unregister events for components in this form. EventDispatcher.removeDispatchDelegate(this); for (OnDestroyListener onDestroyListener : onDestroyListeners) { onDestroyListener.onDestroy(); } } public void registerForOnDestroy(OnDestroyListener component) { onDestroyListeners.add(component); } public Dialog onCreateDialog(int id) { switch (id) { case FullScreenVideoUtil.FULLSCREEN_VIDEO_DIALOG_FLAG: return fullScreenVideoUtil.createFullScreenVideoDialog(); default: return super.onCreateDialog(id); } } public void onPrepareDialog(int id, Dialog dialog) { switch (id) { case FullScreenVideoUtil.FULLSCREEN_VIDEO_DIALOG_FLAG: fullScreenVideoUtil.prepareFullScreenVideoDialog(dialog); break; default: super.onPrepareDialog(id, dialog); } } /** * Compiler-generated method to initialize and add application components to * the form. We just provide an implementation here to artificially make * this class concrete so that it is included in the documentation and * Codeblocks language definition file generated by * {@link com.google.appinventor.components.scripts.DocumentationGenerator} and * {@link com.google.appinventor.components.scripts.LangDefXmlGenerator}, * respectively. The actual implementation appears in {@code runtime.scm}. */ protected void $define() { // This must be declared protected because we are called from Screen1 which subclasses // us and isn't in our package. throw new UnsupportedOperationException(); } @Override public boolean canDispatchEvent(Component component, String eventName) { // Events can only be dispatched after the screen initialized event has completed. boolean canDispatch = screenInitialized || (component == this && eventName.equals("Initialize")); if (canDispatch) { // Set activeForm to this before the event is dispatched. // runtime.scm will call getActiveForm() when the event handler executes. activeForm = this; } return canDispatch; } /** * A trivial implementation to artificially make this class concrete so * that it is included in the documentation and * Codeblocks language definition file generated by * {@link com.google.appinventor.components.scripts.DocumentationGenerator} and * {@link com.google.appinventor.components.scripts.LangDefXmlGenerator}, * respectively. The actual implementation appears in {@code runtime.scm}. */ @Override public boolean dispatchEvent(Component component, String componentName, String eventName, Object[] args) { throw new UnsupportedOperationException(); } /** * Initialize event handler. */ @SimpleEvent(description = "Screen starting") public void Initialize() { // Dispatch the Initialize event only after the screen's width and height are no longer zero. androidUIHandler.post(new Runnable() { public void run() { if (frameLayout != null && frameLayout.getWidth() != 0 && frameLayout.getHeight() != 0) { EventDispatcher.dispatchEvent(Form.this, "Initialize"); screenInitialized = true; // Call all apps registered to be notified when Initialize Event is dispatched for (OnInitializeListener onInitializeListener : onInitializeListeners) { onInitializeListener.onInitialize(); } if (activeForm instanceof ReplForm) { // We are the Companion ((ReplForm) activeForm).HandleReturnValues(); } } else { // Try again later. androidUIHandler.post(this); } } }); } @SimpleEvent(description = "Screen orientation changed") public void ScreenOrientationChanged() { EventDispatcher.dispatchEvent(this, "ScreenOrientationChanged"); } /** * ErrorOccurred event handler. */ @SimpleEvent(description = "Event raised when an error occurs. Only some errors will " + "raise this condition. For those errors, the system will show a notification " + "by default. You can use this event handler to prescribe an error " + "behavior different than the default.") public void ErrorOccurred(Component component, String functionName, int errorNumber, String message) { String componentType = component.getClass().getName(); componentType = componentType.substring(componentType.lastIndexOf(".") + 1); Log.e(LOG_TAG, "Form " + formName + " ErrorOccurred, errorNumber = " + errorNumber + ", componentType = " + componentType + ", functionName = " + functionName + ", messages = " + message); if ((!(EventDispatcher.dispatchEvent(this, "ErrorOccurred", component, functionName, errorNumber, message))) && screenInitialized) { // If dispatchEvent returned false, then no user-supplied error handler was run. // If in addition, the screen initializer was run, then we assume that the // user did not provide an error handler. In this case, we run a default // error handler, namely, showing a notification to the end user of the app. // The app writer can override this by providing an error handler. new Notifier(this).ShowAlert("Error " + errorNumber + ": " + message); } } public void ErrorOccurredDialog(Component component, String functionName, int errorNumber, String message, String title, String buttonText) { String componentType = component.getClass().getName(); componentType = componentType.substring(componentType.lastIndexOf(".") + 1); Log.e(LOG_TAG, "Form " + formName + " ErrorOccurred, errorNumber = " + errorNumber + ", componentType = " + componentType + ", functionName = " + functionName + ", messages = " + message); if ((!(EventDispatcher.dispatchEvent(this, "ErrorOccurred", component, functionName, errorNumber, message))) && screenInitialized) { // If dispatchEvent returned false, then no user-supplied error handler was run. // If in addition, the screen initializer was run, then we assume that the // user did not provide an error handler. In this case, we run a default // error handler, namely, showing a message dialog to the end user of the app. // The app writer can override this by providing an error handler. new Notifier(this).ShowMessageDialog("Error " + errorNumber + ": " + message, title, buttonText); } } public void dispatchErrorOccurredEvent(final Component component, final String functionName, final int errorNumber, final Object... messageArgs) { runOnUiThread(new Runnable() { public void run() { String message = ErrorMessages.formatMessage(errorNumber, messageArgs); ErrorOccurred(component, functionName, errorNumber, message); } }); } // This is like dispatchErrorOccurred, except that it defaults to showing // a message dialog rather than an alert. The app writer can override either of these behaviors, // but using the event dialog version frees the app writer of the need to explicitly override // the alert behavior in the case // where a message dialog is what's generally needed. public void dispatchErrorOccurredEventDialog(final Component component, final String functionName, final int errorNumber, final Object... messageArgs) { runOnUiThread(new Runnable() { public void run() { String message = ErrorMessages.formatMessage(errorNumber, messageArgs); ErrorOccurredDialog(component, functionName, errorNumber, message, "Error in " + functionName, "Dismiss"); } }); } /** * Scrollable property getter method. * * @return true if the screen is vertically scrollable */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "When checked, there will be a vertical scrollbar on the " + "screen, and the height of the application can exceed the physical " + "height of the device. When unchecked, the application height is " + "constrained to the height of the device.") public boolean Scrollable() { return scrollable; } /** * Scrollable property setter method. * * @param scrollable true if the screen should be vertically scrollable */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = "False") @SimpleProperty public void Scrollable(boolean scrollable) { if (this.scrollable == scrollable && frameLayout != null) { return; } // Remove our view from the current frameLayout. if (frameLayout != null) { frameLayout.removeAllViews(); } this.scrollable = scrollable; frameLayout = scrollable ? new ScrollView(this) : new FrameLayout(this); frameLayout.addView(viewLayout.getLayoutManager(), new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); setBackground(frameLayout); setContentView(frameLayout); frameLayout.requestLayout(); } /** * BackgroundColor property getter method. * * @return background RGB color with alpha */ @SimpleProperty(category = PropertyCategory.APPEARANCE) public int BackgroundColor() { return backgroundColor; } /** * BackgroundColor property setter method. * * @param argb background RGB color with alpha */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_COLOR, defaultValue = Component.DEFAULT_VALUE_COLOR_WHITE) @SimpleProperty public void BackgroundColor(int argb) { backgroundColor = argb; // setBackground(viewLayout.getLayoutManager()); // Doesn't seem necessary anymore setBackground(frameLayout); } /** * Returns the path of the background image. * * @return the path of the background image */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "The screen background image.") public String BackgroundImage() { return backgroundImagePath; } /** * Specifies the path of the background image. * * <p/>See {@link MediaUtil#determineMediaSource} for information about what * a path can be. * * @param path the path of the background image */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_ASSET, defaultValue = "") @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "The screen background image.") public void BackgroundImage(String path) { backgroundImagePath = (path == null) ? "" : path; try { backgroundDrawable = MediaUtil.getBitmapDrawable(this, backgroundImagePath); } catch (IOException ioe) { Log.e(LOG_TAG, "Unable to load " + backgroundImagePath); backgroundDrawable = null; } setBackground(frameLayout); } /** * Title property getter method. * * @return form caption */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "The caption for the form, which apears in the title bar") public String Title() { return getTitle().toString(); } /** * Title property setter method: sets a new caption for the form in the * form's title bar. * * @param title new form caption */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "") @SimpleProperty public void Title(String title) { setTitle(title); } /** * AboutScreen property getter method. * * @return AboutScreen string */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "Information about the screen. It appears when \"About this Application\" " + "is selected from the system menu. Use it to inform people about your app. In multiple " + "screen apps, each screen has its own AboutScreen info.") public String AboutScreen() { return aboutScreen; } /** * AboutScreen property setter method: sets a new aboutApp string for the form in the * form's "About this application" menu. * * @param title new form caption */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_TEXTAREA, defaultValue = "") @SimpleProperty public void AboutScreen(String aboutScreen) { this.aboutScreen = aboutScreen; } /** * TitleVisible property getter method. * * @return showTitle boolean */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "The title bar is the top gray bar on the screen. This property reports whether the title bar is visible.") public boolean TitleVisible() { return showTitle; } /** * TitleVisible property setter method. * * @param show boolean */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = "True") @SimpleProperty(category = PropertyCategory.APPEARANCE) public void TitleVisible(boolean show) { if (show != showTitle) { View v = (View) findViewById(android.R.id.title).getParent(); if (v != null) { if (show) { v.setVisibility(View.VISIBLE); } else { v.setVisibility(View.GONE); } showTitle = show; } } } /** * ShowStatusBar property getter method. * * @return showStatusBar boolean */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "The status bar is the topmost bar on the screen. This property reports whether the status bar is visible.") public boolean ShowStatusBar() { return showStatusBar; } /** * ShowStatusBar property setter method. * * @param show boolean */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = "True") @SimpleProperty(category = PropertyCategory.APPEARANCE) public void ShowStatusBar(boolean show) { if (show != showStatusBar) { if (show) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } else { getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); } showStatusBar = show; } } /** * The requested screen orientation. Commonly used values are unspecified (-1), landscape (0), portrait (1), sensor (4), and user (2). " + "See the Android developer documentation for ActivityInfo.Screen_Orientation for the " + "complete list of possible settings. * * ScreenOrientation property getter method. * * @return screen orientation */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "The requested screen orientation, specified as a text value. " + "Commonly used values are " + "landscape, portrait, sensor, user and unspecified. " + "See the Android developer documentation for ActivityInfo.Screen_Orientation for the " + "complete list of possible settings.") public String ScreenOrientation() { switch (getRequestedOrientation()) { case ActivityInfo.SCREEN_ORIENTATION_BEHIND: return "behind"; case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE: return "landscape"; case ActivityInfo.SCREEN_ORIENTATION_NOSENSOR: return "nosensor"; case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT: return "portrait"; case ActivityInfo.SCREEN_ORIENTATION_SENSOR: return "sensor"; case ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED: return "unspecified"; case ActivityInfo.SCREEN_ORIENTATION_USER: return "user"; case 10: // ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR return "fullSensor"; case 8: // ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE return "reverseLandscape"; case 9: // ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT return "reversePortrait"; case 6: // ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE return "sensorLandscape"; case 7: // ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT return "sensorPortrait"; } return "unspecified"; } /** * ScreenOrientation property setter method: sets the screen orientation for * the form. * * @param screenOrientation the screen orientation as a string */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_SCREEN_ORIENTATION, defaultValue = "unspecified") @SimpleProperty(category = PropertyCategory.APPEARANCE) public void ScreenOrientation(String screenOrientation) { if (screenOrientation.equalsIgnoreCase("behind")) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_BEHIND); } else if (screenOrientation.equalsIgnoreCase("landscape")) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } else if (screenOrientation.equalsIgnoreCase("nosensor")) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR); } else if (screenOrientation.equalsIgnoreCase("portrait")) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } else if (screenOrientation.equalsIgnoreCase("sensor")) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); } else if (screenOrientation.equalsIgnoreCase("unspecified")) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } else if (screenOrientation.equalsIgnoreCase("user")) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER); } else if (SdkLevel.getLevel() >= SdkLevel.LEVEL_GINGERBREAD) { if (screenOrientation.equalsIgnoreCase("fullSensor")) { setRequestedOrientation(10); // ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR } else if (screenOrientation.equalsIgnoreCase("reverseLandscape")) { setRequestedOrientation(8); // ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE } else if (screenOrientation.equalsIgnoreCase("reversePortrait")) { setRequestedOrientation(9); // ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT } else if (screenOrientation.equalsIgnoreCase("sensorLandscape")) { setRequestedOrientation(6); // ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE } else if (screenOrientation.equalsIgnoreCase("sensorPortrait")) { setRequestedOrientation(7); // ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT } else { dispatchErrorOccurredEvent(this, "ScreenOrientation", ErrorMessages.ERROR_INVALID_SCREEN_ORIENTATION, screenOrientation); } } else { dispatchErrorOccurredEvent(this, "ScreenOrientation", ErrorMessages.ERROR_INVALID_SCREEN_ORIENTATION, screenOrientation); } } // Note(halabelson): This section on centering is duplicated between Form and HVArrangement // I did not see a clean way to abstract it. Someone should have a look. // Note(halabelson): The numeric encodings of the alignment specifications are specified // in ComponentConstants /** * Returns a number that encodes how contents of the screen are aligned horizontally. * The choices are: 1 = left aligned, 2 = horizontally centered, 3 = right aligned */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "A number that encodes how contents of the screen are aligned " + " horizontally. The choices are: 1 = left aligned, 2 = horizontally centered, " + " 3 = right aligned.") public int AlignHorizontal() { return horizontalAlignment; } /** * Sets the horizontal alignment for contents of the screen * * @param alignment */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_HORIZONTAL_ALIGNMENT, defaultValue = ComponentConstants.HORIZONTAL_ALIGNMENT_DEFAULT + "") @SimpleProperty public void AlignHorizontal(int alignment) { try { // notice that the throw will prevent the alignment from being changed // if the argument is illegal alignmentSetter.setHorizontalAlignment(alignment); horizontalAlignment = alignment; } catch (IllegalArgumentException e) { this.dispatchErrorOccurredEvent(this, "HorizontalAlignment", ErrorMessages.ERROR_BAD_VALUE_FOR_HORIZONTAL_ALIGNMENT, alignment); } } /** * Returns a number that encodes how contents of the arrangement are aligned vertically. * The choices are: 1 = top, 2 = vertically centered, 3 = aligned at the bottom. * Vertical alignment has no effect if the screen is scrollable. */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "A number that encodes how the contents of the arrangement are aligned " + "vertically. The choices are: 1 = aligned at the top, 2 = vertically centered, " + "3 = aligned at the bottom. Vertical alignment has no effect if the screen is scrollable.") public int AlignVertical() { return verticalAlignment; } /** * Sets the vertical alignment for contents of the screen * * @param alignment */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_VERTICAL_ALIGNMENT, defaultValue = ComponentConstants.VERTICAL_ALIGNMENT_DEFAULT + "") @SimpleProperty public void AlignVertical(int alignment) { try { // notice that the throw will prevent the alignment from being changed // if the argument is illegal alignmentSetter.setVerticalAlignment(alignment); verticalAlignment = alignment; } catch (IllegalArgumentException e) { this.dispatchErrorOccurredEvent(this, "VerticalAlignment", ErrorMessages.ERROR_BAD_VALUE_FOR_VERTICAL_ALIGNMENT, alignment); } } /** * Returns the type of open screen animation (default, fade, zoom, slidehorizontal, * slidevertical and none). * * @return open screen animation */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "The animation for switching to another screen. Valid" + " options are default, fade, zoom, slidehorizontal, slidevertical, and none") public String OpenScreenAnimation() { return openAnimType; } /** * Sets the animation type for the transition to another screen. * * @param animType the type of animation to use for the transition */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_SCREEN_ANIMATION, defaultValue = "default") @SimpleProperty public void OpenScreenAnimation(String animType) { if ((animType != "default") && (animType != "fade") && (animType != "zoom") && (animType != "slidehorizontal") && (animType != "slidevertical") && (animType != "none")) { this.dispatchErrorOccurredEvent(this, "Screen", ErrorMessages.ERROR_SCREEN_INVALID_ANIMATION, animType); return; } openAnimType = animType; } /** * Returns the type of close screen animation (default, fade, zoom, slidehorizontal, * slidevertical and none). * * @return open screen animation */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "The animation for closing current screen and returning " + " to the previous screen. Valid options are default, fade, zoom, slidehorizontal, " + "slidevertical, and none") public String CloseScreenAnimation() { return closeAnimType; } /** * Sets the animation type for the transition of this form closing and returning * to a form behind it in the activity stack. * * @param animType the type of animation to use for the transition */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_SCREEN_ANIMATION, defaultValue = "default") @SimpleProperty public void CloseScreenAnimation(String animType) { if ((animType != "default") && (animType != "fade") && (animType != "zoom") && (animType != "slidehorizontal") && (animType != "slidevertical") && (animType != "none")) { this.dispatchErrorOccurredEvent(this, "Screen", ErrorMessages.ERROR_SCREEN_INVALID_ANIMATION, animType); return; } closeAnimType = animType; } /* * Used by ListPicker, and ActivityStarter to get this Form's current opening transition * animation */ public String getOpenAnimType() { return openAnimType; } /** * Specifies the name of the application icon. * * @param name the name of the application icon */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_ASSET, defaultValue = "") @SimpleProperty(userVisible = false) public void Icon(String name) { // We don't actually need to do anything. } /** * Specifies the Version Code. * * @param vCode the version name of the application */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER, defaultValue = "1") @SimpleProperty(userVisible = false, description = "An integer value which must be incremented each time a new Android " + "Application Package File (APK) is created for the Google Play Store.") public void VersionCode(int vCode) { // We don't actually need to do anything. } /** * Specifies the Version Name. * * @param vName the version name of the application */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "1.0") @SimpleProperty(userVisible = false, description = "A string which can be changed to allow Google Play " + "Store users to distinguish between different versions of the App.") public void VersionName(String vName) { // We don't actually need to do anything. } /** * Specifies the App Name. * * @param aName the display name of the installed application in the phone */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "") @SimpleProperty(userVisible = false, description = "This is the display name of the installed application in the phone." + "If the AppName is blank, it will be set to the name of the project when the project is built.") public void AppName(String aName) { // We don't actually need to do anything. } /** * Width property getter method. * * @return width property used by the layout */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "Screen width (x-size).") public int Width() { return frameLayout.getWidth(); } /** * Height property getter method. * * @return height property used by the layout */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "Screen height (y-size).") public int Height() { return frameLayout.getHeight(); } /** * Display a new form. * * @param nextFormName the name of the new form to display */ // This is called from runtime.scm when a "open another screen" block is executed. public static void switchForm(String nextFormName) { if (activeForm != null) { activeForm.startNewForm(nextFormName, null); } else { throw new IllegalStateException("activeForm is null"); } } /** * Display a new form and pass a startup value to the new form. * * @param nextFormName the name of the new form to display * @param startValue the start value to pass to the new form */ // This is called from runtime.scm when a "open another screen with start value" block is // executed. Note that startNewForm will JSON encode the start value public static void switchFormWithStartValue(String nextFormName, Object startValue) { Log.i(LOG_TAG, "Open another screen with start value:" + nextFormName); if (activeForm != null) { activeForm.startNewForm(nextFormName, startValue); } else { throw new IllegalStateException("activeForm is null"); } } // This JSON encodes the startup value protected void startNewForm(String nextFormName, Object startupValue) { Log.i(LOG_TAG, "startNewForm:" + nextFormName); Intent activityIntent = new Intent(); // Note that the following is dependent on form generated class names being the same as // their form names and all forms being in the same package. activityIntent.setClassName(this, getPackageName() + "." + nextFormName); String functionName = (startupValue == null) ? "open another screen" : "open another screen with start value"; String jValue; if (startupValue != null) { Log.i(LOG_TAG, "StartNewForm about to JSON encode:" + startupValue); jValue = jsonEncodeForForm(startupValue, functionName); Log.i(LOG_TAG, "StartNewForm got JSON encoding:" + jValue); } else { jValue = ""; } activityIntent.putExtra(ARGUMENT_NAME, jValue); // Save the nextFormName so that it can be passed to the OtherScreenClosed event in the // future. this.nextFormName = nextFormName; Log.i(LOG_TAG, "about to start new form" + nextFormName); try { Log.i(LOG_TAG, "startNewForm starting activity:" + activityIntent); startActivityForResult(activityIntent, SWITCH_FORM_REQUEST_CODE); } catch (ActivityNotFoundException e) { dispatchErrorOccurredEvent(this, functionName, ErrorMessages.ERROR_SCREEN_NOT_FOUND, nextFormName); } } // functionName is used for including in the error message to be shown // if the JSON encoding fails protected static String jsonEncodeForForm(Object value, String functionName) { String jsonResult = ""; Log.i(LOG_TAG, "jsonEncodeForForm -- creating JSON representation:" + value.toString()); try { // TODO(hal): check that this is OK for raw strings jsonResult = JsonUtil.getJsonRepresentation(value); Log.i(LOG_TAG, "jsonEncodeForForm -- got JSON representation:" + jsonResult); } catch (JSONException e) { activeForm.dispatchErrorOccurredEvent(activeForm, functionName, // showing the bad value here will produce an ugly error on the phone, but it's // more useful than not showing the value ErrorMessages.ERROR_SCREEN_BAD_VALUE_FOR_SENDING, value.toString()); } return jsonResult; } @SimpleEvent(description = "Event raised when another screen has closed and control has " + "returned to this screen.") public void OtherScreenClosed(String otherScreenName, Object result) { Log.i(LOG_TAG, "Form " + formName + " OtherScreenClosed, otherScreenName = " + otherScreenName + ", result = " + result.toString()); EventDispatcher.dispatchEvent(this, "OtherScreenClosed", otherScreenName, result); } // Component implementation @Override public HandlesEventDispatching getDispatchDelegate() { return this; } // ComponentContainer implementation @Override public Activity $context() { return this; } @Override public Form $form() { return this; } @Override public void $add(AndroidViewComponent component) { viewLayout.add(component); components.add(component); } @Override public void setChildWidth(AndroidViewComponent component, int width) { // A form is a vertical layout. Log.i("Form", "Set child view Width:" + component.getView().toString()); ViewUtil.setChildWidthForVerticalLayout(component.getView(), width); } @Override public void setChildHeight(AndroidViewComponent component, int height) { // A form is a vertical layout. Log.i("Form", "Set child view height:" + component.getView().toString()); ViewUtil.setChildHeightForVerticalLayout(component.getView(), height); } /* * This is called from runtime.scm at the beginning of each event handler. * It allows runtime.scm to know which form environment should be used for * looking up symbols. The active form is the form that is currently * (or was most recently) dispatching an event. */ public static Form getActiveForm() { return activeForm; } /** * Returns the string that was passed to this screen when it was opened * * @return StartupText */ // This is called from runtime.scm when a "get plain start text" block is executed. public static String getStartText() { if (activeForm != null) { return activeForm.startupValue; } else { throw new IllegalStateException("activeForm is null"); } } /** * Returns the value that was passed to this screen when it was opened * * @return StartValue */ // TODO(hal): cache this? // Note: This is called as a primitive from runtime.scm and it returns an arbitrary Java object. // Therefore it must be explicitly sanitized by runtime, unlike methods, which // are sanitized via call-component-method. public static Object getStartValue() { if (activeForm != null) { return decodeJSONStringForForm(activeForm.startupValue, "get start value"); } else { throw new IllegalStateException("activeForm is null"); } } /** * Closes the current screen, as opposed to finishApplication, which * exits the entire application. */ // This is called from runtime.scm when a "close screen" block is executed. public static void finishActivity() { if (activeForm != null) { activeForm.closeForm(null); } else { throw new IllegalStateException("activeForm is null"); } } // This is called from runtime.scm when a "close screen with value" block is executed. public static void finishActivityWithResult(Object result) { if (activeForm != null) { if (activeForm instanceof ReplForm) { ((ReplForm) activeForm).setResult(result); activeForm.closeForm(null); // This will call RetValManager.popScreen() } else { String jString = jsonEncodeForForm(result, "close screen with value"); Intent resultIntent = new Intent(); resultIntent.putExtra(RESULT_NAME, jString); activeForm.closeForm(resultIntent); } } else { throw new IllegalStateException("activeForm is null"); } } // This is called from runtime.scm when a "close screen with plain text" block is executed. public static void finishActivityWithTextResult(String result) { if (activeForm != null) { Intent resultIntent = new Intent(); resultIntent.putExtra(RESULT_NAME, result); activeForm.closeForm(resultIntent); } else { throw new IllegalStateException("activeForm is null"); } } protected void closeForm(Intent resultIntent) { if (resultIntent != null) { setResult(Activity.RESULT_OK, resultIntent); } finish(); } // This is called from runtime.scm when a "close application" block is executed. public static void finishApplication() { if (activeForm != null) { activeForm.closeApplicationFromBlocks(); } else { throw new IllegalStateException("activeForm is null"); } } protected void closeApplicationFromBlocks() { closeApplication(); } private void closeApplicationFromMenu() { closeApplication(); } private void closeApplication() { // In a multi-screen application, only Screen1 can successfully call System.exit(0). Here, we // set applicationIsBeingClosed to true. If this is not Screen1, when we call finish() below, // the previous form's onResume method will be called. In onResume, we check // applicationIsBeingClosed and call closeApplication again. The stack of forms will unwind // until we get back to Screen1; then we'll call System.exit(0) below. applicationIsBeingClosed = true; finish(); if (formName.equals("Screen1")) { // I know that this is frowned upon in Android circles but I really think that it's // confusing to users if the exit button doesn't really stop everything, including other // forms in the app (when we support them), non-UI threads, etc. We might need to be // careful about this is we ever support services that start up on boot (since it might // mean that the only way to restart that service) is to reboot but that's a long way off. System.exit(0); } } // Configure the system menu to include items to kill the application and to show "about" // information @Override public boolean onCreateOptionsMenu(Menu menu) { // This procedure is called only once. To change the items dynamically // we would use onPrepareOptionsMenu. super.onCreateOptionsMenu(menu); // add the menu items // Comment out the next line if we don't want the exit button addExitButtonToMenu(menu); addAboutInfoToMenu(menu); return true; } public void addExitButtonToMenu(Menu menu) { MenuItem stopApplicationItem = menu.add(Menu.NONE, Menu.NONE, Menu.FIRST, "Stop this application") .setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { showExitApplicationNotification(); return true; } }); stopApplicationItem.setIcon(android.R.drawable.ic_notification_clear_all); } public void addAboutInfoToMenu(Menu menu) { MenuItem aboutAppItem = menu.add(Menu.NONE, Menu.NONE, 2, "About this application") .setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { showAboutApplicationNotification(); return true; } }); aboutAppItem.setIcon(android.R.drawable.sym_def_app_icon); } private void showExitApplicationNotification() { String title = "Stop application?"; String message = "Stop this application and exit? You'll need to relaunch " + "the application to use it again."; String positiveButton = "Stop and exit"; String negativeButton = "Don't stop"; // These runnables are passed to twoButtonAlert. They perform the corresponding actions // when the button is pressed. Here there's nothing to do for "don't stop" and cancel Runnable stopApplication = new Runnable() { public void run() { closeApplicationFromMenu(); } }; Runnable doNothing = new Runnable() { public void run() { } }; Notifier.twoButtonDialog(this, message, title, positiveButton, negativeButton, false, // cancelable is false stopApplication, doNothing, doNothing); } private String yandexTranslateTagline = ""; void setYandexTranslateTagline() { yandexTranslateTagline = "<p><small>Language translation powered by Yandex.Translate</small></p>"; } private void showAboutApplicationNotification() { String title = "About this app"; String MITtagline = "<p><small><em>Invented with MIT App Inventor<br>appinventor.mit.edu</em></small></p>"; // Users can hide the taglines by including an HTML open comment <!-- in the about screen message String message = aboutScreen + MITtagline + yandexTranslateTagline; message = message.replaceAll("\\n", "<br>"); // Allow for line breaks in the string. String buttonText = "Got it"; Notifier.oneButtonAlert(this, message, title, buttonText); } // This is called from clear-current-form in runtime.scm. public void clear() { viewLayout.getLayoutManager().removeAllViews(); // Set all screen properties to default values. defaultPropertyValues(); screenInitialized = false; } public void deleteComponent(Object component) { if (component instanceof OnStopListener) { OnStopListener onStopListener = (OnStopListener) component; if (onStopListeners.contains(onStopListener)) { onStopListeners.remove(onStopListener); } } if (component instanceof OnNewIntentListener) { OnNewIntentListener onNewIntentListener = (OnNewIntentListener) component; if (onNewIntentListeners.contains(onNewIntentListener)) { onNewIntentListeners.remove(onNewIntentListener); } } if (component instanceof OnResumeListener) { OnResumeListener onResumeListener = (OnResumeListener) component; if (onResumeListeners.contains(onResumeListener)) { onResumeListeners.remove(onResumeListener); } } if (component instanceof OnPauseListener) { OnPauseListener onPauseListener = (OnPauseListener) component; if (onPauseListeners.contains(onPauseListener)) { onPauseListeners.remove(onPauseListener); } } if (component instanceof OnDestroyListener) { OnDestroyListener onDestroyListener = (OnDestroyListener) component; if (onDestroyListeners.contains(onDestroyListener)) { onDestroyListeners.remove(onDestroyListener); } } if (component instanceof OnInitializeListener) { OnInitializeListener onInitializeListener = (OnInitializeListener) component; if (onInitializeListeners.contains(onInitializeListener)) { onInitializeListeners.remove(onInitializeListener); } } if (component instanceof Deleteable) { ((Deleteable) component).onDelete(); } } public void dontGrabTouchEventsForComponent() { // The following call results in the Form not grabbing our events and // handling dragging on its own, which it wants to do to handle scrolling. // Its effect only lasts long as the current set of motion events // generated during this touch and drag sequence. Consequently, if a // component wants to handle dragging it needs to call this in the // onTouchEvent of its View. frameLayout.requestDisallowInterceptTouchEvent(true); } // This is used by Repl to throttle error messages which can get out of // hand, e.g. if triggered by Accelerometer. protected boolean toastAllowed() { long now = System.nanoTime(); if (now > lastToastTime + minimumToastWait) { lastToastTime = now; return true; } return false; } // This is used by runtime.scm to call the Initialize of a component. public void callInitialize(Object component) throws Throwable { Method method; try { method = component.getClass().getMethod("Initialize", (Class<?>[]) null); } catch (SecurityException e) { Log.i(LOG_TAG, "Security exception " + e.getMessage()); return; } catch (NoSuchMethodException e) { //This is OK. return; } try { Log.i(LOG_TAG, "calling Initialize method for Object " + component.toString()); method.invoke(component, (Object[]) null); } catch (InvocationTargetException e) { Log.i(LOG_TAG, "invoke exception: " + e.getMessage()); throw e.getTargetException(); } } /** * Perform some action related to fullscreen video display. * @param action * Can be any of the following: * <ul> * <li> * {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_DURATION} * </li> * <li> * {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_FULLSCREEN} * </li> * <li> * {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_PAUSE} * </li> * <li> * {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_PLAY} * </li> * <li> * {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_SEEK} * </li> * <li> * {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_SOURCE} * </li> * <li> * {@link com.google.appinventor.components.runtime.util.FullScreenVideoUtil#FULLSCREEN_VIDEO_ACTION_STOP} * </li> * </ul> * @param source * The VideoPlayer to use in some actions. * @param data * Used by the method. This object varies depending on the action. * @return Varies depending on what action was passed in. */ public synchronized Bundle fullScreenVideoAction(int action, VideoPlayer source, Object data) { return fullScreenVideoUtil.performAction(action, source, data); } @Override public Iterator<AndroidViewComponent> iterator() { return components.iterator(); } private void setBackground(View bgview) { Drawable setDraw = backgroundDrawable; if (backgroundImagePath != "" && setDraw != null) { setDraw = backgroundDrawable.getConstantState().newDrawable(); setDraw.setColorFilter( (backgroundColor != Component.COLOR_DEFAULT) ? backgroundColor : Component.COLOR_WHITE, PorterDuff.Mode.DST_OVER); } else { setDraw = new ColorDrawable( (backgroundColor != Component.COLOR_DEFAULT) ? backgroundColor : Component.COLOR_WHITE); } ViewUtil.setBackgroundImage(bgview, setDraw); bgview.invalidate(); } }