Commit c212e344 authored by Jeffrey Schiller's avatar Jeffrey Schiller Committed by Evan W. Patton

Add “Shared Backpack”

A shared backpack is enabled at SSO login time[1] by providing a
backPackId parameter in the SSO Token. Once enabled backpack contents
are not stored with the user account but instead in a specific location
defined by the backPackId. Multiple users can use the same shared
backpack at the same time. Each time the shared backpack is opened a
fresh copy is loaded from the server.

There is still a race condition if two users attempt to store blocks at
the same time. One will win and the other’s changes will not be added to
the backpack. This may be corrected or mitigated in a future release.

[1] Note: SSO Login is not currently implemented in the App Engine
version of MIT App Inventor. This commit updates the Backpack code to be
in alignment with the code we use in our Hong Kong deployment which is
not run in App Engine. We plan to implement SSO login in the App Engine
version in the near future.

Change-Id: I5957a42999ff14d7263a47993efaf8f3492c016g
parent e3a504dc
......@@ -737,6 +737,18 @@ public class Ode implements EntryPoint {
user = result.getUser();
isReadOnly = user.isReadOnly();
// load the user's backpack if we are not using a shared
// backpack
String backPackId = user.getBackpackId();
if (backPackId == null || backPackId.isEmpty()) {
loadBackpack();
OdeLog.log("backpack: No shared backpack");
} else {
BlocklyPanel.setSharedBackpackId(backPackId);
OdeLog.log("Have a shared backpack backPackId = " + backPackId);
}
// Setup noop timer (if enabled)
int noop = config.getNoop();
if (noop > 0) {
......@@ -877,20 +889,6 @@ public class Ode implements EntryPoint {
userInfoService.getSystemConfig(sessionId, callback);
// We fetch the user's backpack here. This runs asynchronously with the rest
// of the system initialization.
userInfoService.getUserBackpack(new AsyncCallback<String>() {
@Override
public void onSuccess(String backpack) {
BlocklyPanel.setInitialBackpack(backpack);
}
@Override
public void onFailure(Throwable caught) {
OdeLog.log("Fetching backpack failed");
}
});
History.addValueChangeHandler(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
......@@ -2420,6 +2418,21 @@ public class Ode implements EntryPoint {
}
}
// Load the user's backpack. This is not called if we are using
// a shared backpack
private void loadBackpack() {
userInfoService.getUserBackpack(new AsyncCallback<String>() {
@Override
public void onSuccess(String backpack) {
BlocklyPanel.setInitialBackpack(backpack);
}
@Override
public void onFailure(Throwable caught) {
OdeLog.log("Fetching backpack failed");
}
});
}
// Native code to set the top level rendezvousServer variable
// where blockly code can easily find it.
private native void setRendezvousServer(String server) /*-{
......
......@@ -504,6 +504,49 @@ public class BlocklyPanel extends HTMLPanel {
Ode.getUserSettings().saveSettings(null);
}
/**
* Fetch a shared backpack from the server, call the callback with the
* backpack content.
*
* @param backPackId the backpack id
* @param callback callback to call with the backpack contents
*/
public static void getSharedBackpack(String backPackId, final JavaScriptObject callback) {
Ode.getInstance().getUserInfoService().getSharedBackpack(backPackId,
new AsyncCallback<String>() {
@Override
public void onSuccess(String content) {
doCallBack(callback, content);
}
@Override
public void onFailure(Throwable caught) {
OdeLog.log("getSharedBackpack failed.");
}
});
}
/**
* Store shared backpack to the server.
*
* @param backPackId the backpack id
* @param content the contents to store (XML String)
*/
public static void storeSharedBackpack(String backPackId, String content) {
Ode.getInstance().getUserInfoService().storeSharedBackpack(backPackId, content,
new AsyncCallback<Void>() {
@Override
public void onSuccess(Void v) {
// Nothing to do
}
@Override
public void onFailure(Throwable caught) {
OdeLog.log("storeSharedBackpack failed.");
}
});
}
// ------------ Native methods ------------
/**
......@@ -511,9 +554,11 @@ public class BlocklyPanel extends HTMLPanel {
* and call it.
*
* @param callback the Javascript callback.
* @param arg argument to the callback
*/
private static native void doCallBack(JavaScriptObject callback, String buttonName) /*-{
callback.call(null, buttonName);
private static native void doCallBack(JavaScriptObject callback, String arg) /*-{
callback.call(null, arg);
}-*/;
private static native void exportMethodsToJavascript() /*-{
......@@ -554,6 +599,11 @@ public class BlocklyPanel extends HTMLPanel {
$entry(@com.google.appinventor.client.editor.youngandroid.BlocklyPanel::getSnapEnabled());
$wnd.BlocklyPanel_saveUserSettings =
$entry(@com.google.appinventor.client.editor.youngandroid.BlocklyPanel::saveUserSettings());
$wnd.BlocklyPanel_getSharedBackpack =
$entry(@com.google.appinventor.client.editor.youngandroid.BlocklyPanel::getSharedBackpack(Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;));
$wnd.BlocklyPanel_storeSharedBackpack =
$entry(@com.google.appinventor.client.editor.youngandroid.BlocklyPanel::storeSharedBackpack(Ljava/lang/String;Ljava/lang/String;));
}-*/;
private native void initWorkspace(String projectId, boolean readOnly, boolean rtl)/*-{
......@@ -842,7 +892,11 @@ public class BlocklyPanel extends HTMLPanel {
* @param backpack JSON-serialized backpack contents.
*/
public static native void setInitialBackpack(String backpack)/*-{
Blockly.Backpack.shared_contents = JSON.parse(backpack);
Blockly.Backpack.contents = JSON.parse(backpack);
}-*/;
public static native void setSharedBackpackId(String backPackId)/*-{
Blockly.Backpack.backPackId = backPackId;
}-*/;
/**
......@@ -853,7 +907,7 @@ public class BlocklyPanel extends HTMLPanel {
}-*/;
/**
* Store the backpack's contents to the App Inventor service.
* Store the backpack's contents to the App Inventor server.
*
* @param backpack JSON-serialized backpack contents.
*/
......
......@@ -194,4 +194,32 @@ public class UserInfoServiceImpl extends OdeRemoteServiceServlet implements User
@Override
public void noop() {
}
/**
* fetch the contents of a shared backpack.
*
* @param BackPackId the uuid of the backpack
* @return the backpack's content as an XML string
*/
@Override
public String getSharedBackpack(String backPackId) {
return storageIo.downloadBackpack(backPackId);
}
/**
* store a shared backpack.
*
* Note: We overwrite any existing backpack. If merging of contents
* is desired, our caller has to take care of it.
*
* @param BackPackId the uuid of the shared backpack
* @param the new contents of the backpack
*/
@Override
public void storeSharedBackpack(String backPackId, String content) {
storageIo.uploadBackpack(backPackId, content);
}
}
......@@ -73,6 +73,32 @@ public final class CookieAuth {
* </pre>
*/
long getOneProjectId();
/**
* <code>optional string backpackid = 9;</code>
*
* <pre>
* backpackid for shared backpack
* </pre>
*/
boolean hasBackpackid();
/**
* <code>optional string backpackid = 9;</code>
*
* <pre>
* backpackid for shared backpack
* </pre>
*/
java.lang.String getBackpackid();
/**
* <code>optional string backpackid = 9;</code>
*
* <pre>
* backpackid for shared backpack
* </pre>
*/
com.google.protobuf.ByteString
getBackpackidBytes();
}
/**
* Protobuf type {@code cookieauth.cookie}
......@@ -152,6 +178,12 @@ public final class CookieAuth {
oneProjectId_ = input.readUInt64();
break;
}
case 74: {
com.google.protobuf.ByteString bs = input.readBytes();
bitField0_ |= 0x00000020;
backpackid_ = bs;
break;
}
}
}
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
......@@ -306,12 +338,67 @@ public final class CookieAuth {
return oneProjectId_;
}
public static final int BACKPACKID_FIELD_NUMBER = 9;
private java.lang.Object backpackid_;
/**
* <code>optional string backpackid = 9;</code>
*
* <pre>
* backpackid for shared backpack
* </pre>
*/
public boolean hasBackpackid() {
return ((bitField0_ & 0x00000020) == 0x00000020);
}
/**
* <code>optional string backpackid = 9;</code>
*
* <pre>
* backpackid for shared backpack
* </pre>
*/
public java.lang.String getBackpackid() {
java.lang.Object ref = backpackid_;
if (ref instanceof java.lang.String) {
return (java.lang.String) ref;
} else {
com.google.protobuf.ByteString bs =
(com.google.protobuf.ByteString) ref;
java.lang.String s = bs.toStringUtf8();
if (bs.isValidUtf8()) {
backpackid_ = s;
}
return s;
}
}
/**
* <code>optional string backpackid = 9;</code>
*
* <pre>
* backpackid for shared backpack
* </pre>
*/
public com.google.protobuf.ByteString
getBackpackidBytes() {
java.lang.Object ref = backpackid_;
if (ref instanceof java.lang.String) {
com.google.protobuf.ByteString b =
com.google.protobuf.ByteString.copyFromUtf8(
(java.lang.String) ref);
backpackid_ = b;
return b;
} else {
return (com.google.protobuf.ByteString) ref;
}
}
private void initFields() {
uuid_ = "";
ts_ = 0L;
isAdmin_ = false;
isReadOnly_ = false;
oneProjectId_ = 0L;
backpackid_ = "";
}
private byte memoizedIsInitialized = -1;
public final boolean isInitialized() {
......@@ -349,6 +436,9 @@ public final class CookieAuth {
if (((bitField0_ & 0x00000010) == 0x00000010)) {
output.writeUInt64(6, oneProjectId_);
}
if (((bitField0_ & 0x00000020) == 0x00000020)) {
output.writeBytes(9, getBackpackidBytes());
}
getUnknownFields().writeTo(output);
}
......@@ -378,6 +468,10 @@ public final class CookieAuth {
size += com.google.protobuf.CodedOutputStream
.computeUInt64Size(6, oneProjectId_);
}
if (((bitField0_ & 0x00000020) == 0x00000020)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(9, getBackpackidBytes());
}
size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size;
return size;
......@@ -505,6 +599,8 @@ public final class CookieAuth {
bitField0_ = (bitField0_ & ~0x00000008);
oneProjectId_ = 0L;
bitField0_ = (bitField0_ & ~0x00000010);
backpackid_ = "";
bitField0_ = (bitField0_ & ~0x00000020);
return this;
}
......@@ -553,6 +649,10 @@ public final class CookieAuth {
to_bitField0_ |= 0x00000010;
}
result.oneProjectId_ = oneProjectId_;
if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
to_bitField0_ |= 0x00000020;
}
result.backpackid_ = backpackid_;
result.bitField0_ = to_bitField0_;
onBuilt();
return result;
......@@ -586,6 +686,11 @@ public final class CookieAuth {
if (other.hasOneProjectId()) {
setOneProjectId(other.getOneProjectId());
}
if (other.hasBackpackid()) {
bitField0_ |= 0x00000020;
backpackid_ = other.backpackid_;
onChanged();
}
this.mergeUnknownFields(other.getUnknownFields());
return this;
}
......@@ -849,6 +954,106 @@ public final class CookieAuth {
return this;
}
private java.lang.Object backpackid_ = "";
/**
* <code>optional string backpackid = 9;</code>
*
* <pre>
* backpackid for shared backpack
* </pre>
*/
public boolean hasBackpackid() {
return ((bitField0_ & 0x00000020) == 0x00000020);
}
/**
* <code>optional string backpackid = 9;</code>
*
* <pre>
* backpackid for shared backpack
* </pre>
*/
public java.lang.String getBackpackid() {
java.lang.Object ref = backpackid_;
if (!(ref instanceof java.lang.String)) {
com.google.protobuf.ByteString bs =
(com.google.protobuf.ByteString) ref;
java.lang.String s = bs.toStringUtf8();
if (bs.isValidUtf8()) {
backpackid_ = s;
}
return s;
} else {
return (java.lang.String) ref;
}
}
/**
* <code>optional string backpackid = 9;</code>
*
* <pre>
* backpackid for shared backpack
* </pre>
*/
public com.google.protobuf.ByteString
getBackpackidBytes() {
java.lang.Object ref = backpackid_;
if (ref instanceof String) {
com.google.protobuf.ByteString b =
com.google.protobuf.ByteString.copyFromUtf8(
(java.lang.String) ref);
backpackid_ = b;
return b;
} else {
return (com.google.protobuf.ByteString) ref;
}
}
/**
* <code>optional string backpackid = 9;</code>
*
* <pre>
* backpackid for shared backpack
* </pre>
*/
public Builder setBackpackid(
java.lang.String value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000020;
backpackid_ = value;
onChanged();
return this;
}
/**
* <code>optional string backpackid = 9;</code>
*
* <pre>
* backpackid for shared backpack
* </pre>
*/
public Builder clearBackpackid() {
bitField0_ = (bitField0_ & ~0x00000020);
backpackid_ = getDefaultInstance().getBackpackid();
onChanged();
return this;
}
/**
* <code>optional string backpackid = 9;</code>
*
* <pre>
* backpackid for shared backpack
* </pre>
*/
public Builder setBackpackidBytes(
com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000020;
backpackid_ = value;
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:cookieauth.cookie)
}
......@@ -874,11 +1079,11 @@ public final class CookieAuth {
descriptor;
static {
java.lang.String[] descriptorData = {
"\n\014cookie.proto\022\ncookieauth\"]\n\006cookie\022\014\n\004" +
"\n\014cookie.proto\022\ncookieauth\"q\n\006cookie\022\014\n\004" +
"uuid\030\001 \002(\t\022\n\n\002ts\030\002 \002(\004\022\017\n\007isAdmin\030\003 \001(\010\022" +
"\022\n\nisReadOnly\030\004 \001(\010\022\024\n\014oneProjectId\030\006 \001(" +
"\004B6\n(com.google.appinventor.server.cooki" +
"eauthB\nCookieAuth"
"\004\022\022\n\nbackpackid\030\t \001(\tB6\n(com.google.appi" +
"nventor.server.cookieauthB\nCookieAuth"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() {
......@@ -897,7 +1102,7 @@ public final class CookieAuth {
internal_static_cookieauth_cookie_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_cookieauth_cookie_descriptor,
new java.lang.String[] { "Uuid", "Ts", "IsAdmin", "IsReadOnly", "OneProjectId", });
new java.lang.String[] { "Uuid", "Ts", "IsAdmin", "IsReadOnly", "OneProjectId", "Backpackid", });
}
// @@protoc_insertion_point(outer_class_scope)
......
......@@ -12,5 +12,6 @@ message cookie {
// do not recycle the id number too soon
// optional string locale = 5;
optional uint64 oneProjectId = 6;
optional string backpackid = 9; // backpackid for shared backpack
}
......@@ -25,6 +25,7 @@ import com.google.appinventor.server.CrashReport;
import com.google.appinventor.server.FileExporter;
import com.google.appinventor.server.Server;
import com.google.appinventor.server.flags.Flag;
import com.google.appinventor.server.storage.StoredData.Backpack;
import com.google.appinventor.server.storage.StoredData.CorruptionRecord;
import com.google.appinventor.server.storage.StoredData.FeedbackData;
import com.google.appinventor.server.storage.StoredData.FileData;
......@@ -193,6 +194,7 @@ public class ObjectifyStorageIo implements StorageIo {
ObjectifyService.register(CorruptionRecord.class);
ObjectifyService.register(PWData.class);
ObjectifyService.register(SplashData.class);
ObjectifyService.register(Backpack.class);
// Learn GCS Bucket from App Configuration or App Engine Default
String gcsBucket = Flag.createFlag("gcs.bucket", "").get();
......@@ -2466,6 +2468,10 @@ public class ObjectifyStorageIo implements StorageIo {
return new Key<StoredData.PWData>(PWData.class, uid);
}
private Key<StoredData.Backpack> backpackdataKey(String backPackId) {
return new Key<StoredData.Backpack>(Backpack.class, backPackId);
}
// Create a name for a blob from a project id and file name. This is mostly
// to help with debugging and viewing the blobstore via the admin console.
// We don't currently use these blob names anywhere else.
......@@ -2878,6 +2884,51 @@ public class ObjectifyStorageIo implements StorageIo {
}
}
}
/* Store a shared backpack.
*
* We only store backpacks in the datastore (cached in
* memcache by Objectify). So backpacks are limited in
* size to what can be stored in a data store entity.
*/
@Override
public String downloadBackpack(final String backPackId) {
final Result<Backpack> result = new Result<Backpack>();
try {
runJobWithRetries(new JobRetryHelper() {
@Override
public void run(Objectify datastore) {
Backpack backPack = datastore.find(backpackdataKey(backPackId));
if (backPack != null) {
result.t = backPack;
}
}
}, false);
} catch (ObjectifyException e) {
CrashReport.createAndLogError(LOG, null, null, e);
}
if (result.t != null) {
return result.t.content;
} else {
return "[]"; // No shared backpack, return an empty backpack
}
}
@Override
public void uploadBackpack(String backPackId, String content) {
final Backpack backPack = new Backpack();
backPack.id = backPackId;
backPack.content = content;
try {
runJobWithRetries(new JobRetryHelper() {
@Override
public void run(Objectify datastore) {
datastore.put(backPack);
}
}, true);
} catch (ObjectifyException e) {
throw CrashReport.createAndLogError(LOG, null, null, e);
}
}
}
......@@ -656,4 +656,32 @@ public interface StorageIo {
List<AdminUser> searchUsers(String partialEmail);
void storeUser(AdminUser user) throws AdminInterfaceException;
/**
* There are two kinds of backpacks. User backpacks, which are
* stored with the user's personal files (which today is just the
* backpack and the android keystore used for signing applications.
* The second kind of backpack is a shared backpack. It is
* identified by a uuid. This code is associated with the shared
* backpack. Shared backpacks are used when a person is logged in
* via the SSO mechanism. It can optionally specify a backpack to
* use. If it doesn't specify a backpack, then the normal user
* specific version is used.
*
* @param backPackId uuid used to idenfity this backpack
* @return the contents of the backpack as an XML encoded string
*/
public String downloadBackpack(String backPackId);
/**
* Used to upload a shared backpack Note: This code will over-write
* whatever contents is already stored in the the backpack. It is
* the responsibility of our caller to merge contents if desired.
*
* @param backPackId The uuid of the shared backpack to store
* @param String content the new contents of the backpack
*/
public void uploadBackpack(String backPackId, String content);
}
......@@ -290,4 +290,16 @@ public class StoredData {
public String email; // Email of account in question
}
// A Shared backpack. Shared backpacks are not associated with
// any one user. Instead they are stored independent of projects
// and users. At login time a shared backpack may be specified.
// This requires an SSO Login from an external system to provide
// it.
@Cached(expirationSeconds=120)
@Unindexed
public static final class Backpack {
@Id public String id;
public String content;
}
}
......@@ -48,6 +48,8 @@ public class User implements IsSerializable, UserInfoProvider, Serializable {
private String password; // Hashed password (if using local login system)
private String backPackId = null; // If non-null we have a shared backpack
public final static String usercachekey = "f682688a-1065-4cda-8515-a8bd70200ac9"; // UUID
// This UUID is prepended to any key lookup for User objects. Memcache is a common
// key/value store for the entire application. By prepending a unique value, we ensure
......@@ -317,6 +319,15 @@ public class User implements IsSerializable, UserInfoProvider, Serializable {
return isReadOnly;
}
public String getBackpackId() {
return backPackId;
}
public void setBackpackId(String backPackId) {
this.backPackId = backPackId;
}
public User copy() {
User retval = new User(id, email, name, link, emailFrequency, tosAccepted, isAdmin, type, sessionId);
// We set the isReadOnly flag in the copy in this fashion so we do not have to
......@@ -324,6 +335,8 @@ public class User implements IsSerializable, UserInfoProvider, Serializable {
// only a few places where we assert or read the isReadOnly flag, so we want to
// limit the places where we have to have knowledge of it to just those places that care
retval.setReadOnly(isReadOnly);
retval.setBackpackId(this.backPackId);
retval.name = this.name;
return retval;
}
}
......@@ -100,4 +100,15 @@ public interface UserInfoService extends RemoteService {
*/
void noop();
/**
* Retrieve the contents of a shared backpack.
*/
public String getSharedBackpack(String backPackId);
/**
* Store the contents of a shared backpack.
*/
public void storeSharedBackpack(String backPackId, String content);
}
......@@ -81,4 +81,14 @@ public interface UserInfoServiceAsync {
*/
void noop(AsyncCallback<Void> callback);
/**
* @see UserInfoService#getSharedBackpack(String)
*/
void getSharedBackpack(String backPackId, AsyncCallback<String> callback);
/**
* @see UserInfoService#storeSharedBackpack(String, String)
*/
void storeSharedBackpack(String backPackId, String content, AsyncCallback<Void> callback);
}
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment