diff --git a/android/cli/commands/_build.js b/android/cli/commands/_build.js index b2c6ec8c5ff..f46ca2100a0 100644 --- a/android/cli/commands/_build.js +++ b/android/cli/commands/_build.js @@ -927,7 +927,7 @@ AndroidBuilder.prototype.validate = function validate(logger, config, cli) { if (cli.argv['source-maps']) { this.sourceMaps = true; // if they haven't, respect the tiapp.xml value if set one way or the other - } else if (cli.tiapp.hasOwnProperty['source-maps']) { // they've explicitly set a value in tiapp.xml + } else if (Object.prototype.hasOwnProperty.call(cli.tiapp, 'source-maps')) { // they've explicitly set a value in tiapp.xml this.sourceMaps = cli.tiapp['source-maps'] === true; // respect the tiapp.xml value } else { // otherwise turn on by default for non-production builds this.sourceMaps = this.deployType !== 'production'; @@ -1973,6 +1973,9 @@ AndroidBuilder.prototype.loginfo = function loginfo(next) { this.logger.info(__('Profiler disabled')); } + this.logger.info(__('Transpile javascript: %s', (this.transpile ? 'true' : 'false').cyan)); + this.logger.info(__('Generate source maps: %s', (this.sourceMaps ? 'true' : 'false').cyan)); + next(); }; diff --git a/android/cli/lib/AndroidManifest.js b/android/cli/lib/AndroidManifest.js index e5d936386b1..d064974c15b 100644 --- a/android/cli/lib/AndroidManifest.js +++ b/android/cli/lib/AndroidManifest.js @@ -513,12 +513,39 @@ function AndroidManifest(filename) { case 'uses-feature': this[tag] || (this[tag] = []); src[tag].forEach(function (tagItem) { - // Check for already added features. - let duplicateItem = this[tag].find(function (nextItem) { + // If given "uses-feature" name has already been added, then fetch its object. + const duplicateItem = this[tag].find(function (nextItem) { // Compare them directly or by name. return (nextItem === tagItem) || (nextItem.name === tagItem.name); }); - if (!duplicateItem) { + if (duplicateItem === tagItem) { + // Given reference was already added. Do nothing. + } else if (duplicateItem) { + // This is a duplicate "uses-feature" element name. Merge its settings. + if (typeof tagItem['tools:replace'] === 'string') { + // This attribute provides an array of other attributes that must be replaced. + tagItem['tools:replace'].split(',').forEach(function (attributeName) { + attributeName = attributeName.replace(androidAttrPrefixRegExp, ''); + if (attributeName !== 'name') { + const value = tagItem[attributeName]; + if (typeof value === 'undefined') { + delete duplicateItem[attributeName]; + } else { + duplicateItem[attributeName] = value; + } + } + }); + } else if (duplicateItem.required === false) { + // Do a logical OR on the "required" attribute value. + // If the "required" attribute is not defined, then it is considered true. + if (typeof tagItem.required === 'undefined') { + delete duplicateItem.required; + } else if (tagItem.required) { + duplicateItem.required = tagItem.required; + } + } + } else { + // The given "uses-feature" name has not been added yet. Do so now. this[tag].push(tagItem); } }, this); diff --git a/android/modules/database/src/java/ti/modules/titanium/database/TiDatabaseProxy.java b/android/modules/database/src/java/ti/modules/titanium/database/TiDatabaseProxy.java index d44bae1da0e..a1d1f071a30 100644 --- a/android/modules/database/src/java/ti/modules/titanium/database/TiDatabaseProxy.java +++ b/android/modules/database/src/java/ti/modules/titanium/database/TiDatabaseProxy.java @@ -6,6 +6,8 @@ */ package ti.modules.titanium.database; +import org.appcelerator.kroll.JSError; +import org.appcelerator.kroll.KrollDict; import org.appcelerator.kroll.KrollFunction; import org.appcelerator.kroll.KrollProxy; import org.appcelerator.kroll.annotations.Kroll; @@ -22,8 +24,11 @@ import android.database.sqlite.SQLiteDatabase; import android.os.Looper; +import java.lang.Exception; import java.security.InvalidParameterException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; @@ -36,6 +41,7 @@ public class TiDatabaseProxy extends KrollProxy private Thread thread; private BlockingQueue queue = new LinkedBlockingQueue<>(); private AtomicBoolean executingQueue = new AtomicBoolean(false); + private boolean isClosed = false; protected SQLiteDatabase db; protected String name; @@ -125,37 +131,41 @@ private void waitForQueue() @Kroll.method public void close() { - if (db.isOpen()) { - - // Wait for query queue to empty. - waitForQueue(); - + synchronized (this) + { // Close database. - db.close(); + if (db != null && db.isOpen()) { + db.close(); + } + db = null; + isClosed = true; + + // Abort query queue execution. + if (thread != null) { + thread.interrupt(); + thread = null; + } + executingQueue.set(false); + queue.clear(); } } + private boolean expectResult(String query) + { + String lowerCaseQuery = query.toLowerCase().trim(); + return lowerCaseQuery.startsWith("select") + || (lowerCaseQuery.startsWith("pragma") && !lowerCaseQuery.contains("=")); + } + /** * Synchronously execute a single SQL query. * @param query SQL query to execute on database. * @param parameterObjects Parameters for `query` */ - @Kroll.method - public TiResultSetProxy execute(String query, Object... parameterObjects) + private TiResultSetProxy executeSQL(String query, Object[] parameterObjects) { - // Validate `query` parameter. - if (query == null) { - throw new InvalidParameterException("'query' parameter is required"); - } - // Validate and parse `parameterObjects`. if (parameterObjects != null) { - - // Only an array is defined, use that for parameters - if (parameterObjects.length == 1 && parameterObjects[0] instanceof Object[]) { - parameterObjects = (Object[]) parameterObjects[0]; - } - // Validate query parameters, must be either String or Blob. for (int i = 0; i < parameterObjects.length; i++) { if (parameterObjects[i] instanceof TiBlob) { @@ -164,21 +174,25 @@ public TiResultSetProxy execute(String query, Object... parameterObjects) parameterObjects[i] = TiConvert.toString(parameterObjects[i]); } } + } else { + parameterObjects = new Object[0]; } // If this is a synchronous call on the main thread, wait for all queued queries // to maintain correct execution order and prevent write-locks. waitForQueue(); - // Log.d(TAG, "execute: " + query); - - TiResultSetProxy result = null; - Cursor cursor = null; - // Execute query using rawQuery() in order to receive results. - String lowerCaseQuery = query.toLowerCase().trim(); - if (lowerCaseQuery.startsWith("select") - || (lowerCaseQuery.startsWith("pragma") && !lowerCaseQuery.contains("="))) { + synchronized (this) + { // lock on db proxy instance + if (isClosed) { + throw new IllegalStateException("database is closed"); + } + + if (!expectResult(query)) { + db.execSQL(query, parameterObjects); + return null; + } // Query parameters must be strings. String parameters[] = new String[parameterObjects.length]; @@ -188,34 +202,50 @@ public TiResultSetProxy execute(String query, Object... parameterObjects) } } - cursor = db.rawQuery(query, parameters); + Cursor cursor = db.rawQuery(query, parameters); if (cursor != null) { - // Validate and set query result. if (cursor.getColumnCount() > 0) { - result = new TiResultSetProxy(cursor); + TiResultSetProxy result = new TiResultSetProxy(cursor); if (result.isValidRow()) { result.next(); } - } else { + return result; + } - // Cleanup result. - if (cursor != null) { - try { - cursor.close(); - } catch (Exception e) { - // Ignore... - } - } + // Cleanup result. + try { + cursor.close(); + } catch (Exception e) { + // Ignore... } } - // Query does not return result, use execSQL(). - } else { - db.execSQL(query, parameterObjects); + return null; + } + } + + /** + * Synchronously execute a single SQL query. + * @param query SQL query to execute on database. + * @param parameterObjects Parameters for `query` + */ + @Kroll.method + public TiResultSetProxy execute(String query, Object... parameterObjects) + { + // Validate `query` parameter. + if (query == null) { + throw new InvalidParameterException("'query' parameter is required"); } - return result; + // Support either varargs or a single array as params + if (parameterObjects != null) { + // Only an array is defined, use that for parameters + if (parameterObjects.length == 1 && parameterObjects[0] instanceof Object[]) { + parameterObjects = (Object[]) parameterObjects[0]; + } + } + return executeSQL(query, parameterObjects); } /** @@ -241,9 +271,7 @@ public void executeAsync(final String query, final Object... parameterObjects) // Reconstruct parameters array without `callback` element. final Object parameters[] = new Object[parameterObjects.length - 1]; - for (int i = 0; i < parameters.length; i++) { - parameters[i] = parameterObjects[i]; - } + System.arraycopy(parameterObjects, 0, parameters, 0, parameterObjects.length - 1); executingQueue.set(true); try { @@ -251,8 +279,14 @@ public void executeAsync(final String query, final Object... parameterObjects) @Override public void run() { - final TiResultSetProxy result = execute(query, parameters); - callback.callAsync(getKrollObject(), new Object[] { result }); + Object[] args = null; + try { + final TiResultSetProxy result = executeSQL(query, parameters); + args = new Object[] { null, result }; + } catch (Throwable t) { + args = new Object[] { t }; + } + callback.callAsync(getKrollObject(), args); } }); } catch (InterruptedException e) { @@ -265,17 +299,21 @@ public void run() * @param queries SQL queries to execute on database. */ @Kroll.method - public Object[] executeAll(final String[] queries) + public Object[] executeAll(final String[] queries) throws BatchQueryException { // Validate `queries` parameter. if (queries == null || queries.length == 0) { throw new InvalidParameterException("'query' parameter is required"); } - ArrayList results = new ArrayList<>(queries.length); - for (String query : queries) { - final TiResultSetProxy result = execute(query); - results.add(result); + List results = new ArrayList<>(queries.length); + for (int index = 0; index < queries.length; index++) { + try { + final TiResultSetProxy result = executeSQL(queries[index], null); + results.add(result); + } catch (Throwable t) { + throw new BatchQueryException(t, index, results); + } } return results.toArray(); } @@ -299,8 +337,18 @@ public void executeAllAsync(final String[] queries, final KrollFunction callback @Override public void run() { - final Object[] results = executeAll(queries); - callback.callAsync(getKrollObject(), new Object[] { results }); + Throwable error = null; + List results = new ArrayList<>(queries.length); + for (int index = 0; index < queries.length; index++) { + try { + final TiResultSetProxy result = executeSQL(queries[index], null); + results.add(result); + } catch (Throwable t) { + error = new BatchQueryException(t, index, null); + break; + } + } + callback.callAsync(getKrollObject(), new Object[] { error, results.toArray() }); } }); } catch (InterruptedException e) { @@ -331,7 +379,13 @@ public String getName() public int getLastInsertRowId() // clang-format on { - return (int) DatabaseUtils.longForQuery(db, "select last_insert_rowid()", null); + synchronized (this) + { // lock on db proxy instance + if (isClosed) { + throw new IllegalStateException("database is closed"); + } + return (int) DatabaseUtils.longForQuery(db, "select last_insert_rowid()", null); + } } /** @@ -344,7 +398,13 @@ public int getLastInsertRowId() public int getRowsAffected() // clang-format on { - return (int) DatabaseUtils.longForQuery(db, "select changes()", null); + synchronized (this) + { // lock on db proxy instance + if (isClosed) { + throw new IllegalStateException("database is closed"); + } + return (int) DatabaseUtils.longForQuery(db, "select changes()", null); + } } /** @@ -396,14 +456,29 @@ public String getApiName() public void release() { this.close(); - this.db = null; + super.release(); + } - // Interrupt and dereference thread. - if (this.thread != null) { - this.thread.interrupt(); - this.thread = null; + private static class BatchQueryException extends Exception implements JSError + { + private final int index; + private final List partialResults; + + BatchQueryException(Throwable t, int index, List partialResults) + { + super(t); + this.index = index; + this.partialResults = partialResults; } - super.release(); + public HashMap getJSProperties() + { + HashMap map = new HashMap(); + map.put("index", index); + if (partialResults != null) { + map.put("results", partialResults.toArray()); + } + return map; + } } } diff --git a/android/modules/network/src/java/ti/modules/titanium/network/TiHTTPClient.java b/android/modules/network/src/java/ti/modules/titanium/network/TiHTTPClient.java index 57c8f69bc55..c2ce12ff2ba 100644 --- a/android/modules/network/src/java/ti/modules/titanium/network/TiHTTPClient.java +++ b/android/modules/network/src/java/ti/modules/titanium/network/TiHTTPClient.java @@ -732,6 +732,10 @@ public String getAllResponseHeaders() public void clearCookies(String url) { + if (url == null) { + return; + } + List cookies = new ArrayList(cookieManager.getCookieStore().getCookies()); cookieManager.getCookieStore().removeAll(); String lower_url = url.toLowerCase(); diff --git a/android/modules/ui/res/layout/titanium_ui_list_item.xml b/android/modules/ui/res/layout/titanium_ui_list_item.xml index 5d27da4d463..04980aa2aa9 100644 --- a/android/modules/ui/res/layout/titanium_ui_list_item.xml +++ b/android/modules/ui/res/layout/titanium_ui_list_item.xml @@ -25,7 +25,6 @@ android:layout_height="fill_parent" android:adjustViewBounds="true" android:layout_gravity="right" - android:contentDescription="One of the following images: checkmark, child, or disclosure" android:focusable="false" android:focusableInTouchMode="false" android:maxHeight="17dp" diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java index 770bb3e4eb9..4c0d01d4a66 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java @@ -21,6 +21,7 @@ import org.appcelerator.titanium.TiBaseActivity; import org.appcelerator.titanium.TiBlob; import org.appcelerator.titanium.TiC; +import org.appcelerator.titanium.TiRootActivity; import org.appcelerator.titanium.proxy.TiWindowProxy; import org.appcelerator.titanium.util.TiConvert; import org.appcelerator.titanium.util.TiUIHelper; @@ -388,6 +389,11 @@ protected void handleOpen(KrollDict options) topActivity.overridePendingTransition(enterAnimation, exitAnimation); } else { topActivity.startActivity(intent); + if (topActivity instanceof TiRootActivity) { + // A fade-in transition from root splash screen to first window looks better than a slide-up. + // Also works-around issue where splash in mid-transition might do a 2nd transition on cold start. + topActivity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); + } } } diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/WindowProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/WindowProxy.java index e0beab26a8c..6d9cb25f2a8 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/WindowProxy.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/WindowProxy.java @@ -20,6 +20,7 @@ import org.appcelerator.titanium.TiBaseActivity; import org.appcelerator.titanium.TiC; import org.appcelerator.titanium.TiDimension; +import org.appcelerator.titanium.TiRootActivity; import org.appcelerator.titanium.TiTranslucentActivity; import org.appcelerator.titanium.proxy.ActivityProxy; import org.appcelerator.titanium.proxy.TiWindowProxy; @@ -56,6 +57,7 @@ import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.Window; + // clang-format off @Kroll.proxy(creatableInModule = UIModule.class, propertyAccessors = { @@ -168,6 +170,11 @@ protected void handleOpen(KrollDict options) topActivity.startActivity(intent, createActivityOptionsBundle(topActivity)); } else { topActivity.startActivity(intent); + if (topActivity instanceof TiRootActivity) { + // A fade-in transition from root splash screen to first window looks better than a slide-up. + // Also works-around issue where splash in mid-transition might do a 2nd transition on cold start. + topActivity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); + } } if (options.containsKey(TiC.PROPERTY_SUSTAINED_PERFORMANCE_MODE)) { @@ -310,7 +317,13 @@ public void windowCreated(TiBaseActivity activity, Bundle savedInstanceState) // Handle barColor property. if (hasProperty(TiC.PROPERTY_BAR_COLOR)) { int colorInt = TiColorHelper.parseColor(TiConvert.toString(getProperty(TiC.PROPERTY_BAR_COLOR))); - activity.getSupportActionBar().setBackgroundDrawable(new ColorDrawable(colorInt)); + ActionBar actionBar = activity.getSupportActionBar(); + // Guard for using a theme with actionBar disabled. + if (actionBar != null) { + actionBar.setBackgroundDrawable(new ColorDrawable(colorInt)); + } else { + Log.w(TAG, "Trying to set a barColor on a Window with ActionBar disabled. Property will be ignored."); + } } activity.getActivityProxy().getDecorView().add(this); diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIDialog.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIDialog.java index 3dbfe3a0542..710173ccd9c 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIDialog.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIDialog.java @@ -47,7 +47,7 @@ public ClickHandler(int id) } public void onClick(DialogInterface dialog, int which) { - handleEvent(result); + handleEvent(result, true); hide(null); } } @@ -134,7 +134,7 @@ private void processOptions(String[] optionText, int selectedIndex) @Override public void onClick(DialogInterface dialog, int which) { - handleEvent(which); + handleEvent(which, true); hide(null); } }); @@ -143,7 +143,7 @@ public void onClick(DialogInterface dialog, int which) @Override public void onClick(DialogInterface dialog, int which) { - handleEvent(which); + handleEvent(which, false); hide(null); } }); @@ -285,7 +285,7 @@ public void propertyChanged(String key, Object oldValue, Object newValue, KrollP } ViewCompat.setImportantForAccessibility(listView, importance); } else { - listView.setContentDescription(composeContentDescription()); + listView.setContentDescription(getProxy().composeContentDescription()); } } } @@ -322,7 +322,8 @@ public void onCancel(DialogInterface dlg) ? TiConvert.toInt(proxy.getProperty(TiC.PROPERTY_CANCEL)) : -1; Log.d(TAG, "onCancelListener called. Sending index: " + cancelIndex, Log.DEBUG_MODE); - handleEvent(cancelIndex); + // In case wew cancel the Dialog we should not overwrite the selectedIndex + handleEvent(cancelIndex, false); hide(null); } }); @@ -335,9 +336,9 @@ public void onCancel(DialogInterface dlg) // can also be used. ListView listView = dialog.getListView(); if (listView != null) { - listView.setContentDescription(composeContentDescription()); int importance = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; if (proxy != null) { + listView.setContentDescription(proxy.composeContentDescription()); Object propertyValue = proxy.getProperty(TiC.PROPERTY_ACCESSIBILITY_HIDDEN); if (propertyValue != null && TiConvert.toBoolean(propertyValue)) { importance = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO; @@ -347,7 +348,7 @@ public void onCancel(DialogInterface dlg) } dialogWrapper.setDialog(dialog); - builder = null; + this.builder = null; } try { @@ -438,7 +439,7 @@ private void createBuilder() } } - public void handleEvent(int id) + public void handleEvent(int id, boolean saveSelectedIndex) { int cancelIndex = (proxy.hasProperty(TiC.PROPERTY_CANCEL)) ? TiConvert.toInt(proxy.getProperty(TiC.PROPERTY_CANCEL)) : -1; @@ -452,8 +453,8 @@ public void handleEvent(int id) id &= ~BUTTON_MASK; } else { data.put(TiC.PROPERTY_BUTTON, false); - // If an option was selected and the user accepted it, update the proxy. - if (proxy.hasProperty(TiC.PROPERTY_OPTIONS)) { + // If an option was selected, the user accepted and we are showing radio buttons, update the proxy. + if (proxy.hasProperty(TiC.PROPERTY_OPTIONS) && saveSelectedIndex) { proxy.setProperty(TiC.PROPERTY_SELECTED_INDEX, id); } } diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListSectionProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListSectionProxy.java index ad23986baff..c601745a5c0 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListSectionProxy.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListSectionProxy.java @@ -13,6 +13,7 @@ import java.util.HashMap; import org.appcelerator.kroll.KrollDict; +import org.appcelerator.kroll.KrollProxy; import org.appcelerator.kroll.annotations.Kroll; import org.appcelerator.kroll.common.Log; import org.appcelerator.titanium.TiC; @@ -665,8 +666,10 @@ public void populateViews(KrollDict data, TiBaseListViewItem cellContent, TiList DataItem dataItem = template.getDataItem(binding); ViewItem viewItem = views.get(binding); TiUIView view = viewItem.getView(); + KrollProxy viewProxy = null; //update extra event data for views if (view != null) { + viewProxy = view.getProxy(); appendExtraEventData(view, itemIndex, sectionIndex, binding, itemId); } //if binding is contain in data given to us, process that data, otherwise @@ -675,12 +678,18 @@ public void populateViews(KrollDict data, TiBaseListViewItem cellContent, TiList KrollDict properties = new KrollDict((HashMap) data.get(binding)); KrollDict diffProperties = viewItem.generateDiffProperties(properties); if (!diffProperties.isEmpty()) { + if (viewProxy != null && viewProxy.getProperties() != null) { + viewProxy.getProperties().putAll(diffProperties); + } view.processProperties(diffProperties); } } else if (dataItem != null && view != null) { KrollDict diffProperties = viewItem.generateDiffProperties(dataItem.getDefaultProperties()); if (!diffProperties.isEmpty()) { + if (viewProxy != null && viewProxy.getProperties() != null) { + viewProxy.getProperties().putAll(diffProperties); + } view.processProperties(diffProperties); } } else { diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiListView.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiListView.java index c13c4b87579..d66e1d90fb2 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiListView.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiListView.java @@ -55,7 +55,7 @@ import android.widget.AbsListView.OnScrollListener; import android.widget.BaseAdapter; import android.widget.EditText; -import android.widget.RelativeLayout; +import android.widget.LinearLayout; import android.widget.TextView; public class TiListView extends TiUIView implements OnSearchChangeListener @@ -83,7 +83,7 @@ public class TiListView extends TiUIView implements OnSearchChangeListener private View footerView; private String searchText; private boolean caseInsensitive; - private RelativeLayout searchLayout; + private LinearLayout searchLayout; private static final String TAG = "TiListView"; /* We cache properties that already applied to the recycled list item in ViewItem.java @@ -663,57 +663,48 @@ public void processProperties(KrollDict d) private void layoutSearchView(TiViewProxy searchView) { TiUIView search = searchView.getOrCreateView(); - RelativeLayout layout = new RelativeLayout(proxy.getActivity()); - layout.setGravity(Gravity.NO_GRAVITY); + LinearLayout layout = new LinearLayout(proxy.getActivity()); + layout.setOrientation(LinearLayout.VERTICAL); layout.setPadding(0, 0, 0, 0); addSearchLayout(layout, searchView, search); setNativeView(layout); } - private void addSearchLayout(RelativeLayout layout, TiViewProxy searchView, TiUIView search) + private void addSearchLayout(LinearLayout layout, TiViewProxy searchView, TiUIView search) { - RelativeLayout.LayoutParams p = createBasicSearchLayout(); - p.addRule(RelativeLayout.ALIGN_PARENT_TOP); - + // Fetch the height of the SearchBar/SearchView. TiDimension rawHeight; if (searchView.hasProperty(TiC.PROPERTY_HEIGHT)) { rawHeight = TiConvert.toTiDimension(searchView.getProperty(TiC.PROPERTY_HEIGHT), 0); } else { rawHeight = TiConvert.toTiDimension(MIN_SEARCH_HEIGHT, 0); } - p.height = rawHeight.getAsPixels(layout); - View nativeView = search.getNativeView(); - layout.addView(nativeView, p); + // Add the search view to the top of the vertical layout. + LinearLayout.LayoutParams params = + new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, rawHeight.getAsPixels(layout)); + layout.addView(search.getNativeView(), params); - p = createBasicSearchLayout(); - p.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); - p.addRule(RelativeLayout.BELOW, nativeView.getId()); + // Add the ListView to the bottom of the vertical layout. + params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT); ViewParent parentWrapper = wrapper.getParent(); if (parentWrapper != null && parentWrapper instanceof ViewGroup) { // get the previous layout params so we can reset with new layout - ViewGroup.LayoutParams lp = wrapper.getLayoutParams(); + ViewGroup.LayoutParams lastParams = wrapper.getLayoutParams(); ViewGroup parentView = (ViewGroup) parentWrapper; // remove view from parent parentView.removeView(wrapper); // add new layout - layout.addView(wrapper, p); - parentView.addView(layout, lp); + layout.addView(wrapper, params); + parentView.addView(layout, lastParams); } else { - layout.addView(wrapper, p); + layout.addView(wrapper, params); } this.searchLayout = layout; } - private RelativeLayout.LayoutParams createBasicSearchLayout() - { - RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, - RelativeLayout.LayoutParams.MATCH_PARENT); - p.addRule(RelativeLayout.ALIGN_PARENT_LEFT); - p.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); - return p; - } private void setHeaderOrFooterView(Object viewObj, boolean isHeader) { if (viewObj instanceof TiViewProxy) { diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tableview/TiTableView.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tableview/TiTableView.java index f15890cd3fb..9e9e2afa273 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tableview/TiTableView.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tableview/TiTableView.java @@ -157,7 +157,7 @@ public int getCount() public Object getItem(int position) { - if (position >= index.size()) { + if ((position < 0) || (position >= index.size())) { return null; } @@ -199,49 +199,29 @@ public int getItemViewType(int position) */ public View getView(int position, View convertView, ViewGroup parent) { - Item item = (Item) getItem(position); TiBaseTableViewItem v = null; - if (convertView != null) { - v = (TiBaseTableViewItem) convertView; - // Default creates view for each Item - boolean sameView = false; - if (item.proxy instanceof TableViewRowProxy) { - TableViewRowProxy row = (TableViewRowProxy) item.proxy; - if (row.getTableViewRowProxyItem() != null) { - sameView = row.getTableViewRowProxyItem().equals(convertView); - } - } - - // TIMOB-24560: prevent duplicate TableViewRowProxyItem on Android N - if (Build.VERSION.SDK_INT > 23) { - ArrayList models = viewModel.getViewModel(); - if (models != null && v instanceof TiTableViewRowProxyItem && models.contains(v.getRowData())) { - v = null; - sameView = true; - } - } + // Fetch the indexed row item. + Item item = (Item) getItem(position); + if (item == null) { + Log.w(TAG, "getView() received invalid 'position' index: " + position); + v = new TiTableViewRowProxyItem(proxy.getActivity()); + v.setClassName(TableViewProxy.CLASSNAME_NORMAL); + return v; + } - if (!sameView) { - if (v.getClassName().equals(TableViewProxy.CLASSNAME_DEFAULT)) { - if (v.getRowData() != item) { - v = null; - } - } else if (v.getClassName().equals(TableViewProxy.CLASSNAME_HEADERVIEW)) { - //Always recreate the header view - v = null; - } else { - // otherwise compare class names - if (!v.getClassName().equals(item.className)) { - Log.w(TAG, - "Handed a view to convert with className " + v.getClassName() + " expected " - + item.className, - Log.DEBUG_MODE); - v = null; - } - } - } + // If we've already set up a view container for the item, then use it. (Ignore "convertView" argument.) + // Notes: + // - There is no point in recycling the "convertView" row container since we always store the row's + // child views in memory. If you want to recycle child views, then use "TiListView" instead. + // - If row contains an EditText/TextField/TextArea, then we don't want to change its parent to a + // different "convertView" row container, because it'll reset the connection with the keyboard. + if (item.proxy instanceof TableViewRowProxy) { + TableViewRowProxy row = (TableViewRowProxy) item.proxy; + v = row.getTableViewRowProxyItem(); } + + // If we haven't created a view container for the given row item, then do so now. if (v == null) { if (item.className.equals(TableViewProxy.CLASSNAME_HEADERVIEW)) { TiViewProxy vproxy = item.proxy; @@ -265,7 +245,11 @@ public View getView(int position, View convertView, ViewGroup parent) v.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.MATCH_PARENT)); } + + // Copy the proxy's current settings to the row's views. v.setRowData(item); + + // Return the row view configured above. return v; } diff --git a/android/runtime/common/src/java/org/appcelerator/kroll/JSError.java b/android/runtime/common/src/java/org/appcelerator/kroll/JSError.java new file mode 100644 index 00000000000..14dff95e8c9 --- /dev/null +++ b/android/runtime/common/src/java/org/appcelerator/kroll/JSError.java @@ -0,0 +1,13 @@ +/** + * Appcelerator Titanium Mobile + * Copyright (c) 2019-Present by Appcelerator, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ +package org.appcelerator.kroll; + +import java.util.HashMap; + +public interface JSError { + HashMap getJSProperties(); +} \ No newline at end of file diff --git a/android/runtime/v8/src/native/JNIUtil.cpp b/android/runtime/v8/src/native/JNIUtil.cpp index 23a86d78cf2..a29b8746242 100644 --- a/android/runtime/v8/src/native/JNIUtil.cpp +++ b/android/runtime/v8/src/native/JNIUtil.cpp @@ -56,6 +56,7 @@ jclass JNIUtil::krollAssetHelperClass = NULL; jclass JNIUtil::krollLoggingClass = NULL; jclass JNIUtil::krollDictClass = NULL; jclass JNIUtil::referenceTableClass = NULL; +jclass JNIUtil::jsErrorClass = NULL; jmethodID JNIUtil::classGetNameMethod = NULL; jmethodID JNIUtil::arrayListInitMethod = NULL; @@ -116,6 +117,8 @@ jmethodID JNIUtil::krollLoggingLogWithDefaultLoggerMethod = NULL; jmethodID JNIUtil::krollRuntimeDispatchExceptionMethod = NULL; +jmethodID JNIUtil::getJSPropertiesMethod = NULL; + JNIEnv* JNIScope::current = NULL; /* static */ @@ -337,7 +340,9 @@ void JNIUtil::initCache() krollExceptionClass = findClass("org/appcelerator/kroll/KrollException"); krollDictClass = findClass("org/appcelerator/kroll/KrollDict"); referenceTableClass = findClass("org/appcelerator/kroll/runtime/v8/ReferenceTable"); + jsErrorClass = findClass("org/appcelerator/kroll/JSError"); + getJSPropertiesMethod = getMethodID(jsErrorClass, "getJSProperties", "()Ljava/util/HashMap;", false); classGetNameMethod = getMethodID(classClass, "getName", "()Ljava/lang/String;", false); arrayListInitMethod = getMethodID(arrayListClass, "", "()V", false); arrayListAddMethod = getMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z", false); diff --git a/android/runtime/v8/src/native/JNIUtil.h b/android/runtime/v8/src/native/JNIUtil.h index bbe20dce291..0c1508d7895 100644 --- a/android/runtime/v8/src/native/JNIUtil.h +++ b/android/runtime/v8/src/native/JNIUtil.h @@ -107,6 +107,7 @@ class JNIUtil static jclass krollDictClass; static jclass tiJsErrorDialogClass; static jclass referenceTableClass; + static jclass jsErrorClass; // Java methods static jmethodID classGetNameMethod; @@ -137,9 +138,11 @@ class JNIUtil static jmethodID v8ObjectInitMethod; static jmethodID v8FunctionInitMethod; + // KrollDict static jmethodID krollDictInitMethod; static jmethodID krollDictPutMethod; + // ReferenceTable static jmethodID referenceTableCreateReferenceMethod; static jmethodID referenceTableDestroyReferenceMethod; static jmethodID referenceTableMakeWeakReferenceMethod; @@ -148,12 +151,19 @@ class JNIUtil static jmethodID referenceTableGetReferenceMethod; static jmethodID referenceTableIsStrongReferenceMethod; + // KrollRuntime static jint krollRuntimeDontIntercept; + static jmethodID krollRuntimeDispatchExceptionMethod; + static jmethodID krollInvocationInitMethod; static jmethodID krollExceptionInitMethod; + + // KrollObject static jfieldID krollObjectProxySupportField; static jmethodID krollObjectSetHasListenersForEventTypeMethod; static jmethodID krollObjectOnEventFiredMethod; + + // KrollProxy static jmethodID krollProxyCreateProxyMethod; static jfieldID krollProxyKrollObjectField; static jfieldID krollProxyModelListenerField; @@ -161,11 +171,16 @@ class JNIUtil static jmethodID krollProxyGetIndexedPropertyMethod; static jmethodID krollProxyOnPropertyChangedMethod; static jmethodID krollProxyOnPropertiesChangedMethod; + + // KrollLogging static jmethodID krollLoggingLogWithDefaultLoggerMethod; - static jmethodID krollRuntimeDispatchExceptionMethod; + // KrollAssetHelper static jmethodID krollAssetHelperReadAssetMethod; + // CustomError + static jmethodID getJSPropertiesMethod; + }; class JNIScope diff --git a/android/runtime/v8/src/native/JSException.cpp b/android/runtime/v8/src/native/JSException.cpp index d88ce75eef5..9bab0558a44 100644 --- a/android/runtime/v8/src/native/JSException.cpp +++ b/android/runtime/v8/src/native/JSException.cpp @@ -1,19 +1,13 @@ /** * Appcelerator Titanium Mobile - * Copyright (c) 2011-2018 by Appcelerator, Inc. All Rights Reserved. + * Copyright (c) 2011-Present by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Apache Public License * Please see the LICENSE included with this distribution for details. */ -#include -#include - #include #include -#include "JNIUtil.h" #include "TypeConverter.h" -#include "V8Util.h" - #include "JSException.h" using namespace v8; @@ -34,45 +28,10 @@ Local JSException::fromJavaException(v8::Isolate* isolate, jthrowable jav } env->ExceptionClear(); - jstring javaMessage = (jstring) env->CallObjectMethod(javaException, JNIUtil::throwableGetMessageMethod); - if (!javaMessage) { - return THROW(isolate, "Java Exception occurred"); - } - - Local context = isolate->GetCurrentContext(); - - // Grab the top-level error message - Local message = TypeConverter::javaStringToJsString(isolate, env, javaMessage); - env->DeleteLocalRef(javaMessage); - // Create a JS Error holding this message - // We use .As here because we know that the return value of TypeConverter::javaStringToJsString - // must be a String. Only other variant is Null when the javaMessage is null, which we already checked for above. - // We use .As on Error because an Error is an Object. - Local error = Exception::Error(message.As()).As(); - - // Now loop through the java stack and generate a JS String from the result and assign to Local stack - std::stringstream stackStream; - jobjectArray frames = (jobjectArray) env->CallObjectMethod(javaException, JNIUtil::throwableGetStackTraceMethod); - jsize frames_length = env->GetArrayLength(frames); - for (int i = 0; i < (frames_length > MAX_STACK ? MAX_STACK : frames_length); i++) { - jobject frame = env->GetObjectArrayElement(frames, i); - jstring javaStack = (jstring) env->CallObjectMethod(frame, JNIUtil::stackTraceElementToStringMethod); - - const char* stackPtr = env->GetStringUTFChars(javaStack, NULL); - stackStream << std::endl << " " << stackPtr; - - env->ReleaseStringUTFChars(javaStack, stackPtr); - env->DeleteLocalRef(javaStack); - } + Local error = TypeConverter::javaThrowableToJSError(isolate, env, javaException); if (deleteRef) { env->DeleteLocalRef(javaException); } - stackStream << std::endl; - - // Now explicitly assign our properly generated stacktrace - Local javaStack = String::NewFromUtf8(isolate, stackStream.str().c_str()); - error->Set(context, STRING_NEW(isolate, "nativeStack"), javaStack); - // throw it return isolate->ThrowException(error); } diff --git a/android/runtime/v8/src/native/JSException.h b/android/runtime/v8/src/native/JSException.h index b3415526bf0..70da2668bb3 100644 --- a/android/runtime/v8/src/native/JSException.h +++ b/android/runtime/v8/src/native/JSException.h @@ -1,6 +1,6 @@ /** * Appcelerator Titanium Mobile - * Copyright (c) 2011-2016 by Appcelerator, Inc. All Rights Reserved. + * Copyright (c) 2011-Present by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Apache Public License * Please see the LICENSE included with this distribution for details. */ @@ -11,7 +11,7 @@ #include #include -#define MAX_STACK 10 +#include "JNIUtil.h" #define THROW(isolate, msg) \ isolate->ThrowException(v8::String::NewFromUtf8(isolate, msg)) diff --git a/android/runtime/v8/src/native/TypeConverter.cpp b/android/runtime/v8/src/native/TypeConverter.cpp index e5e8e47567a..fc30e764f40 100644 --- a/android/runtime/v8/src/native/TypeConverter.cpp +++ b/android/runtime/v8/src/native/TypeConverter.cpp @@ -4,7 +4,10 @@ * Licensed under the terms of the Apache Public License * Please see the LICENSE included with this distribution for details. */ +#include +#include #include + #include #include #include @@ -1022,6 +1025,9 @@ Local TypeConverter::javaObjectToJsValue(Isolate* isolate, JNIEnv *env, j } else if (env->IsInstanceOf(javaObject, JNIUtil::booleanArrayClass)) { return javaArrayToJsArray(isolate, (jbooleanArray) javaObject); + } else if (env->IsInstanceOf(javaObject, JNIUtil::throwableClass)) { + return javaThrowableToJSError(isolate, (jthrowable) javaObject); + } else if (env->IsSameObject(JNIUtil::undefinedObject, javaObject)) { return Undefined(isolate); } @@ -1164,3 +1170,77 @@ Local TypeConverter::javaShortArrayToJsNumberArray(Isolate* isolate, JNIE env->ReleaseShortArrayElements(javaShortArray, arrayElements, JNI_ABORT); return jsArray; } + +Local TypeConverter::javaThrowableToJSError(Isolate* isolate, jthrowable javaException) +{ + JNIEnv *env = JNIScope::getEnv(); + if (env == NULL) { + return Local(); + } + return TypeConverter::javaThrowableToJSError(isolate, env, javaException); +} + +Local TypeConverter::javaThrowableToJSError(v8::Isolate* isolate, JNIEnv *env, jthrowable javaException) +{ + // Grab the top-level error message + jstring javaMessage = (jstring) env->CallObjectMethod(javaException, JNIUtil::throwableGetMessageMethod); + Local message; + if (!javaMessage) { + message = STRING_NEW(isolate, "Unknown Java Exception occurred"); + } else { + message = TypeConverter::javaStringToJsString(isolate, env, javaMessage); + env->DeleteLocalRef(javaMessage); + } + + // Create a JS Error holding this message + // We use .As here because we know that the return value of TypeConverter::javaStringToJsString + // must be a String. Only other variant is Null when the javaMessage is null, which we already checked for above. + // We use .As on Error because an Error is an Object. + Local error = Exception::Error(message.As()).As(); + + // Now loop through the java stack and generate a JS String from the result and assign to Local stack + std::stringstream stackStream; + jobjectArray frames = (jobjectArray) env->CallObjectMethod(javaException, JNIUtil::throwableGetStackTraceMethod); + jsize frames_length = env->GetArrayLength(frames); + for (int i = 0; i < (frames_length > MAX_STACK ? MAX_STACK : frames_length); i++) { + jobject frame = env->GetObjectArrayElement(frames, i); + jstring javaStack = (jstring) env->CallObjectMethod(frame, JNIUtil::stackTraceElementToStringMethod); + + const char* stackPtr = env->GetStringUTFChars(javaStack, NULL); + stackStream << std::endl << " " << stackPtr; + + env->ReleaseStringUTFChars(javaStack, stackPtr); + env->DeleteLocalRef(javaStack); + } + stackStream << std::endl; + + Local context = isolate->GetCurrentContext(); + + // Now explicitly assign our properly generated stacktrace + Local javaStack = String::NewFromUtf8(isolate, stackStream.str().c_str()); + error->Set(context, STRING_NEW(isolate, "nativeStack"), javaStack); + + // If we're using our custom error interface we can ask for a map of additional properties ot set on the JS Error + if (env->IsInstanceOf(javaException, JNIUtil::jsErrorClass)) { + jobject customProps = (jobject) env->CallObjectMethod(javaException, JNIUtil::getJSPropertiesMethod); + if (customProps) { + // Grab the custom properties + Local props = TypeConverter::javaHashMapToJsValue(isolate, env, customProps); + env->DeleteLocalRef(customProps); + + // Copy properties over to the JS Error! + Local objectKeys = props->GetOwnPropertyNames(context).ToLocalChecked(); + int numKeys = objectKeys->Length(); + for (int i = 0; i < numKeys; i++) { + // FIXME: Handle when empty! + Local jsObjectPropertyKey = objectKeys->Get(context, (uint32_t) i).ToLocalChecked(); + Local keyName = jsObjectPropertyKey.As(); + + Local jsObjectPropertyValue = props->Get(context, jsObjectPropertyKey).ToLocalChecked(); + error->Set(context, keyName, jsObjectPropertyValue); + } + } + } + + return error; +} diff --git a/android/runtime/v8/src/native/TypeConverter.h b/android/runtime/v8/src/native/TypeConverter.h index 647b1fc9ac7..8281998f253 100644 --- a/android/runtime/v8/src/native/TypeConverter.h +++ b/android/runtime/v8/src/native/TypeConverter.h @@ -12,6 +12,8 @@ #include #include +#define MAX_STACK 10 + namespace titanium { class TypeConverter { @@ -162,6 +164,9 @@ class TypeConverter static v8::Local javaArrayToJsArray(v8::Isolate* isolate, JNIEnv *env, jdoubleArray javaDoubleArray); static v8::Local javaArrayToJsArray(v8::Isolate* isolate, JNIEnv *env, jobjectArray javaObjectArray); + static v8::Local javaThrowableToJSError(v8::Isolate* isolate, jthrowable javaException); + static v8::Local javaThrowableToJSError(v8::Isolate* isolate, JNIEnv *env, jthrowable javaException); + // object convert methods static inline jobject jsValueToJavaObject(v8::Isolate* isolate, v8::Local jsValue) { bool isNew; diff --git a/android/runtime/v8/src/native/V8Util.cpp b/android/runtime/v8/src/native/V8Util.cpp index 8d73e7bbc4d..113b0943e53 100644 --- a/android/runtime/v8/src/native/V8Util.cpp +++ b/android/runtime/v8/src/native/V8Util.cpp @@ -11,7 +11,6 @@ #include "V8Util.h" #include "JNIUtil.h" -#include "JSException.h" #include "AndroidUtil.h" #include "TypeConverter.h" diff --git a/android/templates/build/AssetCryptImpl.java b/android/templates/build/AssetCryptImpl.java index 8e8cf58d6e0..418fc491e8f 100644 --- a/android/templates/build/AssetCryptImpl.java +++ b/android/templates/build/AssetCryptImpl.java @@ -5,17 +5,18 @@ import java.util.HashMap; import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.nio.CharBuffer; -import java.nio.Buffer; import java.nio.ByteBuffer; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.lang.reflect.Method; import java.lang.System; import java.util.Collection; + import org.appcelerator.kroll.util.KrollAssetHelper; import org.appcelerator.kroll.common.Log; import org.appcelerator.titanium.TiApplication; + import android.os.Debug; +import android.util.Base64; public class AssetCryptImpl implements KrollAssetHelper.AssetCrypt { @@ -49,7 +50,7 @@ public String readAsset(String path) if (bytes == null) { return null; } - return new String(bytes); + return new String(bytes, StandardCharsets.UTF_8); } @Override diff --git a/android/titanium/lib/aps-analytics.jar b/android/titanium/lib/aps-analytics.jar index cfc4f92b790..837389e6017 100644 Binary files a/android/titanium/lib/aps-analytics.jar and b/android/titanium/lib/aps-analytics.jar differ diff --git a/android/titanium/src/java/org/appcelerator/kroll/KrollProxy.java b/android/titanium/src/java/org/appcelerator/kroll/KrollProxy.java index 7c28f586c18..ba99ae5f1d4 100644 --- a/android/titanium/src/java/org/appcelerator/kroll/KrollProxy.java +++ b/android/titanium/src/java/org/appcelerator/kroll/KrollProxy.java @@ -6,16 +6,13 @@ */ package org.appcelerator.kroll; -import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; -import org.appcelerator.kroll.KrollDict; import org.appcelerator.kroll.annotations.Kroll; import org.appcelerator.kroll.common.AsyncResult; import org.appcelerator.kroll.common.Log; @@ -35,6 +32,7 @@ import android.os.Looper; import android.os.Message; import android.os.Bundle; +import android.text.TextUtils; import android.util.Pair; import org.json.JSONObject; @@ -978,6 +976,57 @@ public boolean doFireEvent(String event, Object data) return krollObject.fireEvent(source, event, krollData, bubbles, reportSuccess, code, message); } + /** + * Our view proxy supports three properties to match iOS regarding + * the text that is read aloud (or otherwise communicated) by the + * assistive technology: accessibilityLabel, accessibilityHint + * and accessibilityValue. + * + * We combine these to create the single Android property contentDescription. + * (e.g., View.setContentDescription(...)); + */ + public String composeContentDescription() + { + if (properties == null) { + return null; + } + + final String punctuationPattern = "^.*\\p{Punct}\\s*$"; + StringBuilder buffer = new StringBuilder(); + String label = TiConvert.toString(properties.get(TiC.PROPERTY_ACCESSIBILITY_LABEL)); + String hint = TiConvert.toString(properties.get(TiC.PROPERTY_ACCESSIBILITY_HINT)); + String value = TiConvert.toString(properties.get(TiC.PROPERTY_ACCESSIBILITY_VALUE)); + + if (!TextUtils.isEmpty(label)) { + buffer.append(label); + if (!label.matches(punctuationPattern)) { + buffer.append("."); + } + } + + if (!TextUtils.isEmpty(value)) { + if (buffer.length() > 0) { + buffer.append(" "); + } + buffer.append(value); + if (!value.matches(punctuationPattern)) { + buffer.append("."); + } + } + + if (!TextUtils.isEmpty(hint)) { + if (buffer.length() > 0) { + buffer.append(" "); + } + buffer.append(hint); + if (!hint.matches(punctuationPattern)) { + buffer.append("."); + } + } + + return buffer.toString(); + } + public void firePropertyChanged(String name, Object oldValue, Object newValue) { if (modelListener != null) { diff --git a/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java b/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java index e25807825f5..a6a5a81ea65 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java +++ b/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java @@ -951,7 +951,7 @@ public void onBackPressed() } // Handle app exit ourselves since the above window proxy did not handle the back event. - boolean exitOnClose = true; + boolean exitOnClose = (TiActivityWindows.getWindowCount() <= 1); if (this.window != null) { exitOnClose = TiConvert.toBoolean(this.window.getProperty(TiC.PROPERTY_EXIT_ON_CLOSE), exitOnClose); } @@ -1689,19 +1689,33 @@ protected void fireOnDestroy() private boolean shouldFinishRootActivity() { + // Do not finish root activity if disabled globally. (Typically done when restarting LiveView.) if (TiBaseActivity.canFinishRoot == false) { return false; } + // This method only applies to "Ti.UI.Window" based activities. + // If this is the root activity, then let it do its default finish handling. if (this instanceof TiRootActivity) { return false; } - boolean exitOnClose = (TiActivityWindows.getWindowCount() <= 1); + // Determine if this activity's "Ti.UI.Window" reference is still in the global collection. + // - Will not be in the collection if its close() method was called. + // - Will be in collection when pressing Back button or finish() was called natively. + boolean isTiWindowOpen = false; + if (this.launchIntent != null) { + int windowId = + this.launchIntent.getIntExtra(TiC.INTENT_PROPERTY_WINDOW_ID, TiActivityWindows.INVALID_WINDOW_ID); + if (windowId != TiActivityWindows.INVALID_WINDOW_ID) { + isTiWindowOpen = TiActivityWindows.hasWindow(windowId); + } + } + + // If this is the last "Ti.UI.Window" activity, then exit by default unless "exitOnClose" property was set. + boolean exitOnClose = (TiActivityWindows.getWindowCount() <= (isTiWindowOpen ? 1 : 0)); if ((this.window != null) && this.window.hasProperty(TiC.PROPERTY_EXIT_ON_CLOSE)) { exitOnClose = TiConvert.toBoolean(this.window.getProperty(TiC.PROPERTY_EXIT_ON_CLOSE), exitOnClose); - } else if (this.launchIntent != null) { - exitOnClose = this.launchIntent.getBooleanExtra(TiC.INTENT_PROPERTY_FINISH_ROOT, exitOnClose); } return exitOnClose; } diff --git a/android/titanium/src/java/org/appcelerator/titanium/TiRootActivity.java b/android/titanium/src/java/org/appcelerator/titanium/TiRootActivity.java index d4d82f8aa58..f98cfb7a1f4 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/TiRootActivity.java +++ b/android/titanium/src/java/org/appcelerator/titanium/TiRootActivity.java @@ -207,6 +207,16 @@ public void run() } catch (Exception ex) { Log.e(TAG, "Failed to close existing Titanium root activity.", ex); } + } else if (!canResumeActivityUsing(newIntent) || !canResumeActivityUsing(rootActivity.getLaunchIntent())) { + // It's impossible to resume existing root activity. So, we have to destroy it and restart. + // Note: This typically happens with ACTION_SEND and ACTION_*_DOCUMENT intents. + rootActivity.finishAffinity(); + TiApplication.terminateActivityStack(); + if ((newIntent != null) && !newIntent.filterEquals(mainIntent)) { + mainIntent.putExtra(TiC.EXTRA_TI_NEW_INTENT, newIntent); + } + mainIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(mainIntent); } else { // Simulate "singleTask" handling by updating existing root activity's intent with received one. if (newIntent == null) { @@ -215,9 +225,13 @@ public void run() rootActivity.onNewIntent(newIntent); // Resume the pre-existing Titanium root activity. - // Note: On Android, you resume a backgrounded activity by using its initial launch intent. + // Note: You can resume a backgrounded activity by using its initial launch intent, + // but we need to remove flags such as CLEAR_TOP to preserve its child activities. Intent resumeIntent = rootActivity.getLaunchIntent(); - if (resumeIntent == null) { + if (resumeIntent != null) { + resumeIntent = new Intent(resumeIntent); + resumeIntent.setFlags(mainIntent.getFlags()); + } else { resumeIntent = mainIntent; } startActivity(resumeIntent); @@ -543,4 +557,26 @@ public static boolean isScriptRunning() { return TiRootActivity.isScriptRunning; } + + /** + * Determine if an existing activity can be resumed with the given intent via startActivity() method. + *

+ * For example, an intent flag such as "FLAG_ACTIVITY_MULTIPLE_TASK" always creates a new activity instance + * even if an existing activity with a matching intent already exists. This makes resumes impossible. + * @param intent The intent to be checked. Can be null. + * @return + * Returns true if an activity can be resumed based on given intent's flags. + *

+ * Returns false if impossible to resume or if given a null argument. + */ + private static boolean canResumeActivityUsing(Intent intent) + { + if (intent != null) { + final int BAD_FLAGS = (Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT); + if ((intent.getFlags() & BAD_FLAGS) == 0) { + return true; + } + } + return false; + } } diff --git a/android/titanium/src/java/org/appcelerator/titanium/proxy/TiViewProxy.java b/android/titanium/src/java/org/appcelerator/titanium/proxy/TiViewProxy.java index 31b205c44bb..2fa164ab1c7 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/proxy/TiViewProxy.java +++ b/android/titanium/src/java/org/appcelerator/titanium/proxy/TiViewProxy.java @@ -40,6 +40,7 @@ import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Handler; import android.os.Message; @@ -1029,24 +1030,33 @@ public TiViewProxy getParent() public String getBackgroundColor() // clang - format on { - // Return property if available. - if (getProperties().containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_COLOR)) { - return getProperties().getString(TiC.PROPERTY_BACKGROUND_COLOR); + // Return originally assigned property value, if available. + if (hasPropertyAndNotNull(TiC.PROPERTY_BACKGROUND_COLOR)) { + return TiConvert.toString(getProperty(TiC.PROPERTY_BACKGROUND_COLOR)); } + + // Fetch the view. + TiUIView view = getOrCreateView(); + if (view == null) { + return null; + } + // Try to get the background drawable if one is available. - TiBackgroundDrawable backgroundDrawable = getOrCreateView().getBackground(); // If only backgroundColor is defined then no ColorStateList is created, we resort to only the color defined. - if (backgroundDrawable == null) { - View view = getOrCreateView().getNativeView(); - if (view != null) { - if (view.getBackground() instanceof ColorDrawable) { - return TiUIHelper.hexStringFrom(((ColorDrawable) view.getBackground()).getColor()); + TiBackgroundDrawable tiBackgroundDrawable = view.getBackground(); + if (tiBackgroundDrawable == null) { + View nativeView = view.getNativeView(); + if (nativeView != null) { + Drawable drawable = nativeView.getBackground(); + if (drawable instanceof ColorDrawable) { + return TiUIHelper.hexStringFrom(((ColorDrawable) drawable).getColor()); } } return null; } + // It shouldn't matter if we request the color for DEFAULT_STATE_1 or DEFAULT_STATE_2. They are the same. - return TiUIHelper.getBackgroundColorForState(backgroundDrawable, TiUIHelper.BACKGROUND_DEFAULT_STATE_1); + return TiUIHelper.getBackgroundColorForState(tiBackgroundDrawable, TiUIHelper.BACKGROUND_DEFAULT_STATE_1); } // clang-format off @@ -1055,14 +1065,17 @@ public String getBackgroundColor() public String getBackgroundSelectedColor() // clang - format on { - // Return property if available. - if (getProperties().containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_SELECTED_COLOR)) { - return getProperties().getString(TiC.PROPERTY_BACKGROUND_SELECTED_COLOR); + // Return originally assigned property value, if available. + if (hasPropertyAndNotNull(TiC.PROPERTY_BACKGROUND_SELECTED_COLOR)) { + return TiConvert.toString(getProperty(TiC.PROPERTY_BACKGROUND_SELECTED_COLOR)); } + + // Fetch the view. TiUIView view = getOrCreateView(); if (view == null) { return null; } + // Try to get the background drawable if one is available. TiBackgroundDrawable backgroundDrawable = view.getBackground(); if (backgroundDrawable == null) { @@ -1077,12 +1090,19 @@ public String getBackgroundSelectedColor() public String getBackgroundFocusedColor() // clang - format on { - // Return property if available. - if (getProperties().containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_FOCUSED_COLOR)) { - return getProperties().getString(TiC.PROPERTY_BACKGROUND_FOCUSED_COLOR); + // Return originally assigned property value, if available. + if (hasPropertyAndNotNull(TiC.PROPERTY_BACKGROUND_FOCUSED_COLOR)) { + return TiConvert.toString(getProperty(TiC.PROPERTY_BACKGROUND_FOCUSED_COLOR)); } + + // Fetch the view. + TiUIView view = getOrCreateView(); + if (view == null) { + return null; + } + // Try to get the background drawable if one is available. - TiBackgroundDrawable backgroundDrawable = getOrCreateView().getBackground(); + TiBackgroundDrawable backgroundDrawable = view.getBackground(); if (backgroundDrawable == null) { return null; } @@ -1095,12 +1115,19 @@ public String getBackgroundFocusedColor() public String getBackgroundDisabledColor() // clang - format on { - // Return property if available. - if (getProperties().containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_DISABLED_COLOR)) { - return getProperties().getString(TiC.PROPERTY_BACKGROUND_DISABLED_COLOR); + // Return originally assigned property value, if available. + if (hasPropertyAndNotNull(TiC.PROPERTY_BACKGROUND_DISABLED_COLOR)) { + return TiConvert.toString(getProperty(TiC.PROPERTY_BACKGROUND_DISABLED_COLOR)); + } + + // Fetch the view. + TiUIView view = getOrCreateView(); + if (view == null) { + return null; } + // Try to get the background drawable if one is available. - TiBackgroundDrawable backgroundDrawable = getOrCreateView().getBackground(); + TiBackgroundDrawable backgroundDrawable = view.getBackground(); if (backgroundDrawable == null) { return null; } diff --git a/android/titanium/src/java/org/appcelerator/titanium/proxy/TiWindowProxy.java b/android/titanium/src/java/org/appcelerator/titanium/proxy/TiWindowProxy.java index 3ec3de49cbb..445b8fa8364 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/proxy/TiWindowProxy.java +++ b/android/titanium/src/java/org/appcelerator/titanium/proxy/TiWindowProxy.java @@ -556,16 +556,15 @@ protected void fillIntent(Activity activity, Intent intent) intent.putExtra(TiC.PROPERTY_EXTEND_SAFE_AREA, value); } - boolean exitOnClose = false; if (hasProperty(TiC.PROPERTY_EXIT_ON_CLOSE)) { - exitOnClose = TiConvert.toBoolean(getProperty(TiC.PROPERTY_EXIT_ON_CLOSE), exitOnClose); - } else { - // If launching child activity from Titanium root activity, then have it exit out of the app. + // Use proxy's assigned "exitOnClose" property setting. + boolean exitOnClose = TiConvert.toBoolean(getProperty(TiC.PROPERTY_EXIT_ON_CLOSE), false); + intent.putExtra(TiC.INTENT_PROPERTY_FINISH_ROOT, exitOnClose); + } else if (activity.isTaskRoot() || (activity == TiApplication.getInstance().getRootActivity())) { + // We're opening child activity from Titanium root activity. Have it exit out of app by default. // Note: If launched via startActivityForResult(), then root activity won't be the task's root. - exitOnClose = activity.isTaskRoot() || (activity == TiApplication.getInstance().getRootActivity()); - setProperty(TiC.PROPERTY_EXIT_ON_CLOSE, exitOnClose); + intent.putExtra(TiC.INTENT_PROPERTY_FINISH_ROOT, true); } - intent.putExtra(TiC.INTENT_PROPERTY_FINISH_ROOT, exitOnClose); // Set the theme property if (hasProperty(TiC.PROPERTY_THEME)) { diff --git a/android/titanium/src/java/org/appcelerator/titanium/view/TiUIView.java b/android/titanium/src/java/org/appcelerator/titanium/view/TiUIView.java index 6df0829f365..d4dcf8f0f41 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/view/TiUIView.java +++ b/android/titanium/src/java/org/appcelerator/titanium/view/TiUIView.java @@ -31,7 +31,6 @@ import org.appcelerator.titanium.util.TiConvert; import org.appcelerator.titanium.util.TiUIHelper; import org.appcelerator.titanium.view.TiCompositeLayout.LayoutParams; -import org.appcelerator.titanium.view.TiGradientDrawable.GradientType; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -50,7 +49,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.view.ViewCompat; -import android.text.TextUtils; import android.util.Pair; import android.util.SparseArray; import android.util.TypedValue; @@ -882,13 +880,15 @@ public void propertyChanged(String key, Object oldValue, Object newValue, KrollP // it. // This will ensure the border wrapper view is added correctly. TiUIView parentView = parent.getOrCreateView(); - int removedChildIndex = parentView.findChildIndex(this); - parentView.remove(this); - initializeBorder(d, bgColor); - if (removedChildIndex == -1) { - parentView.add(this); - } else { - parentView.add(this, removedChildIndex); + if (parentView != null) { + int removedChildIndex = parentView.findChildIndex(this); + parentView.remove(this); + initializeBorder(d, bgColor); + if (removedChildIndex == -1) { + parentView.add(this); + } else { + parentView.add(this, removedChildIndex); + } } } else if (key.startsWith(TiC.PROPERTY_BORDER_PREFIX)) { handleBorderProperty(key, newValue); @@ -2149,68 +2149,12 @@ private void applyContentDescription() if (proxy == null || nativeView == null) { return; } - String contentDescription = composeContentDescription(); + String contentDescription = getProxy().composeContentDescription(); if (contentDescription != null) { nativeView.setContentDescription(contentDescription); } } - /** - * Our view proxy supports three properties to match iOS regarding - * the text that is read aloud (or otherwise communicated) by the - * assistive technology: accessibilityLabel, accessibilityHint - * and accessibilityValue. - * - * We combine these to create the single Android property contentDescription. - * (e.g., View.setContentDescription(...)); - */ - protected String composeContentDescription() - { - if (proxy == null) { - return null; - } - - KrollDict properties = proxy.getProperties(); - if (properties == null) { - return null; - } - - final String punctuationPattern = "^.*\\p{Punct}\\s*$"; - StringBuilder buffer = new StringBuilder(); - String label = TiConvert.toString(properties.get(TiC.PROPERTY_ACCESSIBILITY_LABEL)); - String hint = TiConvert.toString(properties.get(TiC.PROPERTY_ACCESSIBILITY_HINT)); - String value = TiConvert.toString(properties.get(TiC.PROPERTY_ACCESSIBILITY_VALUE)); - - if (!TextUtils.isEmpty(label)) { - buffer.append(label); - if (!label.matches(punctuationPattern)) { - buffer.append("."); - } - } - - if (!TextUtils.isEmpty(value)) { - if (buffer.length() > 0) { - buffer.append(" "); - } - buffer.append(value); - if (!value.matches(punctuationPattern)) { - buffer.append("."); - } - } - - if (!TextUtils.isEmpty(hint)) { - if (buffer.length() > 0) { - buffer.append(" "); - } - buffer.append(hint); - if (!hint.matches(punctuationPattern)) { - buffer.append("."); - } - } - - return buffer.toString(); - } - private void applyAccessibilityProperties() { if (nativeView != null) { diff --git a/apidoc/Titanium/Database/DB.yml b/apidoc/Titanium/Database/DB.yml index 6c234e77f54..4d28f021b7f 100644 --- a/apidoc/Titanium/Database/DB.yml +++ b/apidoc/Titanium/Database/DB.yml @@ -62,7 +62,7 @@ methods: - name: executeAsync summary: | - Asynchronously executes an SQL statement against the database and fires a callback with a `ResultSet`. + Asynchronously executes an SQL statement against the database and fires a callback with a possible `Error` argument, and a second argument holding a possible `ResultSet`. platforms: [android] parameters: - name: query @@ -78,12 +78,13 @@ methods: - name: callback summary: Callback when query execution has completed. - type: Callback - since: { android: "8.1.0" } + type: Callback + since: { android: "8.1.0", iphone: "8.1.0", ipad: "8.1.0" } - name: executeAll summary: | - Synchronously executes an array of SQL statements against the database and fires a callback with an array of `ResultSet`. + Synchronously executes an array of SQL statements against the database and returns an array of `ResultSet`. + On failure, this will throw an [Error](BatchQueryError) that reports the failed index and partial results returns: type: Array platforms: [android] @@ -91,11 +92,12 @@ methods: - name: queries summary: Array of SQL queries to execute. type: Array - since: { android: "8.1.0" } + since: { android: "8.1.0", iphone: "8.1.0", ipad: "8.1.0" } - name: executeAllAsync summary: | - Asynchronously executes an array of SQL statements against the database and fires a callback with an array of `ResultSet`. + Asynchronously executes an array of SQL statements against the database and fires a callback with a possible Error, and an array of `ResultSet`. + On failure, this will call the callback with an [Error](PossibleBatchQueryError) that reports the failed index, and a second arguemnt with the partial results platforms: [android] parameters: - name: queries @@ -104,8 +106,8 @@ methods: - name: callback summary: Callback when query execution has completed. - type: Callback> - since: { android: "8.1.0" } + type: Callback> + since: { android: "8.1.0", iphone: "8.1.0", ipad: "8.1.0" } - name: remove summary: | @@ -139,3 +141,34 @@ properties: summary: The number of rows affected by the last query. type: Number permission: read-only + +--- +name: BatchQueryError +summary: | + Simple `Error` instance thrown from the + [executeAll](Titanium.Database.DB.executeAll) method in case of failure +since: { android: "8.1.0", iphone: "8.1.0", ipad: "8.1.0" } +properties: + + - name: index + summary: Index of the failed query + type: Number + since: { android: "8.1.0", iphone: "8.1.0", ipad: "8.1.0" } + + - name: results + summary: partial `ResultSet`s of any successful queries before the failure + type: Array + since: { android: "8.1.0", iphone: "8.1.0", ipad: "8.1.0" } + +--- +name: PossibleBatchQueryError +summary: | + Simple `Error` argument provided to the callback from the + [executeAllAsync](Titanium.Database.DB.executeAllAsync) method in case of failure +since: { android: "8.1.0", iphone: "8.1.0", ipad: "8.1.0" } +properties: + + - name: index + summary: Index of the failed query + type: Number + since: { android: "8.1.0", iphone: "8.1.0", ipad: "8.1.0" } diff --git a/apidoc/lib/html_generator.js b/apidoc/lib/html_generator.js index a01b758b2e0..acd8efa4ad2 100644 --- a/apidoc/lib/html_generator.js +++ b/apidoc/lib/html_generator.js @@ -416,7 +416,11 @@ function exportType(api) { t = 'Array<' + convertAPIToLink(t) + '>'; } } else if (t.indexOf('Callback<') === 0) { - t = 'Callback<' + convertAPIToLink(t.substring(t.indexOf('<') + 1, t.lastIndexOf('>'))) + '>'; + // Parse out the multiple types of args! + const subTypes = t.substring(t.indexOf('<') + 1, t.lastIndexOf('>')); + // split by ', ' then convert to link for each and join by ', ' + const linkified = subTypes.split(',').map(t => convertAPIToLink(t.trim())).join(', '); + t = `Callback<${linkified}>`; } else if (t.indexOf('Dictionary<') === 0) { t = 'Dictionary<' + convertAPIToLink(t.substring(t.indexOf('<') + 1, t.lastIndexOf('>'))) + '>'; } else { diff --git a/apidoc/lib/typescript_generator.js b/apidoc/lib/typescript_generator.js index e56c27d0e6c..04d6bf56466 100644 --- a/apidoc/lib/typescript_generator.js +++ b/apidoc/lib/typescript_generator.js @@ -475,12 +475,12 @@ class GlobalTemplateWriter { const normalizedTypes = docType.map(typeName => this.normalizeType(typeName)); return normalizedTypes.indexOf('any') !== -1 ? 'any' : normalizedTypes.join(' | '); } - - if (docType.indexOf('<') !== -1) { - const typeRe = /(\w+)<([\w.,]+)>/; - const matches = docType.match(typeRe); - const baseType = matches[1]; - const subTypes = matches[2].split(',').map(type => this.normalizeType(type)); + const lessThanIndex = docType.indexOf('<'); + if (lessThanIndex !== -1) { + const baseType = docType.slice(0, lessThanIndex); + const greaterThanIndex = docType.lastIndexOf('>'); + const subType = docType.slice(lessThanIndex + 1, greaterThanIndex); + const subTypes = subType.split(',').map(type => this.normalizeType(type.trim())); if (baseType === 'Array') { return subTypes.map(typeName => { if (usageHint === 'parameter') { diff --git a/apidoc/validate.js b/apidoc/validate.js index e418c7f5582..fb785b114ae 100644 --- a/apidoc/validate.js +++ b/apidoc/validate.js @@ -264,28 +264,48 @@ function validateConstants(constants) { * @returns {string[]} array of error strings */ function validateDataType(type) { - var errors = []; if (Array.isArray(type)) { - type.forEach(function (elem) { - errors = errors.concat(validateDataType(elem)); + const errors = []; + type.forEach(elem => { + errors.push(...validateDataType(elem)); }); - } else if ((~type.indexOf('<') && ~type.indexOf('>')) - && (type.indexOf('Array') === 0 || type.indexOf('Callback') === 0 || type.indexOf('Dictionary') === 0)) { + return errors; + } + + const lessThanIndex = type.indexOf('<'); + const greaterThanIndex = type.lastIndexOf('>'); + if (lessThanIndex !== -1 && greaterThanIndex !== -1) { if (type === 'Callback') { - return errors; + return []; } // Compound data type - errors = errors.concat(validateDataType(type.slice(type.indexOf('<') + 1, type.lastIndexOf('>')))); - } else if (!validateClass(type) || ~common.DATA_TYPES.indexOf(type)) { - return errors; - } else if (standaloneFlag) { + const baseType = type.slice(0, lessThanIndex); + const subType = type.slice(lessThanIndex + 1, greaterThanIndex); + if (baseType === 'Callback' || baseType === 'Function') { + const errors = []; + subType.split(',').forEach(sub => { + errors.push(...validateDataType(sub.trim())); + }); + return errors; + } + if (baseType !== 'Array' && baseType !== 'Dictionary') { + return [ `Base type for complex types must be one of Array, Callback, Dictionary, Function, but received ${baseType}` ]; + } + return validateDataType(subType); + } + + // This is awkward and backwards, but if the class is valid OR it's a common type, there's no error, so return empty array + if (!validateClass(type) || ~common.DATA_TYPES.indexOf(type)) { + return []; + } + + if (standaloneFlag) { // For standalone mode, log warning but not an error // Data type can exist in a parent class not in the data set console.warn('WARNING! Could not validate data type: %s'.yellow, type); - } else { - errors.push(type); + return []; } - return errors; + return [ type ]; } /** diff --git a/cli/commands/build.js b/cli/commands/build.js index 6c4ae33a277..53e03beca93 100644 --- a/cli/commands/build.js +++ b/cli/commands/build.js @@ -15,6 +15,7 @@ const appc = require('node-appc'), sprintf = require('sprintf'), ti = require('node-titanium-sdk'), tiappxml = require('node-titanium-sdk/lib/tiappxml'), + semver = require('semver'), __ = appc.i18n(__dirname).__; fields.setup({ @@ -244,6 +245,11 @@ exports.config = function config(logger, config, cli) { }; exports.validate = function validate(logger, config, cli) { + + if (!semver.satisfies(process.versions.node, '>= 10.13')) { + logger.warn('DEPRECATION NOTICE: Titanium SDK 9 will no longer support Node.js 8 or lower. We intend to support Node.js 10/12 LTS, which will be 10.13 or higher.\n'); + } + // Determine if the project is an app or a module, run appropriate build command if (cli.argv.type === 'module') { diff --git a/cli/lib/tasks/process-js-task.js b/cli/lib/tasks/process-js-task.js index dc7b01785a5..5f8eabf6021 100644 --- a/cli/lib/tasks/process-js-task.js +++ b/cli/lib/tasks/process-js-task.js @@ -101,6 +101,9 @@ class ProcessJsTask extends IncrementalFileTask { * we also depend on some config values from the builder this is used to * fallback to a full task run if required. * + * This will also dummy process the unchanged JS files again to properly + * fire expected hooks and populate the builder with required data. + * * @return {Promise} */ async loadResultAndSkip() { @@ -110,7 +113,11 @@ class ProcessJsTask extends IncrementalFileTask { return this.doFullTaskRun(); } - Object.keys(this.data.jsFiles).forEach(relPath => this.builder.unmarkBuildDirFile(this.data.jsFiles[relPath].dest)); + this.jsFiles = this.data.jsFiles; + this.jsBootstrapFiles.splice(0, 0, ...this.data.jsBootstrapFiles); + return Promise.all(Object.keys(this.jsFiles).map(relPath => { + return limit(() => this.processJsFile(this.jsFiles[relPath].src)); + })); } /** diff --git a/common/Resources/ti.internal/extensions/binding.js b/common/Resources/ti.internal/extensions/binding.js index 5276f298de6..bb06678f4c7 100644 --- a/common/Resources/ti.internal/extensions/binding.js +++ b/common/Resources/ti.internal/extensions/binding.js @@ -24,22 +24,26 @@ const redirects = new Map(); * @param {string} path original require path/id * @returns {boolean} */ -function isCoreModuleId(path) { - return !path.includes('.') && !path.includes('/'); +function isHijackableModuleId(path) { + if (!path || path.length < 1) { + return false; + } + const firstChar = path.charAt(0); + return firstChar !== '.' && firstChar !== '/'; } // Hack require to point to this as a core module "binding" const originalRequire = global.require; // This works for iOS as-is, and also intercepts the call on Android for ti.main.js (the first file executed) global.require = function (moduleId) { - if (isCoreModuleId(moduleId)) { - if (bindings.has(moduleId)) { - return bindings.get(moduleId); - } - if (redirects.has(moduleId)) { - moduleId = redirects.get(moduleId); - } + + if (bindings.has(moduleId)) { + return bindings.get(moduleId); } + if (redirects.has(moduleId)) { + moduleId = redirects.get(moduleId); + } + return originalRequire(moduleId); }; @@ -47,39 +51,65 @@ if (Ti.Platform.name === 'android') { // ... but we still need to hack it when requiring from other files for Android const originalModuleRequire = global.Module.prototype.require; global.Module.prototype.require = function (path, context) { - if (isCoreModuleId(path)) { - if (bindings.has(path)) { - return bindings.get(path); - } - if (redirects.has(path)) { - path = redirects.get(path); - } + + if (bindings.has(path)) { + return bindings.get(path); } + if (redirects.has(path)) { + path = redirects.get(path); + } + return originalModuleRequire.call(this, path, context); }; } /** - * Registers a binding from a short module id to the full under the hood filepath. - * This allows for lazy instantiation of the module on-demand. + * Registers a binding from a short module id to an already loaded/constructed object/value to export for that core module id * - * @param {string} coreModuleId the module id to "hijack" - * @param {string} internalPath the full filepath to require under the hood. - * This should be an already resolved absolute path, - * as otherwise the context of the call could change what gets loaded! + * @param {string} moduleId the module id to "hijack" + * @param {*} binding an already constructured value/object to return */ -export function redirectCoreModuleIdToPath(coreModuleId, internalPath) { - redirects.set(coreModuleId, internalPath); -} +export function register(moduleId, binding) { + if (!isHijackableModuleId(moduleId)) { + throw new Error(`Cannot register for relative/absolute file paths; no leading '.' or '/' allowed (was given ${moduleId})`); + } -// TODO: Allow two types of bindings: a "redirect" from a "core" module id to the actual underlying file (as we have here) -// OR binding an object already loaded to a "core" module id + if (redirects.has(moduleId)) { + Ti.API.warn(`Another binding has already registered for module id: '${moduleId}', it will be overwritten...`); + redirects.delete(moduleId); + } else if (bindings.has(moduleId)) { + Ti.API.warn(`Another binding has already registered for module id: '${moduleId}', it will be overwritten...`); + } + + bindings.set(moduleId, binding); +} /** - * Registers a binding from a short module id to already loaded/constructed object to export for that core module id. - * @param {string} coreModuleId the core module id to register under - * @param {object} object the object to hook to respond to require requests for the module id + * Registers a binding from a short module id to the full under the hood filepath if given a string. + * This allows for lazy instantiation of the module on-demand + * + * @param {string} moduleId the module id to "hijack" + * @param {string} filepath the full filepath to require under the hood. + * This should be an already resolved absolute path, + * as otherwise the context of the call could change what gets loaded! */ -export function bindObjectToCoreModuleId(coreModuleId, object) { - bindings.set(coreModuleId, object); +export function redirect(moduleId, filepath) { + if (!isHijackableModuleId(moduleId)) { + throw new Error(`Cannot register for relative/absolute file paths; no leading '.' or '/' allowed (was given ${moduleId})`); + } + + if (bindings.has(moduleId)) { + Ti.API.warn(`Another binding has already registered for module id: '${moduleId}', it will be overwritten...`); + bindings.delete(moduleId); + } else if (redirects.has(moduleId)) { + Ti.API.warn(`Another binding has already registered for module id: '${moduleId}', it will be overwritten...`); + } + + redirects.set(moduleId, filepath); } + +const binding = { + register, + redirect +}; +global.binding = binding; diff --git a/common/Resources/ti.internal/extensions/node/index.js b/common/Resources/ti.internal/extensions/node/index.js index 3b77b1cb80c..1a174acd942 100644 --- a/common/Resources/ti.internal/extensions/node/index.js +++ b/common/Resources/ti.internal/extensions/node/index.js @@ -8,10 +8,10 @@ import assert from './assert'; import events from './events'; // hook our implementations to get loaded by require -import { bindObjectToCoreModuleId } from '../binding'; -bindObjectToCoreModuleId('path', path); -bindObjectToCoreModuleId('os', os); -bindObjectToCoreModuleId('tty', tty); -bindObjectToCoreModuleId('util', util); -bindObjectToCoreModuleId('assert', assert); -bindObjectToCoreModuleId('events', events); +import { register } from '../binding'; +register('path', path); +register('os', os); +register('tty', tty); +register('util', util); +register('assert', assert); +register('events', events); diff --git a/iphone/Classes/CalendarModule.m b/iphone/Classes/CalendarModule.m index 1c23f37649d..49714e46738 100644 --- a/iphone/Classes/CalendarModule.m +++ b/iphone/Classes/CalendarModule.m @@ -225,7 +225,10 @@ - (void)requestAuthorization:(JSValue *)callback forEntityType:(EKEntityType)ent } else { propertiesDict = [TiUtils dictionaryWithCode:[error code] message:[TiUtils messageFromError:error]]; } - [callback callWithArguments:@[ propertiesDict ]]; + TiThreadPerformOnMainThread(^{ + [callback callWithArguments:@[ propertiesDict ]]; + }, + [NSThread isMainThread]); }]; }, NO); diff --git a/iphone/Classes/TiDatabaseProxy.h b/iphone/Classes/TiDatabaseProxy.h index dbb8bbd679e..e6a14c14236 100644 --- a/iphone/Classes/TiDatabaseProxy.h +++ b/iphone/Classes/TiDatabaseProxy.h @@ -24,6 +24,12 @@ READONLY_PROPERTY(NSUInteger, rowsAffected, RowsAffected); - (void)close; // This supports varargs, but we hack it in the impl to check currentArgs - (TiDatabaseResultSetProxy *)execute:(NSString *)sql; +- (void)executeAsync:(NSString *)sql; +- (NSArray *)executeAll:(NSArray *)queries; +JSExportAs(executeAllAsync, + -(void)executeAllAsync + : (NSArray *)queries withCallback + : (JSValue *)callback); - (void)remove; @end diff --git a/iphone/Classes/TiDatabaseProxy.m b/iphone/Classes/TiDatabaseProxy.m index 1413e97a9b8..7266e858cfb 100644 --- a/iphone/Classes/TiDatabaseProxy.m +++ b/iphone/Classes/TiDatabaseProxy.m @@ -164,9 +164,21 @@ - (void)install:(NSString *)path name:(NSString *)name_ - (void)removeStatement:(PLSqliteResultSet *)statement { - [statement close]; - if (statements != nil) { - [statements removeObject:statement]; + @synchronized(self) { + [statement close]; + if (statements != nil) { + [statements removeObject:statement]; + } + } +} + +- (void)addStatement:(PLSqliteResultSet *)statement +{ + @synchronized(self) { + if (statements == nil) { + statements = [[NSMutableArray alloc] initWithCapacity:5]; + } + [statements addObject:statement]; } } @@ -187,65 +199,214 @@ - (NSArray *)sqlParams:(NSArray *)array return result; } -- (TiDatabaseResultSetProxy *)execute:(NSString *)sql +- (PLSqlitePreparedStatement *)prepareStatement:(NSString *)sql withParams:(NSArray *)params withError:(NSError *__nullable *__nullable)error { - sql = [sql stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + // FIXME: Can we use NSError reference and avoid try/catch/throw? Obj-C doesn't like using exceptions + if (database == nil) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:123 + userInfo:@{ + NSLocalizedDescriptionKey : @"database has been closed" + }]; + return nil; + } + NSString *sqlCleaned = [sql stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - NSError *error = nil; - PLSqlitePreparedStatement *statement = (PLSqlitePreparedStatement *)[database prepareStatement:sql error:&error]; - if (error != nil) { - [self throwException:@"invalid SQL statement" subreason:[error description] location:CODELOCATION]; + PLSqlitePreparedStatement *statement; + // Just use a lock here? + @synchronized(self) { + statement = (PLSqlitePreparedStatement *)[database prepareStatement:sqlCleaned error:error]; + } + if (*error != nil) { + return nil; + } + if (params && [params count] > 0) { + [statement bindParameters:params]; } + + return statement; +} + +- (TiDatabaseResultSetProxy *)executeSQL:(NSString *)sql withParams:(NSArray *)params withError:(NSError *__nullable *__nullable)error +{ + @synchronized(self) { + PLSqlitePreparedStatement *statement = [self prepareStatement:sql withParams:params withError:error]; + if (*error != nil) { + return nil; + } + PLSqliteResultSet *result = (PLSqliteResultSet *)[statement executeQuery]; + + // Do we need to lock for the next/close calls? + if ([[result fieldNames] count] == 0) { + [result next]; // we need to do this to make sure lastInsertRowId and rowsAffected work + [result close]; + return nil; + } + + [self addStatement:result]; + + return [[[TiDatabaseResultSetProxy alloc] initWithResults:result database:self] autorelease]; + } +} + +- (TiDatabaseResultSetProxy *)execute:(NSString *)sql +{ + NSArray *params = @[]; // Check for varargs for perepared statement params NSArray *currentArgs = [JSContext currentArguments]; if ([currentArgs count] > 1) { JSValue *possibleParams = [currentArgs objectAtIndex:1]; - NSArray *params; if ([possibleParams isArray]) { params = [possibleParams toArray]; } else { params = [self sqlParams:[currentArgs subarrayWithRange:NSMakeRange(1, [currentArgs count] - 1)]]; } - [statement bindParameters:params]; } - PLSqliteResultSet *result = (PLSqliteResultSet *)[statement executeQuery]; - - if ([[result fieldNames] count] == 0) { - [result next]; // we need to do this to make sure lastInsertRowId and rowsAffected work - [result close]; + NSError *error = nil; + TiDatabaseResultSetProxy *result = [self executeSQL:sql withParams:params withError:&error]; + if (error != nil) { + [self throwException:@"failed to execute SQL statement" subreason:[error description] location:CODELOCATION]; return nil; } + return result; +} + +- (void)executeAsync:(NSString *)sql +{ + NSArray *currentArgs = [JSContext currentArguments]; + if ([currentArgs count] < 2) { + [self throwException:@"callback function must be supplied" subreason:@"" location:CODELOCATION]; + return; + } + JSValue *callback = [currentArgs objectAtIndex:[currentArgs count] - 1]; - if (statements == nil) { - statements = [[NSMutableArray alloc] initWithCapacity:5]; + NSArray *params = @[]; + if ([currentArgs count] > 2) { + JSValue *possibleParams = [currentArgs objectAtIndex:1]; + if ([possibleParams isArray]) { + params = [possibleParams toArray]; + } else { + params = [self sqlParams:[currentArgs subarrayWithRange:NSMakeRange(1, [currentArgs count] - 2)]]; + } } - [statements addObject:result]; + // FIXME: Use a queue per-database! Also, use queue in the sync variants! + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error = nil; + TiDatabaseResultSetProxy *proxy = [self executeSQL:sql withParams:params withError:&error]; + if (error != nil) { + dispatch_async(dispatch_get_main_queue(), ^{ + JSValue *jsError = [JSValue valueWithNewErrorFromMessage:[NSString stringWithFormat:@"failed to execute SQL statement: %@", [error description]] inContext:[callback context]]; + [callback callWithArguments:@[ jsError ]]; + }); + return; + } - TiDatabaseResultSetProxy *proxy = [[[TiDatabaseResultSetProxy alloc] initWithResults:result database:self] autorelease]; + dispatch_async(dispatch_get_main_queue(), ^{ + JSContext *context = [callback context]; + [callback callWithArguments:@[ [JSValue valueWithUndefinedInContext:context], proxy == nil ? [JSValue valueWithNullInContext:context] : proxy ]]; + }); + }); +} - return proxy; +- (NSArray *)executeAll:(NSArray *)queries withContext:(JSContext *)context withError:(NSError *__nullable *__nullable)error +{ + // Do we need to copy the array or something to retain the args? + NSMutableArray *results = [NSMutableArray arrayWithCapacity:[queries count]]; + NSUInteger index = 0; + for (NSString *sql in queries) { + TiDatabaseResultSetProxy *result = [self executeSQL:sql withParams:nil withError:error]; + if (*error != nil) { + return results; // return immediately when we fail, we can report the partial results + } + if (result == nil) { + [results addObject:[JSValue valueWithNullInContext:context]]; + } else { + [results addObject:result]; + } + index++; + } + return results; } -- (void)close +- (NSArray *)executeAll:(NSArray *)queries { - if (statements != nil) { - for (PLSqliteResultSet *result in statements) { - [result close]; + NSError *error = nil; + JSContext *context = [JSContext currentContext]; + NSMutableArray *results = [NSMutableArray arrayWithCapacity:[queries count]]; + NSUInteger index = 0; + for (NSString *sql in queries) { + TiDatabaseResultSetProxy *result = [self executeSQL:sql withParams:nil withError:&error]; + if (error != nil) { + JSValue *jsError = [self createError:@"failed to execute SQL statements" subreason:[error description] location:CODELOCATION inContext:context]; + jsError[@"results"] = result; + jsError[@"index"] = [NSNumber numberWithUnsignedInteger:index]; + [context setException:jsError]; + return nil; } - RELEASE_TO_NIL(statements); + if (result == nil) { + [results addObject:[JSValue valueWithNullInContext:context]]; + } else { + [results addObject:result]; + } + index++; } - if (database != nil) { - if ([database goodConnection]) { - @try { - [database close]; + return results; +} + +- (void)executeAllAsync:(NSArray *)queries withCallback:(JSValue *)callback +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + JSContext *context = [callback context]; + NSError *error = nil; + NSMutableArray *results = [NSMutableArray arrayWithCapacity:[queries count]]; + NSUInteger index = 0; + for (NSString *sql in queries) { + TiDatabaseResultSetProxy *result = [self executeSQL:sql withParams:nil withError:&error]; + if (error != nil) { + JSValue *jsError = [self createError:@"failed to execute SQL statements" subreason:[error description] location:CODELOCATION inContext:context]; + jsError[@"results"] = result; + jsError[@"index"] = [NSNumber numberWithUnsignedInteger:index]; + dispatch_async(dispatch_get_main_queue(), ^{ + [callback callWithArguments:@[ jsError, results ]]; + }); + return; } - @catch (NSException *e) { - NSLog(@"[WARN] attempting to close database, returned error: %@", e); + if (result == nil) { + [results addObject:[JSValue valueWithNullInContext:context]]; + } else { + [results addObject:result]; + } + index++; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [callback callWithArguments:@[ [JSValue valueWithUndefinedInContext:context], results ]]; + }); + }); +} + +- (void)close +{ + @synchronized(self) { + if (statements != nil) { + for (PLSqliteResultSet *result in statements) { + [result close]; + } + RELEASE_TO_NIL(statements); + } + if (database != nil) { + if ([database goodConnection]) { + @try { + [database close]; + } + @catch (NSException *e) { + NSLog(@"[WARN] attempting to close database, returned error: %@", e); + } } + RELEASE_TO_NIL(database); } - RELEASE_TO_NIL(database); } } diff --git a/iphone/Classes/TiUIWebView.m b/iphone/Classes/TiUIWebView.m index 6d43828f73c..456c460ccaf 100644 --- a/iphone/Classes/TiUIWebView.m +++ b/iphone/Classes/TiUIWebView.m @@ -72,7 +72,6 @@ - (WKWebView *)webView WKUserContentController *controller = [[[WKUserContentController alloc] init] autorelease]; [controller addUserScript:[self userScriptTitaniumInjectionForAppEvent]]; - [controller addScriptMessageHandler:self name:@"_Ti_"]; [config setUserContentController:controller]; @@ -184,7 +183,10 @@ - (void)setUrl_:(id)value NSURL *url = [TiUtils toURL:value proxy:self.proxy]; + [_webView.configuration.userContentController removeScriptMessageHandlerForName:@"_Ti_"]; + if ([[self class] isLocalURL:url]) { + [_webView.configuration.userContentController addScriptMessageHandler:self name:@"_Ti_"]; [self loadLocalURL:url]; } else { [self loadRequestWithURL:[NSURL URLWithString:[TiUtils stringValue:value]]]; @@ -220,6 +222,9 @@ - (void)setData_:(id)value NSLog(@"[ERROR] Ti.UI.WebView.data can only be a TiBlob or TiFile object, was %@", [(TiProxy *)value apiName]); } + [_webView.configuration.userContentController removeScriptMessageHandlerForName:@"_Ti_"]; + [_webView.configuration.userContentController addScriptMessageHandler:self name:@"_Ti_"]; + [[self webView] loadData:data MIMEType:[self mimeTypeForData:data] characterEncodingName:@"UTF-8" @@ -259,6 +264,9 @@ - (void)setHtml_:(id)args [[self webView] stopLoading]; } + [_webView.configuration.userContentController removeScriptMessageHandlerForName:@"_Ti_"]; + [_webView.configuration.userContentController addScriptMessageHandler:self name:@"_Ti_"]; + // No options, default load behavior if (options == nil) { [[self webView] loadHTMLString:content baseURL:[NSURL fileURLWithPath:[TiHost resourcePath]]]; diff --git a/iphone/TitaniumKit/TitaniumKit/Libraries/APSAnalytics/APSAnalytics.h b/iphone/TitaniumKit/TitaniumKit/Libraries/APSAnalytics/APSAnalytics.h index 713cd750d9c..a451b822d97 100644 --- a/iphone/TitaniumKit/TitaniumKit/Libraries/APSAnalytics/APSAnalytics.h +++ b/iphone/TitaniumKit/TitaniumKit/Libraries/APSAnalytics/APSAnalytics.h @@ -8,10 +8,10 @@ @import CoreLocation; /** Constant indicating development deployment */ -extern NSString * const APSDeployTypeDevelopment; +extern NSString *const APSDeployTypeDevelopment; /** Constant indicating production deployment */ -extern NSString * const APSDeployTypeProduction; +extern NSString *const APSDeployTypeProduction; /** * The APSAnalytics class configures the application to use the APS analytic services @@ -25,54 +25,54 @@ extern NSString * const APSDeployTypeProduction; /** * Return the singleton instance to the real-time analytics service. */ -+ (instancetype) sharedInstance; ++ (instancetype)sharedInstance; /** * Retrieves the current session identifier. * * @return {NSString*} session identifier. */ --(NSString *)getCurrentSessionId; +- (NSString *)getCurrentSessionId; /** * Retrieves the last event sent. * * @return {NSDictionary *} the last event stored, otherwise null if none have been stored. */ --(NSDictionary *)getLastEvent; +- (NSDictionary *)getLastEvent; /** * Retrieves the derived machine identifier. * * @return {NSString *} machine identifier. */ --(NSString*)getMachineId; +- (NSString *)getMachineId; /** * Obtains machine identifier. */ --(void)setMachineId; +- (void)setMachineId; /** * Checks whether the user has opted out from sending analytics data. * * @return {BOOL} with the decision */ --(BOOL)isOptedOut; +- (BOOL)isOptedOut; /** * Writes the optedOut property in the SharedPreferences instance. * * @param {BOOL} value with the decision to opt out. */ --(void)setOptedOut:(BOOL)optedOut; +- (void)setOptedOut:(BOOL)optedOut; /** * Sends an application enroll event to indicate first launch. * * @deprecated use sendAppInstallEvent() instead. */ --(void)sendAppEnrollEvent; +- (void)sendAppEnrollEvent; /** * Sends an application enroll event to indicate first launch. @@ -80,7 +80,7 @@ extern NSString * const APSDeployTypeProduction; * If this is called multiple times, all executions after the first will * be ignored and do nothing. */ --(void)sendAppInstallEvent; +- (void)sendAppInstallEvent; /** * Sends an application foreground event to indicate a new session. @@ -92,12 +92,12 @@ extern NSString * const APSDeployTypeProduction; * * In both of these cases, the same session identifier will be kept. */ --(void)sendSessionStartEvent; +- (void)sendSessionStartEvent; /** * Sends an application background event to indicate an ended session. */ --(void)sendSessionEndEvent; +- (void)sendSessionEndEvent; /** * Sends an application navigation event which describes moving between views. @@ -106,24 +106,23 @@ extern NSString * const APSDeployTypeProduction; * @param toView the name of the view the user navigated to. * @param data arbitrary data to be sent alongside the nav event. */ --(void)sendAppNavEvent:(NSString * _Nonnull)fromView - toView:(NSString * _Nonnull)toView - event:(NSString * _Nullable)event - data:(NSDictionary * _Nullable)data; +- (void)sendAppNavEvent:(NSString *_Nonnull)fromView + toView:(NSString *_Nonnull)toView + event:(NSString *_Nullable)event + data:(NSDictionary *_Nullable)data; - --(void)sendAppNavEventFromView:(NSString * _Nonnull)fromView - toView:(NSString * _Nonnull)toView - withName:(NSString * _Nullable)event - payload:(NSDictionary * _Nullable)data; +- (void)sendAppNavEventFromView:(NSString *_Nonnull)fromView + toView:(NSString *_Nonnull)toView + withName:(NSString *_Nullable)event + payload:(NSDictionary *_Nullable)data; /** * Sends an application feature event to allow sending custom data. * * @deprecated use sendCustomEvent(String, JSONObject) instead. */ --(void)sendAppFeatureEvent:(NSString * _Nonnull)event - payload:(NSDictionary * _Nullable)data; +- (void)sendAppFeatureEvent:(NSString *_Nonnull)event + payload:(NSDictionary *_Nullable)data; /** * Sends an application feature event to allow sending custom data. @@ -131,22 +130,27 @@ extern NSString * const APSDeployTypeProduction; * @param name the name of the event being sent. * @param data the data to send alongside the event. */ --(void)sendCustomEvent:(NSString * _Nonnull)name - data:(NSDictionary * _Nullable)data; +- (void)sendCustomEvent:(NSString *_Nonnull)name + data:(NSDictionary *_Nullable)data; /** * Sends a crash report as a custom event. * * @deprecated use sendCrashReport(JSONObject) instead. */ --(void)sendAppCrashEvent:(NSDictionary * _Nonnull)data; +- (void)sendAppCrashEvent:(NSDictionary *_Nonnull)data; /** * Sends a crash report as a custom event. * * @param crash the crash data to be included with the payload. */ --(void)sendCrashReport:(NSDictionary * _Nonnull)crash; +- (void)sendCrashReport:(NSDictionary *_Nonnull)crash; + +/** + * Flush event queue. + */ +- (void)flush; /** * Set sdk version to send in analytics. @@ -155,133 +159,133 @@ extern NSString * const APSDeployTypeProduction; * * @deprecated use setSdkVersion() instead. */ --(void)setSDKVersion:(NSString *)version; +- (void)setSDKVersion:(NSString *)version; /** * @deprecated NOT USED, only defined for backwards compatibility. */ --(void)setBuildType:(NSString *) type; +- (void)setBuildType:(NSString *)type; /** * Enables Analytics with a given app-key and deploy-type. * @param appKey The APSAnalytics app-key. * @param deployTime The deploy-type of the application. */ --(void)enableWithAppKey:(NSString *)appKey andDeployType:(NSString *)deployType; +- (void)enableWithAppKey:(NSString *)appKey andDeployType:(NSString *)deployType; /** * Get analytics endpoint url */ --(NSString *)getAnalyticsUrl; +- (NSString *)getAnalyticsUrl; /** * Get device architecture */ --(NSString *)getArchitecture; +- (NSString *)getArchitecture; /** * Get application id */ --(NSString *)getAppId; +- (NSString *)getAppId; /** * Get application name */ --(NSString *)getAppName; +- (NSString *)getAppName; /** * Get application version */ --(NSString *)getAppVersion; +- (NSString *)getAppVersion; /** * Get application deployment type (production, development) */ --(NSString *)getDeployType; +- (NSString *)getDeployType; /** * Get analytics flush interval */ --(NSInteger)getFlushInterval; +- (NSInteger)getFlushInterval; /** * Get analytics flush requeue interval */ --(NSInteger)getFlushRequeue; +- (NSInteger)getFlushRequeue; /** * Get device model */ --(NSString *)getModel; +- (NSString *)getModel; /** * Get network type */ --(NSString *)getNetworkType; +- (NSString *)getNetworkType; /** * Get OS type (32bit, 64bit) */ --(NSString *)getOsType; +- (NSString *)getOsType; /** * Get OS version */ --(NSString *)getOsVersion; +- (NSString *)getOsVersion; /** * Get current platform */ --(NSString *)getPlatform; +- (NSString *)getPlatform; /** * Get device processor count */ --(NSInteger)getProcessorCount; +- (NSInteger)getProcessorCount; /** * Get SDK version */ --(NSString *)getSdkVersion; +- (NSString *)getSdkVersion; /** * Set analytics endpoint url */ --(void)setAnalyticsUrl:(NSString *)url; +- (void)setAnalyticsUrl:(NSString *)url; /** * Set application id */ --(void)setAppId:(NSString *)appId; +- (void)setAppId:(NSString *)appId; /** * Set application name */ --(void)setAppName:(NSString *)appName; +- (void)setAppName:(NSString *)appName; /** * Set application version */ --(void)setAppVersion:(NSString *)appVersion; +- (void)setAppVersion:(NSString *)appVersion; /** * Set application deployment type */ --(void)setDeployType:(NSString *)deployType; +- (void)setDeployType:(NSString *)deployType; /** * Set SDK version */ --(void)setSdkVersion:(NSString *)sdkVersion; +- (void)setSdkVersion:(NSString *)sdkVersion; /** * Set analytics flush interval */ --(void)setFlushInterval:(NSInteger)timeout; +- (void)setFlushInterval:(NSInteger)timeout; /** * Set analytics flush requeue interval */ --(void)setFlushRequeue:(NSInteger)timeout; +- (void)setFlushRequeue:(NSInteger)timeout; @end diff --git a/iphone/TitaniumKit/TitaniumKit/Libraries/APSAnalytics/libAPSAnalytics.a b/iphone/TitaniumKit/TitaniumKit/Libraries/APSAnalytics/libAPSAnalytics.a index a2f0cf2bb02..dbb385a3ce7 100644 Binary files a/iphone/TitaniumKit/TitaniumKit/Libraries/APSAnalytics/libAPSAnalytics.a and b/iphone/TitaniumKit/TitaniumKit/Libraries/APSAnalytics/libAPSAnalytics.a differ diff --git a/iphone/TitaniumKit/TitaniumKit/Sources/API/ObjcProxy.h b/iphone/TitaniumKit/TitaniumKit/Sources/API/ObjcProxy.h index 680e6c45c6e..85da2acea87 100644 --- a/iphone/TitaniumKit/TitaniumKit/Sources/API/ObjcProxy.h +++ b/iphone/TitaniumKit/TitaniumKit/Sources/API/ObjcProxy.h @@ -148,6 +148,9 @@ JSExportAs(fireEvent, + (void)throwException:(NSString *)reason subreason:(NSString *)subreason location:(NSString *)location; - (void)throwException:(NSString *)reason subreason:(NSString *)subreason location:(NSString *)location; ++ (JSValue *)createError:(NSString *)reason subreason:(NSString *)subreason location:(NSString *)location inContext:(JSContext *)context; +- (JSValue *)createError:(NSString *)reason subreason:(NSString *)subreason location:(NSString *)location inContext:(JSContext *)context; + // FIXME: Should id be TiProxy* here? - (id)JSValueToNative:(JSValue *)jsValue; - (JSValue *)NativeToJSValue:(id)proxy; diff --git a/iphone/TitaniumKit/TitaniumKit/Sources/API/ObjcProxy.m b/iphone/TitaniumKit/TitaniumKit/Sources/API/ObjcProxy.m index 6c638bb4792..e2ec73cddff 100644 --- a/iphone/TitaniumKit/TitaniumKit/Sources/API/ObjcProxy.m +++ b/iphone/TitaniumKit/TitaniumKit/Sources/API/ObjcProxy.m @@ -13,10 +13,9 @@ @implementation ObjcProxy @synthesize bubbleParent; -- (void)throwException:(NSString *)reason subreason:(NSString *)subreason location:(NSString *)location ++ (JSValue *)createError:(NSString *)reason subreason:(NSString *)subreason location:(NSString *)location inContext:(JSContext *)context { - NSString *exceptionName = [@"org.appcelerator." stringByAppendingString:NSStringFromClass([self class])]; - JSContext *context = [JSContext currentContext]; + NSString *exceptionName = @"org.appcelerator"; NSDictionary *details = @{ kTiExceptionSubreason : subreason, kTiExceptionLocation : location @@ -24,13 +23,12 @@ - (void)throwException:(NSString *)reason subreason:(NSString *)subreason locati NSException *exc = [NSException exceptionWithName:exceptionName reason:reason userInfo:details]; JSGlobalContextRef jsContext = [context JSGlobalContextRef]; JSValueRef jsValueRef = TiBindingTiValueFromNSObject(jsContext, exc); - [context setException:[JSValue valueWithJSValueRef:jsValueRef inContext:context]]; + return [JSValue valueWithJSValueRef:jsValueRef inContext:context]; } -+ (void)throwException:(NSString *)reason subreason:(NSString *)subreason location:(NSString *)location +- (JSValue *)createError:(NSString *)reason subreason:(NSString *)subreason location:(NSString *)location inContext:(JSContext *)context { - NSString *exceptionName = @"org.appcelerator"; - JSContext *context = [JSContext currentContext]; + NSString *exceptionName = [@"org.appcelerator." stringByAppendingString:NSStringFromClass([self class])]; NSDictionary *details = @{ kTiExceptionSubreason : subreason, kTiExceptionLocation : location @@ -38,7 +36,21 @@ + (void)throwException:(NSString *)reason subreason:(NSString *)subreason locati NSException *exc = [NSException exceptionWithName:exceptionName reason:reason userInfo:details]; JSGlobalContextRef jsContext = [context JSGlobalContextRef]; JSValueRef jsValueRef = TiBindingTiValueFromNSObject(jsContext, exc); - [context setException:[JSValue valueWithJSValueRef:jsValueRef inContext:context]]; + return [JSValue valueWithJSValueRef:jsValueRef inContext:context]; +} + +- (void)throwException:(NSString *)reason subreason:(NSString *)subreason location:(NSString *)location +{ + JSContext *context = [JSContext currentContext]; + JSValue *error = [self createError:reason subreason:subreason location:location inContext:context]; + [context setException:error]; +} + ++ (void)throwException:(NSString *)reason subreason:(NSString *)subreason location:(NSString *)location +{ + JSContext *context = [JSContext currentContext]; + JSValue *error = [ObjcProxy createError:reason subreason:subreason location:location inContext:context]; + [context setException:error]; } // Conversion methods for interacting with "old" KrollObject style proxies diff --git a/iphone/TitaniumKit/TitaniumKit/Sources/API/TiApp.m b/iphone/TitaniumKit/TitaniumKit/Sources/API/TiApp.m index ac27770f506..b15eb1b4950 100644 --- a/iphone/TitaniumKit/TitaniumKit/Sources/API/TiApp.m +++ b/iphone/TitaniumKit/TitaniumKit/Sources/API/TiApp.m @@ -990,7 +990,7 @@ - (void)applicationWillTerminate:(UIApplication *)application [kjsBridge shutdown:condition]; if ([[TiSharedConfig defaultConfig] logServerEnabled]) { - [[TiLogServer defaultLogServer] start]; + [[TiLogServer defaultLogServer] stop]; } //This will shut down the modules. diff --git a/iphone/TitaniumKit/TitaniumKit/Sources/API/TiConsole.h b/iphone/TitaniumKit/TitaniumKit/Sources/API/TiConsole.h index e3b6010900a..0efaeffcc05 100644 --- a/iphone/TitaniumKit/TitaniumKit/Sources/API/TiConsole.h +++ b/iphone/TitaniumKit/TitaniumKit/Sources/API/TiConsole.h @@ -14,6 +14,9 @@ JSExportAs(timeLog, -(void)timeLog : (id)args withData : (NSArray *)logData); +JSExportAs(log, + -(void)log + : (id)unused); @end // This is a version of the API module which has custom support for log() to diff --git a/iphone/TitaniumKit/TitaniumKit/Sources/API/TiConsole.m b/iphone/TitaniumKit/TitaniumKit/Sources/API/TiConsole.m index 38ab785fba1..b39694462e9 100644 --- a/iphone/TitaniumKit/TitaniumKit/Sources/API/TiConsole.m +++ b/iphone/TitaniumKit/TitaniumKit/Sources/API/TiConsole.m @@ -9,9 +9,10 @@ @implementation TiConsole -- (void)log:(id)level withMessage:(id)args +- (void)log:(id)unused { - [self info:level]; + NSArray *currentArgs = [JSContext currentArguments]; + [self logMessage:currentArgs severity:@"info"]; } - (void)time:(NSString *)label diff --git a/iphone/TitaniumKit/TitaniumKit/Sources/API/TiExceptionHandler.m b/iphone/TitaniumKit/TitaniumKit/Sources/API/TiExceptionHandler.m index 04761e913f2..63b3cb6ea59 100644 --- a/iphone/TitaniumKit/TitaniumKit/Sources/API/TiExceptionHandler.m +++ b/iphone/TitaniumKit/TitaniumKit/Sources/API/TiExceptionHandler.m @@ -6,11 +6,15 @@ */ #import "TiExceptionHandler.h" +#import "APSAnalytics.h" #import "TiApp.h" #import "TiBase.h" + #include +#include static void TiUncaughtExceptionHandler(NSException *exception); +static void TiSignalHandler(int signal); static NSUncaughtExceptionHandler *prevUncaughtExceptionHandler = NULL; @@ -26,6 +30,13 @@ + (TiExceptionHandler *)defaultExceptionHandler defaultExceptionHandler = [[self alloc] init]; prevUncaughtExceptionHandler = NSGetUncaughtExceptionHandler(); NSSetUncaughtExceptionHandler(&TiUncaughtExceptionHandler); + + signal(SIGABRT, TiSignalHandler); + signal(SIGILL, TiSignalHandler); + signal(SIGSEGV, TiSignalHandler); + signal(SIGFPE, TiSignalHandler); + signal(SIGBUS, TiSignalHandler); + signal(SIGPIPE, TiSignalHandler); }); return defaultExceptionHandler; } @@ -218,3 +229,11 @@ static void TiUncaughtExceptionHandler(NSException *exception) [NSThread exit]; } } + +static void TiSignalHandler(int code) +{ + NSException *exception = [NSException exceptionWithName:@"SIGNAL_ERROR" reason:[NSString stringWithFormat:@"signal error code: %d", code] userInfo:nil]; + [[TiExceptionHandler defaultExceptionHandler] reportException:exception]; + [[APSAnalytics sharedInstance] flush]; + signal(code, SIG_DFL); +} diff --git a/iphone/cli/commands/_build.js b/iphone/cli/commands/_build.js index 2033909c8a3..4391089b72e 100644 --- a/iphone/cli/commands/_build.js +++ b/iphone/cli/commands/_build.js @@ -1810,7 +1810,7 @@ iOSBuilder.prototype.validate = function validate(logger, config, cli) { if (cli.argv['source-maps']) { this.sourceMaps = true; // if they haven't, respect the tiapp.xml value if set one way or the other - } else if (cli.tiapp.hasOwnProperty['source-maps']) { // they've explicitly set a value in tiapp.xml + } else if (Object.prototype.hasOwnProperty.call(cli.tiapp, 'source-maps')) { // they've explicitly set a value in tiapp.xml this.sourceMaps = cli.tiapp['source-maps'] === true; // respect the tiapp.xml value } else { // otherwise turn on by default for non-production builds this.sourceMaps = this.deployType !== 'production'; @@ -1925,17 +1925,22 @@ iOSBuilder.prototype.validate = function validate(logger, config, cli) { function sortXcodeIds(a, b) { // prioritize selected xcode + if (xcodeInfo[a].selected) { + return -1; + } if (xcodeInfo[b].selected) { return 1; } + // newest to oldest return appc.version.gt(xcodeInfo[a].version, xcodeInfo[b].version) ? -1 : appc.version.lt(xcodeInfo[a].version, xcodeInfo[b].version) ? 1 : 0; } + const sortedXcodeIds = Object.keys(xcodeInfo).sort(sortXcodeIds); if (this.iosSdkVersion) { // find the Xcode for this version - Object.keys(this.iosInfo.xcode).sort(sortXcodeIds).some(function (ver) { - if (this.iosInfo.xcode[ver].sdks.indexOf(this.iosSdkVersion) !== -1) { - this.xcodeEnv = this.iosInfo.xcode[ver]; + sortedXcodeIds.some(function (ver) { + if (xcodeInfo[ver].sdks.includes(this.iosSdkVersion)) { + this.xcodeEnv = xcodeInfo[ver]; return true; } return false; @@ -1948,9 +1953,8 @@ iOSBuilder.prototype.validate = function validate(logger, config, cli) { } } else { // device, simulator, dist-appstore, dist-adhoc - Object.keys(xcodeInfo) - .filter(function (id) { return xcodeInfo[id].supported; }) - .sort(sortXcodeIds) + sortedXcodeIds + .filter(id => xcodeInfo[id].supported) .some(function (id) { return xcodeInfo[id].sdks.sort().reverse().some(function (ver) { if (appc.version.gte(ver, this.minIosVersion)) { @@ -2409,7 +2413,7 @@ iOSBuilder.prototype.initialize = function initialize() { this.buildAssetsDir = path.join(this.buildDir, 'assets'); this.buildManifestFile = path.join(this.buildDir, 'build-manifest.json'); - if ((this.tiapp.properties && this.tiapp.properties.hasOwnProperty('ios.whitelist.appcelerator.com') && this.tiapp.properties['ios.whitelist.appcelerator.com'].value === false) || !this.tiapp.analytics) { + if ((this.tiapp.properties && this.tiapp.properties['ios.whitelist.appcelerator.com'] && this.tiapp.properties['ios.whitelist.appcelerator.com'].value === false) || !this.tiapp.analytics) { // force appcelerator.com to not be whitelisted in the Info.plist ATS section this.whitelistAppceleratorDotCom = false; } @@ -2419,10 +2423,10 @@ iOSBuilder.prototype.initialize = function initialize() { this.defaultLaunchScreenStoryboard = false; } - if (!this.tiapp.ios.hasOwnProperty('use-new-build-system') && appc.version.lt(this.xcodeEnv.version, '10.0.0')) { + if (!Object.prototype.hasOwnProperty.call(this.tiapp.ios, 'use-new-build-system') && appc.version.lt(this.xcodeEnv.version, '10.0.0')) { // if running on Xcode < 10, do not use the new build system by default this.useNewBuildSystem = false; - } else if (this.tiapp.ios.hasOwnProperty('use-new-build-system')) { + } else if (Object.prototype.hasOwnProperty.call(this.tiapp.ios, 'use-new-build-system')) { // if explicitly set via tiapp.xml, go with that one this.useNewBuildSystem = this.tiapp.ios['use-new-build-system']; } else { @@ -2505,78 +2509,40 @@ iOSBuilder.prototype.determineLogServerPort = function determineLogServerPort(ne return next(); } - const _t = this; - - this.logger.debug(__('Checking if log server port %d is available', this.tiLogServerPort)); - - // for simulator builds, the port is shared with the local machine, so we - // just need to detect if the port is available with the help of Node - const server = net.createServer(); - - server.on('error', function () { - // we weren't able to bidn to the port :( - server.close(function () { - _t.logger.debug(__('Log server port %s is in use, testing if it\'s the app we\'re building', _t.tiLogServerPort)); - - let client = null; - - function die(error) { - client && client.destroy(); - if (error && error.code === 'ENOTFOUND') { - _t.logger.error(__('Unable to connect to log server on localhost')); - _t.logger.error(__('Please ensure your /etc/hosts file contains a valid entry for `localhost`')); - } else { - _t.logger.error(__('Another process is currently bound to port %d', _t.tiLogServerPort)); - _t.logger.error(__('Set a unique between 1024 and 65535 in the section of the tiapp.xml') + '\n'); - } - process.exit(1); - } + // The Plan + // + // We are going to try to create a Node.js server to see if the port is available. + // + // If the port is NOT available, then we're gonna randomly try to pick a port until we find an + // open one. - // connect to the port and see if it's a Titanium app... - // - if the port is bound by a Titanium app with the same appid, then assume - // that when we install new build, the old process will be terminated - // - if the port is bound by another process, such as MySQL on port 3306, - // then we will fail out - // - if the port is bound by another process that expects data before the - // response is returned, then we will just timeout and fail out - // - if localhost cannot be resolved then we will fail out and inform - // the user of that - client = net.connect({ - host: 'localhost', - port: _t.tiLogServerPort, - timeout: parseInt(_t.config.get('ios.logServerTestTimeout', 1000)) || null - }) - .on('data', function (data) { - client.destroy(); - try { - const headers = JSON.parse(data.toString().split('\n').shift()); - if (headers.appId !== _t.tiapp.id) { - _t.logger.error(__('Another Titanium app "%s" is currently running and using the log server port %d', headers.appId, _t.tiLogServerPort)); - _t.logger.error(__('Stop the running Titanium app, then rebuild this app')); - _t.logger.error(__('-or-')); - _t.logger.error(__('Set a unique between 1024 and 65535 in the section of the tiapp.xml') + '\n'); - process.exit(1); - } - } catch (e) { - die(e); - } - _t.logger.debug(__('The log server port is being used by the app being built, continuing')); - next(); - }) - .on('error', die) - .on('timeout', die); - }); - }); + let done = false; + async.whilst( + () => !done, + cb => { + // for simulator builds, the port is shared with the local machine, so we + // just need to detect if the port is available with the help of Node + const server = net.createServer(); + + server.on('error', () => { + server.close(() => { + this.logger.debug(__('Log server port %s is in use, trying another port', cyan(String(this.tiLogServerPort)))); + this.tiLogServerPort = parseInt(Math.random() * 50000) + 10000; + cb(); + }); + }); - server.listen({ - host: 'localhost', - port: _t.tiLogServerPort - }, function () { - server.close(function () { - _t.logger.debug(__('Log server port %s is available', _t.tiLogServerPort)); - next(); - }); - }); + server.listen({ + host: '127.0.0.1', + port: this.tiLogServerPort + }, () => { + this.logger.debug(__('Using log server port %s', cyan(String(this.tiLogServerPort)))); + done = true; + server.close(cb); + }); + }, + next + ); }; iOSBuilder.prototype.loginfo = function loginfo() { @@ -2643,6 +2609,9 @@ iOSBuilder.prototype.loginfo = function loginfo() { } else { this.logger.info(__('Set to copy files instead of symlinking')); } + + this.logger.info(__('Transpile javascript: %s', (this.transpile ? 'true' : 'false').cyan)); + this.logger.info(__('Generate source maps: %s', (this.sourceMaps ? 'true' : 'false').cyan)); }; iOSBuilder.prototype.readBuildManifest = function readBuildManifest() { @@ -3849,7 +3818,7 @@ iOSBuilder.prototype.mergePlist = function mergePlist(src, dest) { Object.keys(src).forEach(function (prop) { if (!/^\+/.test(prop)) { if (Object.prototype.toString.call(src[prop]) === '[object Object]') { - dest.hasOwnProperty(prop) || (dest[prop] = {}); + Object.prototype.hasOwnProperty.call(dest, prop) || (dest[prop] = {}); merge(src[prop], dest[prop]); } else { dest[prop] = src[prop]; @@ -3932,16 +3901,16 @@ iOSBuilder.prototype.writeEntitlementsPlist = function writeEntitlementsPlist(ne const pp = this.provisioningProfile; if (pp) { // attempt to customize it by reading provisioning profile - if (!plist.hasOwnProperty('application-identifier')) { + if (!Object.prototype.hasOwnProperty.call(plist, 'application-identifier')) { plist['application-identifier'] = pp.appPrefix + '.' + this.tiapp.id; } if (pp.apsEnvironment) { plist['aps-environment'] = this.target === 'dist-appstore' || this.target === 'dist-adhoc' ? 'production' : 'development'; } - if (this.target === 'dist-appstore' && !plist.hasOwnProperty('beta-reports-active')) { + if (this.target === 'dist-appstore' && !Object.prototype.hasOwnProperty.call(plist, 'beta-reports-active')) { plist['beta-reports-active'] = true; } - if (!plist.hasOwnProperty('get-task-allow')) { + if (!Object.prototype.hasOwnProperty.call(plist, 'get-task-allow')) { plist['get-task-allow'] = pp.getTaskAllow; } Array.isArray(plist['keychain-access-groups']) || (plist['keychain-access-groups'] = []); @@ -3976,7 +3945,7 @@ iOSBuilder.prototype.writeInfoPlist = function writeInfoPlist() { // load the default Info.plist plist.parse(fs.readFileSync(defaultInfoPlistFile).toString().replace(/(__.+__)/g, function (match, key) { - return consts.hasOwnProperty(key) ? consts[key] : ''; // if they key is not a match, just comment out the key + return Object.prototype.hasOwnProperty.call(consts, key) ? consts[key] : ''; // if they key is not a match, just comment out the key })); // override the default versions with the tiapp.xml version @@ -4146,9 +4115,9 @@ iOSBuilder.prototype.writeInfoPlist = function writeInfoPlist() { } // tiapp.xml settings override the default and custom Info.plist - plist.UIRequiresPersistentWiFi = this.tiapp.hasOwnProperty('persistent-wifi') ? !!this.tiapp['persistent-wifi'] : false; - plist.UIPrerenderedIcon = this.tiapp.hasOwnProperty('prerendered-icon') ? !!this.tiapp['prerendered-icon'] : false; - plist.UIStatusBarHidden = this.tiapp.hasOwnProperty('statusbar-hidden') ? !!this.tiapp['statusbar-hidden'] : false; + plist.UIRequiresPersistentWiFi = Object.prototype.hasOwnProperty.call(this.tiapp, 'persistent-wifi') ? !!this.tiapp['persistent-wifi'] : false; + plist.UIPrerenderedIcon = Object.prototype.hasOwnProperty.call(this.tiapp, 'prerendered-icon') ? !!this.tiapp['prerendered-icon'] : false; + plist.UIStatusBarHidden = Object.prototype.hasOwnProperty.call(this.tiapp, 'statusbar-hidden') ? !!this.tiapp['statusbar-hidden'] : false; plist.UIStatusBarStyle = 'UIStatusBarStyleDefault'; if (/opaque_black|opaque|black/.test(this.tiapp['statusbar-style'])) { @@ -4308,7 +4277,7 @@ iOSBuilder.prototype.writeMain = function writeMain() { __DEPLOYTYPE__: this.deployType, __SHOW_ERROR_CONTROLLER__: this.showErrorController, __APP_ID__: this.tiapp.id, - __APP_ANALYTICS__: String(this.tiapp.hasOwnProperty('analytics') ? !!this.tiapp.analytics : true), + __APP_ANALYTICS__: String(Object.prototype.hasOwnProperty.call(this.tiapp, 'analytics') ? !!this.tiapp.analytics : true), __APP_PUBLISHER__: this.tiapp.publisher, __APP_URL__: this.tiapp.url, __APP_NAME__: this.tiapp.name, @@ -4320,7 +4289,7 @@ iOSBuilder.prototype.writeMain = function writeMain() { __APP_DEPLOY_TYPE__: this.buildType }, contents = fs.readFileSync(path.join(this.platformPath, 'main.m')).toString().replace(/(__.+?__)/g, function (match, key) { - const s = consts.hasOwnProperty(key) ? consts[key] : key; + const s = Object.prototype.hasOwnProperty.call(consts, key) ? consts[key] : key; return typeof s === 'string' ? s.replace(/"/g, '\\"').replace(/\n/g, '\\n') : s; }), dest = path.join(this.buildDir, 'main.m'); @@ -5881,7 +5850,7 @@ iOSBuilder.prototype.copyResources = function copyResources(next) { .then(() => { this.tiSymbols = task.data.tiSymbols; - return next(); + return next(); // eslint-disable-line promise/no-callback-in-promise }) .catch(e => { this.logger.error(e); @@ -6408,7 +6377,7 @@ iOSBuilder.prototype.removeFiles = function removeFiles(next) { async.eachSeries([ 'x86_64', 'i386' ], function (architecture, next) { const args = [ '-remove', architecture, titaniumKitPath, '-o', titaniumKitPath ]; this.logger.debug(__('Running: %s', (this.xcodeEnv.executables.lipo + ' ' + args.join(' ')).cyan)); - appc.subprocess.run(this.xcodeEnv.executables.lipo, args, function (code, out) { + appc.subprocess.run(this.xcodeEnv.executables.lipo, args, function (_code, _out) { next(); }); }.bind(this), function () { @@ -6470,7 +6439,7 @@ iOSBuilder.prototype.optimizeFiles = function optimizeFiles(next) { } } }); - }(this.xcodeAppDir, /^(PlugIns|Watch|TitaniumKit\.framework)$/i)); + }(this.xcodeAppDir, /^(PlugIns|Watch|.+\.framework)$/i)); parallel(this, [ function (next) { diff --git a/iphone/cli/hooks/frameworks.js b/iphone/cli/hooks/frameworks.js index ac553185afb..235e2ce882a 100644 --- a/iphone/cli/hooks/frameworks.js +++ b/iphone/cli/hooks/frameworks.js @@ -576,7 +576,7 @@ class FrameworkIntegrator { isa: 'PBXBuildFile', fileRef: fileRefUuid, fileRef_comment: frameworkPackageName, - settings: { ATTRIBUTES: [ 'CodeSignOnCopy' ] } + settings: { ATTRIBUTES: [ 'CodeSignOnCopy', 'RemoveHeadersOnCopy' ] } }; this._xobjs.PBXBuildFile[embeddedBuildFileUuid + '_comment'] = frameworkPackageName + ' in Embed Frameworks'; diff --git a/package-lock.json b/package-lock.json index 2c0c416caf2..28de63ab5ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "titanium-mobile", - "version": "8.1.0", + "version": "8.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2506,9 +2506,9 @@ "integrity": "sha1-6Flo+iNfIXc9OIxhevCFvyEEQlo=" }, "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", + "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==" }, "ci-info": { "version": "2.0.0", @@ -6131,9 +6131,9 @@ "dev": true }, "ioslib": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/ioslib/-/ioslib-1.7.9.tgz", - "integrity": "sha512-Kvr/7aqeyInvsIt9dduW2venRPfE+lKrVsxrkH2bYpInPqv032nlwRidqAUzEFOjUZxivIoTAwyfhnozF/AIRA==", + "version": "1.7.10", + "resolved": "https://registry.npmjs.org/ioslib/-/ioslib-1.7.10.tgz", + "integrity": "sha512-D86U7w0n86glviTTMt/n39swxEM4aU4gQYz7SHsD1FekhAGQ34VV49q/F17TbUc7PtQI9a7xQ8ono00P4BD34g==", "requires": { "always-tail": "0.2.0", "async": "^2.6.1", @@ -6145,9 +6145,9 @@ }, "dependencies": { "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.1.tgz", + "integrity": "sha512-w1YQaVGNC6t2UCPjEawK/vo/dG8OOrVtUmhBT1uJJYxbl5kU2Tj3v6LGqBcsysN1yhuCStJCCA3GqdvKY8sqXQ==", "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -7908,9 +7908,9 @@ "dev": true }, "node-appc": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/node-appc/-/node-appc-0.3.2.tgz", - "integrity": "sha512-B5ZQksJJNX9o9pt/xb6usFCn8eBch7duCm+lO3f3gQMiqrB58Imb9ktT1YxBfXPNreoVp5Zr3MiAqtPPZyjmIg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/node-appc/-/node-appc-0.3.3.tgz", + "integrity": "sha512-UNAuJ/muZvW4V6gd3aSUqSp5VlLQFRpEdirGu1AEAZFPZiUsw02lkADxyUmT6QpNrwhzrOx/6CAS2itS1IEanA==", "requires": { "adm-zip": "^0.4.11", "async": "~2.6.1", diff --git a/package.json b/package.json index dc1876d0e5c..f6cf84f7252 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "titanium-mobile", "description": "Appcelerator Titanium Mobile", - "version": "8.1.0", + "version": "8.1.1", "moduleApiVersion": { "iphone": "2", "android": "4", @@ -74,13 +74,13 @@ "ejs": "^2.6.1", "fields": "0.1.24", "fs-extra": "^8.0.1", - "ioslib": "^1.7.9", + "ioslib": "^1.7.10", "klaw-sync": "^6.0.0", "liveview": "^1.5.0", "lodash.defaultsdeep": "^4.6.0", "markdown": "0.5.0", "moment": "^2.22.2", - "node-appc": "^0.3.2", + "node-appc": "^0.3.3", "node-titanium-sdk": "^3.2.0", "node-uuid": "1.4.8", "p-limit": "^2.2.0", diff --git a/support/android/titanium_prep.linux32 b/support/android/titanium_prep.linux32 index 42f239e48ed..77d60fe6612 100755 Binary files a/support/android/titanium_prep.linux32 and b/support/android/titanium_prep.linux32 differ diff --git a/support/android/titanium_prep.linux64 b/support/android/titanium_prep.linux64 index 7a185c80e8a..5cbfe0f1b17 100755 Binary files a/support/android/titanium_prep.linux64 and b/support/android/titanium_prep.linux64 differ diff --git a/support/android/titanium_prep.macos b/support/android/titanium_prep.macos index 7d5cc148f43..aed913c7d3c 100755 Binary files a/support/android/titanium_prep.macos and b/support/android/titanium_prep.macos differ diff --git a/support/android/titanium_prep.win32.exe b/support/android/titanium_prep.win32.exe index 9b31a919f96..ccced3a0a5b 100755 Binary files a/support/android/titanium_prep.win32.exe and b/support/android/titanium_prep.win32.exe differ diff --git a/support/android/titanium_prep.win64.exe b/support/android/titanium_prep.win64.exe index 07d8f92b5b0..2feb2af97be 100755 Binary files a/support/android/titanium_prep.win64.exe and b/support/android/titanium_prep.win64.exe differ diff --git a/support/module/packaged/modules.json b/support/module/packaged/modules.json index 60f40217e10..c106db08e11 100644 --- a/support/module/packaged/modules.json +++ b/support/module/packaged/modules.json @@ -71,8 +71,8 @@ }, "hyperloop": { "hyperloop": { - "url": "https://github.com/appcelerator-modules/hyperloop-builds/releases/download/v4.0.2/hyperloop-4.0.2.zip", - "integrity": "sha512-oKxHxWEyW1K+6/XgMoXB8Lt4+aZh4yl6L/+Q0F0lr1IlHkgfEUzzUVDsGfJOJ/KCDfP2WYXFKLz4VssJDJRHWg==" + "url": "https://github.com/appcelerator-modules/hyperloop-builds/releases/download/v4.0.3/hyperloop-4.0.3.zip", + "integrity": "sha512-IXraXPaHVfM4MvdGv5KJCfPJb6mXMNfaDiOL/rFWXt7sug6Ekaa8JJ96MtyNNxz6CGLZgGL46lqjJNRnJQj1mQ==" } } } diff --git a/tests/Resources/ti.database.test.js b/tests/Resources/ti.database.test.js new file mode 100644 index 00000000000..7554f5cd697 --- /dev/null +++ b/tests/Resources/ti.database.test.js @@ -0,0 +1,748 @@ +/* + * Appcelerator Titanium Mobile + * Copyright (c) 2011-Present by Appcelerator, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ +/* eslint-env mocha */ +/* eslint no-unused-expressions: "off" */ +'use strict'; +const should = require('./utilities/assertions'); + +describe('Titanium.Database', function () { + it('apiName', function () { + should(Ti.Database.apiName).be.eql('Ti.Database'); + should(Ti.Database).have.readOnlyProperty('apiName').which.is.a.String; + }); + + it('FIELD_TYPE_DOUBLE', function () { + should(Ti.Database).have.constant('FIELD_TYPE_DOUBLE').which.is.a.Number; + }); + + it('FIELD_TYPE_FLOAT', function () { + should(Ti.Database).have.constant('FIELD_TYPE_FLOAT').which.is.a.Number; + }); + + it('FIELD_TYPE_INT', function () { + should(Ti.Database).have.constant('FIELD_TYPE_INT').which.is.a.Number; + }); + + it('FIELD_TYPE_STRING', function () { + should(Ti.Database).have.constant('FIELD_TYPE_STRING').which.is.a.Number; + }); + + describe('.install()', () => { + it('is a function', () => { + should(Ti.Database.install).not.be.undefined; + should(Ti.Database.install).be.a.Function; + }); + + // FIXME Get working for iOS - gets back John Smith\\u0000' + // FIXME Get working on Android, either lastInsertRowId or rowsAffected is starting as 1, not 0 + it.androidAndIosBroken('copies db from app folder', function () { + // Database name + var dbName = 'testDbInstall'; + + // Copy database 'testDbResource.db' over from the application folder + // into the application data folder as 'testDbInstall' + var db = Ti.Database.install('testDbResource.db', dbName); + + // Confirm 'db' is an object + should(db).be.a.Object; + + // Confirm 'db.file' property is a valid object + should(db.file).be.a.Object; + + // Validate the 'db.lastInsertRowId' property + should(db.lastInsertRowId).be.a.Number; + should(db.lastInsertRowId).be.eql(0); + + // Confirm 'db.name' is a string + should(db.name).be.a.String; + should(db.name).be.eql(dbName); + + // Validate the 'db.rowsAffected' property + should(db.rowsAffected).be.a.Number; + should(db.rowsAffected).be.eql(0); + + // Define test data + var testName = 'John Smith'; + var testNumber = 123456789; + var testArray = [ 'Smith John', 987654321 ]; + + // Execute a query to return the rows of the database + var rows = db.execute('SELECT rowid, text, number FROM testTable'); + + // Validate the returned 'rows' object + should(rows).be.a.Object; + should(rows.rowCount).be.eql(2); + should(rows.fieldCount).be.eql(3); + // Validate field names + should(rows.fieldName(0)).be.eql('rowid'); + should(rows.fieldName(1)).be.eql('text'); + should(rows.fieldName(2)).be.eql('number'); + + // Loop through each row + var index = 1; + while (rows.isValidRow()) { + + // Validate the rowid field + var rowid = rows.fieldByName('rowid'); + should(rowid).be.a.Number; + should(rowid).be.eql(index); + + // Case insensitive search + rowid = rows.fieldByName('ROWID'); + should(rowid).be.a.Number; + should(rowid).be.eql(index); + + // Validate the text field + var text = rows.field(1); + should(text).be.a.String; + + // Validate the number field + var number = rows.fieldByName('number'); + should(number).be.a.Number; + + // Case insensitive search + number = rows.fieldByName('NUMBER'); + should(number).be.a.Number; + + // Validate the test data + if (index === 1) { + should(text).be.eql(testName); + should(number).be.eql(testNumber); + } else if (index === 2) { + should(number).be.eql(testArray[1]); + should(text).be.eql(testArray[0]); + } + + // Next row + rows.next(); + index++; + } + + // Close the 'rows' object + rows.close(); + + // test aliased field name + var aliased = db.execute('SELECT rowid, text AS something FROM testTable'); + + // Validate the returned 'rows' object + should(aliased).be.a.Object; + should(aliased.rowCount).be.eql(2); + should(aliased.fieldCount).be.eql(2); + // Validate field names + should(aliased.fieldName(0)).be.eql('rowid'); + should(aliased.fieldName(1)).be.eql('something'); + + // Close the 'rows' object + aliased.close(); + + // Remove the 'testDbInstall' database file + db.remove(); + + // Close the database (unnecessary as remove() does this for us) + db.close(); + }); + + // If the source db file was not found, then install() must throw an exception. + it('throws if missing source db', () => { + should.throws(() => { + Ti.Database.install('BadFilePath.db', 'IShouldNotExist.db'); + }, Error); + }); + }); + + describe('.open()', () => { + // Check if open exists and make sure it does not throw exception + // FIXME Get working on Android, either lastInsertRowId or rowsAffected is starting as 1, not 0 + it.androidBroken('opens or creates database', function () { + should(Ti.Database.open).not.be.undefined; + should(Ti.Database.open).be.a.Function; + + // Database name + var dbName = 'testDbOpen'; + + // Open database 'testDbOpen' if it exists in the + // application data folder, otherwise create a new one + var db = Ti.Database.open(dbName); + + // Confirm 'db' is an object + should(db).be.a.Object; + + // Confirm 'db.file' property is a valid object + should(db.file).be.a.Object; + + // Validate the 'db.lastInsertRowId' property + should(db.lastInsertRowId).be.a.Number; + should(db.lastInsertRowId).be.eql(0); + + // Confirm 'db.name' is a string + should(db.name).be.a.String; + should(db.name).be.eql(dbName); + + // Validate the 'db.rowsAffected' property + should(db.rowsAffected).be.a.Number; + should(db.rowsAffected).be.eql(0); + + // Execute a query to create a test table + db.execute('CREATE TABLE IF NOT EXISTS testTable (text TEXT, number INTEGER)'); + + // Delete any existing data if the table already existed + db.execute('DELETE FROM testTable'); + + // Define test data + var testName = 'John Smith'; + var testNumber = 123456789; + + // Insert test data into the table + db.execute('INSERT INTO testTable (text, number) VALUES (?, ?)', testName, testNumber); + + // Validate that only one row has been affected + should(db.rowsAffected).be.eql(1); + + // Define more test data + var testArray = [ 'Smith John', 987654321 ]; + + // Insert more test data into the table + db.execute('INSERT INTO testTable (text, number) VALUES (?, ?)', testArray); + + // Validate that only one row has been affected + should(db.rowsAffected).be.eql(1); + + // Execute a query to return the rows of the database + var rows = db.execute('SELECT rowid, text, number FROM testTable'); + + // Validate the returned 'rows' object + should(rows).be.a.Object; + should(rows.rowCount).be.eql(2); + should(rows.fieldCount).be.eql(3); + should(rows.validRow).be.true; + should(rows.isValidRow()).be.true; + + // Loop through each row + var index = 1; + while (rows.isValidRow()) { + + // Validate the rowid field + var rowid = rows.fieldByName('rowid'); + should(rowid).be.a.Number; + should(rowid).be.eql(index); + + // Validate the text field + var text = rows.field(1); + should(text).be.a.String; + + // Validate the number field + var number = rows.fieldByName('number'); + should(number).be.a.Number; + + // Validate the test data + if (index === 1) { + should(text).be.eql(testName); + should(number).be.eql(testNumber); + } else if (index === 2) { + should(number).be.eql(testArray[1]); + should(text).be.eql(testArray[0]); + } + + // Next row + rows.next(); + index++; + } + + // Close the 'rows' object + rows.close(); + + // Remove the 'testDbInstall' database file + db.remove(); + + // Close the database (unnecessary as remove() does this for us) + db.close(); + }); + }); + + // Check if it guards against 'closed' results + // FIXME Get working on Android, seems to retain rowCount after Result.close() + it.androidBroken('guards multiple calls to ResultSet#close()', function () { + // Database name + var dbName = 'testDbOpen'; + + // Open database 'testDbOpen' if it exists in the + // application data folder, otherwise create a new one + var db = Ti.Database.open(dbName); + + // Execute a query to create a test table + db.execute('CREATE TABLE IF NOT EXISTS testTable (text TEXT, number INTEGER)'); + + // Delete any existing data if the table already existed + db.execute('DELETE FROM testTable'); + + // Define test data + var testName = 'John Smith'; + var testNumber = 123456789; + + // Insert test data into the table + db.execute('INSERT INTO testTable (text, number) VALUES (?, ?)', testName, testNumber); + + // Validate that only one row has been affected + should(db.rowsAffected).be.eql(1); + + // Define more test data + var testArray = [ 'Smith John', 987654321 ]; + + // Insert more test data into the table + db.execute('INSERT INTO testTable (text, number) VALUES (?, ?)', testArray); + + // Validate that only one row has been affected + should(db.rowsAffected).be.eql(1); + + // Execute a query to return the rows of the database + var rows = db.execute('SELECT rowid, text, number FROM testTable'); + + // Validate the returned 'rows' object + should(rows).be.a.Object; + should(rows.rowCount).be.eql(2); + should(rows.fieldCount).be.eql(3); + should(rows.validRow).be.true; + + // Close the 'rows' object + rows.close(); + + // Make sure row is not 'valid' + should(rows.rowCount).be.eql(0); // Android still reports 2 + should(rows.fieldCount).be.eql(0); + should(rows.validRow).be.false; + + // Validate the rowid field + var rowid = rows.fieldByName('rowid'); + should(rowid).not.exist; // null or undefined + + // Validate the closed field + var field1 = rows.field(1); + should(field1).not.exist; // null or undefined + + var field2 = rows.fieldByName('number'); + should(field2).not.exist; // null or undefined + + // Make sure next doesn't cause crash and return false + should(rows.next()).be.false; + + // Make sure closing again doesn't cause crash + rows.close(); + + // Remove the 'testDbInstall' database file + db.remove(); + + // Close the database (unnecessary as remove() does this for us) + db.close(); + }); + + describe('#execute()', () => { + it('is a function', () => { + const db = Ti.Database.open('execute.db'); + try { + should(db.execute).be.a.Function; + } finally { + db.close(); + } + }); + + // Test behavior expected by alloy code for createCollection. See TIMOB-20222 + // skip this test, this behaviour is undocumented. Our current code will return null + // only if the result contains no fields/columns instead of the result containing no rows + it.skip('returns null instead of empty result set for pragma command', function () { // eslint-disable-line mocha/no-skipped-tests + // Call install on a database file that doesn't exist. We should just make a new db with name 'category' + const db = Ti.Database.install('made.up.sqlite', 'category'); + + try { + // Confirm 'db' is an object + should(db).be.a.Object; + const rows = db.execute('pragma table_info(\'category\');'); + should(rows).be.null; + } finally { + // Remove the 'category' database file + db.remove(); + + // Close the database (unnecessary as remove() does this for us) + db.close(); + } + }); + + it('throws Error for invalid SQL statement', () => { // eslint-disable-line mocha/no-identical-title + const db = Ti.Database.open('execute.db'); + try { + should.throws(() => { + db.execute('THIS IS INVALID SQL'); + }, Error); + } catch (e) { + db.close(); + throw e; + } + }); + }); + + // Integer boundary tests. + // Verify we can read/write largest/smallest 64-bit int values supported by JS number type. + it('db read/write integer boundaries', function () { + const MAX_SIGNED_INT32 = 2147483647; + const MIN_SIGNED_INT32 = -2147483648; + const MAX_SIGNED_INT16 = 32767; + const MIN_SIGNED_INT16 = -32768; + const rows = [ + Number.MAX_SAFE_INTEGER, + MAX_SIGNED_INT32 + 1, + MAX_SIGNED_INT32, + MAX_SIGNED_INT16 + 1, + MAX_SIGNED_INT16, + 1, + 0, + -1, + MIN_SIGNED_INT16, + MIN_SIGNED_INT16 - 1, + MIN_SIGNED_INT32, + MIN_SIGNED_INT32 - 1, + Number.MIN_SAFE_INTEGER + ]; + + const dbConnection = Ti.Database.open('int_test.db'); + dbConnection.execute('CREATE TABLE IF NOT EXISTS intTable(id INTEGER PRIMARY KEY, intValue INTEGER);'); + dbConnection.execute('DELETE FROM intTable;'); + for (let index = 0; index < rows.length; index++) { + dbConnection.execute('INSERT INTO intTable (id, intValue) VALUES (?, ?);', index, rows[index]); + } + const resultSet = dbConnection.execute('SELECT id, intValue FROM intTable ORDER BY id'); + should(resultSet.rowCount).eql(rows.length); + for (let index = 0; resultSet.isValidRow(); resultSet.next(), index++) { + should(resultSet.field(1)).eql(rows[index]); + } + dbConnection.close(); + }); + + describe.windowsMissing('#executeAsync()', () => { + it('is a function', () => { + const db = Ti.Database.open('execute_async.db'); + try { + should(db.executeAsync).be.a.Function; + } finally { + db.close(); + } + }); + + it('executes asynchronously', function (finish) { + this.timeout(5000); + const db = Ti.Database.open('execute_async.db'); + // Execute a query to create a test table + db.executeAsync('CREATE TABLE IF NOT EXISTS testTable (text TEXT, number INTEGER)', err => { + should(err).not.exist; + // Delete any existing data if the table already existed + db.executeAsync('DELETE FROM testTable', err => { + should(err).not.exist; + + // Define test data + const testName = 'John Smith'; + const testNumber = 123456789; + + // Insert test data into the table + db.executeAsync('INSERT INTO testTable (text, number) VALUES (?, ?)', testName, testNumber, err => { + should(err).not.exist; + + // Validate that only one row has been affected + should(db.rowsAffected).be.eql(1); + + // Execute a query to return the rows of the database + db.executeAsync('SELECT rowid, text, number FROM testTable', (err, rows) => { + try { + should(err).not.exist; + // Validate the returned 'rows' object + should(rows).be.a.Object; + should(rows.rowCount).be.eql(1); + should(rows.fieldCount).be.eql(3); + should(rows.validRow).be.true; + + finish(); + } catch (e) { + finish(e); + } finally { + // Close the 'rows' object + rows.close(); + db.close(); + } + }); + }); + }); + }); + }); + + it('calls callback with Error for invalid SQL', function (finish) { + const db = Ti.Database.open('execute_async.db'); + db.executeAsync('THIS IS SOME INVALID SQL', err => { + try { + should(err).exist; + finish(); + } catch (e) { + finish(e); + } finally { + db.close(); + } + }); + }); + }); + + describe.windowsMissing('#executeAll()', () => { + it('is a function', () => { // eslint-disable-line mocha/no-identical-title + const db = Ti.Database.open('execute_all.db'); + try { + should(db.executeAll).be.a.Function; + } finally { + db.close(); + } + }); + + it('executes synchronously', function (finish) { + this.timeout(5000); + const db = Ti.Database.open('execute_all.db'); + + // FIXME: There's no way to send in binding paramaters, you have to bake them into the query string with this API + const queries = [ + // Execute a query to create a test table + 'CREATE TABLE IF NOT EXISTS testTable (text TEXT, number INTEGER)', + // Delete any existing data if the table already existed + 'DELETE FROM testTable', + // Insert test data into the table + 'INSERT INTO testTable (text, number) VALUES (\'John Smith\', 123456789)', + // Execute a query to return the rows of the database + 'SELECT rowid, text, number FROM testTable', + ]; + + let rows; + try { + const results = db.executeAll(queries); + // the returned results array should be the same length as the input query array + should(results.length).eql(queries.length); + + rows = results[3]; + // TODO: If a consumer calls executeAll and some of them return a result set, is the caller expected to explicitly close + // all the non-null result sets returned?! + + // Validate the returned 'rows' object + should(rows).be.a.Object; + should(rows.rowCount).be.eql(1); + should(rows.fieldCount).be.eql(3); + should(rows.validRow).be.true; + + finish(); + } catch (e) { + finish(e); + } finally { + // Close the 'rows' object + if (rows) { + rows.close(); + } + db.close(); + } + }); + + it('throws Error for invalid SQL statement', () => { // eslint-disable-line mocha/no-identical-title + const db = Ti.Database.open('execute_all.db'); + + const queries = [ + 'THIS IS INVALID SQL', + ]; + + try { + db.executeAll(queries); + } catch (e) { + should(e).exist; + // we fire a custom Error with index pointing at the offending query + should(e.index).eql(0); + should(e.results).exist; + return; + } finally { + db.close(); + } + should.fail(true, false, 'Expected to throw an exception for invalid sql'); + }); + }); + + describe.windowsMissing('#executeAllAsync()', () => { + it('is a function', () => { // eslint-disable-line mocha/no-identical-title + const db = Ti.Database.open('execute_all_async.db'); + try { + should(db.executeAllAsync).be.a.Function; + } finally { + db.close(); + } + }); + + it('executes asynchronously', function (finish) { // eslint-disable-line mocha/no-identical-title + this.timeout(5000); + const db = Ti.Database.open('execute_all.db'); + + const queries = [ + // Execute a query to create a test table + 'CREATE TABLE IF NOT EXISTS testTable (text TEXT, number INTEGER)', + // Delete any existing data if the table already existed + 'DELETE FROM testTable', + // Insert test data into the table + 'INSERT INTO testTable (text, number) VALUES (\'John Smith\', 123456789)', + // Execute a query to return the rows of the database + 'SELECT rowid, text, number FROM testTable', + ]; + + try { + db.executeAllAsync(queries, (err, results) => { + let rows; + try { + should(err).not.exist; + // the returned results array should be the same length as the input query array + should(results.length).eql(queries.length); + + rows = results[3]; + // Validate the returned 'rows' object + should(rows).be.a.Object; + should(rows.rowCount).be.eql(1); + should(rows.fieldCount).be.eql(3); + should(rows.validRow).be.true; + + finish(); + } catch (e) { + finish(e); + } finally { + // Close the 'rows' object + if (rows) { + rows.close(); + } + db.close(); + } + }); + } catch (e) { + db.close(); + finish(e); + } + }); + + it('calls callback with Error for invalid SQL statement', function (finish) { // eslint-disable-line mocha/no-identical-title + const db = Ti.Database.open('execute_all.db'); + + const queries = [ + // Execute a query to create a test table + 'CREATE TABLE IF NOT EXISTS testTable (text TEXT, number INTEGER)', + // Delete any existing data if the table already existed + 'DELETE FROM testTable', + // Insert test data into the table + 'INSERT INTO testTable (text, number) VALUES (\'John Smith\', 123456789)', + // Execute a query to return the rows of the database + 'SELECT rowid, text, number FROM testTable', + // invalid, should fail here! + 'THIS IS INVALID SQL', + ]; + + try { + db.executeAllAsync(queries, (err, results) => { + let rows; + try { + should(err).exist; + should(err.index).eql(4); + should(results).be.an.Array; + + // validate our partial results + rows = results[3]; + should(rows).be.a.Object; + should(rows.rowCount).be.eql(1); + should(rows.fieldCount).be.eql(3); + should(rows.validRow).be.true; + + finish(); + } catch (e) { + finish(e); + } finally { + if (rows) { + rows.close(); + } + db.close(); + } + }); + } catch (e) { + // should call callback with error, not throw it! + db.close(); + finish(e); + } + }); + + it('handles being closed mid-query', function (finish) { + this.timeout(30000); + this.slow(2000); + const db = Ti.Database.open('execute_all_async.db'); + const queries = [ + // Execute a query to create a test table + 'CREATE TABLE IF NOT EXISTS testTable (text TEXT, number INTEGER)', + // Delete any existing data if the table already existed + 'DELETE FROM testTable' + ]; + // insert a lot of bogus data + for (let i = 0; i < 10000; i++) { + queries.push(`INSERT INTO testTable (text, number) VALUES ('John Smith ${i}', ${i})`); + } + + db.executeAllAsync(queries, (err, results) => { + // this should eventually throw an error when it gets closed mid-queries + try { + should(err).exist; + // We should be giving custom properties so user can see what index we failed on, get partial results + should(err.index).be.a.Number; + should(results).be.an.Array; + finish(); + } catch (e) { + finish(e); + } finally { + db.close(); + } + }); + // close the db while we're executing queries + setTimeout(() => { + try { + db.close(); + } catch (err) { + finish(err); + } + }, 50); + }); + + function executeQueriesAsync(finish) { + const db = Ti.Database.open('executeQueriesAsync.db'); + + const queries = [ + // Execute a query to create a test table + 'CREATE TABLE IF NOT EXISTS testTable (text TEXT, number INTEGER)', + // Delete any existing data if the table already existed + 'DELETE FROM testTable' + ]; + // insert a lot of bogus data + for (let i = 0; i < 5000; i++) { + queries.push(`INSERT INTO testTable (text, number) VALUES ('John Smith ${i}', ${i})`); + } + // this should just continue on until it's done... + db.executeAllAsync(queries, (err, results) => { + try { + should(err).not.exist; + should(results).exist; + finish(); + } catch (e) { + finish(e); + } + }); + } + + // Try to get the db object to get GC'd while we're running queries! + // Note that I can't really think of any better way to try and test this scenario + it('does not allow DB to be GC\'d', function (finish) { + this.timeout(60000); + this.slow(20000); + // note that we call a fucntion that has a db instance scope to it and not referenced elsewhere, + // not explicitly closed, not referenced in the async callback + executeQueriesAsync(finish); + }); + }); +}); diff --git a/tests/Resources/ti.ui.tableview.addontest.js b/tests/Resources/ti.ui.tableview.addontest.js new file mode 100644 index 00000000000..c13d7250488 --- /dev/null +++ b/tests/Resources/ti.ui.tableview.addontest.js @@ -0,0 +1,101 @@ +/* + * Appcelerator Titanium Mobile + * Copyright (c) 2015-Present by Appcelerator, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ +/* eslint-env mocha */ +/* global Ti */ +/* eslint no-unused-expressions: "off" */ +'use strict'; +var should = require('./utilities/assertions'); + +describe('Titanium.UI.TableView', function () { + var win; + + this.timeout(5000); + + afterEach(function (done) { + if (win) { + win.addEventListener('close', function () { + done(); + }); + win.close(); + } else { + done(); + } + win = null; + }); + + it('row#color row#backgroundColor', function (finish) { + // Set up a TableView with colored rows. + win = Ti.UI.createWindow(); + const section = Ti.UI.createTableViewSection({ headerTitle: 'Section' }); + const row1 = Ti.UI.createTableViewRow({ + title: 'Row 1', + color: 'white', + backgroundColor: 'blue' + }); + const row2 = Ti.UI.createTableViewRow({ + title: 'Row 2', + color: 'black', + backgroundColor: 'yellow' + }); + section.add(row1); + section.add(row2); + const tableView = Ti.UI.createTableView({ data: [ section ] }); + win.add(tableView); + + // Verify row objects return same color values assigned above. + should(row1.color).be.eql('white'); + should(row1.backgroundColor).be.eql('blue'); + should(row2.color).be.eql('black'); + should(row2.backgroundColor).be.eql('yellow'); + + // Open window to test dynamic color changes. + win.addEventListener('open', function () { + row1.color = 'red'; + row1.backgroundColor = 'white'; + row2.color = 'white'; + row2.backgroundColor = 'purple'; + setTimeout(function () { + should(row1.color).be.eql('red'); + should(row1.backgroundColor).be.eql('white'); + should(row2.color).be.eql('white'); + should(row2.backgroundColor).be.eql('purple'); + finish(); + }, 1); + }); + win.open(); + }); + + it('row - read unassigned color properties', function (finish) { + win = Ti.UI.createWindow(); + const section = Ti.UI.createTableViewSection({ headerTitle: 'Section' }); + const row1 = Ti.UI.createTableViewRow({ title: 'Row 1' }); + section.add(row1); + const tableView = Ti.UI.createTableView({ data: [ section ] }); + win.add(tableView); + + function validateRow() { + // Verify we can read row color properties without crashing. (Used to crash on Android.) + // We don't care about the returned value. + // eslint-disable-next-line no-unused-vars + let value; + value = row1.color; + value = row1.backgroundColor; + if (Ti.Android) { + value = row1.backgroundDisabledColor; + value = row1.backgroundFocusedColor; + value = row1.backgroundSelectedColor; + } + } + validateRow(); + + win.addEventListener('open', function () { + validateRow(); + finish(); + }); + win.open(); + }); +}); diff --git a/tests/Resources/ti.ui.window.addontest.js b/tests/Resources/ti.ui.window.addontest.js new file mode 100644 index 00000000000..71181d5c910 --- /dev/null +++ b/tests/Resources/ti.ui.window.addontest.js @@ -0,0 +1,28 @@ +/* + * Appcelerator Titanium Mobile + * Copyright (c) 2011-Present by Appcelerator, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ +/* eslint-env mocha */ +/* eslint no-unused-expressions: "off" */ +'use strict'; +var should = require('./utilities/assertions'); + +describe('Titanium.UI.Window', function () { + var win; + + it.android('.barColor with disabled ActionBar', function (finish) { + win = Ti.UI.createWindow({ + barColor: 'blue', + title: 'My Title', + theme: 'Theme.AppCompat.NoTitleBar', + }); + win.add(Ti.UI.createLabel({ text: 'Window Title Test' })); + win.open(); + win.addEventListener('open', function () { + finish(); + }); + }); + +});