6 package com.google.appinventor.components.runtime;
8 import android.Manifest;
9 import android.app.Activity;
11 import android.database.Cursor;
12 import android.database.sqlite.SQLiteDatabase;
14 import android.net.ConnectivityManager;
15 import android.net.NetworkInfo;
17 import android.os.Handler;
19 import android.util.Base64;
20 import android.util.Log;
43 import java.io.ByteArrayInputStream;
45 import java.io.FileNotFoundException;
46 import java.io.IOException;
48 import java.security.KeyStore;
49 import java.security.cert.Certificate;
50 import java.security.cert.CertificateFactory;
51 import java.security.cert.X509Certificate;
53 import java.util.ArrayList;
54 import java.util.Collections;
55 import java.util.List;
57 import java.util.concurrent.ExecutorService;
58 import java.util.concurrent.Executors;
59 import java.util.concurrent.atomic.AtomicReference;
61 import javax.net.ssl.SSLContext;
62 import javax.net.ssl.SSLSocketFactory;
63 import javax.net.ssl.TrustManagerFactory;
64 import javax.net.ssl.X509TrustManager;
66 import org.json.JSONArray;
67 import org.json.JSONException;
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;
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,
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")
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;
119 private volatile boolean dead =
false;
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";
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";
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";
213 private String defaultRedisServer =
null;
214 private boolean useDefault =
true;
216 private Handler androidUIHandler;
217 private final Activity activity;
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;
226 private SSLSocketFactory SslSockFactory =
null;
230 private volatile boolean listenerRunning =
false;
238 private volatile ExecutorService background = Executors.newSingleThreadExecutor();
246 private final List<storedValue> storeQueue = Collections.synchronizedList(
new ArrayList());
248 private ConnectivityManager cm;
251 private boolean havePermission =
false;
253 private static class storedValue {
255 private JSONArray valueList;
256 storedValue(String tag, JSONArray valueList) {
258 this.valueList = valueList;
261 public String getTag() {
265 public JSONArray getValueList() {
275 super(container.
$form());
280 androidUIHandler =
new Handler();
281 this.activity = container.
$context();
287 cm = (ConnectivityManager) form.$context().getSystemService(android.content.Context.CONNECTIVITY_SERVICE);
295 Log.d(LOG_TAG,
"Initalize called!");
297 if (currentListener ==
null) {
300 form.registerForOnClear(
this);
302 form.registerForOnDestroy(
this);
305 private void stopListener() {
309 Log.d(LOG_TAG,
"Listener stopping!");
311 if (currentListener !=
null) {
313 currentListener =
null;
314 listenerRunning =
false;
327 Log.d(LOG_TAG,
"onClear() called");
334 Log.d(LOG_TAG,
"onDestroy() called");
339 private synchronized void startListener() {
343 if (listenerRunning) {
345 Log.d(LOG_TAG,
"StartListener while already running, no action taken");
349 listenerRunning =
true;
351 Log.d(LOG_TAG,
"Listener starting!");
353 Thread t =
new Thread() {
355 Jedis jedis = getJedis(
true);
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);
364 }
catch (Exception ee) {
368 Log.d(LOG_TAG,
"Listener: connection to Redis failed, sleeping 3 seconds.");
371 Thread.sleep(3*1000);
372 }
catch (InterruptedException ee) {
375 Log.d(LOG_TAG,
"Woke up!");
380 Log.d(LOG_TAG,
"Listener: getJedis(true) returned null, retry in 3...");
383 Thread.sleep(3*1000);
384 }
catch (InterruptedException e) {
387 Log.d(LOG_TAG,
"Woke up! (2)");
390 listenerRunning =
false;
391 if (!dead && !shutdown) {
395 Log.d(LOG_TAG,
"We are dead, listener not retrying.");
403 @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING,
404 defaultValue =
"DEFAULT")
405 public
void RedisServer(String servername) {
406 if (servername.equals(
"DEFAULT")) {
409 if (defaultRedisServer ==
null) {
411 Log.d(LOG_TAG,
"RedisServer called before defaultServer (should not happen!)");
414 redisServer = defaultRedisServer;
420 if (!servername.equals(redisServer)) {
421 redisServer = servername;
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)) {
446 description =
"The Default Redis Server to use.",
448 public
void DefaultRedisServer(String server) {
449 defaultRedisServer = server;
451 redisServer = server;
456 defaultValue =
"6381")
457 public
void RedisPort(
int port) {
458 if (port != redisPort) {
465 description =
"The Redis Server port to use. Defaults to 6381")
466 public
int RedisPort() {
476 description =
"Gets the ProjectID for this CloudDB project.")
477 public String ProjectID() {
478 checkProjectIDNotBlank();
489 public
void ProjectID(String
id) {
490 if (!projectID.equals(
id)) {
493 if (projectID.equals(
"")){
494 throw new RuntimeException(
"CloudDB ProjectID property cannot be blank.");
505 public
void Token(String authToken) {
506 if (!token.equals(authToken)) {
509 if (token.equals(
"")){
510 throw new RuntimeException(
"CloudDB Token property cannot be blank.");
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();
545 defaultValue =
"True")
546 public
void UseSSL(
boolean useSSL) {
547 if (this.useSSL != useSSL) {
548 this.useSSL = useSSL;
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() {
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);";
571 private static final String SET_SUB_SCRIPT_SHA1 =
"765978e4c340012f50733280368a0ccc4a14dfb7";
582 public
void StoreValue(final String tag, final Object valueToStore) {
583 checkProjectIDNotBlank();
584 if (!havePermission) {
587 Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) {
589 public void onGranted() {
590 me.havePermission = true;
591 StoreValue(tag, valueToStore);
597 NetworkInfo networkInfo = cm.getActiveNetworkInfo();
598 boolean isConnected = networkInfo !=
null && networkInfo.isConnected();
601 if (valueToStore !=
null) {
602 String strval = valueToStore.toString();
603 if (strval.startsWith(
"file:///") || strval.startsWith(
"/storage")) {
606 value = JsonUtil.getJsonRepresentation(valueToStore);
611 }
catch(JSONException e) {
612 throw new YailRuntimeError(
"Value failed to convert to JSON.",
"JSON Creation Error.");
617 Log.d(LOG_TAG,
"Device is online...");
619 synchronized(storeQueue) {
620 boolean kickit =
false;
621 if (storeQueue.size() == 0) {
623 Log.d(LOG_TAG,
"storeQueue is zero length, kicking background");
628 Log.d(LOG_TAG,
"storeQueue has " + storeQueue.size() +
" entries");
631 JSONArray valueList =
new JSONArray();
633 valueList.put(0, value);
634 }
catch (JSONException e) {
635 throw new YailRuntimeError(
"JSON Error putting value.",
"value is not convertable");
637 storedValue work =
new storedValue(tag, valueList);
638 storeQueue.add(work);
640 background.submit(
new Runnable() {
642 JSONArray pendingValueList =
null;
643 String pendingTag =
null;
644 String pendingValue =
null;
648 Log.d(LOG_TAG,
"store background task running.");
651 synchronized(storeQueue) {
653 Log.d(LOG_TAG,
"store: In background synchronized block");
655 int size = storeQueue.size();
658 Log.d(LOG_TAG,
"store background task exiting.");
663 Log.d(LOG_TAG,
"store: storeQueue.size() == " + size);
665 work = storeQueue.remove(0);
667 Log.d(LOG_TAG,
"store: got work.");
672 Log.d(LOG_TAG,
"store: left synchronized block");
676 if (pendingTag !=
null) {
677 String jsonValueList = pendingValueList.toString();
679 Log.d(LOG_TAG,
"Workqueue empty, sending pendingTag, valueListLength = " + pendingValueList.length());
681 jEval(SET_SUB_SCRIPT, SET_SUB_SCRIPT_SHA1, 1, pendingTag, pendingValue, jsonValueList, projectID);
683 }
catch (JedisException e) {
684 CloudDBError(e.getMessage());
690 String tag = work.getTag();
691 JSONArray valueList = work.getValueList();
692 if (tag ==
null || valueList ==
null) {
694 Log.d(LOG_TAG,
"Either tag or value is null!");
698 Log.d(LOG_TAG,
"Got Work: tag = " + tag +
" value = " + valueList.get(0));
701 if (pendingTag ==
null) {
703 pendingValueList = valueList;
704 pendingValue = valueList.getString(0);
705 }
else if (pendingTag.equals(tag)) {
706 pendingValue = valueList.getString(0);
707 pendingValueList.put(pendingValue);
710 String jsonValueList = pendingValueList.toString();
712 Log.d(LOG_TAG,
"pendingTag changed sending pendingTag, valueListLength = " + pendingValueList.length());
714 jEval(SET_SUB_SCRIPT, SET_SUB_SCRIPT_SHA1, 1, pendingTag, pendingValue, jsonValueList, projectID);
715 }
catch (JedisException e) {
716 CloudDBError(e.getMessage());
722 pendingValueList = valueList;
723 pendingValue = valueList.getString(0);
726 }
catch (Exception e) {
727 Log.e(LOG_TAG,
"Exception in store worker!", e);
734 CloudDBError(
"Cannot store values off-line.");
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) {
753 Log.d(LOG_TAG,
"getting value ... for tag: " + tag);
755 checkProjectIDNotBlank();
756 if (!havePermission) {
759 Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) {
761 public void onGranted() {
762 me.havePermission = true;
763 GetValue(tag, valueIfTagNotThere);
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();
777 background.submit(
new Runnable() {
779 Jedis jedis = getJedis();
782 Log.d(LOG_TAG,
"about to call jedis.get()");
784 String returnValue = jedis.get(projectID +
":" + tag);
786 Log.d(LOG_TAG,
"finished call jedis.get()");
788 if (returnValue !=
null) {
789 String val = JsonUtil.getJsonRepresentationIfValueFileName(form, returnValue);
790 if(val !=
null) value.set(val);
791 else value.set(returnValue);
795 Log.d(CloudDB.LOG_TAG,
"Value retrieved is null");
797 value.set(JsonUtil.getJsonRepresentation(valueIfTagNotThere));
799 }
catch (JSONException e) {
800 CloudDBError(
"JSON conversion error for " + tag);
802 }
catch (NullPointerException e) {
803 CloudDBError(
"System Error getting tag " + tag);
806 }
catch (JedisException e) {
807 Log.e(LOG_TAG,
"Exception in GetValue", e);
808 CloudDBError(e.getMessage());
811 }
catch (Exception e) {
812 Log.e(LOG_TAG,
"Exception in GetValue", e);
813 CloudDBError(e.getMessage());
818 androidUIHandler.post(
new Runnable() {
823 GotValue(tag, value.get());
830 Log.d(LOG_TAG,
"GetValue(): We're offline");
832 CloudDBError(
"Cannot fetch variables while off-line.");
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();
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 " +
860 public
void FirstRemoved(Object value) {
862 Log.d(
CloudDB.LOG_TAG,
"FirstRemoved: Value = " + value);
864 checkProjectIDNotBlank();
866 if(value !=
null && value instanceof String) {
869 }
catch (JSONException e) {
870 Log.e(
CloudDB.LOG_TAG,
"error while converting to JSON...",e);
873 final Object sValue = value;
874 androidUIHandler.post(
new Runnable() {
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);" +
899 " return error('You can only remove elements from a list');" +
902 private static final String POP_FIRST_SCRIPT_SHA1 =
"ed4cb4717d157f447848fe03524da24e461028e1";
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();
919 final String key = tag;
921 background.submit(
new Runnable() {
923 Jedis jedis = getJedis();
925 FirstRemoved(jEval(POP_FIRST_SCRIPT, POP_FIRST_SCRIPT_SHA1, 1, key, projectID));
926 }
catch (JedisException e) {
927 CloudDBError(e.getMessage());
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);" +
940 "local subTable = {};" +
941 "local subTable1 = {};" +
942 "if (currentValue == false) then " +
945 " newTable = cjson.decode(currentValue);" +
946 " if not (type(newTable) == 'table') then " +
947 " return error('You can only append to a list');" +
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));" +
959 private static final String APPEND_SCRIPT_SHA1 =
"d6cc0f65b29878589f00564d52c8654967e9bcf8";
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 " +
964 public
void AppendValueToList(final String tag, final Object itemToAdd) {
965 checkProjectIDNotBlank();
967 Object itemObject =
new Object();
969 if(itemToAdd !=
null) {
972 }
catch(JSONException e) {
973 throw new YailRuntimeError(
"Value failed to convert to JSON.",
"JSON Creation Error.");
976 final String item = (String) itemObject;
977 final String key = tag;
979 background.submit(
new Runnable() {
981 Jedis jedis = getJedis();
983 jEval(APPEND_SCRIPT, APPEND_SCRIPT_SHA1, 1, key, item, projectID);
984 }
catch(JedisException e) {
985 CloudDBError(e.getMessage());
1001 Log.d(
CloudDB.LOG_TAG,
"GotValue: tag = " + tag +
" value = " + (String) value);
1003 checkProjectIDNotBlank();
1007 if (value ==
null) {
1008 CloudDBError(
"Trouble getting " + tag +
" from the server.");
1014 Log.d(LOG_TAG,
"GotValue: Class of value = " + value.getClass().getName());
1016 if(value !=
null && value instanceof String) {
1019 }
catch(JSONException e) {
1020 throw new YailRuntimeError(
"Value failed to convert from JSON.",
"JSON Retrieval Error.");
1036 public
void ClearTag(final String tag) {
1037 checkProjectIDNotBlank();
1038 background.submit(
new Runnable() {
1041 Jedis jedis = getJedis();
1042 jedis.del(projectID +
":" + tag);
1043 }
catch (Exception e) {
1044 CloudDBError(e.getMessage());
1055 @
SimpleFunction(description =
"Get the list of tags for this application. " +
1056 "When complete a \"TagList\" event will be triggered with the list of " +
1058 public
void GetTagList() {
1059 checkProjectIDNotBlank();
1060 NetworkInfo networkInfo = cm.getActiveNetworkInfo();
1061 boolean isConnected = networkInfo !=
null && networkInfo.isConnected();
1063 background.submit(
new Runnable() {
1066 Jedis jedis = getJedis();
1067 Set<String> value =
null;
1069 value = jedis.keys(projectID +
":*");
1070 }
catch (JedisException e) {
1071 CloudDBError(e.getMessage());
1075 final List<String> listValue =
new ArrayList<String>(value);
1077 for(
int i = 0; i < listValue.size(); i++){
1078 listValue.set(i, listValue.get(i).substring((projectID +
":").length()));
1081 androidUIHandler.post(
new Runnable() {
1090 CloudDBError(
"Not connected to the Internet, cannot list tags");
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();
1116 Object tagValue =
"";
1118 if(value !=
null && value instanceof String) {
1121 }
catch(JSONException e) {
1122 throw new YailRuntimeError(
"Value failed to convert from JSON.",
"JSON Retrieval Error.");
1124 final Object finalTagValue = tagValue;
1126 androidUIHandler.post(
new Runnable() {
1139 @
SimpleEvent(description =
"Indicates that an error occurred while communicating " +
1140 "with the CloudDB Redis server.")
1141 public
void CloudDBError(final String message) {
1143 Log.e(LOG_TAG, message);
1144 androidUIHandler.post(
new Runnable() {
1158 private void checkProjectIDNotBlank(){
1159 if (projectID.equals(
"")){
1160 throw new RuntimeException(
"CloudDB ProjectID property cannot be blank.");
1162 if(token.equals(
"")){
1163 throw new RuntimeException(
"CloudDB Token property cannot be blank");
1178 Log.d(LOG_TAG,
"getJedis(true): Attempting a new connection (createNew = " +
1179 createNew +
" redisServer = " + redisServer +
" redisPort = " +
1180 redisPort +
" useSSL = " +
1186 ensureSslSockFactory();
1187 jedis =
new Jedis(redisServer, redisPort,
true, SslSockFactory,
null,
null);
1189 jedis =
new Jedis(redisServer, redisPort,
false);
1192 Log.d(LOG_TAG,
"getJedis(true): Have new connection.");
1197 if (token.substring(0, 1).equals(
"%")) {
1198 jedis.auth(token.substring(1));
1203 Log.d(LOG_TAG,
"getJedis(true): Authentication complete.");
1205 }
catch (JedisConnectionException e) {
1206 Log.e(LOG_TAG,
"in getJedis()", e);
1207 CloudDBError(e.getMessage());
1209 }
catch (JedisDataException e) {
1211 Log.e(LOG_TAG,
"in getJedis()", e);
1212 CloudDBError(e.getMessage() +
" CloudDB disabled, restart to re-enable.");
1220 if (INSTANCE ==
null) {
1221 INSTANCE = getJedis(
true);
1233 private void flushJedis(
boolean restartListener) {
1234 if (INSTANCE ==
null) {
1240 }
catch (Exception e) {
1247 androidUIHandler.post(
new Runnable() {
1249 List <Runnable> tasks = background.shutdownNow();
1251 Log.d(LOG_TAG,
"Killing background executor, returned tasks = " + tasks);
1253 background = Executors.newSingleThreadExecutor();
1258 if (restartListener) {
1277 private YailList readFile(String fileName) {
1279 String originalFileName = fileName;
1281 if (fileName.startsWith(
"file://")) {
1282 fileName = fileName.substring(7);
1284 if (!fileName.startsWith(
"/")) {
1285 throw new YailRuntimeError(
"Invalid fileName, was " + originalFileName,
"ReadFrom");
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");
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);
1313 public Object
jEval(String script, String scriptsha1,
int argcount, String... args)
throws JedisException {
1314 Jedis jedis = getJedis();
1316 return jedis.evalsha(scriptsha1, argcount, args);
1317 }
catch (JedisNoScriptException e) {
1319 Log.d(LOG_TAG,
"Got a JedisNoScriptException for " + scriptsha1);
1323 return jedis.eval(script, argcount, args);
1330 private synchronized void ensureSslSockFactory() {
1331 if (SslSockFactory !=
null) {
1335 CertificateFactory cf = CertificateFactory.getInstance(
"X.509");
1336 ByteArrayInputStream caInput =
new ByteArrayInputStream(COMODO_ROOT.getBytes(
"UTF-8"));
1337 Certificate ca = cf.generateCertificate(caInput);
1339 caInput =
new ByteArrayInputStream(COMODO_USRTRUST.getBytes(
"UTF-8"));
1340 Certificate inter = cf.generateCertificate(caInput);
1342 caInput =
new ByteArrayInputStream(DST_ROOT_X3.getBytes(
"UTF-8"));
1343 Certificate dstx3 = cf.generateCertificate(caInput);
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());
1350 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
1351 keyStore.load(
null,
null);
1354 for (X509Certificate cert : getSystemCertificates()) {
1355 keyStore.setCertificateEntry(
"root" + count, cert);
1359 Log.d(LOG_TAG,
"Added " + (count -1) +
" system certificates!");
1362 keyStore.setCertificateEntry(
"comodo", ca);
1363 keyStore.setCertificateEntry(
"inter", inter);
1364 keyStore.setCertificateEntry(
"dstx3", dstx3);
1365 TrustManagerFactory tmf = TrustManagerFactory.getInstance(
1366 TrustManagerFactory.getDefaultAlgorithm());
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());
1389 private X509Certificate[] getSystemCertificates() {
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];