Changed the root package from net.sf.briar to org.briarproject.

This commit is contained in:
akwizgran
2014-01-08 16:18:30 +00:00
parent dce70f487c
commit 832476412c
427 changed files with 2507 additions and 2507 deletions

View File

@@ -0,0 +1,82 @@
package org.briarproject.android;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.inject.Inject;
import org.briarproject.api.android.AndroidExecutor;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
class AndroidExecutorImpl implements AndroidExecutor {
private static final int SHUTDOWN = 0, RUN = 1;
private final Runnable loop;
private final AtomicBoolean started = new AtomicBoolean(false);
private final CountDownLatch startLatch = new CountDownLatch(1);
private volatile Handler handler = null;
@Inject
AndroidExecutorImpl() {
loop = new Runnable() {
public void run() {
Looper.prepare();
handler = new FutureTaskHandler();
startLatch.countDown();
Looper.loop();
}
};
}
private void startIfNecessary() {
if(started.getAndSet(true)) return;
new Thread(loop, "AndroidExecutor").start();
try {
startLatch.await();
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public <V> V call(Callable<V> c) throws InterruptedException,
ExecutionException {
startIfNecessary();
Future<V> f = new FutureTask<V>(c);
Message m = Message.obtain(handler, RUN, f);
handler.sendMessage(m);
return f.get();
}
public void shutdown() {
if(handler != null) {
Message m = Message.obtain(handler, SHUTDOWN);
handler.sendMessage(m);
}
}
private static class FutureTaskHandler extends Handler {
@Override
public void handleMessage(Message m) {
switch(m.what) {
case SHUTDOWN:
Looper.myLooper().quit();
break;
case RUN:
((FutureTask<?>) m.obj).run();
break;
default:
throw new IllegalArgumentException();
}
}
}
}

View File

@@ -0,0 +1,19 @@
package org.briarproject.android;
import java.io.File;
import java.io.IOException;
import org.briarproject.api.system.FileUtils;
import android.os.Build;
import android.os.StatFs;
class AndroidFileUtils implements FileUtils {
@SuppressWarnings("deprecation")
public long getFreeSpace(File f) throws IOException {
if(Build.VERSION.SDK_INT >= 9) return f.getUsableSpace();
StatFs s = new StatFs(f.getAbsolutePath());
// These deprecated methods are the best thing available for SDK < 9
return (long) s.getAvailableBlocks() * s.getBlockSize();
}
}

View File

@@ -0,0 +1,148 @@
package org.briarproject.android;
import static android.content.Context.MODE_PRIVATE;
import static java.util.concurrent.TimeUnit.SECONDS;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import javax.inject.Singleton;
import org.briarproject.api.android.AndroidExecutor;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.android.ReferenceManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.db.DatabaseConfig;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.lifecycle.ShutdownManager;
import org.briarproject.api.plugins.PluginExecutor;
import org.briarproject.api.plugins.duplex.DuplexPluginConfig;
import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
import org.briarproject.api.plugins.simplex.SimplexPluginConfig;
import org.briarproject.api.plugins.simplex.SimplexPluginFactory;
import org.briarproject.api.system.FileUtils;
import org.briarproject.api.ui.UiCallback;
import org.briarproject.plugins.droidtooth.DroidtoothPluginFactory;
import org.briarproject.plugins.tcp.DroidLanTcpPluginFactory;
import org.briarproject.plugins.tcp.WanTcpPluginFactory;
import org.briarproject.plugins.tor.TorPluginFactory;
import android.app.Application;
import android.content.Context;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
public class AndroidModule extends AbstractModule {
private final ExecutorService databaseUiExecutor;
private final UiCallback uiCallback;
public AndroidModule() {
// The queue is unbounded, so tasks can be dependent
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
// Discard tasks that are submitted during shutdown
RejectedExecutionHandler policy =
new ThreadPoolExecutor.DiscardPolicy();
// Use a single thread so DB accesses from the UI don't overlap
databaseUiExecutor = new ThreadPoolExecutor(1, 1, 60, SECONDS, queue,
policy);
// Use a dummy UI callback
uiCallback = new UiCallback() {
public int showChoice(String[] options, String... message) {
throw new UnsupportedOperationException();
}
public boolean showConfirmationMessage(String... message) {
throw new UnsupportedOperationException();
}
public void showMessage(String... message) {
throw new UnsupportedOperationException();
}
};
}
protected void configure() {
bind(AndroidExecutor.class).to(AndroidExecutorImpl.class);
bind(ReferenceManager.class).to(
ReferenceManagerImpl.class).in(Singleton.class);
bind(FileUtils.class).to(AndroidFileUtils.class);
bind(UiCallback.class).toInstance(uiCallback);
}
@Provides @Singleton @DatabaseUiExecutor
Executor getDatabaseUiExecutor(LifecycleManager lifecycleManager) {
lifecycleManager.registerForShutdown(databaseUiExecutor);
return databaseUiExecutor;
}
@Provides
SimplexPluginConfig getSimplexPluginConfig() {
return new SimplexPluginConfig() {
public Collection<SimplexPluginFactory> getFactories() {
return Collections.emptyList();
}
};
}
@Provides
DuplexPluginConfig getDuplexPluginConfig(
@PluginExecutor Executor pluginExecutor,
AndroidExecutor androidExecutor, Context appContext,
CryptoComponent crypto, ShutdownManager shutdownManager) {
DuplexPluginFactory droidtooth = new DroidtoothPluginFactory(
pluginExecutor, androidExecutor, appContext,
crypto.getSecureRandom());
DuplexPluginFactory tor = new TorPluginFactory(pluginExecutor,
appContext, shutdownManager);
DuplexPluginFactory lan = new DroidLanTcpPluginFactory(pluginExecutor,
appContext);
DuplexPluginFactory wan = new WanTcpPluginFactory(pluginExecutor,
shutdownManager);
final Collection<DuplexPluginFactory> factories =
Arrays.asList(droidtooth, tor, lan, wan);
return new DuplexPluginConfig() {
public Collection<DuplexPluginFactory> getFactories() {
return factories;
}
};
}
@Provides @Singleton
DatabaseConfig getDatabaseConfig(final Application app) {
final File dir = app.getApplicationContext().getDir("db", MODE_PRIVATE);
return new DatabaseConfig() {
private volatile byte[] key = null;
public boolean databaseExists() {
return dir.isDirectory() && dir.listFiles().length > 0;
}
public File getDatabaseDirectory() {
return dir;
}
public void setEncryptionKey(byte[] key) {
this.key = key;
}
public byte[] getEncryptionKey() {
return key;
}
public long getMaxSize() {
return Long.MAX_VALUE;
}
};
}
}

View File

@@ -0,0 +1,19 @@
package org.briarproject.android;
import java.util.Comparator;
import org.briarproject.api.db.MessageHeader;
public class AscendingHeaderComparator implements Comparator<MessageHeader> {
public static final AscendingHeaderComparator INSTANCE =
new AscendingHeaderComparator();
public int compare(MessageHeader a, MessageHeader b) {
// The oldest message comes first
long aTime = a.getTimestamp(), bTime = b.getTimestamp();
if(aTime < bTime) return -1;
if(aTime > bTime) return 1;
return 0;
}
}

View File

@@ -0,0 +1,143 @@
package org.briarproject.android;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static java.util.logging.Level.INFO;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.api.android.AndroidExecutor;
import org.briarproject.api.db.DatabaseConfig;
import org.briarproject.api.lifecycle.LifecycleManager;
import roboguice.service.RoboService;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
public class BriarService extends RoboService {
private static final Logger LOG =
Logger.getLogger(BriarService.class.getName());
private final Binder binder = new BriarBinder();
@Inject private DatabaseConfig databaseConfig;
private boolean started = false;
// Fields that are accessed from background threads must be volatile
@Inject private volatile LifecycleManager lifecycleManager;
@Inject private volatile AndroidExecutor androidExecutor;
@Override
public void onCreate() {
super.onCreate();
if(LOG.isLoggable(INFO)) LOG.info("Created");
if(databaseConfig.getEncryptionKey() == null) {
if(LOG.isLoggable(INFO)) LOG.info("No database key");
stopSelf();
return;
}
// Show an ongoing notification that the service is running
NotificationCompat.Builder b = new NotificationCompat.Builder(this);
b.setSmallIcon(R.drawable.notification_icon);
b.setContentTitle(getText(R.string.notification_title));
b.setContentText(getText(R.string.notification_text));
b.setWhen(0); // Don't show the time
// Touch the notification to show the home screen
Intent i = new Intent(this, HomeScreenActivity.class);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pi = PendingIntent.getActivity(this, 0, i, 0);
b.setContentIntent(pi);
b.setOngoing(true);
startForeground(1, b.build());
// Start the services in a background thread
new Thread() {
@Override
public void run() {
lifecycleManager.startServices();
}
}.start();
started = true;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(LOG.isLoggable(INFO)) LOG.info("Started");
return START_NOT_STICKY; // Don't restart automatically if killed
}
public IBinder onBind(Intent intent) {
if(LOG.isLoggable(INFO)) LOG.info("Bound");
return binder;
}
@Override
public void onDestroy() {
super.onDestroy();
if(LOG.isLoggable(INFO)) LOG.info("Destroyed");
// Stop the services in a background thread
if(started) new Thread() {
@Override
public void run() {
androidExecutor.shutdown();
lifecycleManager.stopServices();
}
}.start();
}
/** Waits for the database to be opened before returning. */
public void waitForDatabase() throws InterruptedException {
lifecycleManager.waitForDatabase();
}
/** Waits for all services to start before returning. */
public void waitForStartup() throws InterruptedException {
lifecycleManager.waitForStartup();
}
/** Waits for all services to stop before returning. */
public void waitForShutdown() throws InterruptedException {
lifecycleManager.waitForShutdown();
}
/** Starts the shutdown process. */
public void shutdown() {
stopSelf(); // This will call onDestroy()
}
public class BriarBinder extends Binder {
/** Returns the bound service. */
public BriarService getService() {
return BriarService.this;
}
}
public static class BriarServiceConnection implements ServiceConnection {
private final CountDownLatch binderLatch = new CountDownLatch(1);
private volatile IBinder binder = null;
public void onServiceConnected(ComponentName name, IBinder binder) {
this.binder = binder;
binderLatch.countDown();
}
public void onServiceDisconnected(ComponentName name) {}
/** Waits for the service to connect and returns its binder. */
public IBinder waitForBinder() throws InterruptedException {
binderLatch.await();
return binder;
}
}
}

View File

@@ -0,0 +1,391 @@
package org.briarproject.android;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.inputmethod.InputMethodManager.HIDE_IMPLICIT_ONLY;
import static android.widget.LinearLayout.VERTICAL;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.BriarService.BriarBinder;
import org.briarproject.android.BriarService.BriarServiceConnection;
import org.briarproject.android.contact.ContactListActivity;
import org.briarproject.android.groups.GroupListActivity;
import org.briarproject.api.LocalAuthor;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.android.ReferenceManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DatabaseConfig;
import org.briarproject.api.db.DbException;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.util.StringUtils;
import roboguice.activity.RoboActivity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.IBinder;
import android.text.Editable;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
public class HomeScreenActivity extends RoboActivity {
// This build expires on 15 January 2014
private static final long EXPIRY_DATE = 1389657600 * 1000L;
private static final Logger LOG =
Logger.getLogger(HomeScreenActivity.class.getName());
private final BriarServiceConnection serviceConnection =
new BriarServiceConnection();
@Inject private ReferenceManager referenceManager;
@Inject private DatabaseConfig databaseConfig;
@Inject @DatabaseUiExecutor private Executor dbUiExecutor;
@Inject @CryptoExecutor private Executor cryptoExecutor;
private boolean bound = false;
private TextView enterPassword = null;
private Button continueButton = null;
private ProgressBar progress = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile CryptoComponent crypto;
@Inject private volatile DatabaseComponent db;
@Inject private volatile LifecycleManager lifecycleManager;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
long handle = i.getLongExtra("org.briarproject.LOCAL_AUTHOR_HANDLE", -1);
if(handle != -1) {
// The activity was launched from the setup wizard
if(System.currentTimeMillis() < EXPIRY_DATE) {
showSpinner();
startService(new Intent(BriarService.class.getName()));
bindService();
LocalAuthor a = referenceManager.removeReference(handle,
LocalAuthor.class);
// The reference may be null if the activity has been recreated,
// for example due to screen rotation
if(a == null) showButtons();
else storeLocalAuthor(a);
} else {
showExpiryWarning();
}
} else if(databaseConfig.getEncryptionKey() == null) {
// The activity was launched from the splash screen
if(System.currentTimeMillis() < EXPIRY_DATE) showPasswordPrompt();
else showExpiryWarning();
} else {
// The activity has been launched before
showButtons();
bindService();
}
}
private void showSpinner() {
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setGravity(CENTER);
ProgressBar progress = new ProgressBar(this);
progress.setIndeterminate(true);
layout.addView(progress);
setContentView(layout);
}
private void bindService() {
bound = bindService(new Intent(BriarService.class.getName()),
serviceConnection, 0);
}
private void quit() {
new Thread() {
@Override
public void run() {
try {
// Wait for the service to finish starting up
IBinder binder = serviceConnection.waitForBinder();
BriarService service = ((BriarBinder) binder).getService();
service.waitForStartup();
// Shut down the service and wait for it to shut down
if(LOG.isLoggable(INFO)) LOG.info("Shutting down service");
service.shutdown();
service.waitForShutdown();
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for service");
}
// Finish the activity and kill the JVM
runOnUiThread(new Runnable() {
public void run() {
finish();
if(LOG.isLoggable(INFO)) LOG.info("Exiting");
System.exit(0);
}
});
}
}.start();
}
private void storeLocalAuthor(final LocalAuthor a) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
db.addLocalAuthor(a);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Storing author took " + duration + " ms");
runOnUiThread(new Runnable() {
public void run() {
showButtons();
}
});
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void showPasswordPrompt() {
SharedPreferences prefs = getSharedPreferences("db", MODE_PRIVATE);
String hex = prefs.getString("key", null);
if(hex == null) throw new IllegalStateException();
final byte[] encrypted = StringUtils.fromHexString(hex);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
enterPassword = new TextView(this);
enterPassword.setGravity(CENTER);
enterPassword.setTextSize(18);
enterPassword.setPadding(10, 10, 10, 0);
enterPassword.setText(R.string.enter_password);
layout.addView(enterPassword);
final EditText passwordEntry = new EditText(this);
passwordEntry.setId(1);
passwordEntry.setMaxLines(1);
int inputType = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_PASSWORD;
passwordEntry.setInputType(inputType);
passwordEntry.setOnEditorActionListener(new OnEditorActionListener() {
public boolean onEditorAction(TextView v, int action, KeyEvent e) {
validatePassword(encrypted, passwordEntry.getText());
return true;
}
});
layout.addView(passwordEntry);
continueButton = new Button(this);
continueButton.setLayoutParams(WRAP_WRAP);
continueButton.setText(R.string.continue_button);
continueButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
validatePassword(encrypted, passwordEntry.getText());
}
});
layout.addView(continueButton);
progress = new ProgressBar(this);
progress.setLayoutParams(WRAP_WRAP);
progress.setIndeterminate(true);
progress.setVisibility(GONE);
layout.addView(progress);
setContentView(layout);
}
private void validatePassword(final byte[] encrypted, Editable e) {
if(enterPassword == null || continueButton == null || progress == null)
return;
// Hide the soft keyboard
Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).toggleSoftInput(HIDE_IMPLICIT_ONLY, 0);
// Replace the button with a progress bar
continueButton.setVisibility(GONE);
progress.setVisibility(VISIBLE);
// Decrypt the database key in a background thread
int length = e.length();
final char[] password = new char[length];
e.getChars(0, length, password, 0);
e.delete(0, length);
cryptoExecutor.execute(new Runnable() {
public void run() {
byte[] key = crypto.decryptWithPassword(encrypted, password);
if(key == null) {
tryAgain();
} else {
databaseConfig.setEncryptionKey(key);
showButtonsAndStartService();
}
}
});
}
private void tryAgain() {
runOnUiThread(new Runnable() {
public void run() {
enterPassword.setText(R.string.try_again);
continueButton.setVisibility(VISIBLE);
progress.setVisibility(GONE);
}
});
}
private void showButtonsAndStartService() {
runOnUiThread(new Runnable() {
public void run() {
showButtons();
startService(new Intent(BriarService.class.getName()));
bindService();
}
});
}
private void showExpiryWarning() {
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setGravity(CENTER);
TextView warning = new TextView(this);
warning.setGravity(CENTER);
warning.setTextSize(18);
warning.setPadding(10, 10, 10, 10);
warning.setText(R.string.expiry_warning);
layout.addView(warning);
setContentView(layout);
}
private void showButtons() {
ListView.LayoutParams matchMatch =
new ListView.LayoutParams(MATCH_PARENT, MATCH_PARENT);
final List<Button> buttons = new ArrayList<Button>();
Button contactsButton = new Button(this);
contactsButton.setLayoutParams(matchMatch);
contactsButton.setBackgroundResource(0);
contactsButton.setCompoundDrawablesWithIntrinsicBounds(0,
R.drawable.social_person, 0, 0);
contactsButton.setText(R.string.contact_list_button);
contactsButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
startActivity(new Intent(HomeScreenActivity.this,
ContactListActivity.class));
}
});
buttons.add(contactsButton);
Button forumsButton = new Button(this);
forumsButton.setLayoutParams(matchMatch);
forumsButton.setBackgroundResource(0);
forumsButton.setCompoundDrawablesWithIntrinsicBounds(0,
R.drawable.social_chat, 0, 0);
forumsButton.setText(R.string.forums_button);
forumsButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
startActivity(new Intent(HomeScreenActivity.this,
GroupListActivity.class));
}
});
buttons.add(forumsButton);
Button syncButton = new Button(this);
syncButton.setLayoutParams(matchMatch);
syncButton.setBackgroundResource(0);
syncButton.setCompoundDrawablesWithIntrinsicBounds(0,
R.drawable.navigation_refresh, 0, 0);
syncButton.setText(R.string.synchronize_button);
syncButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
// FIXME: Crash testing, remove this
throw new RuntimeException();
}
});
buttons.add(syncButton);
Button quitButton = new Button(this);
quitButton.setLayoutParams(matchMatch);
quitButton.setBackgroundResource(0);
quitButton.setCompoundDrawablesWithIntrinsicBounds(0,
R.drawable.device_access_accounts, 0, 0);
quitButton.setText(R.string.quit_button);
quitButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
showSpinner();
quit();
}
});
buttons.add(quitButton);
GridView grid = new GridView(this);
grid.setLayoutParams(matchMatch);
grid.setGravity(CENTER);
grid.setPadding(5, 5, 5, 5);
grid.setBackgroundColor(getResources().getColor(
R.color.home_screen_background));
grid.setNumColumns(2);
grid.setAdapter(new BaseAdapter() {
public int getCount() {
return buttons.size();
}
public Object getItem(int position) {
return buttons.get(position);
}
public long getItemId(int position) {
return 0;
}
public View getView(int position, View convertView,
ViewGroup parent) {
return buttons.get(position);
}
});
setContentView(grid);
}
@Override
public void onDestroy() {
super.onDestroy();
if(bound) unbindService(serviceConnection);
}
}

