AI2 Component  (Version nb184)
CloudDB.java
Go to the documentation of this file.
1 // -*- mode: java; c-basic-offset: 2; -*-
2 // Copyright 2017-2020 MIT, All rights reserved
3 // Released under the Apache License, Version 2.0
4 // http://www.apache.org/licenses/LICENSE-2.0
5 
6 package com.google.appinventor.components.runtime;
7 
8 import android.Manifest;
9 import android.app.Activity;
10 
11 import android.database.Cursor;
12 import android.database.sqlite.SQLiteDatabase;
13 
14 import android.net.ConnectivityManager;
15 import android.net.NetworkInfo;
16 
17 import android.os.Handler;
18 
19 import android.util.Base64;
20 import android.util.Log;
21 
30 
34 
36 
42 
43 import java.io.ByteArrayInputStream;
44 import java.io.File;
45 import java.io.FileNotFoundException;
46 import java.io.IOException;
47 
48 import java.security.KeyStore;
49 import java.security.cert.Certificate;
50 import java.security.cert.CertificateFactory;
51 import java.security.cert.X509Certificate;
52 
53 import java.util.ArrayList;
54 import java.util.Collections;
55 import java.util.List;
56 import java.util.Set;
57 import java.util.concurrent.ExecutorService;
58 import java.util.concurrent.Executors;
59 import java.util.concurrent.atomic.AtomicReference;
60 
61 import javax.net.ssl.SSLContext;
62 import javax.net.ssl.SSLSocketFactory;
63 import javax.net.ssl.TrustManagerFactory;
64 import javax.net.ssl.X509TrustManager;
65 
66 import org.json.JSONArray;
67 import org.json.JSONException;
68 
69 import redis.clients.jedis.Jedis;
70 import redis.clients.jedis.exceptions.JedisConnectionException;
71 import redis.clients.jedis.exceptions.JedisDataException;
72 import redis.clients.jedis.exceptions.JedisException;
73 import redis.clients.jedis.exceptions.JedisNoScriptException;
74 
93 @DesignerComponent(version = YaVersion.CLOUDDB_COMPONENT_VERSION,
94  description = "Non-visible component allowing you to store data on a Internet " +
95  "connected database server (using Redis software). This allows the users of " +
96  "your App to share data with each other. " +
97  "By default data will be stored in a server maintained by MIT, however you " +
98  "can setup and run your own server. Set the \"RedisServer\" property and " +
99  "\"RedisPort\" Property to access your own server.",
100  designerHelpDescription = "Non-visible component that communicates with CloudDB " +
101  "server to store and retrieve information.",
102  category = ComponentCategory.STORAGE,
103  nonVisible = true,
104  iconName = "images/cloudDB.png")
105 @UsesPermissions(permissionNames = "android.permission.INTERNET," +
106  "android.permission.ACCESS_NETWORK_STATE," +
107  "android.permission.READ_EXTERNAL_STORAGE," +
108  "android.permission.WRITE_EXTERNAL_STORAGE")
109 @UsesLibraries(libraries = "jedis.jar")
110 public final class CloudDB extends AndroidNonvisibleComponent implements Component,
112  private static final boolean DEBUG = false;
113  private static final String LOG_TAG = "CloudDB";
114  private boolean importProject = false;
115  private String projectID = "";
116  private String token = "";
117  private boolean isPublic = false;
118 
119  private volatile boolean dead = false; // On certain fatal errors we declare ourselves
120  // "dead" which means an application restart
121  // is required to get things going again.
122  // For now, only an authentication error
123  // sets this
124 
125  private static final String COMODO_ROOT =
126  "-----BEGIN CERTIFICATE-----\n" +
127  "MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU\n" +
128  "MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs\n" +
129  "IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290\n" +
130  "MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux\n" +
131  "FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h\n" +
132  "bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v\n" +
133  "dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt\n" +
134  "H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9\n" +
135  "uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX\n" +
136  "mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX\n" +
137  "a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN\n" +
138  "E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0\n" +
139  "WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD\n" +
140  "VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0\n" +
141  "Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU\n" +
142  "cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx\n" +
143  "IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN\n" +
144  "AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH\n" +
145  "YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5\n" +
146  "6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC\n" +
147  "Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX\n" +
148  "c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a\n" +
149  "mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=\n" +
150  "-----END CERTIFICATE-----\n";
151 
152  // We have to include this intermediate certificate because of bugs
153  // in older versions of Android
154 
155  private static final String COMODO_USRTRUST =
156  "-----BEGIN CERTIFICATE-----\n" +
157  "MIIFdzCCBF+gAwIBAgIQE+oocFv07O0MNmMJgGFDNjANBgkqhkiG9w0BAQwFADBv\n" +
158  "MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk\n" +
159  "ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF\n" +
160  "eHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFow\n" +
161  "gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK\n" +
162  "ZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD\n" +
163  "VQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjAN\n" +
164  "BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sIs9CsVw127c0n00yt\n" +
165  "UINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnGvDoZtF+mvX2do2NC\n" +
166  "tnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQIjy8/hPwhxR79uQf\n" +
167  "jtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfbIWax1Jt4A8BQOujM\n" +
168  "8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0tyA9yn8iNK5+O2hm\n" +
169  "AUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97Exwzf4TKuzJM7UXiV\n" +
170  "Z4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNVicQNwZNUMBkTrNN9\n" +
171  "N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5D9kCnusSTJV882sF\n" +
172  "qV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJWBp/kjbmUZIO8yZ9\n" +
173  "HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ5lhCLkMaTLTwJUdZ\n" +
174  "+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzGKAgEJTm4Diup8kyX\n" +
175  "HAc/DVL17e8vgg8CAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTv\n" +
176  "A73gJMtUGjAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/\n" +
177  "BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQGA1Ud\n" +
178  "HwQ9MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVzdEV4\n" +
179  "dGVybmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0\n" +
180  "dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggEBAJNl9jeD\n" +
181  "lQ9ew4IcH9Z35zyKwKoJ8OkLJvHgwmp1ocd5yblSYMgpEg7wrQPWCcR23+WmgZWn\n" +
182  "RtqCV6mVksW2jwMibDN3wXsyF24HzloUQToFJBv2FAY7qCUkDrvMKnXduXBBP3zQ\n" +
183  "YzYhBx9G/2CkkeFnvN4ffhkUyWNnkepnB2u0j4vAbkN9w6GAbLIevFOFfdyQoaS8\n" +
184  "Le9Gclc1Bb+7RrtubTeZtv8jkpHGbkD4jylW6l/VXxRTrPBPYer3IsynVgviuDQf\n" +
185  "Jtl7GQVoP7o81DgGotPmjw7jtHFtQELFhLRAlSv0ZaBIefYdgWOWnU914Ph85I6p\n" +
186  "0fKtirOMxyHNwu8=\n" +
187  "-----END CERTIFICATE-----\n";
188 
189  // Digital Signature Trust Root X3 -- For Letsencrypt
190 
191  private static final String DST_ROOT_X3 =
192  "-----BEGIN CERTIFICATE-----\n" +
193  "MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\n" +
194  "MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\n" +
195  "DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\n" +
196  "PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\n" +
197  "Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n" +
198  "AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\n" +
199  "rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\n" +
200  "OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\n" +
201  "xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n" +
202  "7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\n" +
203  "aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\n" +
204  "HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\n" +
205  "SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\n" +
206  "ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\n" +
207  "AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\n" +
208  "R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\n" +
209  "JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\n" +
210  "Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n" +
211  "-----END CERTIFICATE-----\n";
212 
213  private String defaultRedisServer = null;
214  private boolean useDefault = true;
215 
216  private Handler androidUIHandler;
217  private final Activity activity;
218 
219  private Jedis INSTANCE = null;
220  private volatile String redisServer = "DEFAULT";
221  private volatile int redisPort;
222  private volatile boolean useSSL = true;
223  private volatile boolean shutdown = false; // Should this instance of CloudDB
224  // stop?
225 
226  private SSLSocketFactory SslSockFactory = null; // Socket Factory for using
227  // SSL
228 
229  private volatile CloudDBJedisListener currentListener;
230  private volatile boolean listenerRunning = false;
231 
232  // To avoid blocking the UI thread, we do most Jedis operations in the background.
233  // Rather then spawning a new thread for each request, we use an ExcutorService with
234  // a single background thread to perform all the Jedis work. Using a single thread
235  // also means that we can share a single Jedis connection and not worry about thread
236  // synchronization.
237 
238  private volatile ExecutorService background = Executors.newSingleThreadExecutor();
239 
240  // Store can be called frequenly and quickly in some situations. For example
241  // using store inside of a Canvas Drag event (for realtime updating of a remote
242  // canvas). Or in a handler for the Accelerometer (gasp!). To make storing as
243  // effecient as possible, we have a queue of pending store requests and we
244  // have a background task that drains this queue as fast as possible and
245  // iterates over the queue until it is drained.
246  private final List<storedValue> storeQueue = Collections.synchronizedList(new ArrayList());
247 
248  private ConnectivityManager cm;
249 
250  // Do we have storage permission yet
251  private boolean havePermission = false;
252 
253  private static class storedValue {
254  private String tag;
255  private JSONArray valueList;
256  storedValue(String tag, JSONArray valueList) {
257  this.tag = tag;
258  this.valueList = valueList;
259  }
260 
261  public String getTag() {
262  return tag;
263  }
264 
265  public JSONArray getValueList() {
266  return valueList;
267  }
268  }
269 
274  public CloudDB(ComponentContainer container) {
275  super(container.$form());
276  // We use androidUIHandler when we set up operations that run asynchronously
277  // in a separate thread, but which themselves want to cause actions
278  // back in the UI thread. They do this by posting those actions
279  // to androidUIHandler.
280  androidUIHandler = new Handler();
281  this.activity = container.$context();
282  //Defaults set in MockCloudDB.java in appengine/src/com/google/appinventor/client/editor/simple/components
283  projectID = ""; // set in Designer
284  token = ""; //set in Designer
285 
286  redisPort = 6381;
287  cm = (ConnectivityManager) form.$context().getSystemService(android.content.Context.CONNECTIVITY_SERVICE);
288  }
289 
293  public void Initialize() {
294  if (DEBUG) {
295  Log.d(LOG_TAG, "Initalize called!");
296  }
297  if (currentListener == null) { // currentListener may still be set
298  startListener(); // in the Companion
299  }
300  form.registerForOnClear(this); // So we are notified when (clear-current-form)
301  // is called.
302  form.registerForOnDestroy(this); // close our Redis connections when we are leaving
303  }
304 
305  private void stopListener() {
306  // We do this on the UI thread to make sure it is complete
307  // before we repoint the redis server (or port)
308  if (DEBUG) {
309  Log.d(LOG_TAG, "Listener stopping!");
310  }
311  if (currentListener != null) {
312  currentListener.terminate();
313  currentListener = null;
314  listenerRunning = false;
315  }
316  }
317 
318  /*
319  * onClear() -- Called when (clear-current-form) is invoked
320  *
321  */
322  @Override
323  public void onClear() {
324  shutdown = true; // Tell the listener to stop trying
325  flushJedis(false); // to restart
326  if (DEBUG) {
327  Log.d(LOG_TAG, "onClear() called");
328  }
329  }
330 
331  @Override
332  public void onDestroy() {
333  if (DEBUG) {
334  Log.d(LOG_TAG, "onDestroy() called");
335  }
336  onClear();
337  }
338 
339  private synchronized void startListener() {
340  // Retrieve new posts as they are added to the CloudDB.
341  // Note: We use a real thread here rather then the background executor
342  // because this thread will run effectively forever
343  if (listenerRunning) {
344  if (DEBUG) {
345  Log.d(LOG_TAG, "StartListener while already running, no action taken");
346  }
347  return;
348  }
349  listenerRunning = true;
350  if (DEBUG) {
351  Log.d(LOG_TAG, "Listener starting!");
352  }
353  Thread t = new Thread() {
354  public void run() {
355  Jedis jedis = getJedis(true);
356  if (jedis != null) {
357  try {
358  currentListener = new CloudDBJedisListener(CloudDB.this);
359  jedis.subscribe(currentListener, projectID);
360  } catch (Exception e) {
361  Log.e(LOG_TAG, "Error in listener thread", e);
362  try {
363  jedis.close();
364  } catch (Exception ee) {
365  // XXX
366  }
367  if (DEBUG) {
368  Log.d(LOG_TAG, "Listener: connection to Redis failed, sleeping 3 seconds.");
369  }
370  try {
371  Thread.sleep(3*1000);
372  } catch (InterruptedException ee) {
373  }
374  if (DEBUG) {
375  Log.d(LOG_TAG, "Woke up!");
376  }
377  }
378  } else {
379  if (DEBUG) {
380  Log.d(LOG_TAG, "Listener: getJedis(true) returned null, retry in 3...");
381  }
382  try {
383  Thread.sleep(3*1000);
384  } catch (InterruptedException e) {
385  }
386  if (DEBUG) {
387  Log.d(LOG_TAG, "Woke up! (2)");
388  }
389  }
390  listenerRunning = false;
391  if (!dead && !shutdown) {
392  startListener();
393  } else {
394  if (DEBUG) {
395  Log.d(LOG_TAG, "We are dead, listener not retrying.");
396  }
397  }
398  }
399  };
400  t.start();
401  }
402 
403  @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING,
404  defaultValue = "DEFAULT")
405  public void RedisServer(String servername) {
406  if (servername.equals("DEFAULT")) {
407  if (!useDefault) {
408  useDefault = true;
409  if (defaultRedisServer == null) { // Not setup yet
410  if (DEBUG) {
411  Log.d(LOG_TAG, "RedisServer called before defaultServer (should not happen!)");
412  }
413  } else {
414  redisServer = defaultRedisServer;
415  }
416  flushJedis(true); // Re-initialize any existing connections
417  }
418  } else {
419  useDefault = false;
420  if (!servername.equals(redisServer)) {
421  redisServer = servername;
422  flushJedis(true); // Re-initialize any existing connections
423  }
424  }
425  }
426 
428  description = "The Redis Server to use to store data. A setting of \"DEFAULT\" " +
429  "means that the MIT server will be used.")
430  public String RedisServer() {
431  if (redisServer.equals(defaultRedisServer)) {
432  return "DEFAULT";
433  } else {
434  return redisServer;
435  }
436  }
437 
438  // This is a non-documented property because it is hidden in the
439  // UI. Its purpose in life is to transmit the default redis server
440  // from the system into the Companion or packaged app. The Default
441  // server is set in appengine-web.xml (the clouddb.server property). It
442  // is sent to the client from the server via the system config call.
443 
446  description = "The Default Redis Server to use.",
447  userVisible = false)
448  public void DefaultRedisServer(String server) {
449  defaultRedisServer = server;
450  if (useDefault) {
451  redisServer = server;
452  }
453  }
454 
456  defaultValue = "6381")
457  public void RedisPort(int port) {
458  if (port != redisPort) {
459  redisPort = port;
460  flushJedis(true);
461  }
462  }
463 
465  description = "The Redis Server port to use. Defaults to 6381")
466  public int RedisPort() {
467  return redisPort;
468  }
469 
476  description = "Gets the ProjectID for this CloudDB project.")
477  public String ProjectID() {
478  checkProjectIDNotBlank();
479  return projectID;
480  }
481 
488  defaultValue = "")
489  public void ProjectID(String id) {
490  if (!projectID.equals(id)) {
491  projectID = id;
492  }
493  if (projectID.equals("")){
494  throw new RuntimeException("CloudDB ProjectID property cannot be blank.");
495  }
496  }
497 
504  defaultValue = "")
505  public void Token(String authToken) {
506  if (!token.equals(authToken)) {
507  token = authToken;
508  }
509  if (token.equals("")){
510  throw new RuntimeException("CloudDB Token property cannot be blank.");
511  }
512  }
513 
526  @SimpleProperty(category = PropertyCategory.BEHAVIOR, userVisible = false,
527  description = "This field contains the authentication token used to login to " +
528  "the backed Redis server. For the \"DEFAULT\" server, do not edit this " +
529  "value, the system will fill it in for you. A system administrator " +
530  "may also provide a special value to you which can be used to share " +
531  "data between multiple projects from multiple people. If using your own " +
532  "Redis server, set a password in the server's config and enter it here.")
533  public String Token() {
534  checkProjectIDNotBlank();
535  return token;
536  }
537 
545  defaultValue = "True")
546  public void UseSSL(boolean useSSL) {
547  if (this.useSSL != useSSL) {
548  this.useSSL = useSSL;
549  flushJedis(true); // Re-initialize any connections
550  }
551  }
552 
553  @SimpleProperty(category = PropertyCategory.BEHAVIOR, userVisible = false,
554  description = "Set to true to use SSL to talk to CloudDB/Redis server. " +
555  "This should be set to True for the \"DEFAULT\" server.")
556  public boolean UseSSL() {
557  return useSSL;
558  }
559 
560  private static final String SET_SUB_SCRIPT =
561  "local key = KEYS[1];" +
562  "local value = ARGV[1];" +
563  "local topublish = cjson.decode(ARGV[2]);" +
564  "local project = ARGV[3];" +
565  "local newtable = {};" +
566  "table.insert(newtable, key);" +
567  "table.insert(newtable, topublish);" +
568  "redis.call(\"publish\", project, cjson.encode(newtable));" +
569  "return redis.call('set', project .. \":\" .. key, value);";
570 
571  private static final String SET_SUB_SCRIPT_SHA1 = "765978e4c340012f50733280368a0ccc4a14dfb7";
572 
581  @SimpleFunction(description = "Store a value at a tag.")
582  public void StoreValue(final String tag, final Object valueToStore) {
583  checkProjectIDNotBlank();
584  if (!havePermission) {
585  final CloudDB me = this;
586  form.askPermission(new BulkPermissionRequest(this, "CloudDB",
587  Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) {
588  @Override
589  public void onGranted() {
590  me.havePermission = true;
591  StoreValue(tag, valueToStore);
592  }
593  });
594  return;
595  }
596  final String value;
597  NetworkInfo networkInfo = cm.getActiveNetworkInfo();
598  boolean isConnected = networkInfo != null && networkInfo.isConnected();
599 
600  try {
601  if (valueToStore != null) {
602  String strval = valueToStore.toString();
603  if (strval.startsWith("file:///") || strval.startsWith("/storage")) {
604  value = JsonUtil.getJsonRepresentation(readFile(strval));
605  } else {
606  value = JsonUtil.getJsonRepresentation(valueToStore);
607  }
608  } else {
609  value = "";
610  }
611  } catch(JSONException e) {
612  throw new YailRuntimeError("Value failed to convert to JSON.", "JSON Creation Error.");
613  }
614 
615  if (isConnected) {
616  if (DEBUG) {
617  Log.d(LOG_TAG,"Device is online...");
618  }
619  synchronized(storeQueue) {
620  boolean kickit = false;
621  if (storeQueue.size() == 0) { // Need to kick off the background task
622  if (DEBUG) {
623  Log.d(LOG_TAG, "storeQueue is zero length, kicking background");
624  }
625  kickit = true;
626  } else {
627  if (DEBUG) {
628  Log.d(LOG_TAG, "storeQueue has " + storeQueue.size() + " entries");
629  }
630  }
631  JSONArray valueList = new JSONArray();
632  try {
633  valueList.put(0, value);
634  } catch (JSONException e) {
635  throw new YailRuntimeError("JSON Error putting value.", "value is not convertable");
636  }
637  storedValue work = new storedValue(tag, valueList);
638  storeQueue.add(work);
639  if (kickit) {
640  background.submit(new Runnable() {
641  public void run() {
642  JSONArray pendingValueList = null;
643  String pendingTag = null;
644  String pendingValue = null;
645  try {
646  storedValue work;
647  if (DEBUG) {
648  Log.d(LOG_TAG, "store background task running.");
649  }
650  while (true) {
651  synchronized(storeQueue) {
652  if (DEBUG) {
653  Log.d(LOG_TAG, "store: In background synchronized block");
654  }
655  int size = storeQueue.size();
656  if (size == 0) {
657  if (DEBUG) {
658  Log.d(LOG_TAG, "store background task exiting.");
659  }
660  work = null;
661  } else {
662  if (DEBUG) {
663  Log.d(LOG_TAG, "store: storeQueue.size() == " + size);
664  }
665  work = storeQueue.remove(0);
666  if (DEBUG) {
667  Log.d(LOG_TAG, "store: got work.");
668  }
669  }
670  }
671  if (DEBUG) {
672  Log.d(LOG_TAG, "store: left synchronized block");
673  }
674  if (work == null) {
675  try {
676  if (pendingTag != null) {
677  String jsonValueList = pendingValueList.toString();
678  if (DEBUG) {
679  Log.d(LOG_TAG, "Workqueue empty, sending pendingTag, valueListLength = " + pendingValueList.length());
680  }
681  jEval(SET_SUB_SCRIPT, SET_SUB_SCRIPT_SHA1, 1, pendingTag, pendingValue, jsonValueList, projectID);
682  }
683  } catch (JedisException e) {
684  CloudDBError(e.getMessage());
685  flushJedis(true);
686  }
687  return;
688  }
689 
690  String tag = work.getTag();
691  JSONArray valueList = work.getValueList();
692  if (tag == null || valueList == null) {
693  if (DEBUG) {
694  Log.d(LOG_TAG, "Either tag or value is null!");
695  }
696  } else {
697  if (DEBUG) {
698  Log.d(LOG_TAG, "Got Work: tag = " + tag + " value = " + valueList.get(0));
699  }
700  }
701  if (pendingTag == null) { // First time through this invocation
702  pendingTag = tag;
703  pendingValueList = valueList;
704  pendingValue = valueList.getString(0);
705  } else if (pendingTag.equals(tag)) { // work is for the same tag
706  pendingValue = valueList.getString(0);
707  pendingValueList.put(pendingValue);
708  } else { // Work is for a different tag, send what we have
709  try { // and add the new tag,incoming valuelist for the next round
710  String jsonValueList = pendingValueList.toString();
711  if (DEBUG) {
712  Log.d(LOG_TAG, "pendingTag changed sending pendingTag, valueListLength = " + pendingValueList.length());
713  }
714  jEval(SET_SUB_SCRIPT, SET_SUB_SCRIPT_SHA1, 1, pendingTag, pendingValue, jsonValueList, projectID);
715  } catch (JedisException e) {
716  CloudDBError(e.getMessage());
717  flushJedis(true);
718  storeQueue.clear(); // Flush pending changes, we are in
719  return; // an error state
720  }
721  pendingTag = tag;
722  pendingValueList = valueList;
723  pendingValue = valueList.getString(0);
724  }
725  }
726  } catch (Exception e) {
727  Log.e(LOG_TAG, "Exception in store worker!", e);
728  }
729  }
730  });
731  }
732  }
733  } else {
734  CloudDBError("Cannot store values off-line.");
735  }
736  }
737 
748  @SimpleFunction(description = "Get the Value for a tag, doesn't return the " +
749  "value but will cause a GotValue event to fire when the " +
750  "value is looked up.")
751  public void GetValue(final String tag, final Object valueIfTagNotThere) {
752  if (DEBUG) {
753  Log.d(LOG_TAG, "getting value ... for tag: " + tag);
754  }
755  checkProjectIDNotBlank();
756  if (!havePermission) {
757  final CloudDB me = this;
758  form.askPermission(new BulkPermissionRequest(this, "CloudDB",
759  Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) {
760  @Override
761  public void onGranted() {
762  me.havePermission = true;
763  GetValue(tag, valueIfTagNotThere);
764  }
765  });
766  return;
767  }
768  final AtomicReference<Object> value = new AtomicReference<Object>();
769  Cursor cursor = null;
770  SQLiteDatabase db = null;
771  NetworkInfo networkInfo = cm.getActiveNetworkInfo();
772  boolean isConnected = networkInfo != null && networkInfo.isConnected();
773 
774  if (isConnected) {
775  // Set value to either the JSON from the CloudDB
776  // or the JSON representation of valueIfTagNotThere
777  background.submit(new Runnable() {
778  public void run() {
779  Jedis jedis = getJedis();
780  try {
781  if (DEBUG) {
782  Log.d(LOG_TAG,"about to call jedis.get()");
783  }
784  String returnValue = jedis.get(projectID + ":" + tag);
785  if (DEBUG) {
786  Log.d(LOG_TAG, "finished call jedis.get()");
787  }
788  if (returnValue != null) {
789  String val = JsonUtil.getJsonRepresentationIfValueFileName(form, returnValue);
790  if(val != null) value.set(val);
791  else value.set(returnValue);
792  }
793  else {
794  if (DEBUG) {
795  Log.d(CloudDB.LOG_TAG,"Value retrieved is null");
796  }
797  value.set(JsonUtil.getJsonRepresentation(valueIfTagNotThere));
798  }
799  } catch (JSONException e) {
800  CloudDBError("JSON conversion error for " + tag);
801  return;
802  } catch (NullPointerException e) {
803  CloudDBError("System Error getting tag " + tag);
804  flushJedis(true);
805  return;
806  } catch (JedisException e) {
807  Log.e(LOG_TAG, "Exception in GetValue", e);
808  CloudDBError(e.getMessage());
809  flushJedis(true);
810  return;
811  } catch (Exception e) {
812  Log.e(LOG_TAG, "Exception in GetValue", e);
813  CloudDBError(e.getMessage());
814  flushJedis(true);
815  return;
816  }
817 
818  androidUIHandler.post(new Runnable() {
819  public void run() {
820  // Signal an event to indicate that the value was
821  // received. We post this to run in the Application's main
822  // UI thread.
823  GotValue(tag, value.get());
824  }
825  });
826  }
827  });
828  } else {
829  if (DEBUG) {
830  Log.d(LOG_TAG, "GetValue(): We're offline");
831  }
832  CloudDBError("Cannot fetch variables while off-line.");
833  }
834  }
835 
842  @SimpleFunction(description = "returns True if we are on the network and will likely " +
843  "be able to connect to the CloudDB server.")
844  public boolean CloudConnected() {
845  NetworkInfo networkInfo = cm.getActiveNetworkInfo();
846  boolean isConnected = networkInfo != null && networkInfo.isConnected();
847  return isConnected;
848  }
849 
857  @SimpleEvent(description = "Event triggered by the \"RemoveFirstFromList\" function. The " +
858  "argument \"value\" is the object that was the first in the list, and which is now " +
859  "removed.")
860  public void FirstRemoved(Object value) {
861  if (DEBUG) {
862  Log.d(CloudDB.LOG_TAG, "FirstRemoved: Value = " + value);
863  }
864  checkProjectIDNotBlank();
865  try {
866  if(value != null && value instanceof String) {
867  value = JsonUtil.getObjectFromJson((String) value, true);
868  }
869  } catch (JSONException e) {
870  Log.e(CloudDB.LOG_TAG,"error while converting to JSON...",e);
871  return;
872  }
873  final Object sValue = value;
874  androidUIHandler.post(new Runnable() {
875  @Override
876  public void run() {
877  EventDispatcher.dispatchEvent(CloudDB.this, "FirstRemoved", sValue);
878  }
879  });
880  }
881 
882  private static final String POP_FIRST_SCRIPT =
883  "local key = KEYS[1];" +
884  "local project = ARGV[1];" +
885  "local currentValue = redis.call('get', project .. \":\" .. key);" +
886  "local decodedValue = cjson.decode(currentValue);" +
887  "local subTable = {};" +
888  "local subTable1 = {};" +
889  "if (type(decodedValue) == 'table') then " +
890  " local removedValue = table.remove(decodedValue, 1);" +
891  " local newValue = cjson.encode(decodedValue);" +
892  " redis.call('set', project .. \":\" .. key, newValue);" +
893  " table.insert(subTable, key);" +
894  " table.insert(subTable1, newValue);" +
895  " table.insert(subTable, subTable1);" +
896  " redis.call(\"publish\", project, cjson.encode(subTable));" +
897  " return cjson.encode(removedValue);" +
898  "else " +
899  " return error('You can only remove elements from a list');" +
900  "end";
901 
902  private static final String POP_FIRST_SCRIPT_SHA1 = "ed4cb4717d157f447848fe03524da24e461028e1";
903 
912  @SimpleFunction(description = "Return the first element of a list and atomically remove it. " +
913  "If two devices use this function simultaneously, one will get the first element and the " +
914  "the other will get the second element, or an error if there is no available element. " +
915  "When the element is available, the \"FirstRemoved\" event will be triggered.")
916  public void RemoveFirstFromList(final String tag) {
917  checkProjectIDNotBlank();
918 
919  final String key = tag;
920 
921  background.submit(new Runnable() {
922  public void run() {
923  Jedis jedis = getJedis();
924  try {
925  FirstRemoved(jEval(POP_FIRST_SCRIPT, POP_FIRST_SCRIPT_SHA1, 1, key, projectID));
926  } catch (JedisException e) {
927  CloudDBError(e.getMessage());
928  flushJedis(true);
929  }
930  }
931  });
932  }
933 
934  private static final String APPEND_SCRIPT =
935  "local key = KEYS[1];" +
936  "local toAppend = cjson.decode(ARGV[1]);" +
937  "local project = ARGV[2];" +
938  "local currentValue = redis.call('get', project .. \":\" .. key);" +
939  "local newTable;" +
940  "local subTable = {};" +
941  "local subTable1 = {};" +
942  "if (currentValue == false) then " +
943  " newTable = {};" +
944  "else " +
945  " newTable = cjson.decode(currentValue);" +
946  " if not (type(newTable) == 'table') then " +
947  " return error('You can only append to a list');" +
948  " end " +
949  "end " +
950  "table.insert(newTable, toAppend);" +
951  "local newValue = cjson.encode(newTable);" +
952  "redis.call('set', project .. \":\" .. key, newValue);" +
953  "table.insert(subTable1, newValue);" +
954  "table.insert(subTable, key);" +
955  "table.insert(subTable, subTable1);" +
956  "redis.call(\"publish\", project, cjson.encode(subTable));" +
957  "return newValue;";
958 
959  private static final String APPEND_SCRIPT_SHA1 = "d6cc0f65b29878589f00564d52c8654967e9bcf8";
960 
961  @SimpleFunction(description = "Append a value to the end of a list atomically. " +
962  "If two devices use this function simultaneously, both will be appended and no " +
963  "data lost.")
964  public void AppendValueToList(final String tag, final Object itemToAdd) {
965  checkProjectIDNotBlank();
966 
967  Object itemObject = new Object();
968  try {
969  if(itemToAdd != null) {
970  itemObject = JsonUtil.getJsonRepresentation(itemToAdd);
971  }
972  } catch(JSONException e) {
973  throw new YailRuntimeError("Value failed to convert to JSON.", "JSON Creation Error.");
974  }
975 
976  final String item = (String) itemObject;
977  final String key = tag;
978 
979  background.submit(new Runnable() {
980  public void run() {
981  Jedis jedis = getJedis();
982  try {
983  jEval(APPEND_SCRIPT, APPEND_SCRIPT_SHA1, 1, key, item, projectID);
984  } catch(JedisException e) {
985  CloudDBError(e.getMessage());
986  flushJedis(true);
987  }
988  }
989  });
990  }
991 
998  @SimpleEvent
999  public void GotValue(String tag, Object value) {
1000  if (DEBUG) {
1001  Log.d(CloudDB.LOG_TAG, "GotValue: tag = " + tag + " value = " + (String) value);
1002  }
1003  checkProjectIDNotBlank();
1004 
1005  // We can get a null value is the Jedis connection failed in some way.
1006  // not sure what to do here, so we'll signal an error for now.
1007  if (value == null) {
1008  CloudDBError("Trouble getting " + tag + " from the server.");
1009  return;
1010  }
1011 
1012  try {
1013  if (DEBUG) {
1014  Log.d(LOG_TAG, "GotValue: Class of value = " + value.getClass().getName());
1015  }
1016  if(value != null && value instanceof String) {
1017  value = JsonUtil.getObjectFromJson((String) value, true);
1018  }
1019  } catch(JSONException e) {
1020  throw new YailRuntimeError("Value failed to convert from JSON.", "JSON Retrieval Error.");
1021  }
1022 
1023  // Invoke the application's "GotValue" event handler
1024  EventDispatcher.dispatchEvent(this, "GotValue", tag, value);
1025  }
1026 
1035  @SimpleFunction(description = "Remove the tag from CloudDB.")
1036  public void ClearTag(final String tag) {
1037  checkProjectIDNotBlank();
1038  background.submit(new Runnable() {
1039  public void run() {
1040  try {
1041  Jedis jedis = getJedis();
1042  jedis.del(projectID + ":" + tag);
1043  } catch (Exception e) {
1044  CloudDBError(e.getMessage());
1045  flushJedis(true);
1046  }
1047  }
1048  });
1049  }
1050 
1055  @SimpleFunction(description = "Get the list of tags for this application. " +
1056  "When complete a \"TagList\" event will be triggered with the list of " +
1057  "known tags.")
1058  public void GetTagList() {
1059  checkProjectIDNotBlank();
1060  NetworkInfo networkInfo = cm.getActiveNetworkInfo();
1061  boolean isConnected = networkInfo != null && networkInfo.isConnected();
1062  if (isConnected) {
1063  background.submit(new Runnable() {
1064  public void run() {
1065 
1066  Jedis jedis = getJedis();
1067  Set<String> value = null;
1068  try {
1069  value = jedis.keys(projectID + ":*");
1070  } catch (JedisException e) {
1071  CloudDBError(e.getMessage());
1072  flushJedis(true);
1073  return;
1074  }
1075  final List<String> listValue = new ArrayList<String>(value);
1076 
1077  for(int i = 0; i < listValue.size(); i++){
1078  listValue.set(i, listValue.get(i).substring((projectID + ":").length()));
1079  }
1080 
1081  androidUIHandler.post(new Runnable() {
1082  @Override
1083  public void run() {
1084  TagList(listValue);
1085  }
1086  });
1087  }
1088  });
1089  } else {
1090  CloudDBError("Not connected to the Internet, cannot list tags");
1091  }
1092  }
1093 
1100  @SimpleEvent(description = "Event triggered when we have received the list of known tags. " +
1101  "Used with the \"GetTagList\" Function.")
1102  public void TagList(List<String> value) {
1103  checkProjectIDNotBlank();
1104  EventDispatcher.dispatchEvent(this, "TagList", value);
1105  }
1106 
1114  @SimpleEvent
1115  public void DataChanged(final String tag, final Object value) {
1116  Object tagValue = "";
1117  try {
1118  if(value != null && value instanceof String) {
1119  tagValue = JsonUtil.getObjectFromJson((String) value, true);
1120  }
1121  } catch(JSONException e) {
1122  throw new YailRuntimeError("Value failed to convert from JSON.", "JSON Retrieval Error.");
1123  }
1124  final Object finalTagValue = tagValue;
1125 
1126  androidUIHandler.post(new Runnable() {
1127  public void run() {
1128  // Invoke the application's "DataChanged" event handler
1129  EventDispatcher.dispatchEvent(CloudDB.this, "DataChanged", tag, finalTagValue);
1130  }
1131  });
1132  }
1133 
1139  @SimpleEvent(description = "Indicates that an error occurred while communicating " +
1140  "with the CloudDB Redis server.")
1141  public void CloudDBError(final String message) {
1142  // Log the error message for advanced developers
1143  Log.e(LOG_TAG, message);
1144  androidUIHandler.post(new Runnable() {
1145  @Override
1146  public void run() {
1147 
1148  // Invoke the application's "CloudDBError" event handler
1149  boolean dispatched = EventDispatcher.dispatchEvent(CloudDB.this, "CloudDBError", message);
1150  if (!dispatched) {
1151  // If the handler doesn't exist, then put up our own alert
1152  new Notifier(form).ShowAlert("CloudDBError: " + message);
1153  }
1154  }
1155  });
1156  }
1157 
1158  private void checkProjectIDNotBlank(){
1159  if (projectID.equals("")){
1160  throw new RuntimeException("CloudDB ProjectID property cannot be blank.");
1161  }
1162  if(token.equals("")){
1163  throw new RuntimeException("CloudDB Token property cannot be blank");
1164  }
1165  }
1166 
1167  public Form getForm() {
1168  return form;
1169  }
1170 
1171  public Jedis getJedis(boolean createNew) {
1172  Jedis jedis;
1173  if (dead) { // If we are dead, we are dead!
1174  return null;
1175  }
1176  try {
1177  if (DEBUG) {
1178  Log.d(LOG_TAG, "getJedis(true): Attempting a new connection (createNew = " +
1179  createNew + " redisServer = " + redisServer + " redisPort = " +
1180  redisPort + " useSSL = " +
1181  useSSL);
1182  }
1183  if (useSSL) { // Need to create a TrustManager and trust the Comodo
1184  // Root certificate because it isn't present in older
1185  // Android versions
1186  ensureSslSockFactory();
1187  jedis = new Jedis(redisServer, redisPort, true, SslSockFactory, null, null);
1188  } else {
1189  jedis = new Jedis(redisServer, redisPort, false);
1190  }
1191  if (DEBUG) {
1192  Log.d(LOG_TAG, "getJedis(true): Have new connection.");
1193  }
1194  // If the first character of the token is %, we toss it away
1195  // it is used by MockCloudDB.java to determine if the token should
1196  // be kept or fetched from the server when needed
1197  if (token.substring(0, 1).equals("%")) {
1198  jedis.auth(token.substring(1));
1199  } else {
1200  jedis.auth(token);
1201  }
1202  if (DEBUG) {
1203  Log.d(LOG_TAG, "getJedis(true): Authentication complete.");
1204  }
1205  } catch (JedisConnectionException e) {
1206  Log.e(LOG_TAG, "in getJedis()", e);
1207  CloudDBError(e.getMessage());
1208  return null;
1209  } catch (JedisDataException e) {
1210  // This is always an authentication error
1211  Log.e(LOG_TAG, "in getJedis()", e);
1212  CloudDBError(e.getMessage() + " CloudDB disabled, restart to re-enable.");
1213  dead = true;
1214  return null;
1215  }
1216  return jedis;
1217  }
1218 
1219  public synchronized Jedis getJedis() {
1220  if (INSTANCE == null) {
1221  INSTANCE = getJedis(true);
1222  }
1223  return INSTANCE;
1224  }
1225 
1226  /*
1227  * flushJedis -- Flush the singleton jedis connection. This is
1228  * used when we detect an error from jedis. It is possible that after
1229  * an error the jedis connection is in an invalid state (or closed) so
1230  * we want to make sure we get a new one the next time around!
1231  */
1232 
1233  private void flushJedis(boolean restartListener) {
1234  if (INSTANCE == null) {
1235  return; // Nothing to do
1236  }
1237  try {
1238  INSTANCE.close(); // Just in case we still have
1239  // a connection
1240  } catch (Exception e) {
1241  // XXX
1242  }
1243  INSTANCE = null;
1244  // We are now going to kill the executor, as it may
1245  // have hung tasks. We do this on the UI thread as a
1246  // way to synchronize things.
1247  androidUIHandler.post(new Runnable() {
1248  public void run() {
1249  List <Runnable> tasks = background.shutdownNow();
1250  if (DEBUG) {
1251  Log.d(LOG_TAG, "Killing background executor, returned tasks = " + tasks);
1252  }
1253  background = Executors.newSingleThreadExecutor();
1254  }
1255  });
1256 
1257  stopListener(); // This is probably hosed to, so restart
1258  if (restartListener) {
1259  startListener();
1260  }
1261  }
1262 
1277  private YailList readFile(String fileName) {
1278  try {
1279  String originalFileName = fileName;
1280  // Trim off file:// part if present
1281  if (fileName.startsWith("file://")) {
1282  fileName = fileName.substring(7);
1283  }
1284  if (!fileName.startsWith("/")) {
1285  throw new YailRuntimeError("Invalid fileName, was " + originalFileName, "ReadFrom");
1286  }
1287  String extension = getFileExtension(fileName);
1288  byte [] content = FileUtil.readFile(form, fileName);
1289  String encodedContent = Base64.encodeToString(content, Base64.DEFAULT);
1290  Object [] results = new Object[2];
1291  results[0] = "." + extension;
1292  results[1] = encodedContent;
1293  return YailList.makeList(results);
1294  } catch (FileNotFoundException e) {
1295  throw new YailRuntimeError(e.getMessage(), "Read");
1296  } catch (IOException e) {
1297  throw new YailRuntimeError(e.getMessage(), "Read");
1298  }
1299  }
1300 
1301  // Utility to get the file extension from a filename
1302  // Written by Jeff Schiller (jis) for the BinFile Extension
1303  private String getFileExtension(String fullName) {
1304  String fileName = new File(fullName).getName();
1305  int dotIndex = fileName.lastIndexOf(".");
1306  return dotIndex == -1 ? "" : fileName.substring(dotIndex + 1);
1307  }
1308 
1309  public ExecutorService getBackground() {
1310  return background;
1311  }
1312 
1313  public Object jEval(String script, String scriptsha1, int argcount, String... args) throws JedisException {
1314  Jedis jedis = getJedis();
1315  try {
1316  return jedis.evalsha(scriptsha1, argcount, args);
1317  } catch (JedisNoScriptException e) {
1318  if (DEBUG) {
1319  Log.d(LOG_TAG, "Got a JedisNoScriptException for " + scriptsha1);
1320  }
1321  // This happens if the server doesn't have the script loaded
1322  // So we use regular eval, which should then cache the script
1323  return jedis.eval(script, argcount, args);
1324  }
1325  }
1326 
1327  // We are synchronized because we are called simultaneously from two
1328  // different threads. Rather then do the work twice, the first one
1329  // does the work and the second one waits!
1330  private synchronized void ensureSslSockFactory() {
1331  if (SslSockFactory != null) {
1332  return;
1333  } else {
1334  try {
1335  CertificateFactory cf = CertificateFactory.getInstance("X.509");
1336  ByteArrayInputStream caInput = new ByteArrayInputStream(COMODO_ROOT.getBytes("UTF-8"));
1337  Certificate ca = cf.generateCertificate(caInput);
1338  caInput.close();
1339  caInput = new ByteArrayInputStream(COMODO_USRTRUST.getBytes("UTF-8"));
1340  Certificate inter = cf.generateCertificate(caInput);
1341  caInput.close();
1342  caInput = new ByteArrayInputStream(DST_ROOT_X3.getBytes("UTF-8"));
1343  Certificate dstx3 = cf.generateCertificate(caInput);
1344  caInput.close();
1345  if (DEBUG) {
1346  Log.d(LOG_TAG, "comodo=" + ((X509Certificate) ca).getSubjectDN());
1347  Log.d(LOG_TAG, "inter=" + ((X509Certificate) inter).getSubjectDN());
1348  Log.d(LOG_TAG, "dstx3=" + ((X509Certificate) dstx3).getSubjectDN());
1349  }
1350  KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
1351  keyStore.load(null, null);
1352  // First add the system trusted certificates
1353  int count = 1;
1354  for (X509Certificate cert : getSystemCertificates()) {
1355  keyStore.setCertificateEntry("root" + count, cert);
1356  count += 1;
1357  }
1358  if (DEBUG) {
1359  Log.d(LOG_TAG, "Added " + (count -1) + " system certificates!");
1360  }
1361  // Now add our additions
1362  keyStore.setCertificateEntry("comodo", ca);
1363  keyStore.setCertificateEntry("inter", inter);
1364  keyStore.setCertificateEntry("dstx3", dstx3);
1365  TrustManagerFactory tmf = TrustManagerFactory.getInstance(
1366  TrustManagerFactory.getDefaultAlgorithm());
1367  tmf.init(keyStore);
1368  // // DEBUG
1369  // Log.d(LOG_TAG, "And now for something completely different...");
1370  // X509TrustManager tm = (X509TrustManager) tmf.getTrustManagers()[0];
1371  // for (X509Certificate cert : tm.getAcceptedIssuers()) {
1372  // Log.d(LOG_TAG, cert.getSubjectX500Principal().getName());
1373  // }
1374  // // END DEBUG
1375  SSLContext ctx = SSLContext.getInstance("TLS");
1376  ctx.init(null, tmf.getTrustManagers(), null);
1377  SslSockFactory = ctx.getSocketFactory();
1378  } catch (Exception e) {
1379  Log.e(LOG_TAG, "Could not setup SSL Trust Store for CloudDB", e);
1380  throw new YailRuntimeError("Could Not setup SSL Trust Store for CloudDB: ", e.getMessage());
1381  }
1382  }
1383  }
1384 
1385  /*
1386  * Get the list of root CA's trusted by this device
1387  *
1388  */
1389  private X509Certificate[] getSystemCertificates() {
1390  try {
1391  TrustManagerFactory otmf = TrustManagerFactory.getInstance(
1392  TrustManagerFactory.getDefaultAlgorithm());
1393  otmf.init((KeyStore) null);
1394  X509TrustManager otm = (X509TrustManager) otmf.getTrustManagers()[0];
1395  return otm.getAcceptedIssuers();
1396  } catch (Exception e) {
1397  Log.e(LOG_TAG, "Getting System Certificates", e);
1398  return new X509Certificate[0];
1399  }
1400  }
1401 }
com.google.appinventor.components.runtime.EventDispatcher
Definition: EventDispatcher.java:22
com.google.appinventor.components.runtime.util.YailList
Definition: YailList.java:26
com.google.appinventor.components.annotations.SimpleFunction
Definition: SimpleFunction.java:23
com.google.appinventor.components.annotations.UsesLibraries
Definition: UsesLibraries.java:21
com.google.appinventor.components.runtime.util
-*- mode: java; c-basic-offset: 2; -*-
Definition: AccountChooser.java:7
com.google.appinventor.components.runtime.util.FileUtil
Definition: FileUtil.java:37
com.google.appinventor.components.common.YaVersion
Definition: YaVersion.java:14
com.google.appinventor.components.annotations.DesignerProperty
Definition: DesignerProperty.java:25
com.google.appinventor.components.runtime.CloudDB.getJedis
synchronized Jedis getJedis()
Definition: CloudDB.java:1219
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_STRING
static final String PROPERTY_TYPE_STRING
Definition: PropertyTypeConstants.java:237
com.google.appinventor.components.runtime.util.CloudDBJedisListener.terminate
void terminate()
Definition: CloudDBJedisListener.java:75
com.google.appinventor.components
com.google.appinventor.components.runtime.util.JsonUtil
Definition: JsonUtil.java:42
com.google.appinventor.components.runtime.CloudDB.onDestroy
void onDestroy()
Definition: CloudDB.java:332
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN
static final String PROPERTY_TYPE_BOOLEAN
Definition: PropertyTypeConstants.java:35
com.google.appinventor.components.annotations.DesignerComponent
Definition: DesignerComponent.java:22
com.google.appinventor.components.annotations.SimpleEvent
Definition: SimpleEvent.java:20
com.google.appinventor.components.annotations.PropertyCategory.BEHAVIOR
BEHAVIOR
Definition: PropertyCategory.java:15
com.google.appinventor.components.runtime.CloudDB.jEval
Object jEval(String script, String scriptsha1, int argcount, String... args)
Definition: CloudDB.java:1313
com.google.appinventor.components.runtime.CloudDB
Definition: CloudDB.java:110
com.google.appinventor.components.runtime.CloudDB.Initialize
void Initialize()
Definition: CloudDB.java:293
com.google.appinventor.components.runtime.Notifier.ShowAlert
void ShowAlert(final String notice)
Definition: Notifier.java:435
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_INTEGER
static final String PROPERTY_TYPE_INTEGER
Definition: PropertyTypeConstants.java:88
com.google.appinventor.components.annotations.UsesPermissions
Definition: UsesPermissions.java:21
com.google.appinventor.components.runtime.CloudDB.GotValue
void GotValue(String tag, Object value)
Definition: CloudDB.java:999
com.google.appinventor.components.runtime.CloudDB.getForm
Form getForm()
Definition: CloudDB.java:1167
com.google.appinventor.components.runtime.Notifier
Definition: Notifier.java:78
com.google.appinventor.components.runtime.EventDispatcher.dispatchEvent
static boolean dispatchEvent(Component component, String eventName, Object...args)
Definition: EventDispatcher.java:188
com.google.appinventor.components.runtime.OnClearListener
Definition: OnClearListener.java:16
com.google.appinventor.components.runtime.AndroidNonvisibleComponent
Definition: AndroidNonvisibleComponent.java:17
com.google.appinventor.components.runtime.CloudDB.getJedis
Jedis getJedis(boolean createNew)
Definition: CloudDB.java:1171
com.google.appinventor.components.runtime.errors.YailRuntimeError
Definition: YailRuntimeError.java:14
com.google.appinventor.components.annotations.SimpleProperty
Definition: SimpleProperty.java:23
com.google.appinventor.components.runtime.util.BulkPermissionRequest
Definition: BulkPermissionRequest.java:22
com.google.appinventor.components.annotations.PropertyCategory
Definition: PropertyCategory.java:13
com.google.appinventor.components.runtime.ComponentContainer
Definition: ComponentContainer.java:16
com.google.appinventor.components.runtime.CloudDB.CloudDB
CloudDB(ComponentContainer container)
Definition: CloudDB.java:274
com.google.appinventor.components.runtime
Copyright 2009-2011 Google, All Rights reserved.
Definition: AccelerometerSensor.java:8
com.google.appinventor.components.runtime.CloudDB.DataChanged
void DataChanged(final String tag, final Object value)
Definition: CloudDB.java:1115
com.google.appinventor.components.runtime.Component
Definition: Component.java:17
com.google.appinventor.components.common
Definition: ComponentCategory.java:7
com.google.appinventor.components.common.ComponentCategory
Definition: ComponentCategory.java:48
com.google.appinventor.components.runtime.util.JsonUtil.getObjectFromJson
static Object getObjectFromJson(String jsonString)
Definition: JsonUtil.java:317
com.google
com
com.google.appinventor.components.runtime.errors
Definition: ArrayIndexOutOfBoundsError.java:7
com.google.appinventor.components.runtime.CloudDB.onClear
void onClear()
Definition: CloudDB.java:323
com.google.appinventor.components.runtime.ComponentContainer.$form
Form $form()
com.google.appinventor.components.runtime.ComponentContainer.$context
Activity $context()
com.google.appinventor.components.runtime.util.JsonUtil.getJsonRepresentation
static String getJsonRepresentation(Object value)
Definition: JsonUtil.java:246
com.google.appinventor.components.runtime.Form
Definition: Form.java:126
com.google.appinventor.components.runtime.util.CloudDBJedisListener
Definition: CloudDBJedisListener.java:22
com.google.appinventor.components.common.PropertyTypeConstants
Definition: PropertyTypeConstants.java:14
com.google.appinventor.components.runtime.CloudDB.getBackground
ExecutorService getBackground()
Definition: CloudDB.java:1309
com.google.appinventor.components.runtime.OnDestroyListener
Definition: OnDestroyListener.java:15
com.google.appinventor.components.annotations
com.google.appinventor