7 package com.google.appinventor.components.runtime;
9 import static android.Manifest.permission.ACCESS_NETWORK_STATE;
10 import static android.Manifest.permission.ACCESS_WIFI_STATE;
11 import static android.Manifest.permission.INTERNET;
14 import android.Manifest;
15 import android.annotation.SuppressLint;
16 import android.app.Activity;
17 import android.app.Dialog;
18 import android.app.ProgressDialog;
19 import android.content.ActivityNotFoundException;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.pm.ActivityInfo;
23 import android.content.pm.PackageInfo;
24 import android.content.pm.PackageManager;
25 import android.content.res.AssetManager;
26 import android.content.res.Configuration;
27 import android.graphics.PorterDuff;
28 import android.graphics.drawable.ColorDrawable;
29 import android.graphics.drawable.Drawable;
30 import android.os.AsyncTask;
31 import android.os.Build;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.util.Log;
35 import android.view.Menu;
36 import android.view.MenuItem;
37 import android.view.MenuItem.OnMenuItemClickListener;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
41 import android.view.WindowManager;
42 import android.view.inputmethod.InputMethodManager;
43 import android.widget.FrameLayout;
44 import android.widget.ScrollView;
45 import androidx.annotation.VisibleForTesting;
46 import androidx.core.app.ActivityCompat;
47 import androidx.core.content.ContextCompat;
82 import java.io.FileNotFoundException;
83 import java.io.IOException;
84 import java.io.InputStream;
85 import java.lang.reflect.InvocationTargetException;
86 import java.lang.reflect.Method;
88 import java.util.ArrayList;
89 import java.util.Collections;
90 import java.util.HashMap;
91 import java.util.HashSet;
92 import java.util.Iterator;
93 import java.util.LinkedHashMap;
94 import java.util.List;
96 import java.util.Random;
99 import org.json.JSONException;
120 @DesignerComponent(version = YaVersion.FORM_COMPONENT_VERSION,
121 category = ComponentCategory.USERINTERFACE,
122 description =
"Top-level component containing all other components in the program",
123 showOnPalette =
false)
125 @UsesPermissions({INTERNET, ACCESS_WIFI_STATE, ACCESS_NETWORK_STATE})
128 OnGlobalLayoutListener {
130 private static final String LOG_TAG =
"Form";
132 private static final String RESULT_NAME =
"APP_INVENTOR_RESULT";
134 private static final String ARGUMENT_NAME =
"APP_INVENTOR_START";
136 public static final String APPINVENTOR_URL_SCHEME =
"appinventor";
138 public static final String ASSETS_PREFIX =
"file:///android_asset/";
140 private static final int DEFAULT_PRIMARY_COLOR_DARK =
142 private static final int DEFAULT_ACCENT_COLOR =
154 private float deviceDensity;
155 private float compatScalingFactor;
158 private static boolean applicationIsBeingClosed;
160 protected final Handler androidUIHandler =
new Handler();
164 private boolean screenInitialized;
166 private static final int SWITCH_FORM_REQUEST_CODE = 1;
167 private static int nextRequestCode = SWITCH_FORM_REQUEST_CODE + 1;
170 private int backgroundColor;
174 private String aboutScreen;
175 private boolean showStatusBar =
true;
176 private boolean showTitle =
true;
177 protected String title =
"";
179 private String backgroundImagePath =
"";
180 private Drawable backgroundDrawable;
181 private boolean usesDefaultBackground;
182 private boolean usesDarkTheme;
191 private int horizontalAlignment;
192 private int verticalAlignment;
195 private String openAnimType;
196 private String closeAnimType;
199 private int primaryColor = DEFAULT_PRIMARY_COLOR;
200 private int primaryColorDark = DEFAULT_PRIMARY_COLOR_DARK;
201 private int accentColor = DEFAULT_ACCENT_COLOR;
203 private FrameLayout frameLayout;
204 private boolean scrollable;
207 private static boolean sCompatibilityMode;
209 private static boolean showListsAsJson;
211 private final Set<String> permissions =
new HashSet<String>();
214 private final HashMap<Integer, ActivityResultListener> activityResultMap =
Maps.
newHashMap();
216 private final Set<OnStopListener> onStopListeners =
Sets.
newHashSet();
217 private final Set<OnClearListener> onClearListeners =
Sets.
newHashSet();
218 private final Set<OnNewIntentListener> onNewIntentListeners =
Sets.
newHashSet();
219 private final Set<OnResumeListener> onResumeListeners =
Sets.
newHashSet();
220 private final Set<OnOrientationChangeListener> onOrientationChangeListeners =
Sets.
newHashSet();
221 private final Set<OnPauseListener> onPauseListeners =
Sets.
newHashSet();
222 private final Set<OnDestroyListener> onDestroyListeners =
Sets.
newHashSet();
225 private final Set<OnInitializeListener> onInitializeListeners =
Sets.
newHashSet();
228 private final Set<OnCreateOptionsMenuListener> onCreateOptionsMenuListeners =
Sets.
newHashSet();
229 private final Set<OnOptionsItemSelectedListener> onOptionsItemSelectedListeners =
Sets.
newHashSet();
232 private final HashMap<Integer, PermissionResultHandler> permissionHandlers =
Maps.
newHashMap();
234 private final Random permissionRandom =
new Random();
238 protected String startupValue =
"";
241 private static long minimumToastWait = 10000000000L;
242 private long lastToastTime = System.nanoTime() - minimumToastWait;
247 private String nextFormName;
251 private int formWidth;
252 private int formHeight;
254 private boolean actionBarEnabled =
false;
255 private boolean keyboardShown =
false;
257 private ProgressDialog progress;
258 private static boolean _initialized =
false;
262 public static final int MAX_PERMISSION_NONCE = 100000;
264 public static class PercentStorageRecord {
269 this.component = component;
270 this.length = length;
279 private LinkedHashMap<Integer, PercentStorageRecord> dimChanges =
new LinkedHashMap();
281 private static class MultiDexInstaller
extends AsyncTask<Form, Void, Boolean> {
285 protected Boolean doInBackground(
Form... form) {
287 Log.d(LOG_TAG,
"Doing Full MultiDex Install");
292 protected void onPostExecute(Boolean v) {
293 ourForm.onCreateFinish();
300 super.onCreate(icicle);
303 String className = getClass().getName();
304 int lastDot = className.lastIndexOf(
'.');
305 formName = className.substring(lastDot + 1);
306 Log.d(LOG_TAG,
"Form " + formName +
" got onCreate");
309 Log.i(LOG_TAG,
"activeForm is now " + activeForm.
formName);
311 deviceDensity = this.getResources().getDisplayMetrics().density;
312 Log.d(LOG_TAG,
"deviceDensity = " + deviceDensity);
314 Log.i(LOG_TAG,
"compatScalingFactor = " + compatScalingFactor);
319 if (!_initialized && formName.equals(
"Screen1")) {
320 Log.d(LOG_TAG,
"MULTI: _initialized = " + _initialized +
" formName = " + formName);
327 Log.d(LOG_TAG,
"MultiDex already installed.");
330 progress = ProgressDialog.show(
this,
"Please Wait...",
"Installation Finishing");
332 new MultiDexInstaller().execute(
this);
335 Log.d(LOG_TAG,
"NO MULTI: _initialized = " + _initialized +
" formName = " + formName);
356 void onCreateFinish() {
358 Log.d(LOG_TAG,
"onCreateFinish called " + System.currentTimeMillis());
359 if (progress !=
null) {
363 populatePermissions();
373 boolean needSdcardWrite = doesAppDeclarePermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) &&
375 isRepl() && !AppInventorFeatures.doCompanionSplashScreen();
376 if (needSdcardWrite) {
377 askPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE,
378 new PermissionResultHandler() {
380 public void HandlePermissionResponse(String permission,
boolean granted) {
384 Log.i(LOG_TAG,
"WRITE_EXTERNAL_STORAGE Permission denied by user");
386 androidUIHandler.post(
new Runnable() {
389 PermissionDenied(Form.this,
"Initialize", Manifest.permission.WRITE_EXTERNAL_STORAGE);
400 private void onCreateFinish2() {
401 defaultPropertyValues();
404 Intent startIntent = getIntent();
405 if (startIntent !=
null && startIntent.hasExtra(ARGUMENT_NAME)) {
406 startupValue = startIntent.getStringExtra(ARGUMENT_NAME);
409 fullScreenVideoUtil =
new FullScreenVideoUtil(
this, androidUIHandler);
413 WindowManager.LayoutParams params = getWindow().getAttributes();
414 int softInputMode = params.softInputMode;
415 getWindow().setSoftInputMode(
416 softInputMode | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
432 private void populatePermissions() {
434 PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(),
435 PackageManager.GET_PERMISSIONS);
436 Collections.addAll(permissions, packageInfo.requestedPermissions);
437 }
catch (Exception e) {
438 Log.e(LOG_TAG,
"Exception while attempting to learn permissions.", e);
442 private void defaultPropertyValues() {
444 ActionBar(actionBarEnabled);
446 ActionBar(themeHelper.hasActionBar());
449 Sizing(
"Responsive");
452 AlignHorizontal(ComponentConstants.GRAVITY_LEFT);
453 AlignVertical(ComponentConstants.GRAVITY_TOP);
457 ShowListsAsJson(
true);
459 AccentColor(DEFAULT_ACCENT_COLOR);
460 PrimaryColor(DEFAULT_PRIMARY_COLOR);
461 PrimaryColorDark(DEFAULT_PRIMARY_COLOR_DARK);
462 Theme(ComponentConstants.DEFAULT_THEME);
463 ScreenOrientation(
"unspecified");
464 BackgroundColor(Component.COLOR_DEFAULT);
469 super.onConfigurationChanged(newConfig);
470 Log.d(LOG_TAG,
"onConfigurationChanged() called");
471 final int newOrientation = newConfig.orientation;
472 if (newOrientation == Configuration.ORIENTATION_LANDSCAPE ||
473 newOrientation == Configuration.ORIENTATION_PORTRAIT) {
478 androidUIHandler.post(
new Runnable() {
480 boolean dispatchEventNow =
false;
481 if (frameLayout !=
null) {
482 if (newOrientation == Configuration.ORIENTATION_LANDSCAPE) {
483 if (frameLayout.getWidth() >= frameLayout.getHeight()) {
484 dispatchEventNow =
true;
487 if (frameLayout.getHeight() >= frameLayout.getWidth()) {
488 dispatchEventNow =
true;
492 if (dispatchEventNow) {
494 final FrameLayout savedLayout = frameLayout;
495 androidUIHandler.postDelayed(
new Runnable() {
497 if (savedLayout !=
null) {
498 savedLayout.invalidate();
504 ScreenOrientationChanged();
507 androidUIHandler.post(
this);
537 int totalHeight = scaleLayout.getRootView().getHeight();
538 int scaledHeight = scaleLayout.getHeight();
539 int heightDiff = totalHeight - scaledHeight;
543 float diffPercent = (float) heightDiff / (
float) totalHeight;
544 Log.d(LOG_TAG,
"onGlobalLayout(): diffPercent = " + diffPercent);
546 if(diffPercent < 0.25) {
547 Log.d(LOG_TAG,
"keyboard hidden!");
549 keyboardShown =
false;
550 if (sCompatibilityMode) {
551 scaleLayout.
setScale(compatScalingFactor);
552 scaleLayout.invalidate();
556 Log.d(LOG_TAG,
"keyboard shown!");
557 keyboardShown =
true;
558 if (scaleLayout !=
null) {
560 scaleLayout.invalidate();
571 if (!BackPressed()) {
573 super.onBackPressed();
577 @
SimpleEvent(description =
"Device back button pressed.")
578 public
boolean BackPressed() {
589 Log.i(LOG_TAG,
"Form " + formName +
" got onActivityResult, requestCode = " +
590 requestCode +
", resultCode = " + resultCode);
591 if (requestCode == SWITCH_FORM_REQUEST_CODE) {
597 if (data !=
null && data.hasExtra(RESULT_NAME)) {
598 resultString = data.getStringExtra(RESULT_NAME);
602 Object decodedResult = decodeJSONStringForForm(resultString,
"other screen closed");
604 OtherScreenClosed(nextFormName, decodedResult);
608 if (component !=
null) {
612 Set<ActivityResultListener> listeners = activityResultMultiMap.get(requestCode);
613 if (listeners !=
null) {
615 listener.resultReturned(requestCode, resultCode, data);
623 private static Object decodeJSONStringForForm(String jsonString, String functionName) {
624 Log.i(LOG_TAG,
"decodeJSONStringForForm -- decoding JSON representation:" + jsonString);
625 Object valueFromJSON =
"";
628 Log.i(LOG_TAG,
"decodeJSONStringForForm -- got decoded JSON:" + valueFromJSON.toString());
629 }
catch (JSONException e) {
633 ErrorMessages.ERROR_SCREEN_BAD_VALUE_RECEIVED, jsonString);
635 return valueFromJSON;
639 int requestCode = generateNewRequestCode();
640 activityResultMap.put(requestCode, listener);
652 Set<ActivityResultListener> listeners = activityResultMultiMap.get(requestCode);
653 if (listeners ==
null) {
655 activityResultMultiMap.put(requestCode, listeners);
657 listeners.add(listener);
663 if (listener.equals(mapEntry.getValue())) {
664 keysToDelete.add(mapEntry.getKey());
667 for (Integer key : keysToDelete) {
668 activityResultMap.remove(key);
672 Iterator<
Map.Entry<Integer, Set<ActivityResultListener>>> it =
673 activityResultMultiMap.entrySet().
iterator();
674 while (it.hasNext()) {
675 Map.Entry<Integer, Set<ActivityResultListener>> entry = it.next();
676 entry.getValue().remove(listener);
677 if (entry.getValue().size() == 0) {
683 void ReplayFormOrientation() {
686 Log.d(LOG_TAG,
"ReplayFormOrientation()");
687 LinkedHashMap<Integer, PercentStorageRecord> temp = (LinkedHashMap<Integer, PercentStorageRecord>) dimChanges.clone();
690 for (PercentStorageRecord r : temp.values()) {
691 if (r.dim == PercentStorageRecord.Dim.HEIGHT) {
692 r.component.
Height(r.length);
694 r.component.Width(r.length);
699 private Integer generateHashCode(AndroidViewComponent component, PercentStorageRecord.Dim dim) {
700 if (dim == PercentStorageRecord.Dim.HEIGHT) {
701 return component.hashCode() * 2 + 1;
703 return component.hashCode() * 2;
708 PercentStorageRecord r =
new PercentStorageRecord(component, length, dim);
709 Integer key = generateHashCode(component, dim);
710 dimChanges.put(key, r);
715 dimChanges.remove(generateHashCode(component, dim));
718 private static int generateNewRequestCode() {
719 return nextRequestCode++;
725 Log.i(LOG_TAG,
"Form " + formName +
" got onResume");
730 if (applicationIsBeingClosed) {
736 onResumeListener.onResume();
741 onResumeListeners.add(component);
745 onOrientationChangeListeners.add(component);
755 onInitializeListeners.add(component);
760 super.onNewIntent(intent);
761 Log.d(LOG_TAG,
"Form " + formName +
" got onNewIntent " + intent);
763 onNewIntentListener.onNewIntent(intent);
768 onNewIntentListeners.add(component);
774 Log.i(LOG_TAG,
"Form " + formName +
" got onPause");
776 onPauseListener.onPause();
781 onPauseListeners.add(component);
787 Log.i(LOG_TAG,
"Form " + formName +
" got onStop");
789 onStopListener.onStop();
794 onStopListeners.add(component);
798 onClearListeners.add(component);
804 Log.i(LOG_TAG,
"Form " + formName +
" got onDestroy");
810 onDestroyListener.onDestroy();
818 onDestroyListeners.add(component);
822 onCreateOptionsMenuListeners.add(component);
826 onOptionsItemSelectedListeners.add(component);
834 return super.onCreateDialog(
id);
844 super.onPrepareDialog(
id, dialog);
859 throw new UnsupportedOperationException();
865 boolean canDispatch = screenInitialized ||
866 (component ==
this && eventName.equals(
"Initialize"));
888 throw new UnsupportedOperationException();
893 boolean notAlreadyHandled, Object[] args) {
894 throw new UnsupportedOperationException();
897 @
SimpleEvent(description =
"The Initialize event is run when the Screen starts and is only run "
898 +
"once per screen.")
899 public
void Initialize() {
901 androidUIHandler.post(
new Runnable() {
903 if (frameLayout !=
null && frameLayout.getWidth() != 0 && frameLayout.getHeight() != 0) {
905 if (sCompatibilityMode) {
908 Sizing(
"Responsive");
910 screenInitialized =
true;
914 onInitializeListener.onInitialize();
916 if (activeForm instanceof
ReplForm) {
917 ((
ReplForm)activeForm).HandleReturnValues();
921 androidUIHandler.post(
this);
927 @
SimpleEvent(description =
"Screen orientation changed")
928 public
void ScreenOrientationChanged() {
930 onOrientationChangeListener.onOrientationChange();
936 description =
"Event raised when an error occurs. Only some errors will " +
937 "raise this condition. For those errors, the system will show a notification " +
938 "by default. You can use this event handler to prescribe an error " +
939 "behavior different than the default.")
940 public
void ErrorOccurred(
Component component, String functionName,
int errorNumber,
942 String componentType = component.getClass().getName();
943 componentType = componentType.substring(componentType.lastIndexOf(
".") + 1);
944 Log.e(LOG_TAG,
"Form " + formName +
" ErrorOccurred, errorNumber = " + errorNumber +
945 ", componentType = " + componentType +
", functionName = " + functionName +
946 ", messages = " + message);
948 this,
"ErrorOccurred", component, functionName, errorNumber, message)))
949 && screenInitialized) {
961 String message, String title, String buttonText) {
962 String componentType = component.getClass().getName();
963 componentType = componentType.substring(componentType.lastIndexOf(
".") + 1);
964 Log.e(LOG_TAG,
"Form " + formName +
" ErrorOccurred, errorNumber = " + errorNumber +
965 ", componentType = " + componentType +
", functionName = " + functionName +
966 ", messages = " + message);
968 this,
"ErrorOccurred", component, functionName, errorNumber, message)))
969 && screenInitialized) {
989 exception.printStackTrace();
1002 final String permissionName) {
1003 runOnUiThread(
new Runnable() {
1006 PermissionDenied(component, functionName, permissionName);
1012 final int errorNumber,
final Object... messageArgs) {
1013 runOnUiThread(
new Runnable() {
1016 ErrorOccurred(component, functionName, errorNumber, message);
1027 final int errorNumber,
final Object... messageArgs) {
1028 runOnUiThread(
new Runnable() {
1031 ErrorOccurredDialog(
1036 "Error in " + functionName,
1049 Log.d(
"FORM_RUNTIME_ERROR",
"functionName is " + functionName);
1050 Log.d(
"FORM_RUNTIME_ERROR",
"errorNumber is " + errorNumber);
1051 Log.d(
"FORM_RUNTIME_ERROR",
"message is " + message);
1052 dispatchErrorOccurredEvent((
Component) activeForm, functionName, errorNumber, message);
1064 if (permissionName.startsWith(
"android.permission.")) {
1066 permissionName = permissionName.replace(
"android.permission.",
"");
1079 @
SimpleEvent(description =
"Event to handle when the app user has granted a needed permission. "
1080 +
"This event is only run when permission is granted in response to the AskForPermission "
1082 public
void PermissionGranted(String permissionName) {
1083 if (permissionName.startsWith(
"android.permission.")) {
1085 permissionName = permissionName.replace(
"android.permission.",
"");
1104 @
SimpleFunction(description =
"Ask the user to grant access to a dangerous permission.")
1105 public
void AskForPermission(String permissionName) {
1106 if (!permissionName.contains(
".")) {
1107 permissionName =
"android.permission." + permissionName;
1111 public void HandlePermissionResponse(String permission,
boolean granted) {
1113 PermissionGranted(permission);
1115 PermissionDenied(
Form.this,
"RequestPermission", permission);
1127 description =
"When checked, there will be a vertical scrollbar on the "
1128 +
"screen, and the height of the application can exceed the physical "
1129 +
"height of the device. When unchecked, the application height is "
1130 +
"constrained to the height of the device.")
1131 public
boolean Scrollable() {
1143 defaultValue =
"False")
1146 if (this.scrollable == scrollable && frameLayout !=
null) {
1150 this.scrollable = scrollable;
1154 private void recomputeLayout() {
1156 Log.d(LOG_TAG,
"recomputeLayout called");
1158 if (frameLayout !=
null) {
1159 frameLayout.removeAllViews();
1161 boolean needsTitleBar = titleBar !=
null && titleBar.getParent() == frameWithTitle;
1162 frameWithTitle.removeAllViews();
1163 if (needsTitleBar) {
1164 frameWithTitle.addView(titleBar,
new ViewGroup.LayoutParams(
1165 ViewGroup.LayoutParams.MATCH_PARENT,
1166 ViewGroup.LayoutParams.WRAP_CONTENT
1181 frameLayout =
new ScrollView(
this);
1182 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1185 ((ScrollView) frameLayout).setFillViewport(
true);
1188 frameLayout =
new FrameLayout(
this);
1190 frameLayout.addView(viewLayout.
getLayoutManager(),
new ViewGroup.LayoutParams(
1191 ViewGroup.LayoutParams.MATCH_PARENT,
1192 ViewGroup.LayoutParams.MATCH_PARENT));
1194 setBackground(frameLayout);
1196 Log.d(LOG_TAG,
"About to create a new ScaledFrameLayout");
1197 scaleLayout =
new ScaledFrameLayout(
this);
1198 scaleLayout.addView(frameLayout,
new ViewGroup.LayoutParams(
1199 ViewGroup.LayoutParams.MATCH_PARENT,
1200 ViewGroup.LayoutParams.MATCH_PARENT));
1201 frameWithTitle.addView(scaleLayout,
new ViewGroup.LayoutParams(
1202 ViewGroup.LayoutParams.MATCH_PARENT,
1203 ViewGroup.LayoutParams.MATCH_PARENT));
1204 frameLayout.getViewTreeObserver().addOnGlobalLayoutListener(
this);
1205 scaleLayout.requestLayout();
1206 androidUIHandler.post(
new Runnable() {
1208 if (frameLayout !=
null && frameLayout.getWidth() != 0 && frameLayout.getHeight() != 0) {
1209 if (sCompatibilityMode) {
1212 Sizing(
"Responsive");
1214 ReplayFormOrientation();
1216 frameWithTitle.requestLayout();
1219 androidUIHandler.post(
this);
1230 @SimpleProperty(category = PropertyCategory.APPEARANCE)
1233 return backgroundColor;
1248 usesDefaultBackground =
true;
1250 usesDefaultBackground =
false;
1251 backgroundColor = argb;
1254 setBackground(frameLayout);
1264 description =
"The screen background image.")
1265 public String BackgroundImage() {
1266 return backgroundImagePath;
1284 description =
"The screen background image.")
1285 public
void BackgroundImage(String path) {
1286 backgroundImagePath = (path ==
null) ?
"" : path;
1290 }
catch (IOException ioe) {
1291 Log.e(LOG_TAG,
"Unable to load " + backgroundImagePath);
1292 backgroundDrawable =
null;
1294 setBackground(frameLayout);
1303 description =
"The caption for the form, which apears in the title bar")
1304 public String Title() {
1305 return getTitle().toString();
1319 if (titleBar !=
null) {
1320 titleBar.setText(title);
1333 description =
"Information about the screen. It appears when \"About this Application\" "
1334 +
"is selected from the system menu. Use it to inform people about your app. In multiple "
1335 +
"screen apps, each screen has its own AboutScreen info.")
1336 public String AboutScreen() {
1351 this.aboutScreen = aboutScreen;
1360 description =
"The title bar is the top gray bar on the screen. This property reports whether the title bar is visible.")
1361 public
boolean TitleVisible() {
1372 defaultValue =
"True")
1374 public
void TitleVisible(
boolean show) {
1375 if (show != showTitle) {
1377 if (actionBarEnabled) {
1378 actionBarEnabled = themeHelper.setActionBarVisible(show);
1380 maybeShowTitleBar();
1391 description =
"The status bar is the topmost bar on the screen. This property reports whether the status bar is visible.")
1392 public
boolean ShowStatusBar() {
1393 return showStatusBar;
1403 defaultValue =
"True")
1405 public
void ShowStatusBar(
boolean show) {
1406 if (show != showStatusBar) {
1408 getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
1409 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1411 getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1412 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
1414 showStatusBar = show;
1429 description =
"The requested screen orientation, specified as a text value. " +
1430 "Commonly used values are " +
1431 "landscape, portrait, sensor, user and unspecified. " +
1432 "See the Android developer documentation for ActivityInfo.Screen_Orientation for the " +
1433 "complete list of possible settings.")
1434 public String ScreenOrientation() {
1435 switch (getRequestedOrientation()) {
1436 case ActivityInfo.SCREEN_ORIENTATION_BEHIND:
1438 case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE:
1440 case ActivityInfo.SCREEN_ORIENTATION_NOSENSOR:
1442 case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT:
1444 case ActivityInfo.SCREEN_ORIENTATION_SENSOR:
1446 case ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED:
1447 return "unspecified";
1448 case ActivityInfo.SCREEN_ORIENTATION_USER:
1451 return "fullSensor";
1453 return "reverseLandscape";
1455 return "reversePortrait";
1457 return "sensorLandscape";
1459 return "sensorPortrait";
1462 return "unspecified";
1473 @SuppressLint(
"SourceLockedOrientationActivity")
1475 defaultValue =
"unspecified")
1477 public
void ScreenOrientation(String screenOrientation) {
1478 if (screenOrientation.equalsIgnoreCase(
"behind")) {
1479 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_BEHIND);
1480 }
else if (screenOrientation.equalsIgnoreCase(
"landscape")) {
1481 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
1482 }
else if (screenOrientation.equalsIgnoreCase(
"nosensor")) {
1483 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
1484 }
else if (screenOrientation.equalsIgnoreCase(
"portrait")) {
1485 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
1486 }
else if (screenOrientation.equalsIgnoreCase(
"sensor")) {
1487 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
1488 }
else if (screenOrientation.equalsIgnoreCase(
"unspecified")) {
1489 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
1490 }
else if (screenOrientation.equalsIgnoreCase(
"user")) {
1491 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER);
1493 if (screenOrientation.equalsIgnoreCase(
"fullSensor")) {
1494 setRequestedOrientation(10);
1495 }
else if (screenOrientation.equalsIgnoreCase(
"reverseLandscape")) {
1496 setRequestedOrientation(8);
1497 }
else if (screenOrientation.equalsIgnoreCase(
"reversePortrait")) {
1498 setRequestedOrientation(9);
1499 }
else if (screenOrientation.equalsIgnoreCase(
"sensorLandscape")) {
1500 setRequestedOrientation(6);
1501 }
else if (screenOrientation.equalsIgnoreCase(
"sensorPortrait")) {
1502 setRequestedOrientation(7);
1504 dispatchErrorOccurredEvent(
this,
"ScreenOrientation",
1508 dispatchErrorOccurredEvent(
this,
"ScreenOrientation",
1514 defaultValue =
"False")
1516 public
void ActionBar(
boolean enabled) {
1521 if (actionBarEnabled != enabled) {
1522 setActionBarEnabled(enabled);
1525 actionBarEnabled = themeHelper.setActionBarVisible(showTitle);
1527 maybeShowTitleBar();
1528 actionBarEnabled = themeHelper.setActionBarVisible(
false);
1530 actionBarEnabled = enabled;
1546 description =
"A number that encodes how contents of the screen are aligned " +
1547 " horizontally. The choices are: 1 = left aligned, 2 = horizontally centered, " +
1548 " 3 = right aligned.")
1549 public
int AlignHorizontal() {
1550 return horizontalAlignment;
1570 horizontalAlignment = alignment;
1571 }
catch (IllegalArgumentException e) {
1572 this.dispatchErrorOccurredEvent(
this,
"HorizontalAlignment",
1584 description =
"A number that encodes how the contents of the arrangement are aligned " +
1585 "vertically. The choices are: 1 = aligned at the top, 2 = vertically centered, " +
1586 "3 = aligned at the bottom. Vertical alignment has no effect if the screen is scrollable.")
1587 public
int AlignVertical() {
1588 return verticalAlignment;
1609 verticalAlignment = alignment;
1610 }
catch (IllegalArgumentException e) {
1611 this.dispatchErrorOccurredEvent(
this,
"VerticalAlignment",
1623 description =
"The animation for switching to another screen. Valid" +
1624 " options are default, fade, zoom, slidehorizontal, slidevertical, and none" )
1625 public String OpenScreenAnimation() {
1626 return openAnimType;
1636 defaultValue =
"default")
1639 if ((animType !=
"default") &&
1640 (animType !=
"fade") && (animType !=
"zoom") && (animType !=
"slidehorizontal") &&
1641 (animType !=
"slidevertical") && (animType !=
"none")) {
1642 this.dispatchErrorOccurredEvent(
this,
"Screen",
1646 openAnimType = animType;
1656 description =
"The animation for closing current screen and returning " +
1657 " to the previous screen. Valid options are default, fade, zoom, slidehorizontal, " +
1658 "slidevertical, and none")
1659 public String CloseScreenAnimation() {
1660 return closeAnimType;
1670 defaultValue =
"default")
1673 if ((animType !=
"default") &&
1674 (animType !=
"fade") && (animType !=
"zoom") && (animType !=
"slidehorizontal") &&
1675 (animType !=
"slidevertical") && (animType !=
"none")) {
1676 this.dispatchErrorOccurredEvent(
this,
"Screen",
1680 closeAnimType = animType;
1688 return openAnimType;
1701 public
void Icon(String name) {
1714 description =
"An integer value which must be incremented each time a new Android "
1715 +
"Application Package File (APK) is created for the Google Play Store.")
1716 public
void VersionCode(
int vCode) {
1727 defaultValue =
"1.0")
1729 description =
"A string which can be changed to allow Google Play "
1730 +
"Store users to distinguish between different versions of the App.")
1731 public
void VersionName(String vName) {
1745 defaultValue =
"Responsive", alwaysSend =
true)
1748 description =
"If set to fixed, screen layouts will be created for a single fixed-size screen and autoscaled. " +
1749 "If set to responsive, screen layouts will use the actual resolution of the device. " +
1750 "See the documentation on responsive design in App Inventor for more information. " +
1751 "This property appears on Screen1 only and controls the sizing for all screens in the app.")
1752 public
void Sizing(String value) {
1755 Log.d(LOG_TAG,
"Sizing(" + value +
")");
1756 formWidth = (int)((
float) this.getResources().getDisplayMetrics().widthPixels / deviceDensity);
1757 formHeight = (int)((
float) this.getResources().getDisplayMetrics().heightPixels / deviceDensity);
1758 if (value.equals(
"Fixed")) {
1759 sCompatibilityMode =
true;
1760 formWidth /= compatScalingFactor;
1761 formHeight /= compatScalingFactor;
1763 sCompatibilityMode =
false;
1765 scaleLayout.
setScale(sCompatibilityMode ? compatScalingFactor : 1.0f);
1766 if (frameLayout !=
null) {
1767 frameLayout.invalidate();
1769 Log.d(LOG_TAG,
"formWidth = " + formWidth +
" formHeight = " + formHeight);
1787 defaultValue =
"True", alwaysSend =
true)
1790 description =
"If false, lists will be converted to strings using Lisp "
1791 +
"notation, i.e., as symbols separated by spaces, e.g., (a 1 b2 (c "
1792 +
"d). If true, lists will appear as in Json or Python, e.g. [\"a\", 1, "
1793 +
"\"b\", 2, [\"c\", \"d\"]]. This property appears only in Screen 1, "
1794 +
"and the value for Screen 1 determines the behavior for all "
1795 +
"screens. The property defaults to \"true\" meaning that the App "
1796 +
"Inventor programmer must explicitly set it to \"false\" if Lisp "
1797 +
"syntax is desired. In older versions of App Inventor, this setting "
1798 +
"defaulted to false. Older projects should not have been affected by "
1799 +
"this default settings update."
1801 public
void ShowListsAsJson(
boolean asJson) {
1802 showListsAsJson = asJson;
1814 public
boolean ShowListsAsJson() {
1815 return showListsAsJson;
1827 description =
"This is the display name of the installed application in the phone." +
1828 "If the AppName is blank, it will be set to the name of the project when the project is built.")
1829 public
void AppName(String aName) {
1835 @
SimpleProperty(userVisible =
false, description =
"This is the primary color used for " +
1837 public
void PrimaryColor(final
int color) {
1838 setPrimaryColor(color);
1848 return primaryColor;
1853 @
SimpleProperty(userVisible =
false, description =
"This is the primary color used for darker " +
1855 public
void PrimaryColorDark(
int color) {
1856 primaryColorDark = color;
1866 return primaryColorDark;
1871 @
SimpleProperty(userVisible =
false, description =
"This is the accent color used for " +
1873 public
void AccentColor(
int color) {
1874 accentColor = color;
1901 @
SimpleProperty(userVisible =
false, description =
"Sets the theme used by the application.")
1905 setBackground(frameLayout);
1908 if (usesDefaultBackground) {
1909 if (theme.equalsIgnoreCase(
"AppTheme") && !isClassicMode()) {
1914 setBackground(frameLayout);
1916 usesDarkTheme =
false;
1917 if (theme.equals(
"Classic")) {
1919 }
else if (theme.equals(
"AppTheme.Light.DarkActionBar")) {
1921 }
else if (theme.equals(
"AppTheme.Light")) {
1923 }
else if (theme.equals(
"AppTheme")) {
1924 usesDarkTheme =
true;
1935 description =
"Screen width (x-size).")
1936 public
int Width() {
1937 Log.d(LOG_TAG,
"Form.Width = " + formWidth);
1947 description =
"Screen height (y-size).")
1948 public
int Height() {
1949 Log.d(LOG_TAG,
"Form.Height = " + formHeight);
1962 description =
"A URL to use to populate the Tutorial Sidebar while "
1963 +
"editing a project. Used as a teaching aid.")
1964 public
void TutorialURL(String url) {
1972 description =
"A JSON string representing the subset for the screen. Authors of template apps "
1973 +
"can use this to control what components, designer properties, and blocks are available "
1974 +
"in the project.")
1975 public
void BlocksToolkit(String json) {
1986 @
SimpleProperty(description =
"The platform the app is running on, for example \"Android\" or "
1988 public String Platform() {
1999 @
SimpleProperty(description =
"The dotted version number of the platform, such as 4.2.2 or 10.0. "
2000 +
"This is platform specific and there is no guarantee that it has a particular format.")
2001 public String PlatformVersion() {
2002 return Build.VERSION.RELEASE;
2012 if (activeForm !=
null) {
2015 throw new IllegalStateException(
"activeForm is null");
2028 Log.i(LOG_TAG,
"Open another screen with start value:" + nextFormName);
2029 if (activeForm !=
null) {
2032 throw new IllegalStateException(
"activeForm is null");
2038 Log.i(LOG_TAG,
"startNewForm:" + nextFormName);
2039 Intent activityIntent =
new Intent();
2042 activityIntent.setClassName(
this, getPackageName() +
"." + nextFormName);
2043 String functionName = (startupValue ==
null) ?
"open another screen" :
2044 "open another screen with start value";
2046 if (startupValue !=
null) {
2047 Log.i(LOG_TAG,
"StartNewForm about to JSON encode:" + startupValue);
2048 jValue = jsonEncodeForForm(startupValue, functionName);
2049 Log.i(LOG_TAG,
"StartNewForm got JSON encoding:" + jValue);
2053 activityIntent.putExtra(ARGUMENT_NAME, jValue);
2056 this.nextFormName = nextFormName;
2057 Log.i(LOG_TAG,
"about to start new form" + nextFormName);
2059 Log.i(LOG_TAG,
"startNewForm starting activity:" + activityIntent);
2060 startActivityForResult(activityIntent, SWITCH_FORM_REQUEST_CODE);
2062 }
catch (ActivityNotFoundException e) {
2063 dispatchErrorOccurredEvent(
this, functionName,
2071 String jsonResult =
"";
2072 Log.i(LOG_TAG,
"jsonEncodeForForm -- creating JSON representation:" + value.toString());
2076 Log.i(LOG_TAG,
"jsonEncodeForForm -- got JSON representation:" + jsonResult);
2077 }
catch (JSONException e) {
2086 @
SimpleEvent(description =
"Event raised when another screen has closed and control has " +
2087 "returned to this screen.")
2088 public
void OtherScreenClosed(String otherScreenName, Object result) {
2089 Log.i(LOG_TAG,
"Form " + formName +
" OtherScreenClosed, otherScreenName = " +
2090 otherScreenName +
", result = " + result.toString());
2116 viewLayout.
add(component);
2120 return this.deviceDensity;
2124 return this.compatScalingFactor;
2129 int cWidth = Width();
2131 final int fWidth = width;
2132 androidUIHandler.postDelayed(
new Runnable() {
2135 System.err.println(
"(Form)Width not stable yet... trying again");
2136 setChildWidth(component, fWidth);
2140 System.err.println(
"Form.setChildWidth(): width = " + width +
" parent Width = " + cWidth +
" child = " + component);
2141 if (width <= LENGTH_PERCENT_TAG) {
2142 width = cWidth * (- (width - LENGTH_PERCENT_TAG)) / 100;
2154 int cHeight = Height();
2156 final int fHeight = height;
2157 androidUIHandler.postDelayed(
new Runnable() {
2160 System.err.println(
"(Form)Height not stable yet... trying again");
2161 setChildHeight(component, fHeight);
2165 if (height <= LENGTH_PERCENT_TAG) {
2166 height = Height() * (- (height - LENGTH_PERCENT_TAG)) / 100;
2193 if (activeForm !=
null) {
2196 throw new IllegalStateException(
"activeForm is null");
2211 if (activeForm !=
null) {
2212 return decodeJSONStringForForm(activeForm.
startupValue,
"get start value");
2214 throw new IllegalStateException(
"activeForm is null");
2225 if (activeForm !=
null) {
2228 throw new IllegalStateException(
"activeForm is null");
2234 if (activeForm !=
null) {
2235 if (activeForm instanceof
ReplForm) {
2236 ((
ReplForm)activeForm).setResult(result);
2237 activeForm.closeForm(
null);
2239 String jString = jsonEncodeForForm(result,
"close screen with value");
2240 Intent resultIntent =
new Intent();
2241 resultIntent.putExtra(RESULT_NAME, jString);
2245 throw new IllegalStateException(
"activeForm is null");
2251 if (activeForm !=
null) {
2252 Intent resultIntent =
new Intent();
2253 resultIntent.putExtra(RESULT_NAME, result);
2256 throw new IllegalStateException(
"activeForm is null");
2262 if (resultIntent !=
null) {
2263 setResult(Activity.RESULT_OK, resultIntent);
2271 if (activeForm !=
null) {
2274 throw new IllegalStateException(
"activeForm is null");
2282 private void closeApplicationFromMenu() {
2286 private void closeApplication() {
2292 applicationIsBeingClosed =
true;
2296 if (formName.equals(
"Screen1")) {
2313 super.onCreateOptionsMenu(menu);
2316 addExitButtonToMenu(menu);
2317 addAboutInfoToMenu(menu);
2319 onCreateOptionsMenuListener.onCreateOptionsMenu(menu);
2325 MenuItem stopApplicationItem = menu.add(Menu.NONE, Menu.NONE, Menu.FIRST,
2326 "Stop this application")
2327 .setOnMenuItemClickListener(
new OnMenuItemClickListener() {
2328 public boolean onMenuItemClick(MenuItem item) {
2329 showExitApplicationNotification();
2333 stopApplicationItem.setIcon(android.R.drawable.ic_notification_clear_all);
2337 MenuItem aboutAppItem = menu.add(Menu.NONE, Menu.NONE, 2,
2338 "About this application")
2339 .setOnMenuItemClickListener(
new OnMenuItemClickListener() {
2340 public boolean onMenuItemClick(MenuItem item) {
2341 showAboutApplicationNotification();
2345 aboutAppItem.setIcon(android.R.drawable.sym_def_app_icon);
2351 if (onOptionsItemSelectedListener.onOptionsItemSelected(item)) {
2358 private void showExitApplicationNotification() {
2359 String title =
"Stop application?";
2360 String message =
"Stop this application and exit? You'll need to relaunch " +
2361 "the application to use it again.";
2362 String positiveButton =
"Stop and exit";
2363 String negativeButton =
"Don't stop";
2366 Runnable stopApplication =
new Runnable() {
public void run () {closeApplicationFromMenu();}};
2367 Runnable doNothing =
new Runnable () {
public void run() {}};
2368 Notifier.twoButtonDialog(
2380 private String yandexTranslateTagline =
"";
2382 void setYandexTranslateTagline(){
2383 yandexTranslateTagline =
"<p><small>Language translation powered by Yandex.Translate</small></p>";
2386 private void showAboutApplicationNotification() {
2387 String title =
"About this app";
2388 String MITtagline =
"<p><small><em>Invented with MIT App Inventor<br>appinventor.mit.edu</em></small></p>";
2390 String message = aboutScreen + MITtagline + yandexTranslateTagline;
2391 message = message.replaceAll(
"\\n",
"<br>");
2392 String buttonText =
"Got it";
2393 Notifier.oneButtonAlert(
this, message, title, buttonText);
2398 Log.d(LOG_TAG,
"Form " + formName +
" clear called");
2400 if (frameLayout !=
null) {
2401 frameLayout.removeAllViews();
2405 defaultPropertyValues();
2406 onStopListeners.clear();
2407 onNewIntentListeners.clear();
2408 onResumeListeners.clear();
2409 onOrientationChangeListeners.clear();
2410 onPauseListeners.clear();
2411 onDestroyListeners.clear();
2412 onInitializeListeners.clear();
2413 onCreateOptionsMenuListeners.clear();
2414 onOptionsItemSelectedListeners.clear();
2415 screenInitialized =
false;
2418 onClearListener.onClear();
2421 onClearListeners.clear();
2422 System.err.println(
"Form.clear() About to do moby GC!");
2429 onStopListeners.remove(component);
2432 onNewIntentListeners.remove(component);
2435 onResumeListeners.remove(component);
2438 onOrientationChangeListeners.remove(component);
2441 onPauseListeners.remove(component);
2444 onDestroyListeners.remove(component);
2447 onInitializeListeners.remove(component);
2450 onCreateOptionsMenuListeners.remove(component);
2453 onOptionsItemSelectedListeners.remove(component);
2467 frameLayout.requestDisallowInterceptTouchEvent(
true);
2474 long now = System.nanoTime();
2475 if (now > lastToastTime + minimumToastWait) {
2476 lastToastTime = now;
2486 method = component.getClass().getMethod(
"Initialize", (Class<?>[])
null);
2487 }
catch (SecurityException e) {
2488 Log.i(LOG_TAG,
"Security exception " + e.getMessage());
2490 }
catch (NoSuchMethodException e) {
2495 Log.i(LOG_TAG,
"calling Initialize method for Object " + component.toString());
2496 method.invoke(component, (Object[])
null);
2497 }
catch (InvocationTargetException e){
2498 Log.i(LOG_TAG,
"invoke exception: " + e.getMessage());
2499 throw e.getTargetException();
2537 return fullScreenVideoUtil.
performAction(action, source, data);
2540 private void setBackground(View bgview) {
2541 Drawable setDraw = backgroundDrawable;
2542 if (backgroundImagePath !=
"" && setDraw !=
null) {
2543 setDraw = backgroundDrawable.getConstantState().newDrawable();
2545 PorterDuff.Mode.DST_OVER);
2547 setDraw =
new ColorDrawable(
2548 (backgroundColor != Component.COLOR_DEFAULT) ? backgroundColor : Component.COLOR_WHITE);
2550 ViewUtil.setBackgroundImage(bgview, setDraw);
2551 bgview.invalidate();
2555 return sCompatibilityMode;
2561 @
SimpleFunction(description =
"Hide the onscreen soft keyboard.")
2562 public
void HideKeyboard() {
2563 View view = this.getCurrentFocus();
2567 InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
2568 imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
2572 themeHelper.setTitle(title);
2578 super.maybeShowTitleBar();
2580 super.hideTitleBar();
2585 return usesDarkTheme;
2597 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
2598 ContextCompat.checkSelfPermission(
this, permission) == PackageManager.PERMISSION_DENIED;
2608 if (isDeniedPermission(permission)) {
2634 final Form form =
this;
2635 if (!isDeniedPermission(permission)) {
2640 androidUIHandler.post(
new Runnable() {
2643 int nonce = permissionRandom.nextInt(MAX_PERMISSION_NONCE);
2644 Log.d(LOG_TAG,
"askPermission: permission = " + permission +
2645 " requestCode = " + nonce);
2646 permissionHandlers.put(nonce, responseRequestor);
2647 ActivityCompat.requestPermissions((Activity)form,
2648 new String[] {permission}, nonce);
2660 Iterator<String> it = permissionsToAsk.iterator();
2661 while (it.hasNext()) {
2662 if (!isDeniedPermission(it.next())) {
2666 if (permissionsToAsk.size() == 0) {
2671 androidUIHandler.post(
new Runnable() {
2674 final Iterator<String> it = permissionsToAsk.iterator();
2676 final List<String> deniedPermissions =
new ArrayList<String>();
2679 public void HandlePermissionResponse(String permission,
boolean granted) {
2681 deniedPermissions.add(permission);
2684 askPermission(it.next(),
this);
2686 if (deniedPermissions.size() == 0) {
2689 request.
onDenied(deniedPermissions.toArray(
new String[] {}));
2694 askPermission(it.next(), handler);
2702 String permissions[],
int[] grantResults) {
2704 if (responder ==
null) {
2706 Log.e(LOG_TAG,
"Received permission response which we cannot match.");
2709 if (grantResults.length > 0) {
2710 if(grantResults[0] == PackageManager.PERMISSION_GRANTED) {
2716 Log.d(LOG_TAG,
"onRequestPermissionsResult: grantResults.length = " + grantResults.length +
2717 " requestCode = " + requestCode);
2719 permissionHandlers.remove(requestCode);
2729 @SuppressWarnings(
"WeakerAccess")
2730 public
boolean doesAppDeclarePermission(String permissionName) {
2731 return permissions.contains(permissionName);
2741 return ASSETS_PREFIX + asset;
2751 @SuppressWarnings({
"WeakerAccess"})
2752 public InputStream
openAsset(String asset)
throws IOException {
2753 return openAssetInternal(getAssetPath(asset));
2766 String extPkgName = component.getClass().getPackage().getName();
2767 return ASSETS_PREFIX + extPkgName +
"/" + asset;
2780 @SuppressWarnings(
"unused")
2781 public InputStream openAssetForExtension(
Component component, String asset) throws IOException {
2782 return openAssetInternal(getAssetPathForExtension(component, asset));
2785 @SuppressWarnings(
"WeakerAccess")
2787 InputStream openAssetInternal(String path)
throws IOException {
2788 if (path.startsWith(ASSETS_PREFIX)) {
2789 final AssetManager am = getAssets();
2790 return am.open(path.substring(ASSETS_PREFIX.length()));
2791 }
else if (path.startsWith(
"file:")) {
2792 return FileUtil.openFile(
this, URI.create(path));
2794 return FileUtil.openFile(
this, path);