View File

@@ -0,0 +1,61 @@
package org.briarproject.android;
import static java.util.logging.Level.INFO;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import org.briarproject.api.android.ReferenceManager;
class ReferenceManagerImpl implements ReferenceManager {
private static final Logger LOG =
Logger.getLogger(ReferenceManagerImpl.class.getName());
// Locking: this
private final Map<Class<?>, Map<Long, Object>> outerMap =
new HashMap<Class<?>, Map<Long, Object>>();
private long nextHandle = 0; // Locking: this
public synchronized <T> T getReference(long handle, Class<T> c) {
Map<Long, Object> innerMap = outerMap.get(c);
if(innerMap == null) {
if(LOG.isLoggable(INFO))
LOG.info("0 handles for " + c.getName());
return null;
}
if(LOG.isLoggable(INFO))
LOG.info(innerMap.size() + " handles for " + c.getName());
Object o = innerMap.get(handle);
return c.cast(o);
}
public synchronized <T> long putReference(T reference, Class<T> c) {
Map<Long, Object> innerMap = outerMap.get(c);
if(innerMap == null) {
innerMap = new HashMap<Long, Object>();
outerMap.put(c, innerMap);
}
long handle = nextHandle++;
innerMap.put(handle, reference);
if(LOG.isLoggable(INFO)) {
LOG.info(innerMap.size() + " handles for " + c.getName() +
" after put");
}
return handle;
}
public synchronized <T> T removeReference(long handle, Class<T> c) {
Map<Long, Object> innerMap = outerMap.get(c);
if(innerMap == null) return null;
Object o = innerMap.remove(handle);
if(innerMap.isEmpty()) outerMap.remove(c);
if(LOG.isLoggable(INFO)) {
LOG.info(innerMap.size() + " handles for " + c.getName() +
" after remove");
}
return c.cast(o);
}
}

View File

@@ -0,0 +1,225 @@
package org.briarproject.android;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_WORDS;
import static android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.LinearLayout.VERTICAL;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import java.util.Arrays;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.api.AuthorFactory;
import org.briarproject.api.LocalAuthor;
import org.briarproject.api.android.ReferenceManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.crypto.KeyPair;
import org.briarproject.api.db.DatabaseConfig;
import org.briarproject.util.StringUtils;
import roboguice.activity.RoboActivity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.text.Editable;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
public class SetupActivity extends RoboActivity implements OnClickListener {
private static final int MIN_PASSWORD_LENGTH = 8;
@Inject @CryptoExecutor private Executor cryptoExecutor;
private EditText nicknameEntry = null;
private EditText passwordEntry = null, passwordConfirmation = null;
private Button continueButton = null;
private ProgressBar progress = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile CryptoComponent crypto;
@Inject private volatile DatabaseConfig databaseConfig;
@Inject private volatile AuthorFactory authorFactory;
@Inject private volatile ReferenceManager referenceManager;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
TextView chooseNickname = new TextView(this);
chooseNickname.setGravity(CENTER);
chooseNickname.setTextSize(18);
chooseNickname.setPadding(10, 10, 10, 0);
chooseNickname.setText(R.string.choose_nickname);
layout.addView(chooseNickname);
nicknameEntry = new EditText(this) {
@Override
protected void onTextChanged(CharSequence text, int start,
int lengthBefore, int lengthAfter) {
enableOrDisableContinueButton();
}
};
nicknameEntry.setId(1);
nicknameEntry.setMaxLines(1);
int inputType = TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_CAP_WORDS;
nicknameEntry.setInputType(inputType);
layout.addView(nicknameEntry);
TextView choosePassword = new TextView(this);
choosePassword.setGravity(CENTER);
choosePassword.setTextSize(18);
choosePassword.setPadding(10, 10, 10, 0);
choosePassword.setText(R.string.choose_password);
layout.addView(choosePassword);
passwordEntry = new EditText(this) {
@Override
protected void onTextChanged(CharSequence text, int start,
int lengthBefore, int lengthAfter) {
enableOrDisableContinueButton();
}
};
passwordEntry.setId(2);
passwordEntry.setMaxLines(1);
inputType = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_PASSWORD;
passwordEntry.setInputType(inputType);
layout.addView(passwordEntry);
TextView confirmPassword = new TextView(this);
confirmPassword.setGravity(CENTER);
confirmPassword.setTextSize(18);
confirmPassword.setPadding(10, 10, 10, 0);
confirmPassword.setText(R.string.confirm_password);
layout.addView(confirmPassword);
passwordConfirmation = new EditText(this) {
@Override
protected void onTextChanged(CharSequence text, int start,
int lengthBefore, int lengthAfter) {
enableOrDisableContinueButton();
}
};
passwordConfirmation.setId(3);
passwordConfirmation.setMaxLines(1);
inputType = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_PASSWORD;
passwordConfirmation.setInputType(inputType);
layout.addView(passwordConfirmation);
TextView minPasswordLength = new TextView(this);
minPasswordLength.setGravity(CENTER);
minPasswordLength.setTextSize(14);
minPasswordLength.setPadding(10, 10, 10, 10);
String format = getResources().getString(R.string.format_min_password);
minPasswordLength.setText(String.format(format, MIN_PASSWORD_LENGTH));
layout.addView(minPasswordLength);
continueButton = new Button(this);
continueButton.setLayoutParams(WRAP_WRAP);
continueButton.setText(R.string.continue_button);
continueButton.setEnabled(false);
continueButton.setOnClickListener(this);
layout.addView(continueButton);
progress = new ProgressBar(this);
progress.setLayoutParams(WRAP_WRAP);
progress.setIndeterminate(true);
progress.setVisibility(GONE);
layout.addView(progress);
ScrollView scroll = new ScrollView(this);
scroll.addView(layout);
setContentView(scroll);
}
private void enableOrDisableContinueButton() {
if(nicknameEntry == null || passwordEntry == null ||
passwordConfirmation == null || continueButton == null) return;
boolean nicknameNotEmpty = nicknameEntry.getText().length() > 0;
char[] firstPassword = getChars(passwordEntry.getText());
char[] secondPassword = getChars(passwordConfirmation.getText());
boolean passwordLength = firstPassword.length >= MIN_PASSWORD_LENGTH;
boolean passwordsMatch = Arrays.equals(firstPassword, secondPassword);
for(int i = 0; i < firstPassword.length; i++) firstPassword[i] = 0;
for(int i = 0; i < secondPassword.length; i++) secondPassword[i] = 0;
boolean valid = nicknameNotEmpty && passwordLength && passwordsMatch;
continueButton.setEnabled(valid);
}
private char[] getChars(Editable e) {
int length = e.length();
char[] c = new char[length];
e.getChars(0, length, c, 0);
return c;
}
public void onClick(View view) {
final String nickname = nicknameEntry.getText().toString();
final char[] password = getChars(passwordEntry.getText());
delete(passwordEntry.getText());
delete(passwordConfirmation.getText());
// Replace the button with a progress bar
continueButton.setVisibility(GONE);
progress.setVisibility(VISIBLE);
// Store the DB key and create the identity in a background thread
cryptoExecutor.execute(new Runnable() {
public void run() {
byte[] key = crypto.generateSecretKey().getEncoded();
byte[] encrypted = crypto.encryptWithPassword(key, password);
storeEncryptedDatabaseKey(encrypted);
databaseConfig.setEncryptionKey(key);
KeyPair keyPair = crypto.generateSignatureKeyPair();
final byte[] publicKey = keyPair.getPublic().getEncoded();
final byte[] privateKey = keyPair.getPrivate().getEncoded();
LocalAuthor a = authorFactory.createLocalAuthor(nickname,
publicKey, privateKey);
showHomeScreen(referenceManager.putReference(a,
LocalAuthor.class));
}
});
}
private void delete(Editable e) {
e.delete(0, e.length());
}
private void storeEncryptedDatabaseKey(byte[] encrypted) {
SharedPreferences prefs = getSharedPreferences("db", MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putString("key", StringUtils.toHexString(encrypted));
editor.commit();
}
private void showHomeScreen(final long handle) {
runOnUiThread(new Runnable() {
public void run() {
Intent i = new Intent(SetupActivity.this,
HomeScreenActivity.class);
i.putExtra("org.briarproject.LOCAL_AUTHOR_HANDLE", handle);
i.setFlags(FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
finish();
}
});
}
}

View File

@@ -0,0 +1,46 @@
package org.briarproject.android;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.view.Gravity.CENTER;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import org.briarproject.api.db.DatabaseConfig;
import roboguice.RoboGuice;
import roboguice.activity.RoboSplashActivity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import com.google.inject.Injector;
public class SplashScreenActivity extends RoboSplashActivity {
public SplashScreenActivity() {
minDisplayMs = 0;
}
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setGravity(CENTER);
ProgressBar spinner = new ProgressBar(this);
spinner.setIndeterminate(true);
layout.addView(spinner);
setContentView(layout);
}
protected void startNextActivity() {
Injector guice = RoboGuice.getBaseApplicationInjector(getApplication());
if(guice.getInstance(DatabaseConfig.class).databaseExists()) {
Intent i = new Intent(this, HomeScreenActivity.class);
i.setFlags(FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
} else {
Intent i = new Intent(this, SetupActivity.class);
i.setFlags(FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
}
}
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.android.contact;
import org.briarproject.api.Contact;
public class ContactItem {
public static final ContactItem NEW = new ContactItem(null);
private final Contact contact;
public ContactItem(Contact contact) {
this.contact = contact;
}
public Contact getContact() {
return contact;
}
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.android.contact;
import java.util.Comparator;
public class ContactItemComparator implements Comparator<ContactItem> {
public static final ContactItemComparator INSTANCE =
new ContactItemComparator();
public int compare(ContactItem a, ContactItem b) {
if(a == b) return 0;
if(a == ContactItem.NEW) return 1;
if(b == ContactItem.NEW) return -1;
String aName = a.getContact().getAuthor().getName();
String bName = b.getContact().getAuthor().getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
}

View File

@@ -0,0 +1,320 @@
package org.briarproject.android.contact;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.LinearLayout.VERTICAL;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
import java.util.Collection;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.invitation.AddContactActivity;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.api.AuthorId;
import org.briarproject.api.Contact;
import org.briarproject.api.ContactId;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.event.ContactAddedEvent;
import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageExpiredEvent;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.transport.ConnectionListener;
import org.briarproject.api.transport.ConnectionRegistry;
import roboguice.activity.RoboActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ListView;
public class ContactListActivity extends RoboActivity
implements OnClickListener, OnItemClickListener, EventListener,
ConnectionListener {
private static final Logger LOG =
Logger.getLogger(ContactListActivity.class.getName());
@Inject private ConnectionRegistry connectionRegistry;
private ContactListAdapter adapter = null;
private ListView list = null;
private ListLoadingProgressBar loading = null;
private ImageButton addContactButton = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
adapter = new ContactListAdapter(this);
list = new ListView(this);
// Give me all the width and all the unused height
list.setLayoutParams(MATCH_WRAP_1);
list.setAdapter(adapter);
list.setOnItemClickListener(this);
layout.addView(list);
// Show a progress bar while the list is loading
list.setVisibility(GONE);
loading = new ListLoadingProgressBar(this);
layout.addView(loading);
layout.addView(new HorizontalBorder(this));
addContactButton = new ImageButton(this);
addContactButton.setBackgroundResource(0);
addContactButton.setImageResource(R.drawable.social_add_person);
addContactButton.setOnClickListener(this);
layout.addView(addContactButton);
setContentView(layout);
}
@Override
public void onResume() {
super.onResume();
db.addListener(this);
connectionRegistry.addListener(this);
loadContacts();
}
private void loadContacts() {
clearContacts();
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Map<ContactId, Long> times = db.getLastConnected();
for(Contact c : db.getContacts()) {
Long lastConnected = times.get(c.getId());
if(lastConnected == null) continue;
try {
GroupId inbox = db.getInboxGroupId(c.getId());
Collection<MessageHeader> headers =
db.getInboxMessageHeaders(c.getId());
displayContact(c, lastConnected, inbox, headers);
} catch(NoSuchContactException e) {
if(LOG.isLoggable(INFO))
LOG.info("Contact removed");
}
}
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Full load took " + duration + " ms");
hideProgressBar();
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void clearContacts() {
runOnUiThread(new Runnable() {
public void run() {
list.setVisibility(GONE);
loading.setVisibility(VISIBLE);
adapter.clear();
adapter.notifyDataSetChanged();
}
});
}
private void displayContact(final Contact c, final long lastConnected,
final GroupId inbox, final Collection<MessageHeader> headers) {
runOnUiThread(new Runnable() {
public void run() {
list.setVisibility(VISIBLE);
loading.setVisibility(GONE);
boolean connected = connectionRegistry.isConnected(c.getId());
// Remove the old item, if any
ContactListItem item = findItem(c.getId());
if(item != null) adapter.remove(item);
// Add a new item
adapter.add(new ContactListItem(c, connected, lastConnected,
inbox, headers));
adapter.sort(ItemComparator.INSTANCE);
adapter.notifyDataSetChanged();
}
});
}
private void hideProgressBar() {
runOnUiThread(new Runnable() {
public void run() {
list.setVisibility(VISIBLE);
loading.setVisibility(GONE);
}
});
}
private ContactListItem findItem(ContactId c) {
int count = adapter.getCount();
for(int i = 0; i < count; i++) {
ContactListItem item = adapter.getItem(i);
if(item.getContact().getId().equals(c)) return item;
}
return null; // Not found
}
@Override
public void onPause() {
super.onPause();
db.removeListener(this);
connectionRegistry.removeListener(this);
}
public void onClick(View view) {
startActivity(new Intent(this, AddContactActivity.class));
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
ContactListItem item = adapter.getItem(position);
ContactId contactId = item.getContact().getId();
String contactName = item.getContact().getAuthor().getName();
GroupId inbox = item.getInboxGroupId();
AuthorId localAuthorId = item.getContact().getLocalAuthorId();
Intent i = new Intent(this, ConversationActivity.class);
i.putExtra("org.briarproject.CONTACT_ID", contactId.getInt());
i.putExtra("org.briarproject.CONTACT_NAME", contactName);
i.putExtra("org.briarproject.GROUP_ID", inbox.getBytes());
i.putExtra("org.briarproject.LOCAL_AUTHOR_ID", localAuthorId.getBytes());
startActivity(i);
}
public void eventOccurred(Event e) {
if(e instanceof ContactAddedEvent) {
loadContacts();
} else if(e instanceof ContactRemovedEvent) {
// Reload the conversation, expecting NoSuchContactException
if(LOG.isLoggable(INFO)) LOG.info("Contact removed, reloading");
reloadContact(((ContactRemovedEvent) e).getContactId());
} else if(e instanceof MessageAddedEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
ContactId source = ((MessageAddedEvent) e).getContactId();
if(source == null) loadContacts();
else reloadContact(source);
} else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
loadContacts();
}
}
private void reloadContact(final ContactId c) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Collection<MessageHeader> headers =
db.getInboxMessageHeaders(c);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Partial load took " + duration + " ms");
updateItem(c, headers);
} catch(NoSuchContactException e) {
if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
removeItem(c);
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void updateItem(final ContactId c,
final Collection<MessageHeader> headers) {
runOnUiThread(new Runnable() {
public void run() {
ContactListItem item = findItem(c);
if(item != null) item.setHeaders(headers);
}
});
}
private void removeItem(final ContactId c) {
runOnUiThread(new Runnable() {
public void run() {
ContactListItem item = findItem(c);
if(item != null) {
adapter.remove(item);
adapter.notifyDataSetChanged();
}
}
});
}
public void contactConnected(ContactId c) {
setConnected(c, true);
}
public void contactDisconnected(ContactId c) {
setConnected(c, false);
}
private void setConnected(final ContactId c, final boolean connected) {
runOnUiThread(new Runnable() {
public void run() {
ContactListItem item = findItem(c);
if(item == null) return;
if(LOG.isLoggable(INFO)) LOG.info("Updating connection time");
item.setConnected(connected);
item.setLastConnected(System.currentTimeMillis());
list.invalidateViews();
}
});
}
private static class ItemComparator implements Comparator<ContactListItem> {
private static final ItemComparator INSTANCE = new ItemComparator();
public int compare(ContactListItem a, ContactListItem b) {
String aName = a.getContact().getAuthor().getName();
String bName = b.getContact().getAuthor().getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
}
}

View File

@@ -0,0 +1,73 @@
package org.briarproject.android.contact;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.widget.LinearLayout.HORIZONTAL;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import java.util.ArrayList;
import org.briarproject.R;
import android.content.Context;
import android.content.res.Resources;
import android.text.Html;
import android.text.format.DateUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
class ContactListAdapter extends ArrayAdapter<ContactListItem> {
ContactListAdapter(Context ctx) {
super(ctx, android.R.layout.simple_expandable_list_item_1,
new ArrayList<ContactListItem>());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ContactListItem item = getItem(position);
Context ctx = getContext();
LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(HORIZONTAL);
layout.setGravity(CENTER_VERTICAL);
Resources res = ctx.getResources();
if(item.getUnreadCount() > 0)
layout.setBackgroundColor(res.getColor(R.color.unread_background));
ImageView bulb = new ImageView(ctx);
bulb.setPadding(5, 5, 5, 5);
if(item.isConnected())
bulb.setImageResource(R.drawable.contact_connected);
else bulb.setImageResource(R.drawable.contact_disconnected);
layout.addView(bulb);
TextView name = new TextView(ctx);
// Give me all the unused width
name.setLayoutParams(WRAP_WRAP_1);
name.setTextSize(18);
name.setMaxLines(1);
name.setPadding(0, 10, 10, 10);
int unread = item.getUnreadCount();
String contactName = item.getContact().getAuthor().getName();
if(unread > 0) name.setText(contactName + " (" + unread + ")");
else name.setText(contactName);
layout.addView(name);
TextView connected = new TextView(ctx);
connected.setTextSize(14);
connected.setPadding(0, 10, 10, 10);
if(item.isConnected()) {
connected.setText(R.string.contact_connected);
} else {
String format = res.getString(R.string.format_last_connected);
long then = item.getLastConnected();
CharSequence ago = DateUtils.getRelativeTimeSpanString(then);
connected.setText(Html.fromHtml(String.format(format, ago)));
}
layout.addView(connected);
return layout;
}
}

View File

@@ -0,0 +1,76 @@
package org.briarproject.android.contact;
import java.util.Collection;
import org.briarproject.api.Contact;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.messaging.GroupId;
// This class is not thread-safe
class ContactListItem {
private final Contact contact;
private final GroupId inbox;
private boolean connected;
private long lastConnected;
private boolean empty;
private long timestamp;
private int unread;
ContactListItem(Contact contact, boolean connected, long lastConnected,
GroupId inbox, Collection<MessageHeader> headers) {
this.contact = contact;
this.inbox = inbox;
this.connected = connected;
this.lastConnected = lastConnected;
setHeaders(headers);
}
void setHeaders(Collection<MessageHeader> headers) {
empty = headers.isEmpty();
timestamp = 0;
unread = 0;
if(!empty) {
for(MessageHeader h : headers) {
if(h.getTimestamp() > timestamp) timestamp = h.getTimestamp();
if(!h.isRead()) unread++;
}
}
}
Contact getContact() {
return contact;
}
GroupId getInboxGroupId() {
return inbox;
}
long getLastConnected() {
return lastConnected;
}
void setLastConnected(long lastConnected) {
this.lastConnected = lastConnected;
}
boolean isConnected() {
return connected;
}
void setConnected(boolean connected) {
this.connected = connected;
}
boolean isEmpty() {
return empty;
}
long getTimestamp() {
return timestamp;
}
int getUnreadCount() {
return unread;
}
}

View File

@@ -0,0 +1,248 @@
package org.briarproject.android.contact;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.LinearLayout.VERTICAL;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.api.AuthorId;
import org.briarproject.api.ContactId;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageExpiredEvent;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.GroupId;
import roboguice.activity.RoboActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ListView;
public class ConversationActivity extends RoboActivity
implements EventListener, OnClickListener, OnItemClickListener {
private static final Logger LOG =
Logger.getLogger(ConversationActivity.class.getName());
private String contactName = null;
private ConversationAdapter adapter = null;
private ListView list = null;
private ListLoadingProgressBar loading = null;
private ImageButton composeButton = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
private volatile ContactId contactId = null;
private volatile GroupId groupId = null;
private volatile AuthorId localAuthorId = null;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
int id = i.getIntExtra("org.briarproject.CONTACT_ID", -1);
if(id == -1) throw new IllegalStateException();
contactId = new ContactId(id);
contactName = i.getStringExtra("org.briarproject.CONTACT_NAME");
if(contactName == null) throw new IllegalStateException();
setTitle(contactName);
byte[] b = i.getByteArrayExtra("org.briarproject.GROUP_ID");
if(b == null) throw new IllegalStateException();
groupId = new GroupId(b);
b = i.getByteArrayExtra("org.briarproject.LOCAL_AUTHOR_ID");
if(b == null) throw new IllegalStateException();
localAuthorId = new AuthorId(b);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
adapter = new ConversationAdapter(this);
list = new ListView(this);
// Give me all the width and all the unused height
list.setLayoutParams(MATCH_WRAP_1);
list.setAdapter(adapter);
list.setOnItemClickListener(this);
layout.addView(list);
// Show a progress bar while the list is loading
list.setVisibility(GONE);
loading = new ListLoadingProgressBar(this);
layout.addView(loading);
layout.addView(new HorizontalBorder(this));
composeButton = new ImageButton(this);
composeButton.setBackgroundResource(0);
composeButton.setImageResource(R.drawable.content_new_email);
composeButton.setEnabled(false); // Enabled after loading the headers
composeButton.setOnClickListener(this);
layout.addView(composeButton);
setContentView(layout);
}
@Override
public void onResume() {
super.onResume();
db.addListener(this);
loadHeaders();
}
private void loadHeaders() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Collection<MessageHeader> headers =
db.getInboxMessageHeaders(contactId);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayHeaders(headers);
} catch(NoSuchContactException e) {
if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
runOnUiThread(new Runnable() {
public void run() {
finish();
}
});
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void displayHeaders(final Collection<MessageHeader> headers) {
runOnUiThread(new Runnable() {
public void run() {
list.setVisibility(VISIBLE);
loading.setVisibility(GONE);
composeButton.setEnabled(true);
adapter.clear();
for(MessageHeader h : headers)
adapter.add(new ConversationItem(h));
adapter.sort(ConversationItemComparator.INSTANCE);
adapter.notifyDataSetChanged();
selectFirstUnread();
}
});
}
private void selectFirstUnread() {
int firstUnread = -1, count = adapter.getCount();
for(int i = 0; i < count; i++) {
if(!adapter.getItem(i).getHeader().isRead()) {
firstUnread = i;
break;
}
}
if(firstUnread == -1) list.setSelection(count - 1);
else list.setSelection(firstUnread);
}
@Override
public void onActivityResult(int request, int result, Intent data) {
if(result == ReadPrivateMessageActivity.RESULT_PREV) {
int position = request - 1;
if(position >= 0 && position < adapter.getCount())
displayMessage(position);
} else if(result == ReadPrivateMessageActivity.RESULT_NEXT) {
int position = request + 1;
if(position >= 0 && position < adapter.getCount())
displayMessage(position);
}
}
@Override
public void onPause() {
super.onPause();
db.removeListener(this);
}
public void eventOccurred(Event e) {
if(e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e;
if(c.getContactId().equals(contactId)) {
if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
runOnUiThread(new Runnable() {
public void run() {
finish();
}
});
}
} else if(e instanceof MessageAddedEvent) {
GroupId g = ((MessageAddedEvent) e).getGroup().getId();
if(g.equals(groupId)) {
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
loadHeaders();
}
} else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
loadHeaders();
}
}
public void onClick(View view) {
Intent i = new Intent(this, WritePrivateMessageActivity.class);
i.putExtra("org.briarproject.CONTACT_NAME", contactName);
i.putExtra("org.briarproject.GROUP_ID", groupId.getBytes());
i.putExtra("org.briarproject.LOCAL_AUTHOR_ID", localAuthorId.getBytes());
startActivity(i);
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
displayMessage(position);
}
private void displayMessage(int position) {
MessageHeader header = adapter.getItem(position).getHeader();
Intent i = new Intent(this, ReadPrivateMessageActivity.class);
i.putExtra("org.briarproject.CONTACT_ID", contactId.getInt());
i.putExtra("org.briarproject.CONTACT_NAME", contactName);
i.putExtra("org.briarproject.GROUP_ID", groupId.getBytes());
i.putExtra("org.briarproject.LOCAL_AUTHOR_ID", localAuthorId.getBytes());
i.putExtra("org.briarproject.AUTHOR_NAME", header.getAuthor().getName());
i.putExtra("org.briarproject.MESSAGE_ID", header.getId().getBytes());
i.putExtra("org.briarproject.CONTENT_TYPE", header.getContentType());
i.putExtra("org.briarproject.TIMESTAMP", header.getTimestamp());
startActivityForResult(i, position);
}
}

View File

@@ -0,0 +1,57 @@
package org.briarproject.android.contact;
import static android.widget.LinearLayout.HORIZONTAL;
import static java.text.DateFormat.SHORT;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import java.util.ArrayList;
import org.briarproject.R;
import org.briarproject.api.db.MessageHeader;
import android.content.Context;
import android.content.res.Resources;
import android.text.format.DateUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
class ConversationAdapter extends ArrayAdapter<ConversationItem> {
ConversationAdapter(Context ctx) {
super(ctx, android.R.layout.simple_expandable_list_item_1,
new ArrayList<ConversationItem>());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
MessageHeader header = getItem(position).getHeader();
Context ctx = getContext();
LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(HORIZONTAL);
if(!header.isRead()) {
Resources res = ctx.getResources();
layout.setBackgroundColor(res.getColor(R.color.unread_background));
}
TextView name = new TextView(ctx);
// Give me all the unused width
name.setLayoutParams(WRAP_WRAP_1);
name.setTextSize(18);
name.setMaxLines(1);
name.setPadding(10, 10, 10, 10);
name.setText(header.getAuthor().getName());
layout.addView(name);
TextView date = new TextView(ctx);
date.setTextSize(14);
date.setPadding(0, 10, 10, 10);
long then = header.getTimestamp(), now = System.currentTimeMillis();
date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT));
layout.addView(date);
return layout;
}
}

View File

@@ -0,0 +1,37 @@
package org.briarproject.android.contact;
import org.briarproject.api.db.MessageHeader;
// This class is not thread-safe
class ConversationItem {
private final MessageHeader header;
private boolean expanded;
private byte[] body;
ConversationItem(MessageHeader header) {
this.header = header;
expanded = false;
body = null;
}
MessageHeader getHeader() {
return header;
}
boolean isExpanded() {
return expanded;
}
void setExpanded(boolean expanded) {
this.expanded = expanded;
}
byte[] getBody() {
return body;
}
void setBody(byte[] body) {
this.body = body;
}
}

View File

@@ -0,0 +1,19 @@
package org.briarproject.android.contact;
import java.util.Comparator;
public class ConversationItemComparator
implements Comparator<ConversationItem> {
public static final ConversationItemComparator INSTANCE =
new ConversationItemComparator();
public int compare(ConversationItem a, ConversationItem b) {
// The oldest message comes first
long aTime = a.getHeader().getTimestamp();
long bTime = b.getHeader().getTimestamp();
if(aTime < bTime) return -1;
if(aTime > bTime) return 1;
return 0;
}
}

View File

@@ -0,0 +1,281 @@
package org.briarproject.android.contact;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;
import static java.text.DateFormat.SHORT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.HorizontalSpace;
import org.briarproject.api.AuthorId;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchMessageException;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.MessageId;
import roboguice.activity.RoboActivity;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.format.DateUtils;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
public class ReadPrivateMessageActivity extends RoboActivity
implements OnClickListener {
static final int RESULT_REPLY = RESULT_FIRST_USER;
static final int RESULT_PREV = RESULT_FIRST_USER + 1;
static final int RESULT_NEXT = RESULT_FIRST_USER + 2;
private static final Logger LOG =
Logger.getLogger(ReadPrivateMessageActivity.class.getName());
private String contactName = null;
private AuthorId localAuthorId = null;
private boolean read = false;
private ImageButton readButton = null, prevButton = null, nextButton = null;
private ImageButton replyButton = null;
private TextView content = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
private volatile MessageId messageId = null;
private volatile GroupId groupId = null;
private volatile long timestamp = -1;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
contactName = i.getStringExtra("org.briarproject.CONTACT_NAME");
if(contactName == null) throw new IllegalStateException();
setTitle(contactName);
byte[] b = i.getByteArrayExtra("org.briarproject.LOCAL_AUTHOR_ID");
if(b == null) throw new IllegalStateException();
localAuthorId = new AuthorId(b);
String authorName = i.getStringExtra("org.briarproject.AUTHOR_NAME");
if(authorName == null) throw new IllegalStateException();
b = i.getByteArrayExtra("org.briarproject.MESSAGE_ID");
if(b == null) throw new IllegalStateException();
messageId = new MessageId(b);
b = i.getByteArrayExtra("org.briarproject.GROUP_ID");
if(b == null) throw new IllegalStateException();
groupId = new GroupId(b);
String contentType = i.getStringExtra("org.briarproject.CONTENT_TYPE");
if(contentType == null) throw new IllegalStateException();
timestamp = i.getLongExtra("org.briarproject.TIMESTAMP", -1);
if(timestamp == -1) throw new IllegalStateException();
if(state == null) {
read = false;
setReadInDatabase(true);
} else {
read = state.getBoolean("org.briarproject.READ");
}
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_WRAP);
layout.setOrientation(VERTICAL);
ScrollView scrollView = new ScrollView(this);
// Give me all the width and all the unused height
scrollView.setLayoutParams(MATCH_WRAP_1);
LinearLayout message = new LinearLayout(this);
message.setOrientation(VERTICAL);
Resources res = getResources();
message.setBackgroundColor(res.getColor(R.color.content_background));
LinearLayout header = new LinearLayout(this);
header.setLayoutParams(MATCH_WRAP);
header.setOrientation(HORIZONTAL);
header.setGravity(CENTER_VERTICAL);
TextView name = new TextView(this);
// Give me all the unused width
name.setLayoutParams(WRAP_WRAP_1);
name.setTextSize(18);
name.setMaxLines(1);
name.setPadding(10, 10, 10, 10);
name.setText(authorName);
header.addView(name);
TextView date = new TextView(this);
date.setTextSize(14);
date.setPadding(0, 10, 10, 10);
long now = System.currentTimeMillis();
date.setText(DateUtils.formatSameDayTime(timestamp, now, SHORT, SHORT));
header.addView(date);
message.addView(header);
if(contentType.equals("text/plain")) {
// Load and display the message body
content = new TextView(this);
content.setPadding(10, 0, 10, 10);
message.addView(content);
loadMessageBody();
}
scrollView.addView(message);
layout.addView(scrollView);
layout.addView(new HorizontalBorder(this));
LinearLayout footer = new LinearLayout(this);
footer.setLayoutParams(MATCH_WRAP);
footer.setOrientation(HORIZONTAL);
footer.setGravity(CENTER);
readButton = new ImageButton(this);
readButton.setBackgroundResource(0);
if(read) readButton.setImageResource(R.drawable.content_unread);
else readButton.setImageResource(R.drawable.content_read);
readButton.setOnClickListener(this);
footer.addView(readButton);
footer.addView(new HorizontalSpace(this));
prevButton = new ImageButton(this);
prevButton.setBackgroundResource(0);
prevButton.setImageResource(R.drawable.navigation_previous_item);
prevButton.setOnClickListener(this);
footer.addView(prevButton);
footer.addView(new HorizontalSpace(this));
nextButton = new ImageButton(this);
nextButton.setBackgroundResource(0);
nextButton.setImageResource(R.drawable.navigation_next_item);
nextButton.setOnClickListener(this);
footer.addView(nextButton);
footer.addView(new HorizontalSpace(this));
replyButton = new ImageButton(this);
replyButton.setBackgroundResource(0);
replyButton.setImageResource(R.drawable.social_reply);
replyButton.setOnClickListener(this);
footer.addView(replyButton);
layout.addView(footer);
setContentView(layout);
}
private void setReadInDatabase(final boolean read) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
db.setReadFlag(messageId, read);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Setting flag took " + duration + " ms");
setReadInUi(read);
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void setReadInUi(final boolean read) {
runOnUiThread(new Runnable() {
public void run() {
ReadPrivateMessageActivity.this.read = read;
if(read) readButton.setImageResource(R.drawable.content_unread);
else readButton.setImageResource(R.drawable.content_read);
}
});
}
private void loadMessageBody() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
byte[] body = db.getMessageBody(messageId);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Loading message took " + duration + " ms");
final String text = new String(body, "UTF-8");
runOnUiThread(new Runnable() {
public void run() {
content.setText(text);
}
});
} catch(NoSuchMessageException e) {
if(LOG.isLoggable(INFO)) LOG.info("Message removed");
runOnUiThread(new Runnable() {
public void run() {
finish();
}
});
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
} catch(UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
});
}
@Override
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
state.putBoolean("org.briarproject.READ", read);
}
public void onClick(View view) {
if(view == readButton) {
setReadInDatabase(!read);
} else if(view == prevButton) {
setResult(RESULT_PREV);
finish();
} else if(view == nextButton) {
setResult(RESULT_NEXT);
finish();
} else if(view == replyButton) {
Intent i = new Intent(this, WritePrivateMessageActivity.class);
i.putExtra("org.briarproject.CONTACT_NAME", contactName);
i.putExtra("org.briarproject.GROUP_ID", groupId.getBytes());
i.putExtra("org.briarproject.LOCAL_AUTHOR_ID",
localAuthorId.getBytes());
i.putExtra("org.briarproject.PARENT_ID", messageId.getBytes());
i.putExtra("org.briarproject.TIMESTAMP", timestamp);
startActivity(i);
setResult(RESULT_REPLY);
finish();
}
}
}

