blob: a8a803911537a5ddd8dec07ee487bb01c4e1f6aa [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.android.apps.syncslides.db;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.widget.Toast;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.joda.time.Duration;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import io.v.android.apps.syncslides.R;
import io.v.android.apps.syncslides.misc.V23Manager;
import io.v.android.apps.syncslides.model.Deck;
import io.v.android.apps.syncslides.model.DeckFactory;
import io.v.android.apps.syncslides.model.Listener;
import io.v.android.apps.syncslides.model.NoopList;
import io.v.android.apps.syncslides.model.Slide;
import io.v.android.apps.syncslides.model.SlideImpl;
import io.v.android.v23.V;
import io.v.impl.google.naming.NamingUtil;
import io.v.impl.google.services.syncbase.SyncbaseServer;
import io.v.v23.context.CancelableVContext;
import io.v.v23.context.VContext;
import io.v.v23.rpc.Server;
import io.v.v23.security.BlessingPattern;
import io.v.v23.security.Blessings;
import io.v.v23.security.access.AccessList;
import io.v.v23.security.access.Constants;
import io.v.v23.security.access.Permissions;
import io.v.v23.services.syncbase.nosql.PrefixPermissions;
import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
import io.v.v23.services.syncbase.nosql.TableRow;
import io.v.v23.services.syncbase.nosql.SyncgroupSpec;
import io.v.v23.services.watch.ResumeMarker;
import io.v.v23.syncbase.Syncbase;
import io.v.v23.syncbase.SyncbaseApp;
import io.v.v23.syncbase.SyncbaseService;
import io.v.v23.syncbase.nosql.BatchDatabase;
import io.v.v23.syncbase.nosql.ChangeType;
import io.v.v23.syncbase.nosql.Database;
import io.v.v23.syncbase.nosql.DatabaseCore;
import io.v.v23.syncbase.nosql.RowRange;
import io.v.v23.syncbase.nosql.Stream;
import io.v.v23.syncbase.nosql.Syncgroup;
import io.v.v23.syncbase.nosql.Table;
import io.v.v23.syncbase.nosql.WatchChange;
import io.v.v23.vdl.VdlAny;
import io.v.v23.vdl.VdlOptional;
import io.v.v23.verror.Errors;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;
public class SyncbaseDB implements DB {
private static final String TAG = "SyncbaseDB";
private static final String SYNCBASE_APP = "syncslides";
private static final String SYNCBASE_DB = "syncslides";
private static final String DECKS_TABLE = "Decks";
private static final String NOTES_TABLE = "Notes";
static final String PRESENTATIONS_TABLE = "Presentations";
static final String CURRENT_SLIDE = "CurrentSlide";
static final String QUESTIONS = "questions";
private static final String SYNCGROUP_PRESENTATION_DESCRIPTION = "Live Presentation";
private boolean mInitialized = false;
private Handler mHandler;
private Permissions mPermissions;
private Context mContext;
private VContext mVContext;
private Database mDB;
private Table mDecks;
private Table mNotes;
private Table mPresentations;
private final Map<String, CurrentSlideWatcher> mCurrentSlideWatchers;
private final Map<String, QuestionWatcher> mQuestionWatchers;
private final Map<String, DriverWatcher> mDriverWatchers;
private Server mSyncbaseServer;
private final DeckFactory mDeckFactory;
SyncbaseDB(Context context) {
mContext = context;
mCurrentSlideWatchers = Maps.newHashMap();
mQuestionWatchers = Maps.newHashMap();
mDriverWatchers = Maps.newHashMap();
mDeckFactory = DeckFactory.Singleton.get(context);
}
@Override
public void init() {
Log.d(TAG, "init");
if (mInitialized) {
Log.d(TAG, "already initialized");
return;
}
mHandler = new Handler(Looper.getMainLooper());
// If blessings aren't in place, the fragment that called this
// initialization may continue to load and use DB, but nothing will
// work so DB methods should return noop values. It's assumed that
// the calling fragment will send the user to the AccountManager,
// accept blessings on return, then re-call this init.
if (!V23Manager.Singleton.get().isBlessed()) {
Log.d(TAG, "no blessings.");
return;
}
mVContext = V23Manager.Singleton.get().getVContext();
setupSyncbase();
}
// TODO(kash): Run this in an AsyncTask so it doesn't block the UI.
private void setupSyncbase() {
Blessings blessings = V23Manager.Singleton.get().getBlessings();
AccessList everyoneAcl = new AccessList(
ImmutableList.of(new BlessingPattern("...")), ImmutableList.<String>of());
AccessList justMeAcl = new AccessList(
ImmutableList.of(new BlessingPattern(blessings.toString())),
ImmutableList.<String>of());
mPermissions = new Permissions(ImmutableMap.of(
Constants.RESOLVE.getValue(), everyoneAcl,
Constants.READ.getValue(), justMeAcl,
Constants.WRITE.getValue(), justMeAcl,
Constants.ADMIN.getValue(), justMeAcl));
// Prepare the syncbase storage directory.
File storageDir = new File(mContext.getFilesDir(), "syncbase");
storageDir.mkdirs();
try {
TelephonyManager tm = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
String id = tm.getDeviceId();
if (id == null) {
// NOTE(spetrovic): on a tablet, there is no TelephonyManager, so we try something
// else.
id = Settings.Secure.getString(
mContext.getContentResolver(), Settings.Secure.ANDROID_ID);
}
mVContext = SyncbaseServer.withNewServer(mVContext, new SyncbaseServer.Params()
.withPermissions(mPermissions)
.withName(V23Manager.syncName(id))
.withStorageRootDir(storageDir.getAbsolutePath()));
} catch (SyncbaseServer.StartException e) {
handleError("Couldn't start syncbase server");
return;
}
try {
mSyncbaseServer = V.getServer(mVContext);
Log.i(TAG, "Endpoints: " + Arrays.toString(mSyncbaseServer.getStatus().getEndpoints()));
String serverName = "/" + mSyncbaseServer.getStatus().getEndpoints()[0];
SyncbaseService service = Syncbase.newService(serverName);
SyncbaseApp app = service.getApp(SYNCBASE_APP);
if (!app.exists(mVContext)) {
app.create(mVContext, mPermissions);
}
mDB = app.getNoSqlDatabase(SYNCBASE_DB, null);
if (!mDB.exists(mVContext)) {
mDB.create(mVContext, mPermissions);
}
mDecks = mDB.getTable(DECKS_TABLE);
if (!mDecks.exists(mVContext)) {
mDecks.create(mVContext, mPermissions);
}
mNotes = mDB.getTable(NOTES_TABLE);
if (!mNotes.exists(mVContext)) {
mNotes.create(mVContext, mPermissions);
}
mPresentations = mDB.getTable(PRESENTATIONS_TABLE);
if (!mPresentations.exists(mVContext)) {
mPresentations.create(mVContext, mPermissions);
}
//importDecks();
} catch (VException e) {
handleError("Couldn't setup syncbase service: " + e.getMessage());
return;
}
mInitialized = true;
}
@Override
public void createPresentation(final String deckId,
final Callback<CreatePresentationResult> callback) {
new Thread(new Runnable() {
@Override
public void run() {
createPresentationRunnable(deckId, callback);
}
}).start();
}
private void createPresentationRunnable(final String deckId,
final Callback<CreatePresentationResult> callback) {
final String presentationId = UUID.randomUUID().toString();
String prefix = NamingUtil.join(deckId, presentationId);
try {
Blessings blessings = V23Manager.Singleton.get().getBlessings();
AccessList everyoneAcl = new AccessList(
ImmutableList.of(new BlessingPattern("...")), ImmutableList.<String>of());
AccessList justMeAcl = new AccessList(
ImmutableList.of(new BlessingPattern(blessings.toString())),
ImmutableList.<String>of());
Permissions groupReadPermissions = new Permissions(ImmutableMap.of(
Constants.RESOLVE.getValue(), everyoneAcl,
Constants.READ.getValue(), everyoneAcl,
Constants.WRITE.getValue(), justMeAcl,
Constants.ADMIN.getValue(), justMeAcl));
Permissions groupWritePermissions = new Permissions(ImmutableMap.of(
Constants.RESOLVE.getValue(), everyoneAcl,
Constants.READ.getValue(), everyoneAcl,
Constants.WRITE.getValue(), justMeAcl,
Constants.ADMIN.getValue(), justMeAcl));
// Add rows to Presentations table.
VPresentation presentation = new VPresentation(); // Empty for now.
mPresentations.put(mVContext, prefix, presentation, VPresentation.class);
VCurrentSlide current = new VCurrentSlide(0);
mPresentations.put(mVContext, NamingUtil.join(prefix, CURRENT_SLIDE),
current, VCurrentSlide.class);
mPresentations.setPrefixPermissions(mVContext, RowRange.prefix(prefix),
groupReadPermissions);
// Anybody can add a question.
mPresentations.setPrefixPermissions(
mVContext,
RowRange.prefix(NamingUtil.join(prefix, QUESTIONS)),
groupWritePermissions);
mDecks.setPrefixPermissions(mVContext, RowRange.prefix(deckId), groupReadPermissions);
// Create the syncgroup.
final String syncgroupName = NamingUtil.join(
mSyncbaseServer.getStatus().getMounts()[0].getName(),
"%%sync/syncslides",
prefix);
Log.i(TAG, "Creating syncgroup " + syncgroupName);
Syncgroup syncgroup = mDB.getSyncgroup(syncgroupName);
CancelableVContext context = mVContext.withTimeout(Duration.millis(5000));
try {
syncgroup.create(
context,
new SyncgroupSpec(
SYNCGROUP_PRESENTATION_DESCRIPTION,
groupReadPermissions,
Arrays.asList(
new TableRow(PRESENTATIONS_TABLE, prefix),
new TableRow(DECKS_TABLE, deckId)),
Arrays.asList(V23Manager.syncName("sg")),
false
),
new SyncgroupMemberInfo((byte) 10));
} catch (VException e) {
if (e.is(Errors.EXIST)) {
Log.i(TAG, "Syncgroup already exists");
} else {
throw e;
}
}
Log.i(TAG, "Finished creating syncgroup");
// TODO(kash): Create a syncgroup for Notes? Not sure if we should do that
// here or somewhere else. We're not going to demo sync across a user's
// devices right away, so we'll figure this out later.
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
callback.done(new CreatePresentationResult(presentationId, syncgroupName));
}
});
} catch (VException e) {
// TODO(kash): Change Callback to take an error parameter.
handleError(e.toString());
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// TODO(kash): fix me!
callback.done(new CreatePresentationResult(presentationId, "dummy name"));
}
});
}
}
@Override
public void joinPresentation(final String syncgroupName, final Callback<Void> callback) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Log.i(TAG, "Joining: " + syncgroupName);
Syncgroup syncgroup = mDB.getSyncgroup(syncgroupName);
Log.d(TAG, "syncgroup = " + syncgroup);
syncgroup.join(mVContext, new SyncgroupMemberInfo((byte) 1));
for (String member : syncgroup.getMembers(mVContext).keySet()) {
Log.i(TAG, "Member: " + member);
}
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
callback.done(null);
}
});
} catch (VException e) {
handleError(e.toString());
}
}
}).start();
}
@Override
public DBList<Deck> getDecks() {
if (!mInitialized) {
return new NoopList<>();
}
return new DeckList(mVContext, mDB, mDeckFactory);
}
private static class DeckList implements DBList<Deck> {
private final CancelableVContext mVContext;
private final Database mDB;
private final DeckFactory mDeckFactory;
private final Handler mHandler;
private ResumeMarker mWatchMarker;
private boolean mIsDiscarded;
private Listener mListener;
private List<Deck> mDecks;
public DeckList(VContext vContext, Database db, DeckFactory df) {
mVContext = vContext.withCancel();
mDB = db;
mDeckFactory = df;
mIsDiscarded = false;
mDecks = Lists.newArrayList();
mHandler = new Handler(Looper.getMainLooper());
new Thread(new Runnable() {
@Override
public void run() {
fetchExistingDecks();
}
}).start();
}
private void fetchExistingDecks() {
// TODO(kash): Uncomment this once we've figured out why performance is so bad.
//Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
BatchDatabase batch = mDB.beginBatch(mVContext, null);
mWatchMarker = batch.getResumeMarker(mVContext);
DatabaseCore.ResultStream stream = batch.exec(mVContext,
"SELECT k, v FROM Decks WHERE Type(v) like \"%VDeck\"");
for (List<VdlAny> row : stream) {
if (row.size() != 2) {
throw new VException("Wrong number of columns: " + row.size());
}
String key = (String) row.get(0).getElem();
Log.i(TAG, "Fetched deck " + key);
final Deck deck = mDeckFactory.make((VDeck) row.get(1).getElem(), key);
mHandler.post(new Runnable() {
@Override
public void run() {
put(deck);
}
});
}
watchForDeckChanges();
} catch (VException e) {
Log.e(TAG, e.toString());
}
}
private void watchForDeckChanges() {
Stream<WatchChange> changeStream = null;
try {
changeStream = mDB.watch(mVContext, DECKS_TABLE, "", mWatchMarker);
} catch (VException e) {
Log.e(TAG, "Couldn't watch for changes to the Decks table: " + e.toString());
return;
}
for (WatchChange change : changeStream) {
if (!change.getTableName().equals(DECKS_TABLE)) {
Log.e(TAG, "Wrong change table name: " + change.getTableName() + ", wanted: " +
DECKS_TABLE);
continue;
}
final String key = change.getRowName();
// Ignore slide changes.
if (NamingUtil.split(key).size() != 1) {
Log.d(TAG, "Ignoring slide change: " + key);
continue;
}
Log.d(TAG, "Processing change to deck: " + key);
if (change.getChangeType().equals(ChangeType.PUT_CHANGE)) {
// New deck or change to an existing deck.
VDeck vDeck = null;
try {
vDeck = (VDeck) VomUtil.decode(change.getVomValue(), VDeck.class);
} catch (VException e) {
Log.e(TAG, "Couldn't decode deck: " + e.toString());
}
final Deck deck = mDeckFactory.make(vDeck, key);
mHandler.post(new Runnable() {
@Override
public void run() {
put(deck);
}
});
} else { // ChangeType.DELETE_CHANGE
// Existing deck deleted.
mHandler.post(new Runnable() {
@Override
public void run() {
delete(key);
}
});
}
}
Log.i(TAG, "Deck change thread exiting");
}
@Override
public int getItemCount() {
if (mDecks != null) {
return mDecks.size();
}
return 0;
}
@Override
public Deck get(int i) {
if (mDecks != null) {
return mDecks.get(i);
}
return null;
}
@Override
public void setListener(Listener listener) {
mListener = listener;
}
@Override
public void discard() {
Log.i(TAG, "Discarding deck list.");
mIsDiscarded = true;
mVContext.cancel(); // this will cause the watcher thread to exit
mHandler.removeCallbacksAndMessages(null);
}
private void put(Deck deck) {
// We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
// been called), because that method doesn't prevent future post()s being made on
// the handler. So the following scenario is possible:
// - fetcher thread is about to execute post().
// - discard clears all pending messages from the handler.
// - fetcher executes the post().
if (mIsDiscarded) {
return;
}
// Keep the same sorted order as the Syncbase table, otherwise decks will shuffle
// whenever we refresh them.
int idx = 0;
for (; idx < mDecks.size(); ++idx) {
int comp = mDecks.get(idx).getId().compareTo(deck.getId());
if (comp == 0) {
// Existing deck.
mDecks.set(idx, deck);
if (mListener != null) {
mListener.notifyItemChanged(idx);
}
return;
} else if (comp > 0) {
break;
}
}
// New deck.
mDecks.add(idx, deck);
if (mListener != null) {
mListener.notifyItemInserted(idx);
}
}
private void delete(String deckId) {
// We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
// been called), because that method doesn't prevent future post()s being made on
// the handler. So the following scenario is possible:
// - fetcher thread is about to execute post().
// - discard clears all pending messages from the handler.
// - fetcher executes the post().
if (mIsDiscarded) {
return;
}
for (int i = 0; i < mDecks.size(); ++i) {
if (mDecks.get(i).getId().equals(deckId)) {
mDecks.remove(i);
if (mListener != null) {
mListener.notifyItemRemoved(i);
}
return;
}
}
}
}
@Override
public void getSlides(final String deckId, final Callback<List<Slide>> callback) {
new Thread(new Runnable() {
@Override
public void run() {
// We could probably share this block of code with SlideList.fetchData(),
// but they are just different enough to make that annoying. Not sure
// that we'd really save anything in the end.
try {
BatchDatabase batch = mDB.beginBatch(mVContext, null);
Table table = batch.getTable(NOTES_TABLE);
String query = "SELECT k, v FROM Decks WHERE Type(v) LIKE \"%VSlide\" " +
"AND k LIKE \"" + NamingUtil.join(deckId, "slides") + "%\"";
DatabaseCore.ResultStream stream = batch.exec(mVContext, query);
// TODO(kash): Abort execution if interrupted. Perhaps we should derive
// a new VContext so it can be cancelled.
final List<Slide> slides = Lists.newArrayList();
for (List<VdlAny> row : stream) {
if (row.size() != 2) {
throw new VException("Wrong number of columns: " + row.size());
}
String key = (String) row.get(0).getElem();
Log.i(TAG, "Fetched slide " + key);
VSlide slide = (VSlide) row.get(1).getElem();
String note = notesForSlide(mVContext, table, key);
slides.add(new SlideImpl(slide.getThumbnail(), note));
}
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
callback.done(slides);
}
});
} catch (VException e) {
Log.e(TAG, e.toString());
}
}
}).start();
}
@Override
public DBList<Slide> getSlides(String deckId) {
if (!mInitialized) {
return new NoopList<>();
}
return new SlideList(mVContext, mDB, deckId);
}
private static class SlideList implements DBList {
private final CancelableVContext mVContext;
private final Database mDB;
private final Handler mHandler;
private final String mDeckId;
private ResumeMarker mWatchMarker;
private boolean mIsDiscarded;
// Storage for slides, mirroring the slides in the Syncbase. Since slide numbers can
// have "holes" in them (e.g., 1, 2, 4, 6, 8), we maintain a map from slide key
// to the slide, as well as an ordered list which is returned to the caller.
private TreeMap<String, Slide> mSlidesMap;
private List<Slide> mSlides;
private Listener mListener;
public SlideList(VContext vContext, Database db, String deckId) {
Log.i(TAG, "Fetching slides for " + deckId);
mVContext = vContext.withCancel();
mDB = db;
mDeckId = deckId;
mIsDiscarded = false;
mSlidesMap = new TreeMap<>();
mSlides = Lists.newArrayList();
mHandler = new Handler(Looper.getMainLooper());
new Thread(new Runnable() {
@Override
public void run() {
fetchExistingSlides();
}
}).start();
}
private void fetchExistingSlides() {
// TODO(kash): Uncomment this once we've figured out why performance is so bad.
//Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
BatchDatabase batch = mDB.beginBatch(mVContext, null);
mWatchMarker = batch.getResumeMarker(mVContext);
Table notesTable = batch.getTable(NOTES_TABLE);
String query = "SELECT k, v FROM Decks WHERE Type(v) LIKE \"%VSlide\" " +
"AND k LIKE \"" + NamingUtil.join(mDeckId, "slides") + "%\"";
DatabaseCore.ResultStream stream = batch.exec(mVContext, query);
for (List<VdlAny> row : stream) {
if (row.size() != 2) {
throw new VException("Wrong number of columns: " + row.size());
}
final String key = (String) row.get(0).getElem();
Log.i(TAG, "Fetched slide " + key);
VSlide slide = (VSlide) row.get(1).getElem();
String notes = notesForSlide(mVContext, notesTable, key);
final SlideImpl newSlide = new SlideImpl(slide.getThumbnail(), notes);
mHandler.post(new Runnable() {
@Override
public void run() {
put(key, newSlide);
}
});
}
watchForSlideChanges();
} catch (VException e) {
Log.e(TAG, e.toString());
}
}
private void watchForSlideChanges() {
Table notesTable = mDB.getTable(NOTES_TABLE);
Stream<WatchChange> changeStream;
try {
changeStream = mDB.watch(mVContext, DECKS_TABLE, "", mWatchMarker);
} catch (VException e) {
Log.e(TAG, "Couldn't watch for changes to the Decks table: " + e.toString());
return;
}
for (WatchChange change : changeStream) {
if (!change.getTableName().equals(DECKS_TABLE)) {
Log.e(TAG, "Wrong change table name: " + change.getTableName() + ", wanted: " +
DECKS_TABLE);
continue;
}
final String key = change.getRowName();
// Ignore deck changes.
if (NamingUtil.split(key).size() <= 1) {
Log.d(TAG, "Ignoring deck change: " + key);
continue;
}
if (change.getChangeType().equals(ChangeType.PUT_CHANGE)) {
// New slide or change to an existing slide.
VSlide vSlide = null;
try {
vSlide = (VSlide) VomUtil.decode(change.getVomValue(), VSlide.class);
} catch (VException e) {
Log.e(TAG, "Couldn't decode slide: " + e.toString());
}
String notes = notesForSlide(mVContext, notesTable, key);
final SlideImpl slide = new SlideImpl(vSlide.getThumbnail(), notes);
mHandler.post(new Runnable() {
@Override
public void run() {
put(key, slide);
}
});
} else { // ChangeType.DELETE_CHANGE
// Existing slide deleted.
mHandler.post(new Runnable() {
@Override
public void run() {
delete(key);
}
});
}
}
Log.i(TAG, "Slides watcher thread exiting");
}
@Override
public int getItemCount() {
return mSlides.size();
}
@Override
public Slide get(int i) {
return mSlides.get(i);
}
@Override
public void setListener(Listener listener) {
mListener = listener;
}
@Override
public void discard() {
Log.i(TAG, "Discarding slides list");
mIsDiscarded = true;
mVContext.cancel(); // this will cause the watcher thread to exit
mHandler.removeCallbacksAndMessages(null);
}
private void put(String key, Slide slide) {
// We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
// been called), because that method doesn't prevent future post()s being made on
// the handler. So the following scenario is possible:
// - fetcher thread is about to execute post().
// - discard clears all pending messages from the handler.
// - fetcher executes the post().
if (mIsDiscarded) {
return;
}
Slide oldSlide = mSlidesMap.put(key, slide);
mSlides = Lists.newArrayList(mSlidesMap.values());
int idx = mSlides.indexOf(slide);
if (idx == -1) {
// Should never happen.
throw new RuntimeException("Can't find a newly inserted slide");
}
if (mListener != null) {
if (oldSlide != null) { // Update to an existing slide.
mListener.notifyItemChanged(idx);
} else {
mListener.notifyItemInserted(idx);
}
}
}
private void delete(String key) {
// We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
// been called), because that method doesn't prevent future post()s being made on
// the handler. So the following scenario is possible:
// - fetcher thread is about to execute post().
// - discard clears all pending messages from the handler.
// - fetcher executes the post().
if (mIsDiscarded) {
return;
}
Slide deletedSlide = mSlidesMap.remove(key);
if (deletedSlide == null) {
Log.e(TAG, "Deleting a slide that doesn't exist: " + key);
return;
}
int idx = mSlides.indexOf(deletedSlide);
if (idx == -1) {
// Should never happen.
throw new RuntimeException("Couldn't find a deleted slide in the list");
}
mSlides.remove(idx);
if (mListener != null) {
mListener.notifyItemRemoved(idx);
}
}
}
private static String notesForSlide(VContext context, Table notesTable, String key) {
try {
return ((VNote) notesTable.get(context, key, VNote.class)).getText();
} catch (VException e) {
// TODO(kash): Should really differentiate between row not existing
// and some server error.
return "";
}
}
@Override
public void setCurrentSlide(final String deckId, final String presentationId,
final int slideNum) {
new Thread(new Runnable() {
@Override
public void run() {
try {
String rowKey = NamingUtil.join(deckId, presentationId, CURRENT_SLIDE);
Log.i(TAG, "Writing row " + rowKey + " with " + slideNum);
mPresentations.put(mVContext, rowKey, new VCurrentSlide(slideNum),
VCurrentSlide.class);
} catch (VException e) {
handleError(e.toString());
}
}
}).start();
}
@Override
public void setSlideNotes(final String deckId, final int slideNum, final String slideNotes) {
new Thread(new Runnable() {
@Override
public void run() {
try {
String rowKey = slideRowKey(deckId, slideNum);
Log.i(TAG, "Saving notes " + rowKey + " with " + slideNotes);
Table notesTable = mDB.getTable(NOTES_TABLE);
notesTable.put(mVContext, rowKey, new VNote(slideNotes), VNote.class);
} catch (VException e) {
handleError(e.toString());
}
}
}).start();
}
private String slideRowKey(String deckId, int slideNum) {
return NamingUtil.join(deckId, "slides", String.format("%04d", slideNum));
}
@Override
public void addCurrentSlideListener(String deckId, String presentationId,
CurrentSlideListener listener) {
String key = NamingUtil.join(deckId, presentationId);
Log.i(TAG, "addCurrentSlideListener " + key);
CurrentSlideWatcher watcher = mCurrentSlideWatchers.get(key);
if (watcher == null) {
watcher = new CurrentSlideWatcher(mVContext, mDB, deckId, presentationId);
mCurrentSlideWatchers.put(key, watcher);
}
watcher.addListener(listener);
}
@Override
public void removeCurrentSlideListener(String deckId, String presentationId,
CurrentSlideListener listener) {
String key = NamingUtil.join(deckId, presentationId);
CurrentSlideWatcher watcher = mCurrentSlideWatchers.get(key);
if (watcher == null) {
return;
}
watcher.removeListener(listener);
if (!watcher.hasListeners()) {
mCurrentSlideWatchers.remove(key);
}
}
@Override
public void setQuestionListener(String deckId, String presentationId,
QuestionListener listener) {
String key = NamingUtil.join(deckId, presentationId);
QuestionWatcher oldWatcher = mQuestionWatchers.get(key);
if (oldWatcher != null) {
oldWatcher.discard();
}
QuestionWatcher watcher = new QuestionWatcher(
new WatcherState(mVContext, mDB, deckId, presentationId),
listener);
mQuestionWatchers.put(key, watcher);
}
@Override
public void removeQuestionListener(String deckId, String presentationId, QuestionListener listener) {
String key = NamingUtil.join(deckId, presentationId);
QuestionWatcher oldWatcher = mQuestionWatchers.get(key);
if (oldWatcher != null) {
mQuestionWatchers.remove(oldWatcher);
oldWatcher.discard();
}
}
@Override
public void askQuestion(final String deckId, final String presentationId,
final String personName) {
final Blessings blessings = V23Manager.Singleton.get().getBlessings();
new Thread(new Runnable() {
@Override
public void run() {
try {
String rowKey = NamingUtil.join(deckId, presentationId, QUESTIONS,
UUID.randomUUID().toString());
Log.i(TAG, "Writing row " + rowKey + " with " + personName);
BatchDatabase batch = mDB.beginBatch(mVContext, null);
Table presentations = batch.getTable(PRESENTATIONS_TABLE);
VQuestion question = new VQuestion(
new VPerson(blessings.toString(), personName),
System.currentTimeMillis(),
false // Not yet answered.
);
presentations.put(mVContext, rowKey, question, VQuestion.class);
// There should be PrefixPermissions on
// <deckId>/<presentationId>/questions
// <deckId>/<presentationId>
// ""
// We want to copy <deckId>/<presentationId> for our question but also
// add ourselves.
PrefixPermissions[] permissions =
presentations.getPrefixPermissions(mVContext, rowKey);
for (PrefixPermissions prefix : permissions) {
Log.i(TAG, "Found permissions: " + prefix.toString());
}
if (permissions.length < 3) {
handleError("Missing permissions for questions: "
+ Arrays.toString(permissions));
batch.abort(mVContext);
return;
}
Permissions perms = permissions[1].getPerms();
perms.get(Constants.WRITE.getValue()).getIn().add(
new BlessingPattern(blessings.toString()));
presentations.setPrefixPermissions(mVContext, RowRange.prefix(rowKey), perms);
batch.commit(mVContext);
} catch (VException e) {
handleError(e.toString());
}
}
}).start();
}
@Override
public void handoffQuestion(final String deckId, final String presentationId,
final String questionId) {
new Thread(new Runnable() {
@Override
public void run() {
try {
BatchDatabase batch = mDB.beginBatch(mVContext, null);
Table presentations = batch.getTable(PRESENTATIONS_TABLE);
// Mark the question as answered so it disappears from the list.
String questionKey =
NamingUtil.join(deckId, presentationId, QUESTIONS, questionId);
VQuestion question =
(VQuestion) presentations.get(mVContext, questionKey, VQuestion.class);
if (question.getAnswered()) {
// Already answered. Nothing to do.
batch.abort(mVContext);
return;
}
question.setAnswered(true);
presentations.put(mVContext, questionKey, question, VQuestion.class);
// Set the questioner as the driver of the presentation.
String presentationKey = NamingUtil.join(deckId, presentationId);
VPresentation presentation = new VPresentation();
presentation.setDriver(VdlOptional.of(question.getQuestioner()));
presentations.put(mVContext, presentationKey, presentation,
VPresentation.class);
// Give the questioner permission to set the current slide.
String currentSlideKey = NamingUtil.join(deckId, presentationId, CURRENT_SLIDE);
Blessings blessings = V23Manager.Singleton.get().getBlessings();
AccessList everyoneAcl = new AccessList(
ImmutableList.of(new BlessingPattern("...")),
ImmutableList.<String>of());
AccessList questionerAcl = new AccessList(
ImmutableList.of(
new BlessingPattern(question.getQuestioner().getBlessing())),
ImmutableList.<String>of());
AccessList justMeAcl = new AccessList(
ImmutableList.of(new BlessingPattern(blessings.toString())),
ImmutableList.<String>of());
Permissions permissions = new Permissions(ImmutableMap.of(
Constants.RESOLVE.getValue(), everyoneAcl,
Constants.READ.getValue(), everyoneAcl,
Constants.WRITE.getValue(), questionerAcl,
Constants.ADMIN.getValue(), justMeAcl));
presentations.setPrefixPermissions(mVContext, RowRange.prefix(currentSlideKey),
permissions);
batch.commit(mVContext);
} catch (VException e) {
handleError(e.toString());
}
}
}).start();
}
@Override
public void resumeControl(final String deckId, final String presentationId) {
new Thread(new Runnable() {
@Override
public void run() {
try {
BatchDatabase batch = mDB.beginBatch(mVContext, null);
Table presentations = batch.getTable(PRESENTATIONS_TABLE);
// Clear the driver of the presentation.
String presentationKey = NamingUtil.join(deckId, presentationId);
VPresentation presentation = new VPresentation();
presentations.put(mVContext, presentationKey, presentation,
VPresentation.class);
// Give the questioner permission to set the current slide.
String currentSlideKey = NamingUtil.join(deckId, presentationId, CURRENT_SLIDE);
Blessings blessings = V23Manager.Singleton.get().getBlessings();
AccessList everyoneAcl = new AccessList(
ImmutableList.of(new BlessingPattern("...")),
ImmutableList.<String>of());
AccessList justMeAcl = new AccessList(
ImmutableList.of(new BlessingPattern(blessings.toString())),
ImmutableList.<String>of());
Permissions permissions = new Permissions(ImmutableMap.of(
Constants.RESOLVE.getValue(), everyoneAcl,
Constants.READ.getValue(), everyoneAcl,
Constants.WRITE.getValue(), justMeAcl,
Constants.ADMIN.getValue(), justMeAcl));
presentations.setPrefixPermissions(mVContext, RowRange.prefix(currentSlideKey),
permissions);
batch.commit(mVContext);
} catch (VException e) {
handleError(e.toString());
}
}
}).start();
}
@Override
public void setDriverListener(String deckId, String presentationId, DriverListener listener) {
String key = NamingUtil.join(deckId, presentationId);
DriverWatcher oldWatcher = mDriverWatchers.get(key);
if (oldWatcher != null) {
oldWatcher.discard();
}
DriverWatcher watcher = new DriverWatcher(
new WatcherState(mVContext, mDB, deckId, presentationId),
listener);
mDriverWatchers.put(key, watcher);
}
@Override
public void removeDriverListener(String deckId, String presentationId,
DriverListener listener) {
String key = NamingUtil.join(deckId, presentationId);
DriverWatcher oldWatcher = mDriverWatchers.get(key);
if (oldWatcher != null) {
mDriverWatchers.remove(oldWatcher);
oldWatcher.discard();
}
}
private void handleError(final String msg) {
Log.e(TAG, msg);
mHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, msg, Toast.LENGTH_LONG).show();
}
});
}
private void importDecks() {
importDeckFromResources(UUID.randomUUID().toString(), "Car Business", R.drawable.thumb_deck1);
// These slow down app startup, so skip these for now.
// importDeckFromResources("deckId2", "Baku Discovery", R.drawable.thumb_deck2);
// importDeckFromResources("deckId3", "Vanadium", R.drawable.thumb_deck3);
}
private static final int[] SLIDEDRAWABLES = new int[]{
R.drawable.slide1_thumb,
R.drawable.slide2_thumb,
R.drawable.slide3_thumb,
R.drawable.slide4_thumb,
R.drawable.slide5_thumb,
R.drawable.slide6_thumb,
R.drawable.slide7_thumb,
R.drawable.slide8_thumb,
R.drawable.slide9_thumb,
R.drawable.slide10_thumb,
R.drawable.slide11_thumb
};
private static final String[] SLIDENOTES = {
"This is the teaser slide. It should be memorable and descriptive of what your " +
"company is trying to do", "",
"The bigger the pain, the better",
"How do you solve this problem? How is it better or different from existing solutions?",
"Demo the product", "", "[REDACTED]",
"They may have tractor traction, but we still have the competitive advantage",
"I'm not a businessman. I'm a business, man", "There is no 'i' on this slide",
"Sqrt(all evil)"};
private void importDeckFromResources(String prefix, String title, int resourceId) {
try {
putDeck(prefix, title, getImageBytes(resourceId));
for (int i = 0; i < SLIDENOTES.length; i++) {
putSlide(prefix, i, getImageBytes(SLIDEDRAWABLES[i]), SLIDENOTES[i]);
}
} catch (VException e) {
handleError(e.toString());
}
}
@Override
public void importDeck(io.v.android.apps.syncslides.model.Deck deck,
io.v.android.apps.syncslides.model.Slide[] slides) {
try {
putDeck(deck.getId(), deck.getTitle(), getImageBytes(deck.getThumb()));
for (int i = 0; i < slides.length; ++i) {
io.v.android.apps.syncslides.model.Slide slide = slides[i];
putSlide(deck.getId(), i, getImageBytes(slide.getImage()), slide.getNotes());
}
} catch (VException e) {
handleError(e.toString());
}
}
@Override
public void importDeck(final io.v.android.apps.syncslides.model.Deck deck,
final io.v.android.apps.syncslides.model.Slide[] slides,
final Callback<Void> callback) {
new Thread() {
@Override
public void run() {
importDeck(deck, slides);
if (callback != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
callback.done(null);
}
});
}
}
}.start();
}
private void putDeck(String prefix, String title, byte[] thumbData) throws VException {
Log.i(TAG, String.format("Adding deck %s, %s", prefix, title));
if (!mDecks.getRow(prefix).exists(mVContext)) {
mDecks.put(
mVContext,
prefix,
new VDeck(title, thumbData),
VDeck.class);
}
}
private void putSlide(String prefix, int idx, byte[] thumbData, String note) throws VException {
String key = slideRowKey(prefix, idx);
Log.i(TAG, "Adding slide " + key);
if (!mDecks.getRow(key).exists(mVContext)) {
mDecks.put(
mVContext,
key,
new VSlide(thumbData),
VSlide.class);
}
Log.i(TAG, "Adding note: " + note);
mNotes.put(mVContext, key, new VNote(note), VNote.class);
mNotes.put(
mVContext,
NamingUtil.join(prefix, "LastViewed"),
System.currentTimeMillis(),
Long.class);
}
@Override
public Deck getDeck(String deckId) {
VDeck vDeck = null;
try {
vDeck = (VDeck) mDecks.get(mVContext, deckId, VDeck.class);
} catch (VException e) {
// TODO(kash): Uncomment this when PresentationActivity.onCreate no
// longer needs to loop on getDeck(), waiting for it to return non-null.
//handleError(e.toString());
}
if (vDeck != null) {
return mDeckFactory.make(vDeck, deckId);
}
return null;
}
@Override
public void deleteDeck(String deckId) {
try {
mDecks.deleteRange(mVContext, RowRange.prefix(deckId));
} catch (VException e) {
handleError(e.toString());
}
}
@Override
public void deleteDeck(final String deckId, final Callback<Void> callback) {
new Thread() {
@Override
public void run() {
deleteDeck(deckId);
if (callback != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
callback.done(null);
}
});
}
}
}.start();
}
private byte[] getImageBytes(int resourceId) {
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), resourceId);
return getImageBytes(bitmap);
}
private byte[] getImageBytes(Bitmap bitmap) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 60, stream);
return stream.toByteArray();
}
}