6 package com.google.appinventor.components.runtime;
9 import android.app.Activity;
10 import android.os.Handler;
11 import android.util.Log;
13 import com.firebase.client.AuthData;
14 import com.firebase.client.ChildEventListener;
15 import com.firebase.client.Config;
16 import com.firebase.client.DataSnapshot;
17 import com.firebase.client.Firebase;
18 import com.firebase.client.FirebaseError;
19 import com.firebase.client.MutableData;
20 import com.firebase.client.Transaction;
21 import com.firebase.client.ValueEventListener;
40 import java.util.concurrent.atomic.AtomicReference;
41 import java.util.ArrayList;
42 import java.util.HashMap;
43 import java.util.List;
45 import org.json.JSONException;
62 @DesignerComponent(version = YaVersion.FIREBASE_COMPONENT_VERSION,
63 description =
"Non-visible component that communicates with Firebase to store and " +
64 "retrieve information.",
65 designerHelpDescription =
"Non-visible component that communicates with a Firebase" +
66 " to store and retrieve information.",
67 category = ComponentCategory.EXPERIMENTAL,
70 iconName =
"images/firebaseDB.png")
72 @UsesPermissions(permissionNames =
"android.permission.INTERNET")
73 @UsesLibraries(libraries =
"firebase.jar")
76 private static final String LOG_TAG =
"Firebase";
77 private String firebaseURL =
null;
78 private String defaultURL =
null;
79 private boolean useDefault =
true;
80 private String developerBucket;
81 private String projectBucket;
82 private String firebaseToken;
85 private static boolean isInitialized =
false;
87 private static boolean persist =
false;
90 private Handler androidUIHandler;
91 private final Activity activity;
92 private Firebase myFirebase;
93 private ChildEventListener childListener;
94 private Firebase.AuthStateListener authListener;
98 private static class ReturnVal {
108 private abstract static class Transactional {
111 final ReturnVal retv;
113 Transactional(Object arg1, Object arg2, ReturnVal retv) {
119 abstract Transaction.Result run(MutableData currentData);
121 ReturnVal getResult() {
133 super(container.
$form());
138 androidUIHandler =
new Handler();
139 this.activity = container.
$context();
140 Firebase.setAndroidContext(activity);
142 developerBucket =
"";
146 childListener =
new ChildEventListener() {
149 public void onChildAdded(
final DataSnapshot snapshot, String previousChildKey) {
150 androidUIHandler.post(
new Runnable() {
154 DataChanged(snapshot.getKey(), snapshot.getValue());
160 public void onCancelled(
final FirebaseError error) {
161 androidUIHandler.post(
new Runnable() {
165 FirebaseError(error.getMessage());
171 public void onChildChanged(
final DataSnapshot snapshot, String previousChildKey) {
172 androidUIHandler.post(
new Runnable() {
176 DataChanged(snapshot.getKey(), snapshot.getValue());
182 public void onChildMoved(DataSnapshot snapshot, String previousChildKey) {
186 public void onChildRemoved(
final DataSnapshot snapshot) {
187 Log.i(LOG_TAG,
"onChildRemoved: " + snapshot.getKey() +
" removed.");
202 authListener =
new Firebase.AuthStateListener() {
204 public void onAuthStateChanged(AuthData data) {
205 Log.i(LOG_TAG,
"onAuthStateChanged: data = " + data);
207 myFirebase.authWithCustomToken(firebaseToken,
new Firebase.AuthResultHandler() {
209 public void onAuthenticated(AuthData authData) {
210 Log.i(LOG_TAG,
"Auth Successful.");
214 public void onAuthenticationError(FirebaseError error) {
215 Log.e(LOG_TAG,
"Auth Failed: " + error.getMessage());
236 Log.i(LOG_TAG,
"Initalize called!");
237 isInitialized =
true;
247 description =
"Gets the URL for this FirebaseDB.",
249 public String FirebaseURL() {
266 defaultValue =
"DEFAULT")
267 @
SimpleProperty(description =
"Sets the URL for this FirebaseDB.")
268 public
void FirebaseURL(String url) {
269 if (url.equals(
"DEFAULT")) {
272 if (defaultURL ==
null) {
273 Log.d(LOG_TAG,
"FirebaseURL called before DefaultURL (should not happen!)");
275 firebaseURL = defaultURL;
279 firebaseURL = defaultURL;
283 url = url + (url.endsWith(
"/") ?
"" :
"/");
285 if (firebaseURL.equals(url)) {
308 developerBucket = bucket;
318 public String DeveloperBucket() {
319 return developerBucket;
329 @
SimpleProperty(description =
"Sets the ProjectBucket for this FirebaseDB.")
330 public
void ProjectBucket(String bucket) {
331 if (!projectBucket.equals(bucket)) {
332 projectBucket = bucket;
343 description =
"Gets the ProjectBucket for this FirebaseDB.")
344 public String ProjectBucket() {
345 return projectBucket;
367 public String FirebaseToken() {
368 return firebaseToken;
372 defaultValue =
"False")
374 description =
"If true, variables will retain their values when off-line and the App " +
375 "exits. Values will be uploaded to Firebase the next time the App is " +
376 "run while connected to the network. This is useful for applications " +
377 "which will gather data while not connected to the network. Note: " +
378 "AppendValue and RemoveFirst will not work correctly when off-line, " +
379 "they require a network connection.<br/><br/> " +
380 "<i>Note</i>: If you set Persist on any Firebase component, on any " +
381 "screen, it makes all Firebase components on all screens persistent. " +
382 "This is a limitation of the low level Firebase library. Also be " +
383 "aware that if you want to set persist to true, you should do so " +
384 "before connecting the Companion for incremental development.")
385 public
void Persist(
boolean value) {
386 Log.i(LOG_TAG,
"Persist Called: Value = " + value);
387 if (persist != value) {
389 throw new RuntimeException(
"You cannot change the Persist value of Firebase " +
390 "after Application Initialization, this includes the Companion");
392 Config config = Firebase.getDefaultConfig();
393 config.setPersistenceEnabled(value);
394 Firebase.setDefaultConfig(config);
400 private void resetListener() {
403 if (!isInitialized) {
407 if (myFirebase !=
null) {
408 myFirebase.removeEventListener(childListener);
409 myFirebase.removeAuthStateListener(authListener);
448 @SimpleFunction(description =
"Remove the tag from Firebase")
449 public
void ClearTag(final String tag) {
450 this.myFirebase.child(tag).removeValue();
461 public void StoreValue(
final String tag, Object valueToStore) {
463 if(valueToStore !=
null) {
466 }
catch(JSONException e) {
467 throw new YailRuntimeError(
"Value failed to convert to JSON.",
"JSON Creation Error.");
471 this.myFirebase.child(tag).setValue(valueToStore);
484 public void GetValue(
final String tag,
final Object valueIfTagNotThere) {
485 this.myFirebase.child(tag).addListenerForSingleValueEvent(
new ValueEventListener() {
487 public void onDataChange(
final DataSnapshot snapshot) {
488 final AtomicReference<Object> value =
new AtomicReference<Object>();
493 if (snapshot.exists()) {
494 value.set(snapshot.getValue());
498 }
catch(JSONException e) {
499 throw new YailRuntimeError(
"Value failed to convert to JSON.",
"JSON Creation Error.");
502 androidUIHandler.post(
new Runnable() {
507 GotValue(tag, value.get());
513 public void onCancelled(
final FirebaseError error) {
514 androidUIHandler.post(
new Runnable() {
519 FirebaseError(error.getMessage());
535 if(value !=
null && value instanceof String) {
538 }
catch(JSONException e) {
539 throw new YailRuntimeError(
"Value failed to convert from JSON.",
"JSON Retrieval Error.");
556 if(value !=
null && value instanceof String) {
559 }
catch(JSONException e) {
560 throw new YailRuntimeError(
"Value failed to convert from JSON.",
"JSON Retrieval Error.");
575 Log.e(LOG_TAG, message);
585 private void connectFirebase() {
588 "Android Too Old",
"OK");
592 myFirebase =
new Firebase(firebaseURL +
"developers/" + developerBucket + projectBucket);
594 myFirebase =
new Firebase(firebaseURL + projectBucket);
597 myFirebase.addChildEventListener(childListener);
598 myFirebase.addAuthStateListener(authListener);
621 @SimpleFunction(description =
"If you are having difficulty with the Companion and you " +
622 "are switching between different Firebase accounts, you may need to use this function " +
623 "to clear internal Firebase caches. You can just use the \"Do It\" function on this block " +
624 "in the blocks editor. Note: You should not normally need to use this block as part of " +
626 public
void Unauthenticate() {
627 if (myFirebase ==
null) {
643 public
void DefaultURL(String url) {
646 firebaseURL = defaultURL;
651 @
SimpleFunction(description =
"Return the first element of a list and atomically remove it. " +
652 "If two devices use this function simultaneously, one will get the first element and the " +
653 "the other will get the second element, or an error if there is no available element. " +
654 "When the element is available, the \"FirstRemoved\" event will be triggered.")
655 public
void RemoveFirst(final String tag) {
656 final ReturnVal result =
new ReturnVal();
657 Firebase firebaseChild = myFirebase.child(tag);
658 Transactional toRun =
new Transactional(
null,
null, result) {
660 Transaction.Result run(MutableData currentData) {
661 Object value = currentData.getValue();
663 result.err =
"Previous value was empty.";
664 return Transaction.abort();
667 if (value instanceof String) {
670 result.err =
"Invalid JSON object in database (shouldn't happen!)";
671 return Transaction.abort();
673 }
catch (JSONException e) {
674 result.err =
"Invalid JSON object in database (shouldn't happen!)";
675 return Transaction.abort();
677 if (value instanceof List) {
678 if (((List)value).isEmpty()) {
679 result.err =
"The list was empty";
680 return Transaction.abort();
682 result.retval = ((List)value).remove(0);
685 }
catch (JSONException e) {
686 result.err =
"Could not convert value to JSON.";
687 return Transaction.abort();
689 currentData.setValue(value);
690 return Transaction.success(currentData);
692 result.err =
"You can only remove elements from a list.";
693 return Transaction.abort();
697 firebaseTransaction(toRun, firebaseChild,
new Runnable() {
700 FirstRemoved(result.getRetval());
705 @
SimpleFunction(description =
"Get the list of tags for this application. " +
706 "When complete a \"TagList\" event will be triggered with the list of " +
708 public
void GetTagList() {
709 Firebase zFireBase = myFirebase.child(
"");
710 zFireBase.addListenerForSingleValueEvent(
new ValueEventListener() {
712 public void onDataChange(DataSnapshot data) {
713 Object value = data.getValue();
714 if (value instanceof HashMap) {
715 value =
new ArrayList(((HashMap)value).keySet());
716 final List listValue = (List) value;
717 androidUIHandler.post(
new Runnable() {
726 public void onCancelled(FirebaseError error) {
732 @
SimpleEvent(description =
"Event triggered when we have received the list of known tags. " +
733 "Used with the \"GetTagList\" Function.")
734 public
void TagList(List value) {
738 @
SimpleEvent(description =
"Event triggered by the \"RemoveFirst\" function. The " +
739 "argument \"value\" is the object that was the first in the list, and which is now " +
741 public
void FirstRemoved(Object value) {
745 @
SimpleFunction(description =
"Append a value to the end of a list atomically. " +
746 "If two devices use this function simultaneously, both will be appended and no " +
748 public
void AppendValue(final String tag, final Object valueToAdd) {
749 final ReturnVal result =
new ReturnVal();
750 Firebase firebaseChild = myFirebase.child(tag);
751 Transactional toRun =
new Transactional(
null,
null, result) {
753 Transaction.Result run(MutableData currentData) {
754 Object value = currentData.getValue();
756 result.err =
"Previous value was empty.";
757 return Transaction.abort();
760 if (value instanceof String) {
763 result.err =
"Invalid JSON object in database (shouldn't happen!)";
764 return Transaction.abort();
766 }
catch (JSONException e) {
767 result.err =
"Invalid JSON object in database (shouldn't happen!)";
768 return Transaction.abort();
770 if (value instanceof List) {
771 ((List)value).add(valueToAdd);
774 }
catch (JSONException e) {
775 result.err =
"Could not convert value to JSON.";
776 return Transaction.abort();
778 currentData.setValue(value);
779 return Transaction.success(currentData);
781 result.err =
"You can only append to a list.";
782 return Transaction.abort();
786 firebaseTransaction(toRun, firebaseChild,
null);
789 private void firebaseTransaction(
final Transactional toRun, Firebase firebase,
final Runnable whenDone) {
790 final ReturnVal result = toRun.getResult();
791 firebase.runTransaction(
new Transaction.Handler() {
793 public Transaction.Result doTransaction(MutableData currentData) {
794 return toRun.run(currentData);
798 public void onComplete(
final FirebaseError firebaseError,
boolean committed,
799 DataSnapshot currentData) {
800 if (firebaseError !=
null) {
801 androidUIHandler.post(
new Runnable() {
804 Log.i(LOG_TAG,
"AppendValue(onComplete): firebase: " + firebaseError.getMessage());
805 Log.i(LOG_TAG,
"AppendValue(onComplete): result.err: " + result.err);
806 FirebaseError(firebaseError.getMessage());
812 androidUIHandler.post(
new Runnable() {
815 Log.i(LOG_TAG,
"AppendValue(!committed): result.err: " + result.err);
816 FirebaseError(result.err);
820 if (whenDone !=
null) {
821 androidUIHandler.post(whenDone);