View File

@@ -0,0 +1,65 @@
package org.briarproject.android.contact;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.briarproject.R;
import org.briarproject.api.Contact;
import org.briarproject.api.ContactId;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
public class SelectContactsDialog extends DialogFragment
implements DialogInterface.OnMultiChoiceClickListener {
private final Set<ContactId> selected = new HashSet<ContactId>();
private Listener listener = null;
private Contact[] contacts = null;
public void setListener(Listener listener) {
this.listener = listener;
}
public void setContacts(Collection<Contact> contacts) {
this.contacts = contacts.toArray(new Contact[contacts.size()]);
}
@Override
public Dialog onCreateDialog(Bundle state) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
String[] names = new String[contacts.length];
for(int i = 0; i < contacts.length; i++)
names[i] = contacts[i].getAuthor().getName();
builder.setMultiChoiceItems(names, new boolean[contacts.length], this);
builder.setPositiveButton(R.string.done_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.contactsSelected(selected);
}
});
builder.setNegativeButton(R.string.cancel_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.contactSelectionCancelled();
}
});
return builder.create();
}
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
if(isChecked) selected.add(contacts[which].getId());
else selected.remove(contacts[which].getId());
}
public interface Listener {
void contactsSelected(Collection<ContactId> selected);
void contactSelectionCancelled();
}
}

View File

@@ -0,0 +1,230 @@
package org.briarproject.android.contact;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.util.HorizontalSpace;
import org.briarproject.api.AuthorId;
import org.briarproject.api.LocalAuthor;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.db.NoSuchSubscriptionException;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.Message;
import org.briarproject.api.messaging.MessageFactory;
import org.briarproject.api.messaging.MessageId;
import roboguice.activity.RoboActivity;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.InputType;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
public class WritePrivateMessageActivity extends RoboActivity
implements OnClickListener {
private static final Logger LOG =
Logger.getLogger(WritePrivateMessageActivity.class.getName());
private TextView from = null, to = null;
private ImageButton sendButton = null;
private EditText content = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
@Inject @CryptoExecutor private volatile Executor cryptoExecutor;
@Inject private volatile MessageFactory messageFactory;
private volatile String contactName = null;
private volatile GroupId groupId = null;
private volatile AuthorId localAuthorId = null;
private volatile MessageId parentId = null;
private volatile long timestamp = -1;
private volatile LocalAuthor localAuthor = null;
private volatile Group group = null;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
contactName = i.getStringExtra("org.briarproject.CONTACT_NAME");
if(contactName == null) throw new IllegalStateException();
byte[] b = i.getByteArrayExtra("org.briarproject.GROUP_ID");
if(b == null) throw new IllegalStateException();
groupId = new GroupId(b);
b = i.getByteArrayExtra("org.briarproject.LOCAL_AUTHOR_ID");
if(b == null) throw new IllegalStateException();
localAuthorId = new AuthorId(b);
b = i.getByteArrayExtra("org.briarproject.PARENT_ID");
if(b != null) parentId = new MessageId(b);
timestamp = i.getLongExtra("org.briarproject.TIMESTAMP", -1);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_WRAP);
layout.setOrientation(VERTICAL);
LinearLayout header = new LinearLayout(this);
header.setLayoutParams(MATCH_WRAP);
header.setOrientation(HORIZONTAL);
header.setGravity(CENTER_VERTICAL);
from = new TextView(this);
from.setTextSize(18);
from.setPadding(10, 10, 10, 10);
from.setText(R.string.from);
header.addView(from);
header.addView(new HorizontalSpace(this));
sendButton = new ImageButton(this);
sendButton.setBackgroundResource(0);
sendButton.setImageResource(R.drawable.social_send_now);
sendButton.setEnabled(false); // Enabled after loading the group
sendButton.setOnClickListener(this);
header.addView(sendButton);
layout.addView(header);
to = new TextView(this);
to.setTextSize(18);
to.setPadding(10, 0, 10, 10);
String format = getResources().getString(R.string.format_to);
to.setText(String.format(format, contactName));
layout.addView(to);
content = new EditText(this);
content.setId(1);
int inputType = TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE
| TYPE_TEXT_FLAG_CAP_SENTENCES;
content.setInputType(inputType);
layout.addView(content);
setContentView(layout);
}
@Override
public void onResume() {
super.onResume();
loadAuthorAndGroup();
}
private void loadAuthorAndGroup() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
localAuthor = db.getLocalAuthor(localAuthorId);
group = db.getGroup(groupId);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayLocalAuthor();
} catch(NoSuchContactException e) {
finish();
} catch(NoSuchSubscriptionException e) {
finish();
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void displayLocalAuthor() {
runOnUiThread(new Runnable() {
public void run() {
Resources res = getResources();
String format = res.getString(R.string.format_from);
String name = localAuthor.getName();
from.setText(String.format(format, name));
sendButton.setEnabled(true);
}
});
}
public void onClick(View view) {
if(localAuthor == null) throw new IllegalStateException();
try {
createMessage(content.getText().toString().getBytes("UTF-8"));
} catch(UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
Toast.makeText(this, R.string.message_sent_toast, LENGTH_LONG).show();
finish();
}
private void createMessage(final byte[] body) {
cryptoExecutor.execute(new Runnable() {
public void run() {
// Don't use an earlier timestamp than the parent
long time = System.currentTimeMillis();
time = Math.max(time, timestamp + 1);
Message m;
try {
m = messageFactory.createAnonymousMessage(parentId, group,
"text/plain", time, body);
} catch(GeneralSecurityException e) {
throw new RuntimeException(e);
} catch(IOException e) {
throw new RuntimeException(e);
}
storeMessage(m);
}
});
}
private void storeMessage(final Message m) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
db.addLocalMessage(m);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Storing message took " + duration + " ms");
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
}

View File

@@ -0,0 +1,247 @@
package org.briarproject.android.groups;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.LinearLayout.VERTICAL;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.contact.SelectContactsDialog;
import org.briarproject.android.invitation.AddContactActivity;
import org.briarproject.api.Contact;
import org.briarproject.api.ContactId;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupId;
import roboguice.activity.RoboFragmentActivity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.RadioGroup;
public class ConfigureGroupActivity extends RoboFragmentActivity
implements OnClickListener, NoContactsDialog.Listener,
SelectContactsDialog.Listener {
private static final Logger LOG =
Logger.getLogger(ConfigureGroupActivity.class.getName());
private boolean subscribed = false;
private CheckBox subscribeCheckBox = null;
private RadioGroup radioGroup = null;
private RadioButton visibleToAll = null, visibleToSome = null;
private Button doneButton = null;
private ProgressBar progress = null;
private NoContactsDialog noContactsDialog = null;
private SelectContactsDialog selectContactsDialog = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
private volatile Group group = null;
private volatile Collection<ContactId> selected = Collections.emptyList();
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra("org.briarproject.GROUP_ID");
if(b == null) throw new IllegalStateException();
GroupId id = new GroupId(b);
String name = i.getStringExtra("org.briarproject.GROUP_NAME");
if(name == null) throw new IllegalStateException();
setTitle(name);
b = i.getByteArrayExtra("org.briarproject.GROUP_SALT");
if(b == null) throw new IllegalStateException();
group = new Group(id, name, b);
subscribed = i.getBooleanExtra("org.briarproject.SUBSCRIBED", false);
boolean all = i.getBooleanExtra("org.briarproject.VISIBLE_TO_ALL", false);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
subscribeCheckBox = new CheckBox(this);
subscribeCheckBox.setId(1);
subscribeCheckBox.setText(R.string.subscribe_to_this_forum);
subscribeCheckBox.setChecked(subscribed);
subscribeCheckBox.setOnClickListener(this);
layout.addView(subscribeCheckBox);
radioGroup = new RadioGroup(this);
radioGroup.setOrientation(VERTICAL);
visibleToAll = new RadioButton(this);
visibleToAll.setId(2);
visibleToAll.setText(R.string.forum_visible_to_all);
visibleToAll.setEnabled(subscribed);
visibleToAll.setOnClickListener(this);
radioGroup.addView(visibleToAll);
visibleToSome = new RadioButton(this);
visibleToSome.setId(3);
visibleToSome.setText(R.string.forum_visible_to_some);
visibleToSome.setEnabled(subscribed);
visibleToSome.setOnClickListener(this);
radioGroup.addView(visibleToSome);
if(!subscribed || all) radioGroup.check(visibleToAll.getId());
else radioGroup.check(visibleToSome.getId());
layout.addView(radioGroup);
doneButton = new Button(this);
doneButton.setLayoutParams(WRAP_WRAP);
doneButton.setText(R.string.done_button);
doneButton.setOnClickListener(this);
layout.addView(doneButton);
progress = new ProgressBar(this);
progress.setLayoutParams(WRAP_WRAP);
progress.setIndeterminate(true);
progress.setVisibility(GONE);
layout.addView(progress);
setContentView(layout);
FragmentManager fm = getSupportFragmentManager();
Fragment f = fm.findFragmentByTag("NoContactsDialog");
if(f == null) noContactsDialog = new NoContactsDialog();
else noContactsDialog = (NoContactsDialog) f;
noContactsDialog.setListener(this);
f = fm.findFragmentByTag("SelectContactsDialog");
if(f == null) selectContactsDialog = new SelectContactsDialog();
else selectContactsDialog = (SelectContactsDialog) f;
selectContactsDialog.setListener(this);
}
public void onClick(View view) {
if(view == subscribeCheckBox) {
boolean subscribe = subscribeCheckBox.isChecked();
visibleToAll.setEnabled(subscribe);
visibleToSome.setEnabled(subscribe);
} else if(view == visibleToSome) {
loadContacts();
} else if(view == doneButton) {
boolean subscribe = subscribeCheckBox.isChecked();
boolean all = visibleToAll.isChecked();
Collection<ContactId> visible =
Collections.unmodifiableCollection(selected);
// Replace the button with a progress bar
doneButton.setVisibility(GONE);
progress.setVisibility(VISIBLE);
// Update the blog in a background thread
if(subscribe || subscribed)
updateGroup(subscribe, subscribed, all, visible);
}
}
private void loadContacts() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Collection<Contact> contacts = db.getContacts();
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayContacts(contacts);
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void displayContacts(final Collection<Contact> contacts) {
runOnUiThread(new Runnable() {
public void run() {
FragmentManager fm = getSupportFragmentManager();
if(contacts.isEmpty()) {
noContactsDialog.show(fm, "NoContactsDialog");
} else {
selectContactsDialog.setContacts(contacts);
selectContactsDialog.show(fm, "SelectContactsDialog");
}
}
});
}
private void updateGroup(final boolean subscribe,
final boolean wasSubscribed, final boolean all,
final Collection<ContactId> visible) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
if(subscribe) {
if(!wasSubscribed) db.addGroup(group);
db.setVisibleToAll(group.getId(), all);
if(!all) db.setVisibility(group.getId(), visible);
} else if(wasSubscribed) {
db.removeGroup(group);
}
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Update took " + duration + " ms");
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
runOnUiThread(new Runnable() {
public void run() {
finish();
}
});
}
});
}
public void contactCreationSelected() {
startActivity(new Intent(this, AddContactActivity.class));
}
public void contactCreationCancelled() {}
public void contactsSelected(Collection<ContactId> selected) {
this.selected = selected;
}
public void contactSelectionCancelled() {}
}

View File

@@ -0,0 +1,264 @@
package org.briarproject.android.groups;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.InputMethodManager.HIDE_IMPLICIT_ONLY;
import static android.widget.LinearLayout.VERTICAL;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.contact.SelectContactsDialog;
import org.briarproject.android.invitation.AddContactActivity;
import org.briarproject.api.Contact;
import org.briarproject.api.ContactId;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupFactory;
import roboguice.activity.RoboFragmentActivity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
public class CreateGroupActivity extends RoboFragmentActivity
implements OnEditorActionListener, OnClickListener, NoContactsDialog.Listener,
SelectContactsDialog.Listener {
private static final Logger LOG =
Logger.getLogger(CreateGroupActivity.class.getName());
private EditText nameEntry = null;
private RadioGroup radioGroup = null;
private RadioButton visibleToAll = null, visibleToSome = null;
private Button createButton = null;
private ProgressBar progress = null;
private NoContactsDialog noContactsDialog = null;
private SelectContactsDialog selectContactsDialog = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile GroupFactory groupFactory;
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
private volatile Collection<ContactId> selected = Collections.emptyList();
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
TextView chooseName = new TextView(this);
chooseName.setGravity(CENTER);
chooseName.setTextSize(18);
chooseName.setPadding(10, 10, 10, 0);
chooseName.setText(R.string.choose_forum_name);
layout.addView(chooseName);
nameEntry = new EditText(this) {
@Override
protected void onTextChanged(CharSequence text, int start,
int lengthBefore, int lengthAfter) {
enableOrDisableCreateButton();
}
};
nameEntry.setId(1);
nameEntry.setMaxLines(1);
nameEntry.setInputType(TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_CAP_SENTENCES);
nameEntry.setOnEditorActionListener(this);
layout.addView(nameEntry);
radioGroup = new RadioGroup(this);
radioGroup.setOrientation(VERTICAL);
visibleToAll = new RadioButton(this);
visibleToAll.setId(2);
visibleToAll.setText(R.string.forum_visible_to_all);
visibleToAll.setOnClickListener(this);
radioGroup.addView(visibleToAll);
visibleToSome = new RadioButton(this);
visibleToSome.setId(3);
visibleToSome.setText(R.string.forum_visible_to_some);
visibleToSome.setOnClickListener(this);
radioGroup.addView(visibleToSome);
layout.addView(radioGroup);
createButton = new Button(this);
createButton.setLayoutParams(WRAP_WRAP);
createButton.setText(R.string.create_button);
createButton.setOnClickListener(this);
layout.addView(createButton);
progress = new ProgressBar(this);
progress.setLayoutParams(WRAP_WRAP);
progress.setIndeterminate(true);
progress.setVisibility(GONE);
layout.addView(progress);
setContentView(layout);
FragmentManager fm = getSupportFragmentManager();
Fragment f = fm.findFragmentByTag("NoContactsDialog");
if(f == null) noContactsDialog = new NoContactsDialog();
else noContactsDialog = (NoContactsDialog) f;
noContactsDialog.setListener(this);
f = fm.findFragmentByTag("SelectContactsDialog");
if(f == null) selectContactsDialog = new SelectContactsDialog();
else selectContactsDialog = (SelectContactsDialog) f;
selectContactsDialog.setListener(this);
}
private void enableOrDisableCreateButton() {
if(nameEntry == null || radioGroup == null || createButton == null)
return; // Activity not created yet
boolean nameNotEmpty = nameEntry.getText().length() > 0;
boolean visibilitySelected = radioGroup.getCheckedRadioButtonId() != -1;
createButton.setEnabled(nameNotEmpty && visibilitySelected);
}
public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
validateName();
return true;
}
public void onClick(View view) {
if(view == visibleToAll) {
enableOrDisableCreateButton();
} else if(view == visibleToSome) {
loadContacts();
} else if(view == createButton) {
if(!validateName()) return;
final String name = nameEntry.getText().toString();
final boolean all = visibleToAll.isChecked();
final Collection<ContactId> visible =
Collections.unmodifiableCollection(selected);
// Replace the button with a progress bar
createButton.setVisibility(GONE);
progress.setVisibility(VISIBLE);
// Create and store the group in a background thread
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
Group g = groupFactory.createGroup(name);
long now = System.currentTimeMillis();
db.addGroup(g);
if(all) db.setVisibleToAll(g.getId(), true);
else db.setVisibility(g.getId(), visible);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Storing group took " + duration + " ms");
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
runOnUiThread(new Runnable() {
public void run() {
finish();
}
});
}
});
}
}
private void loadContacts() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Collection<Contact> contacts = db.getContacts();
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayContacts(contacts);
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void displayContacts(final Collection<Contact> contacts) {
runOnUiThread(new Runnable() {
public void run() {
FragmentManager fm = getSupportFragmentManager();
if(contacts.isEmpty()) {
noContactsDialog.show(fm, "NoContactsDialog");
} else {
selectContactsDialog.setContacts(contacts);
selectContactsDialog.show(fm, "SelectContactsDialog");
}
}
});
}
private boolean validateName() {
if(nameEntry.getText().toString().equals("")) return false;
// Hide the soft keyboard
Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).toggleSoftInput(HIDE_IMPLICIT_ONLY, 0);
return true;
}
public void contactCreationSelected() {
startActivity(new Intent(this, AddContactActivity.class));
}
public void contactCreationCancelled() {
enableOrDisableCreateButton();
}
public void contactsSelected(Collection<ContactId> selected) {
this.selected = selected;
enableOrDisableCreateButton();
}
public void contactSelectionCancelled() {
enableOrDisableCreateButton();
}
}

View File

@@ -0,0 +1,237 @@
package org.briarproject.android.groups;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.LinearLayout.VERTICAL;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.groups.ReadGroupPostActivity.RESULT_NEXT;
import static org.briarproject.android.groups.ReadGroupPostActivity.RESULT_PREV;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.AscendingHeaderComparator;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.api.Author;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.db.NoSuchSubscriptionException;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageExpiredEvent;
import org.briarproject.api.event.SubscriptionRemovedEvent;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.GroupId;
import roboguice.activity.RoboActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ListView;
public class GroupActivity extends RoboActivity implements EventListener,
OnClickListener, OnItemClickListener {
private static final Logger LOG =
Logger.getLogger(GroupActivity.class.getName());
private String groupName = null;
private GroupAdapter adapter = null;
private ListView list = null;
private ListLoadingProgressBar loading = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
private volatile GroupId groupId = null;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra("org.briarproject.GROUP_ID");
if(b == null) throw new IllegalStateException();
groupId = new GroupId(b);
groupName = i.getStringExtra("org.briarproject.GROUP_NAME");
if(groupName == null) throw new IllegalStateException();
setTitle(groupName);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
adapter = new GroupAdapter(this);
list = new ListView(this);
// Give me all the width and all the unused height
list.setLayoutParams(MATCH_WRAP_1);
list.setAdapter(adapter);
list.setOnItemClickListener(this);
layout.addView(list);
// Show a progress bar while the list is loading
list.setVisibility(GONE);
loading = new ListLoadingProgressBar(this);
layout.addView(loading);
layout.addView(new HorizontalBorder(this));
ImageButton composeButton = new ImageButton(this);
composeButton.setBackgroundResource(0);
composeButton.setImageResource(R.drawable.content_new_email);
composeButton.setOnClickListener(this);
layout.addView(composeButton);
setContentView(layout);
}
@Override
public void onResume() {
super.onResume();
db.addListener(this);
loadHeaders();
}
private void loadHeaders() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Collection<MessageHeader> headers =
db.getMessageHeaders(groupId);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayHeaders(headers);
} catch(NoSuchSubscriptionException e) {
if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
runOnUiThread(new Runnable() {
public void run() {
finish();
}
});
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void displayHeaders(final Collection<MessageHeader> headers) {
runOnUiThread(new Runnable() {
public void run() {
list.setVisibility(VISIBLE);
loading.setVisibility(GONE);
adapter.clear();
for(MessageHeader h : headers) adapter.add(h);
adapter.sort(AscendingHeaderComparator.INSTANCE);
adapter.notifyDataSetChanged();
selectFirstUnread();
}
});
}
private void selectFirstUnread() {
int firstUnread = -1, count = adapter.getCount();
for(int i = 0; i < count; i++) {
if(!adapter.getItem(i).isRead()) {
firstUnread = i;
break;
}
}
if(firstUnread == -1) list.setSelection(count - 1);
else list.setSelection(firstUnread);
}
@Override
public void onActivityResult(int request, int result, Intent data) {
if(result == RESULT_PREV) {
int position = request - 1;
if(position >= 0 && position < adapter.getCount())
displayMessage(position);
} else if(result == RESULT_NEXT) {
int position = request + 1;
if(position >= 0 && position < adapter.getCount())
displayMessage(position);
}
}
@Override
public void onPause() {
super.onPause();
db.removeListener(this);
}
public void eventOccurred(Event e) {
if(e instanceof MessageAddedEvent) {
if(((MessageAddedEvent) e).getGroup().getId().equals(groupId)) {
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
loadHeaders();
}
} else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
loadHeaders();
} else if(e instanceof SubscriptionRemovedEvent) {
SubscriptionRemovedEvent s = (SubscriptionRemovedEvent) e;
if(s.getGroup().getId().equals(groupId)) {
if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
runOnUiThread(new Runnable() {
public void run() {
finish();
}
});
}
}
}
public void onClick(View view) {
Intent i = new Intent(this, WriteGroupPostActivity.class);
i.putExtra("org.briarproject.GROUP_ID", groupId.getBytes());
startActivity(i);
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
displayMessage(position);
}
private void displayMessage(int position) {
MessageHeader item = adapter.getItem(position);
Intent i = new Intent(this, ReadGroupPostActivity.class);
i.putExtra("org.briarproject.GROUP_ID", groupId.getBytes());
i.putExtra("org.briarproject.GROUP_NAME", groupName);
i.putExtra("org.briarproject.MESSAGE_ID", item.getId().getBytes());
Author author = item.getAuthor();
if(author != null) {
i.putExtra("org.briarproject.AUTHOR_ID", author.getId().getBytes());
i.putExtra("org.briarproject.AUTHOR_NAME", author.getName());
}
i.putExtra("org.briarproject.CONTENT_TYPE", item.getContentType());
i.putExtra("org.briarproject.TIMESTAMP", item.getTimestamp());
startActivityForResult(i, position);
}
}

View File

@@ -0,0 +1,63 @@
package org.briarproject.android.groups;
import static android.widget.LinearLayout.HORIZONTAL;
import static java.text.DateFormat.SHORT;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import java.util.ArrayList;
import org.briarproject.R;
import org.briarproject.api.Author;
import org.briarproject.api.db.MessageHeader;
import android.content.Context;
import android.content.res.Resources;
import android.text.format.DateUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
class GroupAdapter extends ArrayAdapter<MessageHeader> {
GroupAdapter(Context ctx) {
super(ctx, android.R.layout.simple_expandable_list_item_1,
new ArrayList<MessageHeader>());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
MessageHeader item = getItem(position);
Context ctx = getContext();
Resources res = ctx.getResources();
LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(HORIZONTAL);
if(!item.isRead())
layout.setBackgroundColor(res.getColor(R.color.unread_background));
TextView name = new TextView(ctx);
// Give me all the unused width
name.setLayoutParams(WRAP_WRAP_1);
name.setTextSize(18);
name.setMaxLines(1);
name.setPadding(10, 10, 10, 10);
Author author = item.getAuthor();
if(author == null) {
name.setTextColor(res.getColor(R.color.anonymous_author));
name.setText(R.string.anonymous);
} else {
name.setText(author.getName());
}
layout.addView(name);
TextView date = new TextView(ctx);
date.setTextSize(14);
date.setPadding(10, 10, 10, 10);
long then = item.getTimestamp(), now = System.currentTimeMillis();
date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT));
layout.addView(date);
return layout;
}
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.android.groups;
import org.briarproject.api.messaging.Group;
class GroupItem {
static final GroupItem NEW = new GroupItem(null);
private final Group group;
GroupItem(Group group) {
this.group = group;
}
Group getGroup() {
return group;
}
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.android.groups;
import static org.briarproject.android.groups.GroupItem.NEW;
import java.util.Comparator;
class GroupItemComparator implements Comparator<GroupItem> {
static final GroupItemComparator INSTANCE = new GroupItemComparator();
public int compare(GroupItem a, GroupItem b) {
if(a == b) return 0;
if(a == NEW) return 1;
if(b == NEW) return -1;
String aName = a.getGroup().getName(), bName = b.getGroup().getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
}

View File

@@ -0,0 +1,375 @@
package org.briarproject.android.groups;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.groups.GroupListItem.MANAGE;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
import java.util.Collection;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.HorizontalSpace;
import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.db.NoSuchSubscriptionException;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageExpiredEvent;
import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
import org.briarproject.api.event.SubscriptionAddedEvent;
import org.briarproject.api.event.SubscriptionRemovedEvent;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.GroupStatus;
import roboguice.activity.RoboFragmentActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ListView;
public class GroupListActivity extends RoboFragmentActivity
implements EventListener, OnClickListener, OnItemClickListener {
private static final Logger LOG =
Logger.getLogger(GroupListActivity.class.getName());
private final Map<GroupId,GroupId> groups =
new ConcurrentHashMap<GroupId,GroupId>();
private GroupListAdapter adapter = null;
private ListView list = null;
private ListLoadingProgressBar loading = null;
private ImageButton newGroupButton = null, manageGroupsButton = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
adapter = new GroupListAdapter(this);
list = new ListView(this);
// Give me all the width and all the unused height
list.setLayoutParams(MATCH_WRAP_1);
list.setAdapter(adapter);
list.setOnItemClickListener(this);
layout.addView(list);
// Show a progress bar while the list is loading
list.setVisibility(GONE);
loading = new ListLoadingProgressBar(this);
layout.addView(loading);
layout.addView(new HorizontalBorder(this));
LinearLayout footer = new LinearLayout(this);
footer.setLayoutParams(MATCH_WRAP);
footer.setOrientation(HORIZONTAL);
footer.setGravity(CENTER);
footer.addView(new HorizontalSpace(this));
newGroupButton = new ImageButton(this);
newGroupButton.setBackgroundResource(0);
newGroupButton.setImageResource(R.drawable.social_new_chat);
newGroupButton.setOnClickListener(this);
footer.addView(newGroupButton);
footer.addView(new HorizontalSpace(this));
manageGroupsButton = new ImageButton(this);
manageGroupsButton.setBackgroundResource(0);
manageGroupsButton.setImageResource(R.drawable.action_settings);
manageGroupsButton.setOnClickListener(this);
footer.addView(manageGroupsButton);
footer.addView(new HorizontalSpace(this));
layout.addView(footer);
setContentView(layout);
}
@Override
public void onResume() {
super.onResume();
db.addListener(this);
loadHeaders();
}
private void loadHeaders() {
clearHeaders();
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
int available = 0;
long now = System.currentTimeMillis();
for(GroupStatus s : db.getAvailableGroups()) {
Group g = s.getGroup();
if(s.isSubscribed()) {
try {
Collection<MessageHeader> headers =
db.getMessageHeaders(g.getId());
displayHeaders(g, headers);
} catch(NoSuchSubscriptionException e) {
if(LOG.isLoggable(INFO))
LOG.info("Subscription removed");
}
} else {
available++;
}
}
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Full load took " + duration + " ms");
displayAvailable(available);
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void clearHeaders() {
runOnUiThread(new Runnable() {
public void run() {
groups.clear();
list.setVisibility(GONE);
loading.setVisibility(VISIBLE);
adapter.clear();
adapter.notifyDataSetChanged();
}
});
}
private void displayHeaders(final Group g,
final Collection<MessageHeader> headers) {
runOnUiThread(new Runnable() {
public void run() {
GroupId id = g.getId();
groups.put(id, id);
list.setVisibility(VISIBLE);
loading.setVisibility(GONE);
// Remove the old item, if any
GroupListItem item = findGroup(id);
if(item != null) adapter.remove(item);
// Add a new item
adapter.add(new GroupListItem(g, headers));
adapter.sort(ItemComparator.INSTANCE);
adapter.notifyDataSetChanged();
selectFirstUnread();
}
});
}
private void displayAvailable(final int available) {
runOnUiThread(new Runnable() {
public void run() {
list.setVisibility(VISIBLE);
loading.setVisibility(GONE);
adapter.setAvailable(available);
adapter.notifyDataSetChanged();
}
});
}
private GroupListItem findGroup(GroupId g) {
int count = adapter.getCount();
for(int i = 0; i < count; i++) {
GroupListItem item = adapter.getItem(i);
if(item == MANAGE) continue;
if(item.getGroup().getId().equals(g)) return item;
}
return null; // Not found
}
private void selectFirstUnread() {
int firstUnread = -1, count = adapter.getCount();
for(int i = 0; i < count; i++) {
GroupListItem item = adapter.getItem(i);
if(item == MANAGE) continue;
if(item.getUnreadCount() > 0) {
firstUnread = i;
break;
}
}
if(firstUnread == -1) list.setSelection(count - 1);
else list.setSelection(firstUnread);
}
@Override
public void onPause() {
super.onPause();
db.removeListener(this);
}
public void eventOccurred(Event e) {
if(e instanceof MessageAddedEvent) {
Group g = ((MessageAddedEvent) e).getGroup();
if(groups.containsKey(g.getId())) {
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
loadHeaders(g);
}
} else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
loadHeaders();
} else if(e instanceof RemoteSubscriptionsUpdatedEvent) {
if(LOG.isLoggable(INFO))
LOG.info("Remote subscriptions changed, reloading");
loadAvailable();
} else if(e instanceof SubscriptionAddedEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
loadHeaders();
} else if(e instanceof SubscriptionRemovedEvent) {
Group g = ((SubscriptionRemovedEvent) e).getGroup();
if(groups.containsKey(g.getId())) {
// Reload the group, expecting NoSuchSubscriptionException
if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
loadHeaders(g);
}
}
}
private void loadHeaders(final Group g) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Collection<MessageHeader> headers =
db.getMessageHeaders(g.getId());
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Partial load took " + duration + " ms");
displayHeaders(g, headers);
} catch(NoSuchSubscriptionException e) {
if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
removeGroup(g.getId());
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void removeGroup(final GroupId g) {
runOnUiThread(new Runnable() {
public void run() {
GroupListItem item = findGroup(g);
if(item != null) {
groups.remove(g);
adapter.remove(item);
adapter.notifyDataSetChanged();
selectFirstUnread();
}
}
});
}
private void loadAvailable() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
int available = 0;
long now = System.currentTimeMillis();
for(GroupStatus s : db.getAvailableGroups())
if(!s.isSubscribed()) available++;
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Loading available took " + duration + " ms");
displayAvailable(available);
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
public void onClick(View view) {
if(view == newGroupButton) {
startActivity(new Intent(this, CreateGroupActivity.class));
} else if(view == manageGroupsButton) {
startActivity(new Intent(this, ManageGroupsActivity.class));
}
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
GroupListItem item = adapter.getItem(position);
if(item == MANAGE) {
startActivity(new Intent(this, ManageGroupsActivity.class));
} else {
Intent i = new Intent(this, GroupActivity.class);
Group g = item.getGroup();
i.putExtra("org.briarproject.GROUP_ID", g.getId().getBytes());
i.putExtra("org.briarproject.GROUP_NAME", g.getName());
startActivity(i);
}
}
private static class ItemComparator implements Comparator<GroupListItem> {
private static final ItemComparator INSTANCE = new ItemComparator();
public int compare(GroupListItem a, GroupListItem b) {
if(a == b) return 0;
// The manage groups item comes last
if(a == MANAGE) return 1;
if(b == MANAGE) return -1;
// The item with the newest message comes first
long aTime = a.getTimestamp(), bTime = b.getTimestamp();
if(aTime > bTime) return -1;
if(aTime < bTime) return 1;
// Break ties by group name
String aName = a.getGroup().getName();
String bName = b.getGroup().getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
}
}

View File

@@ -0,0 +1,121 @@
package org.briarproject.android.groups;
import static android.view.Gravity.CENTER;
import static android.widget.LinearLayout.HORIZONTAL;
import static java.text.DateFormat.SHORT;
import static org.briarproject.android.groups.GroupListItem.MANAGE;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.briarproject.R;
import android.content.Context;
import android.content.res.Resources;
import android.text.format.DateUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
class GroupListAdapter extends BaseAdapter {
private final Context ctx;
private final List<GroupListItem> list = new ArrayList<GroupListItem>();
private int available = 0;
GroupListAdapter(Context ctx) {
this.ctx = ctx;
}
public void setAvailable(int available) {
this.available = available;
}
public void add(GroupListItem item) {
list.add(item);
}
public void clear() {
list.clear();
}
public int getCount() {
return available == 0 ? list.size() : list.size() + 1;
}
public GroupListItem getItem(int position) {
return position == list.size() ? MANAGE : list.get(position);
}
public long getItemId(int position) {
return android.R.layout.simple_expandable_list_item_1;
}
public View getView(int position, View convertView, ViewGroup parent) {
GroupListItem item = getItem(position);
Resources res = ctx.getResources();
if(item == MANAGE) {
TextView manage = new TextView(ctx);
manage.setGravity(CENTER);
manage.setTextSize(18);
manage.setPadding(10, 10, 10, 10);
String format = res.getQuantityString(R.plurals.forums_available,
available);
manage.setText(String.format(format, available));
return manage;
}
LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(HORIZONTAL);
if(item.getUnreadCount() > 0)
layout.setBackgroundColor(res.getColor(R.color.unread_background));
TextView name = new TextView(ctx);
// Give me all the unused width
name.setLayoutParams(WRAP_WRAP_1);
name.setTextSize(18);
name.setMaxLines(1);
name.setPadding(10, 10, 10, 10);
int unread = item.getUnreadCount();
String groupName = item.getGroup().getName();
if(unread > 0) name.setText(groupName + " (" + unread + ")");
else name.setText(groupName);
layout.addView(name);
if(item.isEmpty()) {
TextView noPosts = new TextView(ctx);
noPosts.setTextSize(14);
noPosts.setPadding(10, 0, 10, 10);
noPosts.setTextColor(res.getColor(R.color.no_posts));
noPosts.setText(R.string.no_posts);
layout.addView(noPosts);
} else {
TextView date = new TextView(ctx);
date.setTextSize(14);
date.setPadding(10, 0, 10, 10);
long then = item.getTimestamp(), now = System.currentTimeMillis();
date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT));
layout.addView(date);
}
return layout;
}
@Override
public boolean isEmpty() {
return list.isEmpty() && available == 0;
}
public void remove(GroupListItem item) {
list.remove(item);
}
public void sort(Comparator<GroupListItem> comparator) {
Collections.sort(list, comparator);
}
}

View File

@@ -0,0 +1,72 @@
package org.briarproject.android.groups;
import java.util.Collection;
import java.util.Collections;
import org.briarproject.api.Author;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.messaging.Group;
class GroupListItem {
static final GroupListItem MANAGE = new GroupListItem(null,
Collections.<MessageHeader>emptyList());
private final Group group;
private final boolean empty;
private final String authorName, contentType;
private final long timestamp;
private final int unread;
GroupListItem(Group group, Collection<MessageHeader> headers) {
this.group = group;
empty = headers.isEmpty();
if(empty) {
authorName = null;
contentType = null;
timestamp = 0;
unread = 0;
} else {
MessageHeader newest = null;
long timestamp = 0;
int unread = 0;
for(MessageHeader h : headers) {
if(h.getTimestamp() > timestamp) {
timestamp = h.getTimestamp();
newest = h;
}
if(!h.isRead()) unread++;
}
Author a = newest.getAuthor();
if(a == null) authorName = null;
else authorName = a.getName();
contentType = newest.getContentType();
this.timestamp = newest.getTimestamp();
this.unread = unread;
}
}
Group getGroup() {
return group;
}
boolean isEmpty() {
return empty;
}
String getAuthorName() {
return authorName;
}
String getContentType() {
return contentType;
}
long getTimestamp() {
return timestamp;
}
int getUnreadCount() {
return unread;
}
}

View File

@@ -0,0 +1,157 @@
package org.briarproject.android.groups;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.groups.ManageGroupsItem.NONE;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import java.util.Collection;
import java.util.Comparator;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
import org.briarproject.api.event.SubscriptionAddedEvent;
import org.briarproject.api.event.SubscriptionRemovedEvent;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupStatus;
import roboguice.activity.RoboFragmentActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
public class ManageGroupsActivity extends RoboFragmentActivity
implements EventListener, OnItemClickListener {
private static final Logger LOG =
Logger.getLogger(ManageGroupsActivity.class.getName());
private ManageGroupsAdapter adapter = null;
private ListView list = null;
private ListLoadingProgressBar loading = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
adapter = new ManageGroupsAdapter(this);
list = new ListView(this);
list.setLayoutParams(MATCH_MATCH);
list.setAdapter(adapter);
list.setOnItemClickListener(this);
// Show a progress bar while the list is loading
loading = new ListLoadingProgressBar(this);
setContentView(loading);
}
@Override
public void onResume() {
super.onResume();
db.addListener(this);
loadGroups();
}
private void loadGroups() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Collection<GroupStatus> available = db.getAvailableGroups();
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayGroups(available);
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void displayGroups(final Collection<GroupStatus> available) {
runOnUiThread(new Runnable() {
public void run() {
setContentView(list);
adapter.clear();
for(GroupStatus s : available)
adapter.add(new ManageGroupsItem(s));
adapter.sort(ItemComparator.INSTANCE);
adapter.notifyDataSetChanged();
}
});
}
@Override
public void onPause() {
super.onPause();
db.removeListener(this);
}
public void eventOccurred(Event e) {
if(e instanceof RemoteSubscriptionsUpdatedEvent) {
if(LOG.isLoggable(INFO))
LOG.info("Remote subscriptions changed, reloading");
loadGroups();
} else if(e instanceof SubscriptionAddedEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
loadGroups();
} else if(e instanceof SubscriptionRemovedEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
loadGroups();
}
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
ManageGroupsItem item = adapter.getItem(position);
if(item == NONE) return;
GroupStatus s = item.getGroupStatus();
Group g = s.getGroup();
Intent i = new Intent(this, ConfigureGroupActivity.class);
i.putExtra("org.briarproject.GROUP_ID", g.getId().getBytes());
i.putExtra("org.briarproject.GROUP_NAME", g.getName());
i.putExtra("org.briarproject.GROUP_SALT", g.getSalt());
i.putExtra("org.briarproject.SUBSCRIBED", s.isSubscribed());
i.putExtra("org.briarproject.VISIBLE_TO_ALL", s.isVisibleToAll());
startActivity(i);
}
private static class ItemComparator
implements Comparator<ManageGroupsItem> {
private static final ItemComparator INSTANCE = new ItemComparator();
public int compare(ManageGroupsItem a, ManageGroupsItem b) {
if(a == b) return 0;
if(a == NONE) return 1;
if(b == NONE) return -1;
String aName = a.getGroupStatus().getGroup().getName();
String bName = b.getGroupStatus().getGroup().getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
}
}

View File

@@ -0,0 +1,113 @@
package org.briarproject.android.groups;
import static android.view.Gravity.CENTER;
import static android.view.View.INVISIBLE;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;
import static org.briarproject.android.groups.ManageGroupsItem.NONE;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.briarproject.R;
import org.briarproject.api.messaging.GroupStatus;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
class ManageGroupsAdapter extends BaseAdapter {
private final Context ctx;
private final List<ManageGroupsItem> list =
new ArrayList<ManageGroupsItem>();
ManageGroupsAdapter(Context ctx) {
this.ctx = ctx;
}
public void add(ManageGroupsItem item) {
list.add(item);
}
public void clear() {
list.clear();
}
public int getCount() {
return list.isEmpty() ? 1 : list.size();
}
public ManageGroupsItem getItem(int position) {
return list.isEmpty() ? NONE : list.get(position);
}
public long getItemId(int position) {
return android.R.layout.simple_expandable_list_item_1;
}
public View getView(int position, View convertView, ViewGroup parent) {
ManageGroupsItem item = getItem(position);
if(item == NONE) {
TextView none = new TextView(ctx);
none.setGravity(CENTER);
none.setTextSize(18);
none.setPadding(10, 10, 10, 10);
none.setText(R.string.no_forums_available);
return none;
}
GroupStatus s = item.getGroupStatus();
LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(HORIZONTAL);
ImageView subscribed = new ImageView(ctx);
subscribed.setPadding(5, 5, 5, 5);
subscribed.setImageResource(R.drawable.navigation_accept);
if(!s.isSubscribed()) subscribed.setVisibility(INVISIBLE);
layout.addView(subscribed);
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(VERTICAL);
TextView name = new TextView(ctx);
name.setTextSize(18);
name.setMaxLines(1);
name.setPadding(0, 10, 10, 10);
name.setText(s.getGroup().getName());
innerLayout.addView(name);
TextView status = new TextView(ctx);
status.setTextSize(14);
status.setPadding(0, 0, 10, 10);
if(s.isSubscribed()) {
if(s.isVisibleToAll()) status.setText(R.string.subscribed_all);
else status.setText(R.string.subscribed_some);
} else {
status.setText(R.string.not_subscribed);
}
innerLayout.addView(status);
layout.addView(innerLayout);
return layout;
}
@Override
public boolean isEmpty() {
return false;
}
public void remove(ManageGroupsItem item) {
list.remove(item);
}
public void sort(Comparator<ManageGroupsItem> comparator) {
Collections.sort(list, comparator);
}
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.android.groups;
import org.briarproject.api.messaging.GroupStatus;
class ManageGroupsItem {
static final ManageGroupsItem NONE = new ManageGroupsItem(null);
private final GroupStatus status;
ManageGroupsItem(GroupStatus status) {
this.status = status;
}
GroupStatus getGroupStatus() {
return status;
}
}

View File

@@ -0,0 +1,43 @@
package org.briarproject.android.groups;
import org.briarproject.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
public class NoContactsDialog extends DialogFragment {
private Listener listener = null;
public void setListener(Listener listener) {
this.listener = listener;
}
@Override
public Dialog onCreateDialog(Bundle state) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage(R.string.no_contacts);
builder.setPositiveButton(R.string.add_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.contactCreationSelected();
}
});
builder.setNegativeButton(R.string.cancel_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.contactCreationCancelled();
}
});
return builder.create();
}
public interface Listener {
void contactCreationSelected();
void contactCreationCancelled();
}
}

View File

@@ -0,0 +1,276 @@
package org.briarproject.android.groups;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;
import static java.text.DateFormat.SHORT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.HorizontalSpace;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchMessageException;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.MessageId;
import roboguice.activity.RoboActivity;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.format.DateUtils;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
public class ReadGroupPostActivity extends RoboActivity
implements OnClickListener {
static final int RESULT_REPLY = RESULT_FIRST_USER;
static final int RESULT_PREV = RESULT_FIRST_USER + 1;
static final int RESULT_NEXT = RESULT_FIRST_USER + 2;
private static final Logger LOG =
Logger.getLogger(ReadGroupPostActivity.class.getName());
private GroupId groupId = null;
private boolean read;
private ImageButton readButton = null, prevButton = null, nextButton = null;
private ImageButton replyButton = null;
private TextView content = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
private volatile MessageId messageId = null;
private volatile long timestamp = -1;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra("org.briarproject.GROUP_ID");
if(b == null) throw new IllegalStateException();
groupId = new GroupId(b);
String groupName = i.getStringExtra("org.briarproject.GROUP_NAME");
if(groupName == null) throw new IllegalStateException();
setTitle(groupName);
b = i.getByteArrayExtra("org.briarproject.MESSAGE_ID");
if(b == null) throw new IllegalStateException();
messageId = new MessageId(b);
String contentType = i.getStringExtra("org.briarproject.CONTENT_TYPE");
if(contentType == null) throw new IllegalStateException();
timestamp = i.getLongExtra("org.briarproject.TIMESTAMP", -1);
if(timestamp == -1) throw new IllegalStateException();
String authorName = i.getStringExtra("org.briarproject.AUTHOR_NAME");
if(state == null) {
read = false;
setReadInDatabase(true);
} else {
read = state.getBoolean("org.briarproject.READ");
}
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_WRAP);
layout.setOrientation(VERTICAL);
ScrollView scrollView = new ScrollView(this);
// Give me all the width and all the unused height
scrollView.setLayoutParams(MATCH_WRAP_1);
LinearLayout message = new LinearLayout(this);
message.setOrientation(VERTICAL);
Resources res = getResources();
message.setBackgroundColor(res.getColor(R.color.content_background));
LinearLayout header = new LinearLayout(this);
header.setLayoutParams(MATCH_WRAP);
header.setOrientation(HORIZONTAL);
header.setGravity(CENTER_VERTICAL);
TextView name = new TextView(this);
// Give me all the unused width
name.setLayoutParams(WRAP_WRAP_1);
name.setTextSize(18);
name.setMaxLines(1);
name.setPadding(10, 10, 10, 10);
if(authorName == null) {
name.setTextColor(res.getColor(R.color.anonymous_author));
name.setText(R.string.anonymous);
} else {
name.setText(authorName);
}
header.addView(name);
TextView date = new TextView(this);
date.setTextSize(14);
date.setPadding(0, 10, 10, 10);
long now = System.currentTimeMillis();
date.setText(DateUtils.formatSameDayTime(timestamp, now, SHORT, SHORT));
header.addView(date);
message.addView(header);
if(contentType.equals("text/plain")) {
// Load and display the message body
content = new TextView(this);
content.setPadding(10, 0, 10, 10);
message.addView(content);
loadMessageBody();
}
scrollView.addView(message);
layout.addView(scrollView);
layout.addView(new HorizontalBorder(this));
LinearLayout footer = new LinearLayout(this);
footer.setLayoutParams(MATCH_WRAP);
footer.setOrientation(HORIZONTAL);
footer.setGravity(CENTER);
readButton = new ImageButton(this);
readButton.setBackgroundResource(0);
if(read) readButton.setImageResource(R.drawable.content_unread);
else readButton.setImageResource(R.drawable.content_read);
readButton.setOnClickListener(this);
footer.addView(readButton);
footer.addView(new HorizontalSpace(this));
prevButton = new ImageButton(this);
prevButton.setBackgroundResource(0);
prevButton.setImageResource(R.drawable.navigation_previous_item);
prevButton.setOnClickListener(this);
footer.addView(prevButton);
footer.addView(new HorizontalSpace(this));
nextButton = new ImageButton(this);
nextButton.setBackgroundResource(0);
nextButton.setImageResource(R.drawable.navigation_next_item);
nextButton.setOnClickListener(this);
footer.addView(nextButton);
footer.addView(new HorizontalSpace(this));
replyButton = new ImageButton(this);
replyButton.setBackgroundResource(0);
replyButton.setImageResource(R.drawable.social_reply_all);
replyButton.setOnClickListener(this);
footer.addView(replyButton);
layout.addView(footer);
setContentView(layout);
}
private void setReadInDatabase(final boolean read) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
db.setReadFlag(messageId, read);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Setting flag took " + duration + " ms");
setReadInUi(read);
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void setReadInUi(final boolean read) {
runOnUiThread(new Runnable() {
public void run() {
ReadGroupPostActivity.this.read = read;
if(read) readButton.setImageResource(R.drawable.content_unread);
else readButton.setImageResource(R.drawable.content_read);
}
});
}
private void loadMessageBody() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
byte[] body = db.getMessageBody(messageId);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Loading message took " + duration + " ms");
final String text = new String(body, "UTF-8");
runOnUiThread(new Runnable() {
public void run() {
content.setText(text);
}
});
} catch(NoSuchMessageException e) {
if(LOG.isLoggable(INFO)) LOG.info("Message removed");
runOnUiThread(new Runnable() {
public void run() {
finish();
}
});
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
} catch(UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
});
}
@Override
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
state.putBoolean("org.briarproject.READ", read);
}
public void onClick(View view) {
if(view == readButton) {
setReadInDatabase(!read);
} else if(view == prevButton) {
setResult(RESULT_PREV);
finish();
} else if(view == nextButton) {
setResult(RESULT_NEXT);
finish();
} else if(view == replyButton) {
Intent i = new Intent(this, WriteGroupPostActivity.class);
i.putExtra("org.briarproject.GROUP_ID", groupId.getBytes());
i.putExtra("org.briarproject.PARENT_ID", messageId.getBytes());
i.putExtra("org.briarproject.TIMESTAMP", timestamp);
startActivity(i);
setResult(RESULT_REPLY);
finish();
}
}
}

View File

@@ -0,0 +1,299 @@
package org.briarproject.android.groups;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.identity.LocalAuthorItem;
import org.briarproject.android.identity.LocalAuthorItemComparator;
import org.briarproject.android.identity.LocalAuthorSpinnerAdapter;
import org.briarproject.android.util.HorizontalSpace;
import org.briarproject.api.AuthorId;
import org.briarproject.api.LocalAuthor;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.crypto.KeyParser;
import org.briarproject.api.crypto.PrivateKey;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.Message;
import org.briarproject.api.messaging.MessageFactory;
import org.briarproject.api.messaging.MessageId;
import roboguice.activity.RoboActivity;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.InputType;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
public class WriteGroupPostActivity extends RoboActivity
implements OnItemSelectedListener, OnClickListener {
private static final Logger LOG =
Logger.getLogger(WriteGroupPostActivity.class.getName());
private LocalAuthorSpinnerAdapter adapter = null;
private Spinner spinner = null;
private ImageButton sendButton = null;
private TextView to = null;
private EditText content = null;
private AuthorId localAuthorId = null;
private GroupId groupId = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
@Inject @CryptoExecutor private volatile Executor cryptoExecutor;
@Inject private volatile CryptoComponent crypto;
@Inject private volatile MessageFactory messageFactory;
private volatile MessageId parentId = null;
private volatile long timestamp = -1;
private volatile LocalAuthor localAuthor = null;
private volatile Group group = null;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra("org.briarproject.GROUP_ID");
if(b == null) throw new IllegalStateException();
groupId = new GroupId(b);
b = i.getByteArrayExtra("org.briarproject.PARENT_ID");
if(b != null) parentId = new MessageId(b);
timestamp = i.getLongExtra("org.briarproject.TIMESTAMP", -1);
if(state != null) {
b = state.getByteArray("org.briarproject.LOCAL_AUTHOR_ID");
if(b != null) localAuthorId = new AuthorId(b);
}
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_WRAP);
layout.setOrientation(VERTICAL);
LinearLayout header = new LinearLayout(this);
header.setLayoutParams(MATCH_WRAP);
header.setOrientation(HORIZONTAL);
header.setGravity(CENTER_VERTICAL);
TextView from = new TextView(this);
from.setTextSize(18);
from.setPadding(10, 10, 0, 10);
from.setText(R.string.from);
header.addView(from);
adapter = new LocalAuthorSpinnerAdapter(this, true);
spinner = new Spinner(this);
spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(this);
header.addView(spinner);
header.addView(new HorizontalSpace(this));
sendButton = new ImageButton(this);
sendButton.setBackgroundResource(0);
sendButton.setImageResource(R.drawable.social_send_now);
sendButton.setEnabled(false); // Enabled after loading the group
sendButton.setOnClickListener(this);
header.addView(sendButton);
layout.addView(header);
to = new TextView(this);
to.setTextSize(18);
to.setPadding(10, 0, 10, 10);
to.setText(R.string.to);
layout.addView(to);
content = new EditText(this);
content.setId(1);
int inputType = TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE
| TYPE_TEXT_FLAG_CAP_SENTENCES;
content.setInputType(inputType);
layout.addView(content);
setContentView(layout);
}
@Override
public void onResume() {
super.onResume();
loadAuthorsAndGroup();
}
private void loadAuthorsAndGroup() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Collection<LocalAuthor> localAuthors = db.getLocalAuthors();
group = db.getGroup(groupId);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayAuthorsAndGroup(localAuthors);
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void displayAuthorsAndGroup(
final Collection<LocalAuthor> localAuthors) {
runOnUiThread(new Runnable() {
public void run() {
if(localAuthors.isEmpty()) throw new IllegalStateException();
adapter.clear();
for(LocalAuthor a : localAuthors)
adapter.add(new LocalAuthorItem(a));
adapter.sort(LocalAuthorItemComparator.INSTANCE);
adapter.notifyDataSetChanged();
int count = adapter.getCount();
for(int i = 0; i < count; i++) {
LocalAuthorItem item = adapter.getItem(i);
if(item == LocalAuthorItem.ANONYMOUS) continue;
if(item == LocalAuthorItem.NEW) continue;
if(item.getLocalAuthor().getId().equals(localAuthorId)) {
localAuthor = item.getLocalAuthor();
spinner.setSelection(i);
break;
}
}
Resources res = getResources();
String format = res.getString(R.string.format_to);
to.setText(String.format(format, group.getName()));
sendButton.setEnabled(true);
}
});
}
@Override
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
if(localAuthorId != null) {
byte[] b = localAuthorId.getBytes();
state.putByteArray("org.briarproject.LOCAL_AUTHOR_ID", b);
}
}
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
LocalAuthorItem item = adapter.getItem(position);
if(item == LocalAuthorItem.ANONYMOUS) {
localAuthor = null;
localAuthorId = null;
} else if(item == LocalAuthorItem.NEW) {
localAuthor = null;
localAuthorId = null;
startActivity(new Intent(this, CreateIdentityActivity.class));
} else {
localAuthor = item.getLocalAuthor();
localAuthorId = localAuthor.getId();
}
}
public void onNothingSelected(AdapterView<?> parent) {
localAuthor = null;
localAuthorId = null;
}
public void onClick(View view) {
if(group == null) throw new IllegalStateException();
try {
createMessage(content.getText().toString().getBytes("UTF-8"));
} catch(UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
Toast.makeText(this, R.string.post_sent_toast, LENGTH_LONG).show();
finish();
}
private void createMessage(final byte[] body) {
cryptoExecutor.execute(new Runnable() {
public void run() {
// Don't use an earlier timestamp than the parent
long time = System.currentTimeMillis();
time = Math.max(time, timestamp + 1);
Message m;
try {
if(localAuthor == null) {
m = messageFactory.createAnonymousMessage(parentId,
group, "text/plain", time, body);
} else {
KeyParser keyParser = crypto.getSignatureKeyParser();
byte[] b = localAuthor.getPrivateKey();
PrivateKey authorKey = keyParser.parsePrivateKey(b);
m = messageFactory.createPseudonymousMessage(parentId,
group, localAuthor, authorKey, "text/plain",
time, body);
}
} catch(GeneralSecurityException e) {
throw new RuntimeException(e);
} catch(IOException e) {
throw new RuntimeException(e);
}
storeMessage(m);
}
});
}
private void storeMessage(final Message m) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
db.addLocalMessage(m);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Storing message took " + duration + " ms");
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
}

View File

@@ -0,0 +1,166 @@
package org.briarproject.android.identity;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_WORDS;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.InputMethodManager.HIDE_IMPLICIT_ONLY;
import static android.widget.LinearLayout.VERTICAL;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.api.AuthorFactory;
import org.briarproject.api.LocalAuthor;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.crypto.KeyPair;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.lifecycle.LifecycleManager;
import roboguice.activity.RoboActivity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
public class CreateIdentityActivity extends RoboActivity
implements OnEditorActionListener, OnClickListener {
private static final Logger LOG =
Logger.getLogger(CreateIdentityActivity.class.getName());
@Inject @CryptoExecutor private Executor cryptoExecutor;
private EditText nicknameEntry = null;
private Button createButton = null;
private ProgressBar progress = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile CryptoComponent crypto;
@Inject private volatile AuthorFactory authorFactory;
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
TextView chooseNickname = new TextView(this);
chooseNickname.setGravity(CENTER);
chooseNickname.setTextSize(18);
chooseNickname.setPadding(10, 10, 10, 0);
chooseNickname.setText(R.string.choose_nickname);
layout.addView(chooseNickname);
nicknameEntry = new EditText(this) {
@Override
protected void onTextChanged(CharSequence text, int start,
int lengthBefore, int lengthAfter) {
if(createButton != null)
createButton.setEnabled(getText().length() > 0);
}
};
nicknameEntry.setId(1);
nicknameEntry.setMaxLines(1);
int inputType = TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_CAP_WORDS;
nicknameEntry.setInputType(inputType);
nicknameEntry.setOnEditorActionListener(this);
layout.addView(nicknameEntry);
createButton = new Button(this);
createButton.setLayoutParams(WRAP_WRAP);
createButton.setText(R.string.create_button);
createButton.setEnabled(false);
createButton.setOnClickListener(this);
layout.addView(createButton);
progress = new ProgressBar(this);
progress.setLayoutParams(WRAP_WRAP);
progress.setIndeterminate(true);
progress.setVisibility(GONE);
layout.addView(progress);
setContentView(layout);
}
public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
validateNickname();
return true;
}
public void onClick(View view) {
if(!validateNickname()) return;
final String nickname = nicknameEntry.getText().toString();
// Replace the button with a progress bar
createButton.setVisibility(GONE);
progress.setVisibility(VISIBLE);
// Create the identity in a background thread
cryptoExecutor.execute(new Runnable() {
public void run() {
KeyPair keyPair = crypto.generateSignatureKeyPair();
final byte[] publicKey = keyPair.getPublic().getEncoded();
final byte[] privateKey = keyPair.getPrivate().getEncoded();
LocalAuthor a = authorFactory.createLocalAuthor(nickname,
publicKey, privateKey);
storeLocalAuthor(a);
}
});
}
private void storeLocalAuthor(final LocalAuthor a) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
db.addLocalAuthor(a);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Storing author took " + duration + " ms");
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
runOnUiThread(new Runnable() {
public void run() {
finish();
}
});
}
});
}
private boolean validateNickname() {
if(nicknameEntry.getText().toString().equals("")) return false;
// Hide the soft keyboard
Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).toggleSoftInput(HIDE_IMPLICIT_ONLY, 0);
return true;
}
}

View File

@@ -0,0 +1,19 @@
package org.briarproject.android.identity;
import org.briarproject.api.LocalAuthor;
public class LocalAuthorItem {
public static final LocalAuthorItem ANONYMOUS = new LocalAuthorItem(null);
public static final LocalAuthorItem NEW = new LocalAuthorItem(null);
private final LocalAuthor localAuthor;
public LocalAuthorItem(LocalAuthor localAuthor) {
this.localAuthor = localAuthor;
}
public LocalAuthor getLocalAuthor() {
return localAuthor;
}
}

View File

@@ -0,0 +1,21 @@
package org.briarproject.android.identity;
import static org.briarproject.android.identity.LocalAuthorItem.ANONYMOUS;
import static org.briarproject.android.identity.LocalAuthorItem.NEW;
import java.util.Comparator;
public class LocalAuthorItemComparator implements Comparator<LocalAuthorItem> {
public static final LocalAuthorItemComparator INSTANCE =
new LocalAuthorItemComparator();
public int compare(LocalAuthorItem a, LocalAuthorItem b) {
if(a == b) return 0;
if(a == ANONYMOUS || b == NEW) return -1;
if(a == NEW || b == ANONYMOUS) return 1;
String aName = a.getLocalAuthor().getName();
String bName = b.getLocalAuthor().getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
}

View File

@@ -0,0 +1,88 @@
package org.briarproject.android.identity;
import static org.briarproject.android.identity.LocalAuthorItem.ANONYMOUS;
import static org.briarproject.android.identity.LocalAuthorItem.NEW;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.briarproject.R;
import android.content.Context;
import android.content.res.Resources;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.SpinnerAdapter;
import android.widget.TextView;
public class LocalAuthorSpinnerAdapter extends BaseAdapter
implements SpinnerAdapter {
private final Context ctx;
private final boolean includeAnonymous;
private final List<LocalAuthorItem> list = new ArrayList<LocalAuthorItem>();
public LocalAuthorSpinnerAdapter(Context ctx, boolean includeAnonymous) {
this.ctx = ctx;
this.includeAnonymous = includeAnonymous;
}
public void add(LocalAuthorItem item) {
list.add(item);
}
public void clear() {
list.clear();
}
public int getCount() {
if(list.isEmpty()) return 0;
return includeAnonymous ? list.size() + 2 : list.size() + 1;
}
@Override
public View getDropDownView(int position, View convertView,
ViewGroup parent) {
return getView(position, convertView, parent);
}
public LocalAuthorItem getItem(int position) {
if(includeAnonymous) {
if(position == 0) return ANONYMOUS;
if(position == list.size() + 1) return NEW;
return list.get(position - 1);
} else {
if(position == list.size()) return NEW;
return list.get(position);
}
}
public long getItemId(int position) {
return android.R.layout.simple_spinner_item;
}
public View getView(int position, View convertView, ViewGroup parent) {
TextView name = new TextView(ctx);
name.setTextSize(18);
name.setMaxLines(1);
Resources res = ctx.getResources();
int pad = res.getInteger(R.integer.spinner_padding);
name.setPadding(pad, pad, pad, pad);
LocalAuthorItem item = getItem(position);
if(item == ANONYMOUS) name.setText(R.string.anonymous);
else if(item == NEW) name.setText(R.string.new_identity_item);
else name.setText(item.getLocalAuthor().getName());
return name;
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
public void sort(Comparator<LocalAuthorItem> comparator) {
Collections.sort(list, comparator);
}
}

View File

@@ -0,0 +1,465 @@
package org.briarproject.android.invitation;
import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static android.net.wifi.WifiManager.NETWORK_STATE_CHANGED_ACTION;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.identity.LocalAuthorItem;
import org.briarproject.android.identity.LocalAuthorItemComparator;
import org.briarproject.android.identity.LocalAuthorSpinnerAdapter;
import org.briarproject.api.AuthorId;
import org.briarproject.api.LocalAuthor;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.android.ReferenceManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.invitation.InvitationListener;
import org.briarproject.api.invitation.InvitationState;
import org.briarproject.api.invitation.InvitationTask;
import org.briarproject.api.invitation.InvitationTaskFactory;
import org.briarproject.api.lifecycle.LifecycleManager;
import roboguice.activity.RoboActivity;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.widget.Toast;
public class AddContactActivity extends RoboActivity
implements InvitationListener {
private static final Logger LOG =
Logger.getLogger(AddContactActivity.class.getName());
@Inject private CryptoComponent crypto;
@Inject private InvitationTaskFactory invitationTaskFactory;
@Inject private ReferenceManager referenceManager;
private AddContactView view = null;
private InvitationTask task = null;
private long taskHandle = -1;
private AuthorId localAuthorId = null;
private String networkName = null;
private boolean bluetoothEnabled = false;
private BluetoothWifiStateReceiver receiver = null;
private int localInvitationCode = -1, remoteInvitationCode = -1;
private int localConfirmationCode = -1, remoteConfirmationCode = -1;
private boolean connected = false, connectionFailed = false;
private boolean localCompared = false, remoteCompared = false;
private boolean localMatched = false, remoteMatched = false;
private String contactName = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
@Inject private volatile LifecycleManager lifecycleManager;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
if(state == null) {
// This is a new activity
setView(new NetworkSetupView(this));
} else {
// Restore the activity's state
byte[] b = state.getByteArray("org.briarproject.LOCAL_AUTHOR_ID");
if(b != null) localAuthorId = new AuthorId(b);
taskHandle = state.getLong("org.briarproject.TASK_HANDLE", -1);
task = referenceManager.getReference(taskHandle,
InvitationTask.class);
if(task == null) {
// No background task - we must be in an initial or final state
localInvitationCode = state.getInt("org.briarproject.LOCAL_CODE");
remoteInvitationCode = state.getInt("org.briarproject.REMOTE_CODE");
connectionFailed = state.getBoolean("org.briarproject.FAILED");
contactName = state.getString("org.briarproject.CONTACT_NAME");
if(contactName != null) {
localCompared = remoteCompared = true;
localMatched = remoteMatched = true;
}
// Set the appropriate view for the state
if(localInvitationCode == -1) {
setView(new NetworkSetupView(this));
} else if(remoteInvitationCode == -1) {
setView(new InvitationCodeView(this));
} else if(connectionFailed) {
setView(new ConnectionFailedView(this));
} else if(contactName == null) {
setView(new CodesDoNotMatchView(this));
} else {
showToastAndFinish();
return;
}
} else {
// A background task exists - listen to it and get its state
InvitationState s = task.addListener(this);
localInvitationCode = s.getLocalInvitationCode();
remoteInvitationCode = s.getRemoteInvitationCode();
localConfirmationCode = s.getLocalConfirmationCode();
remoteConfirmationCode = s.getRemoteConfirmationCode();
connected = s.getConnected();
connectionFailed = s.getConnectionFailed();
localCompared = s.getLocalCompared();
remoteCompared = s.getRemoteCompared();
localMatched = s.getLocalMatched();
remoteMatched = s.getRemoteMatched();
contactName = s.getContactName();
// Set the appropriate view for the state
if(localInvitationCode == -1) {
setView(new NetworkSetupView(this));
} else if(remoteInvitationCode == -1) {
setView(new InvitationCodeView(this));
} else if(connectionFailed) {
setView(new ConnectionFailedView(this));
} else if(connected && localConfirmationCode == -1) {
setView(new ConnectedView(this));
} else if(localConfirmationCode == -1) {
setView(new ConnectionView(this));
} else if(!localCompared) {
setView(new ConfirmationCodeView(this));
} else if(!remoteCompared) {
setView(new WaitForContactView(this));
} else if(localMatched && remoteMatched) {
if(contactName == null) {
setView(new ContactDetailsView(this));
} else {
showToastAndFinish();
return;
}
} else {
setView(new CodesDoNotMatchView(this));
}
}
}
// Listen for Bluetooth and WiFi state changes
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STATE_CHANGED);
filter.addAction(ACTION_SCAN_MODE_CHANGED);
filter.addAction(NETWORK_STATE_CHANGED_ACTION);
receiver = new BluetoothWifiStateReceiver();
registerReceiver(receiver, filter);
// Get the current Bluetooth and WiFi state
BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
if(bluetooth != null) bluetoothEnabled = bluetooth.isEnabled();
view.bluetoothStateChanged();
WifiManager wifi = (WifiManager) getSystemService(WIFI_SERVICE);
if(wifi != null && wifi.isWifiEnabled()) {
WifiInfo info = wifi.getConnectionInfo();
if(info.getNetworkId() != -1) networkName = info.getSSID();
}
view.wifiStateChanged();
}
private void showToastAndFinish() {
Toast.makeText(this, R.string.contact_added_toast, LENGTH_LONG).show();
finish();
}
@Override
public void onResume() {
super.onResume();
view.populate();
}
@Override
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
if(localAuthorId != null) {
byte[] b = localAuthorId.getBytes();
state.putByteArray("org.briarproject.LOCAL_AUTHOR_ID", b);
}
state.putInt("org.briarproject.LOCAL_CODE", localInvitationCode);
state.putInt("org.briarproject.REMOTE_CODE", remoteInvitationCode);
state.putBoolean("org.briarproject.FAILED", connectionFailed);
state.putString("org.briarproject.CONTACT_NAME", contactName);
if(task != null) state.putLong("org.briarproject.TASK_HANDLE", taskHandle);
}
@Override
public void onDestroy() {
super.onDestroy();
if(task != null) task.removeListener(this);
if(receiver != null) unregisterReceiver(receiver);
}
void setView(AddContactView view) {
this.view = view;
view.init(this);
setContentView(view);
}
void reset(AddContactView view) {
// Don't reset localAuthorId, networkName or bluetoothEnabled
task = null;
taskHandle = -1;
localInvitationCode = -1;
localConfirmationCode = remoteConfirmationCode = -1;
connected = connectionFailed = false;
localCompared = remoteCompared = false;
localMatched = remoteMatched = false;
contactName = null;
setView(view);
}
void loadLocalAuthors(final LocalAuthorSpinnerAdapter adapter) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Collection<LocalAuthor> localAuthors = db.getLocalAuthors();
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Loading authors took " + duration + " ms");
displayLocalAuthors(adapter, localAuthors);
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void displayLocalAuthors(final LocalAuthorSpinnerAdapter adapter,
final Collection<LocalAuthor> localAuthors) {
runOnUiThread(new Runnable() {
public void run() {
if(localAuthors.isEmpty()) throw new IllegalStateException();
adapter.clear();
for(LocalAuthor a : localAuthors)
adapter.add(new LocalAuthorItem(a));
adapter.sort(LocalAuthorItemComparator.INSTANCE);
adapter.notifyDataSetChanged();
}
});
}
void setLocalAuthorId(AuthorId localAuthorId) {
this.localAuthorId = localAuthorId;
}
AuthorId getLocalAuthorId() {
return localAuthorId;
}
String getNetworkName() {
return networkName;
}
boolean isBluetoothEnabled() {
return bluetoothEnabled;
}
int getLocalInvitationCode() {
if(localInvitationCode == -1)
localInvitationCode = crypto.generateInvitationCode();
return localInvitationCode;
}
void remoteInvitationCodeEntered(int code) {
if(localAuthorId == null) throw new IllegalStateException();
if(localInvitationCode == -1) throw new IllegalStateException();
setView(new ConnectionView(this));
task = invitationTaskFactory.createTask(localAuthorId,
localInvitationCode, code);
taskHandle = referenceManager.putReference(task, InvitationTask.class);
task.addListener(AddContactActivity.this);
// Add a second listener so we can remove the first in onDestroy(),
// allowing the activity to be garbage collected if it's destroyed
task.addListener(new ReferenceCleaner(referenceManager, taskHandle));
task.connect();
}
int getLocalConfirmationCode() {
return localConfirmationCode;
}
void remoteConfirmationCodeEntered(int code) {
localCompared = true;
if(code == remoteConfirmationCode) {
localMatched = true;
if(remoteMatched) setView(new ContactDetailsView(this));
else if(remoteCompared) setView(new CodesDoNotMatchView(this));
else setView(new WaitForContactView(this));
task.localConfirmationSucceeded();
} else {
localMatched = false;
setView(new CodesDoNotMatchView(this));
task.localConfirmationFailed();
}
}
String getContactName() {
return contactName;
}
public void connectionSucceeded() {
runOnUiThread(new Runnable() {
public void run() {
connected = true;
setView(new ConnectedView(AddContactActivity.this));
}
});
}
public void connectionFailed() {
runOnUiThread(new Runnable() {
public void run() {
connectionFailed = true;
setView(new ConnectionFailedView(AddContactActivity.this));
}
});
}
public void keyAgreementSucceeded(final int localCode,
final int remoteCode) {
runOnUiThread(new Runnable() {
public void run() {
localConfirmationCode = localCode;
remoteConfirmationCode = remoteCode;
setView(new ConfirmationCodeView(AddContactActivity.this));
}
});
}
public void keyAgreementFailed() {
runOnUiThread(new Runnable() {
public void run() {
connectionFailed = true;
setView(new ConnectionFailedView(AddContactActivity.this));
}
});
}
public void remoteConfirmationSucceeded() {
runOnUiThread(new Runnable() {
public void run() {
remoteCompared = true;
remoteMatched = true;
if(localMatched)
setView(new ContactDetailsView(AddContactActivity.this));
}
});
}
public void remoteConfirmationFailed() {
runOnUiThread(new Runnable() {
public void run() {
remoteCompared = true;
remoteMatched = false;
if(localMatched)
setView(new CodesDoNotMatchView(AddContactActivity.this));
}
});
}
public void pseudonymExchangeSucceeded(final String remoteName) {
runOnUiThread(new Runnable() {
public void run() {
contactName = remoteName;
showToastAndFinish();
}
});
}
public void pseudonymExchangeFailed() {
runOnUiThread(new Runnable() {
public void run() {
setView(new ConnectionFailedView(AddContactActivity.this));
}
});
}
private class BluetoothWifiStateReceiver extends BroadcastReceiver {
public void onReceive(Context ctx, Intent intent) {
String action = intent.getAction();
if(action.equals(ACTION_STATE_CHANGED)) {
int state = intent.getIntExtra(EXTRA_STATE, 0);
bluetoothEnabled = state == STATE_ON;
view.bluetoothStateChanged();
} else if(action.equals(ACTION_SCAN_MODE_CHANGED)) {
view.bluetoothStateChanged();
} else if(action.equals(NETWORK_STATE_CHANGED_ACTION)) {
WifiManager wifi = (WifiManager) getSystemService(WIFI_SERVICE);
if(wifi == null || !wifi.isWifiEnabled()) {
networkName = null;
} else {
WifiInfo info = wifi.getConnectionInfo();
if(info.getNetworkId() == -1) networkName = null;
else networkName = info.getSSID();
}
view.wifiStateChanged();
}
}
}
/**
* Cleans up the reference to the invitation task when the task completes.
* This class is static to prevent memory leaks.
*/
private static class ReferenceCleaner implements InvitationListener {
private final ReferenceManager referenceManager;
private final long handle;
private ReferenceCleaner(ReferenceManager referenceManager,
long handle) {
this.referenceManager = referenceManager;
this.handle = handle;
}
public void connectionSucceeded() {
// Wait for key agreement to succeed or fail
}
public void connectionFailed() {
referenceManager.removeReference(handle, InvitationTask.class);
}
public void keyAgreementSucceeded(int localCode, int remoteCode) {
// Wait for remote confirmation to succeed or fail
}
public void keyAgreementFailed() {
referenceManager.removeReference(handle, InvitationTask.class);
}
public void remoteConfirmationSucceeded() {
// Wait for the pseudonym exchange to succeed or fail
}
public void remoteConfirmationFailed() {
referenceManager.removeReference(handle, InvitationTask.class);
}
public void pseudonymExchangeSucceeded(String remoteName) {
referenceManager.removeReference(handle, InvitationTask.class);
}
public void pseudonymExchangeFailed() {
referenceManager.removeReference(handle, InvitationTask.class);
}
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.android.invitation;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import android.content.Context;
import android.widget.LinearLayout;
abstract class AddContactView extends LinearLayout {
protected AddContactActivity container = null;
AddContactView(Context ctx) {
super(ctx);
}
void init(AddContactActivity container) {
this.container = container;
setLayoutParams(MATCH_MATCH);
setOrientation(VERTICAL);
setGravity(CENTER_HORIZONTAL);
populate();
}
abstract void populate();
void wifiStateChanged() {}
void bluetoothStateChanged() {}
}

View File

@@ -0,0 +1,84 @@
package org.briarproject.android.invitation;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
import static android.provider.Settings.ACTION_BLUETOOTH_SETTINGS;
import static android.view.Gravity.CENTER;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import org.briarproject.R;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.Intent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
class BluetoothStatusView extends LinearLayout implements OnClickListener {
public BluetoothStatusView(Context ctx) {
super(ctx);
}
void init() {
setOrientation(HORIZONTAL);
setGravity(CENTER);
populate();
}
void populate() {
removeAllViews();
Context ctx = getContext();
TextView status = new TextView(ctx);
status.setLayoutParams(WRAP_WRAP_1);
status.setTextSize(14);
status.setPadding(10, 10, 10, 10);
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if(adapter == null) {
ImageView warning = new ImageView(ctx);
warning.setPadding(5, 5, 5, 5);
warning.setImageResource(R.drawable.alerts_and_states_warning);
addView(warning);
status.setText(R.string.bluetooth_not_available);
addView(status);
} else if(adapter.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
ImageView ok = new ImageView(ctx);
ok.setPadding(5, 5, 5, 5);
ok.setImageResource(R.drawable.navigation_accept);
addView(ok);
status.setText(R.string.bluetooth_discoverable);
addView(status);
ImageButton settings = new ImageButton(ctx);
settings.setImageResource(R.drawable.action_settings);
settings.setOnClickListener(this);
addView(settings);
} else if(adapter.isEnabled()) {
ImageView warning = new ImageView(ctx);
warning.setPadding(5, 5, 5, 5);
warning.setImageResource(R.drawable.alerts_and_states_warning);
addView(warning);
status.setText(R.string.bluetooth_not_discoverable);
addView(status);
ImageButton settings = new ImageButton(ctx);
settings.setImageResource(R.drawable.action_settings);
settings.setOnClickListener(this);
addView(settings);
} else {
ImageView warning = new ImageView(ctx);
warning.setPadding(5, 5, 5, 5);
warning.setImageResource(R.drawable.alerts_and_states_warning);
addView(warning);
status.setText(R.string.bluetooth_disabled);
addView(status);
ImageButton settings = new ImageButton(ctx);
settings.setImageResource(R.drawable.action_settings);
settings.setOnClickListener(this);
addView(settings);
}
}
public void onClick(View view) {
getContext().startActivity(new Intent(ACTION_BLUETOOTH_SETTINGS));
}
}

View File

@@ -0,0 +1,6 @@
package org.briarproject.android.invitation;
interface CodeEntryListener {
void codeEntered(int remoteCode);
}

View File

@@ -0,0 +1,98 @@
package org.briarproject.android.invitation;
import static android.content.Context.INPUT_METHOD_SERVICE;
import static android.text.InputType.TYPE_CLASS_NUMBER;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.inputmethod.InputMethodManager.HIDE_IMPLICIT_ONLY;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import org.briarproject.R;
import android.content.Context;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
class CodeEntryView extends LinearLayout
implements OnEditorActionListener, OnClickListener {
private CodeEntryListener listener = null;
private EditText codeEntry = null;
private Button continueButton = null;
public CodeEntryView(Context ctx) {
super(ctx);
}
void init(CodeEntryListener listener, String prompt) {
this.listener = listener;
setOrientation(VERTICAL);
setGravity(CENTER_HORIZONTAL);
Context ctx = getContext();
TextView enterCode = new TextView(ctx);
enterCode.setGravity(CENTER_HORIZONTAL);
enterCode.setTextSize(14);
enterCode.setPadding(10, 10, 10, 0);
enterCode.setText(prompt);
addView(enterCode);
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
codeEntry = new EditText(ctx) {
@Override
protected void onTextChanged(CharSequence text, int start,
int lengthBefore, int lengthAfter) {
if(continueButton != null)
continueButton.setEnabled(getText().length() == 6);
}
};
codeEntry.setId(1); // FIXME: State is not saved and restored
codeEntry.setTextSize(26);
codeEntry.setOnEditorActionListener(this);
codeEntry.setMinEms(5);
codeEntry.setMaxEms(5);
codeEntry.setMaxLines(1);
codeEntry.setInputType(TYPE_CLASS_NUMBER);
innerLayout.addView(codeEntry);
continueButton = new Button(ctx);
continueButton.setLayoutParams(WRAP_WRAP);
continueButton.setText(R.string.continue_button);
continueButton.setEnabled(false);
continueButton.setOnClickListener(this);
innerLayout.addView(continueButton);
addView(innerLayout);
}
public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
if(!validateAndReturnCode()) codeEntry.setText("");
return true;
}
public void onClick(View view) {
if(!validateAndReturnCode()) codeEntry.setText("");
}
private boolean validateAndReturnCode() {
String remoteCodeString = codeEntry.getText().toString();
int remoteCode;
try {
remoteCode = Integer.parseInt(remoteCodeString);
} catch(NumberFormatException e) {
return false;
}
// Hide the soft keyboard
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).toggleSoftInput(HIDE_IMPLICIT_ONLY, 0);
listener.codeEntered(remoteCode);
return true;
}
}

View File

@@ -0,0 +1,55 @@
package org.briarproject.android.invitation;
import static android.view.Gravity.CENTER;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import org.briarproject.R;
import android.content.Context;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
class CodesDoNotMatchView extends AddContactView implements OnClickListener {
CodesDoNotMatchView(Context ctx) {
super(ctx);
}
void populate() {
removeAllViews();
Context ctx = getContext();
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ImageView icon = new ImageView(ctx);
icon.setImageResource(R.drawable.alerts_and_states_error);
innerLayout.addView(icon);
TextView failed = new TextView(ctx);
failed.setTextSize(22);
failed.setPadding(10, 10, 10, 10);
failed.setText(R.string.codes_do_not_match);
innerLayout.addView(failed);
addView(innerLayout);
TextView interfering = new TextView(ctx);
interfering.setTextSize(14);
interfering.setPadding(10, 0, 10, 10);
interfering.setText(R.string.interfering);
addView(interfering);
Button tryAgainButton = new Button(ctx);
tryAgainButton.setLayoutParams(WRAP_WRAP);
tryAgainButton.setText(R.string.try_again_button);
tryAgainButton.setOnClickListener(this);
addView(tryAgainButton);
}
public void onClick(View view) {
// Try again
container.reset(new NetworkSetupView(container));
}
}

View File

@@ -0,0 +1,60 @@
package org.briarproject.android.invitation;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import org.briarproject.R;
import android.content.Context;
import android.content.res.Resources;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
class ConfirmationCodeView extends AddContactView implements CodeEntryListener {
ConfirmationCodeView(Context ctx) {
super(ctx);
}
void populate() {
removeAllViews();
Context ctx = getContext();
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ImageView icon = new ImageView(ctx);
icon.setImageResource(R.drawable.navigation_accept);
innerLayout.addView(icon);
TextView connected = new TextView(ctx);
connected.setTextSize(22);
connected.setPadding(10, 10, 10, 10);
connected.setText(R.string.connected_to_contact);
innerLayout.addView(connected);
addView(innerLayout);
TextView yourCode = new TextView(ctx);
yourCode.setGravity(CENTER_HORIZONTAL);
yourCode.setTextSize(14);
yourCode.setPadding(10, 10, 10, 10);
yourCode.setText(R.string.your_confirmation_code);
addView(yourCode);
TextView code = new TextView(ctx);
code.setGravity(CENTER_HORIZONTAL);
code.setTextSize(50);
code.setPadding(10, 0, 10, 10);
int localCode = container.getLocalConfirmationCode();
code.setText(String.format("%06d", localCode));
addView(code);
CodeEntryView codeEntry = new CodeEntryView(ctx);
Resources res = getResources();
codeEntry.init(this, res.getString(R.string.enter_confirmation_code));
addView(codeEntry);
}
public void codeEntered(int remoteCode) {
container.remoteConfirmationCodeEntered(remoteCode);
}
}

View File

@@ -0,0 +1,49 @@
package org.briarproject.android.invitation;
import static android.view.Gravity.CENTER;
import org.briarproject.R;
import android.content.Context;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
class ConnectedView extends AddContactView {
ConnectedView(Context ctx) {
super(ctx);
}
void populate() {
removeAllViews();
Context ctx = getContext();
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ImageView icon = new ImageView(ctx);
icon.setImageResource(R.drawable.navigation_accept);
innerLayout.addView(icon);
TextView connected = new TextView(ctx);
connected.setTextSize(22);
connected.setPadding(10, 10, 10, 10);
connected.setText(R.string.connected_to_contact);
innerLayout.addView(connected);
addView(innerLayout);
innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ProgressBar progress = new ProgressBar(ctx);
progress.setIndeterminate(true);
progress.setPadding(10, 10, 10, 10);
innerLayout.addView(progress);
TextView connecting = new TextView(ctx);
connecting.setText(R.string.calculating_confirmation_code);
innerLayout.addView(connecting);
addView(innerLayout);
}
}

View File

@@ -0,0 +1,85 @@
package org.briarproject.android.invitation;
import static android.view.Gravity.CENTER;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import org.briarproject.R;
import android.content.Context;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
class ConnectionFailedView extends AddContactView implements OnClickListener {
private WifiStatusView wifi = null;
private BluetoothStatusView bluetooth = null;
private Button tryAgainButton = null;
ConnectionFailedView(Context ctx) {
super(ctx);
}
void populate() {
removeAllViews();
Context ctx = getContext();
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ImageView icon = new ImageView(ctx);
icon.setImageResource(R.drawable.alerts_and_states_error);
innerLayout.addView(icon);
TextView failed = new TextView(ctx);
failed.setTextSize(22);
failed.setPadding(10, 10, 10, 10);
failed.setText(R.string.connection_failed);
innerLayout.addView(failed);
addView(innerLayout);
TextView checkNetwork = new TextView(ctx);
checkNetwork.setTextSize(14);
checkNetwork.setPadding(10, 0, 10, 10);
checkNetwork.setText(R.string.check_same_network);
addView(checkNetwork);
wifi = new WifiStatusView(ctx);
wifi.init();
addView(wifi);
bluetooth = new BluetoothStatusView(ctx);
bluetooth.init();
addView(bluetooth);
tryAgainButton = new Button(ctx);
tryAgainButton.setLayoutParams(WRAP_WRAP);
tryAgainButton.setText(R.string.try_again_button);
tryAgainButton.setOnClickListener(this);
enableOrDisableTryAgainButton();
addView(tryAgainButton);
}
void wifiStateChanged() {
if(wifi != null) wifi.populate();
enableOrDisableTryAgainButton();
}
void bluetoothStateChanged() {
if(bluetooth != null) bluetooth.populate();
enableOrDisableTryAgainButton();
}
private void enableOrDisableTryAgainButton() {
if(tryAgainButton == null) return; // Activity not created yet
boolean bluetoothEnabled = container.isBluetoothEnabled();
String networkName = container.getNetworkName();
tryAgainButton.setEnabled(bluetoothEnabled || networkName != null);
}
public void onClick(View view) {
// Try again
container.reset(new InvitationCodeView(container));
}
}

View File

@@ -0,0 +1,72 @@
package org.briarproject.android.invitation;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import org.briarproject.R;
import android.content.Context;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
class ConnectionView extends AddContactView {
ConnectionView(Context ctx) {
super(ctx);
}
void populate() {
removeAllViews();
Context ctx = getContext();
TextView yourCode = new TextView(ctx);
yourCode.setGravity(CENTER_HORIZONTAL);
yourCode.setTextSize(14);
yourCode.setPadding(10, 10, 10, 10);
yourCode.setText(R.string.your_invitation_code);
addView(yourCode);
TextView code = new TextView(ctx);
code.setGravity(CENTER_HORIZONTAL);
code.setTextSize(50);
code.setPadding(10, 0, 10, 10);
int localCode = container.getLocalInvitationCode();
code.setText(String.format("%06d", localCode));
addView(code);
String networkName = container.getNetworkName();
if(networkName != null) {
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ProgressBar progress = new ProgressBar(ctx);
progress.setIndeterminate(true);
progress.setPadding(10, 10, 10, 10);
innerLayout.addView(progress);
TextView connecting = new TextView(ctx);
String format = getResources().getString(
R.string.format_connecting_wifi);
connecting.setText(String.format(format, networkName));
innerLayout.addView(connecting);
addView(innerLayout);
}
if(container.isBluetoothEnabled()) {
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ProgressBar progress = new ProgressBar(ctx);
progress.setPadding(10, 10, 10, 10);
progress.setIndeterminate(true);
innerLayout.addView(progress);
TextView connecting = new TextView(ctx);
connecting.setText(R.string.connecting_bluetooth);
innerLayout.addView(connecting);
addView(innerLayout);
}
}
}

View File

@@ -0,0 +1,65 @@
package org.briarproject.android.invitation;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import org.briarproject.R;
import android.content.Context;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
class ContactDetailsView extends AddContactView {
ContactDetailsView(Context ctx) {
super(ctx);
}
void populate() {
removeAllViews();
Context ctx = getContext();
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ImageView icon = new ImageView(ctx);
icon.setImageResource(R.drawable.navigation_accept);
innerLayout.addView(icon);
TextView connected = new TextView(ctx);
connected.setTextSize(22);
connected.setPadding(10, 10, 10, 10);
connected.setText(R.string.connected_to_contact);
innerLayout.addView(connected);
addView(innerLayout);
TextView yourCode = new TextView(ctx);
yourCode.setGravity(CENTER_HORIZONTAL);
yourCode.setTextSize(14);
yourCode.setPadding(10, 0, 10, 10);
yourCode.setText(R.string.your_confirmation_code);
addView(yourCode);
TextView code = new TextView(ctx);
code.setGravity(CENTER_HORIZONTAL);
code.setTextSize(50);
code.setPadding(10, 0, 10, 10);
int localCode = container.getLocalConfirmationCode();
code.setText(String.format("%06d", localCode));
addView(code);
innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ProgressBar progress = new ProgressBar(ctx);
progress.setIndeterminate(true);
progress.setPadding(10, 10, 10, 10);
innerLayout.addView(progress);
TextView connecting = new TextView(ctx);
connecting.setText(R.string.exchanging_contact_details);
innerLayout.addView(connecting);
addView(innerLayout);
}
}

View File

@@ -0,0 +1,42 @@
package org.briarproject.android.invitation;
import static android.view.Gravity.CENTER_HORIZONTAL;
import org.briarproject.R;
import android.content.Context;
import android.content.res.Resources;
import android.widget.TextView;
class InvitationCodeView extends AddContactView implements CodeEntryListener {
InvitationCodeView(Context ctx) {
super(ctx);
}
void populate() {
removeAllViews();
Context ctx = getContext();
TextView yourCode = new TextView(ctx);
yourCode.setGravity(CENTER_HORIZONTAL);
yourCode.setTextSize(14);
yourCode.setPadding(10, 10, 10, 10);
yourCode.setText(R.string.your_invitation_code);
addView(yourCode);
TextView code = new TextView(ctx);
code.setGravity(CENTER_HORIZONTAL);
code.setTextSize(50);
code.setPadding(10, 0, 10, 10);
int localCode = container.getLocalInvitationCode();
code.setText(String.format("%06d", localCode));
addView(code);
CodeEntryView codeEntry = new CodeEntryView(ctx);
Resources res = getResources();
codeEntry.init(this, res.getString(R.string.enter_invitation_code));
addView(codeEntry);
}
public void codeEntered(int remoteCode) {
container.remoteInvitationCodeEntered(remoteCode);
}
}

View File

@@ -0,0 +1,121 @@
package org.briarproject.android.invitation;
import static android.view.Gravity.CENTER;
import static org.briarproject.android.identity.LocalAuthorItem.NEW;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import org.briarproject.R;
import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.identity.LocalAuthorItem;
import org.briarproject.android.identity.LocalAuthorSpinnerAdapter;
import org.briarproject.api.AuthorId;
import android.content.Context;
import android.content.Intent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
class NetworkSetupView extends AddContactView
implements OnItemSelectedListener, OnClickListener {
private LocalAuthorSpinnerAdapter adapter = null;
private Spinner spinner = null;
private WifiStatusView wifi = null;
private BluetoothStatusView bluetooth = null;
private Button continueButton = null;
NetworkSetupView(Context ctx) {
super(ctx);
}
void populate() {
removeAllViews();
Context ctx = getContext();
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setLayoutParams(MATCH_WRAP);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
TextView yourNickname = new TextView(ctx);
yourNickname.setTextSize(18);
yourNickname.setPadding(10, 10, 10, 10);
yourNickname.setText(R.string.your_nickname);
innerLayout.addView(yourNickname);
adapter = new LocalAuthorSpinnerAdapter(ctx, false);
spinner = new Spinner(ctx);
spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(this);
container.loadLocalAuthors(adapter);
innerLayout.addView(spinner);
addView(innerLayout);
wifi = new WifiStatusView(ctx);
wifi.init();
addView(wifi);
bluetooth = new BluetoothStatusView(ctx);
bluetooth.init();
addView(bluetooth);
TextView faceToFace = new TextView(ctx);
faceToFace.setGravity(CENTER);
faceToFace.setTextSize(14);
faceToFace.setPadding(10, 10, 10, 10);
faceToFace.setText(R.string.fact_to_face);
addView(faceToFace);
continueButton = new Button(ctx);
continueButton.setLayoutParams(WRAP_WRAP);
continueButton.setText(R.string.continue_button);
continueButton.setOnClickListener(this);
enableOrDisableContinueButton();
addView(continueButton);
}
void wifiStateChanged() {
if(wifi != null) wifi.populate();
enableOrDisableContinueButton();
}
void bluetoothStateChanged() {
if(bluetooth != null) bluetooth.populate();
enableOrDisableContinueButton();
}
private void enableOrDisableContinueButton() {
if(continueButton == null) return; // Activity not created yet
AuthorId localAuthorId = container.getLocalAuthorId();
boolean bluetoothEnabled = container.isBluetoothEnabled();
String networkName = container.getNetworkName();
boolean networkAvailable = bluetoothEnabled || networkName != null;
continueButton.setEnabled(localAuthorId != null && networkAvailable);
}
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
LocalAuthorItem item = adapter.getItem(position);
if(item == NEW) {
container.setLocalAuthorId(null);
Intent i = new Intent(container, CreateIdentityActivity.class);
container.startActivity(i);
} else {
container.setLocalAuthorId(item.getLocalAuthor().getId());
}
enableOrDisableContinueButton();
}
public void onNothingSelected(AdapterView<?> parent) {
container.setLocalAuthorId(null);
}
public void onClick(View view) {
container.setView(new InvitationCodeView(container));
}
}

View File

@@ -0,0 +1,65 @@
package org.briarproject.android.invitation;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import org.briarproject.R;
import android.content.Context;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
class WaitForContactView extends AddContactView {
WaitForContactView(Context ctx) {
super(ctx);
}
void populate() {
removeAllViews();
Context ctx = getContext();
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ImageView icon = new ImageView(ctx);
icon.setImageResource(R.drawable.navigation_accept);
innerLayout.addView(icon);
TextView connected = new TextView(ctx);
connected.setTextSize(22);
connected.setPadding(10, 10, 10, 10);
connected.setText(R.string.connected_to_contact);
innerLayout.addView(connected);
addView(innerLayout);
TextView yourCode = new TextView(ctx);
yourCode.setGravity(CENTER_HORIZONTAL);
yourCode.setTextSize(14);
yourCode.setPadding(10, 0, 10, 10);
yourCode.setText(R.string.your_confirmation_code);
addView(yourCode);
TextView code = new TextView(ctx);
code.setGravity(CENTER_HORIZONTAL);
code.setTextSize(50);
code.setPadding(10, 0, 10, 10);
int localCode = container.getLocalConfirmationCode();
code.setText(String.format("%06d", localCode));
addView(code);
innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ProgressBar progress = new ProgressBar(ctx);
progress.setIndeterminate(true);
progress.setPadding(10, 10, 10, 10);
innerLayout.addView(progress);
TextView connecting = new TextView(ctx);
connecting.setText(R.string.waiting_for_contact);
innerLayout.addView(connecting);
addView(innerLayout);
}
}

View File

@@ -0,0 +1,92 @@
package org.briarproject.android.invitation;
import static android.content.Context.WIFI_SERVICE;
import static android.provider.Settings.ACTION_WIFI_SETTINGS;
import static android.view.Gravity.CENTER;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import org.briarproject.R;
import android.content.Context;
import android.content.Intent;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
class WifiStatusView extends LinearLayout implements OnClickListener {
public WifiStatusView(Context ctx) {
super(ctx);
}
void init() {
setOrientation(HORIZONTAL);
setGravity(CENTER);
populate();
}
void populate() {
removeAllViews();
Context ctx = getContext();
TextView status = new TextView(ctx);
status.setTextSize(14);
status.setPadding(10, 10, 10, 10);
status.setLayoutParams(WRAP_WRAP_1);
WifiManager wifi = (WifiManager) ctx.getSystemService(WIFI_SERVICE);
if(wifi == null) {
ImageView warning = new ImageView(ctx);
warning.setPadding(5, 5, 5, 5);
warning.setImageResource(R.drawable.alerts_and_states_warning);
addView(warning);
status.setText(R.string.wifi_not_available);
addView(status);
} else if(wifi.isWifiEnabled()) {
WifiInfo info = wifi.getConnectionInfo();
String networkName = info.getSSID();
int networkId = info.getNetworkId();
if(networkName == null || networkId == -1) {
ImageView warning = new ImageView(ctx);
warning.setPadding(5, 5, 5, 5);
warning.setImageResource(R.drawable.alerts_and_states_warning);
addView(warning);
status.setText(R.string.wifi_disconnected);
addView(status);
ImageButton settings = new ImageButton(ctx);
settings.setImageResource(R.drawable.action_settings);
settings.setOnClickListener(this);
addView(settings);
} else {
ImageView ok = new ImageView(ctx);
ok.setPadding(5, 5, 5, 5);
ok.setImageResource(R.drawable.navigation_accept);
addView(ok);
String format = getResources().getString(
R.string.format_wifi_connected);
status.setText(String.format(format, networkName));
addView(status);
ImageButton settings = new ImageButton(ctx);
settings.setImageResource(R.drawable.action_settings);
settings.setOnClickListener(this);
addView(settings);
}
} else {
ImageView warning = new ImageView(ctx);
warning.setPadding(5, 5, 5, 5);
warning.setImageResource(R.drawable.alerts_and_states_warning);
addView(warning);
status.setText(R.string.wifi_disabled);
addView(status);
ImageButton settings = new ImageButton(ctx);
settings.setImageResource(R.drawable.action_settings);
settings.setOnClickListener(this);
addView(settings);
}
}
public void onClick(View view) {
getContext().startActivity(new Intent(ACTION_WIFI_SETTINGS));
}
}

View File

@@ -0,0 +1,23 @@
package org.briarproject.android.util;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import android.widget.LinearLayout;
public class CommonLayoutParams {
public static final LinearLayout.LayoutParams MATCH_MATCH =
new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
public static final LinearLayout.LayoutParams MATCH_WRAP =
new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
public static final LinearLayout.LayoutParams MATCH_WRAP_1 =
new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT, 1);
public static final LinearLayout.LayoutParams WRAP_WRAP =
new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
public static final LinearLayout.LayoutParams WRAP_WRAP_1 =
new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1);
}

View File

@@ -0,0 +1,19 @@
package org.briarproject.android.util;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import org.briarproject.R;
import android.content.Context;
import android.content.res.Resources;
import android.view.View;
import android.widget.LinearLayout.LayoutParams;
public class HorizontalBorder extends View {
public HorizontalBorder(Context ctx) {
super(ctx);
Resources res = ctx.getResources();
int width = res.getInteger(R.integer.horizontal_border_width);
setLayoutParams(new LayoutParams(MATCH_PARENT, width));
setBackgroundColor(getResources().getColor(R.color.horizontal_border));
}
}

View File

@@ -0,0 +1,14 @@
package org.briarproject.android.util;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import android.content.Context;
import android.view.View;
import android.widget.LinearLayout.LayoutParams;
public class HorizontalSpace extends View {
public HorizontalSpace(Context ctx) {
super(ctx);
setLayoutParams(new LayoutParams(WRAP_CONTENT, 0, 1));
}
}

View File

@@ -0,0 +1,21 @@
package org.briarproject.android.util;
import static android.view.Gravity.CENTER;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import android.content.Context;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
public class ListLoadingProgressBar extends LinearLayout {
public ListLoadingProgressBar(Context ctx) {
super(ctx);
setLayoutParams(MATCH_WRAP_1);
setGravity(CENTER);
ProgressBar progress = new ProgressBar(ctx);
progress.setLayoutParams(WRAP_WRAP);
progress.setIndeterminate(true);
addView(progress);
}
}